Komponen Agular dalam Angular

Saat Anda bekerja di perpustakaan komponen yang dapat digunakan kembali, pertanyaan API sangat akut. Di satu sisi, Anda perlu membuat keputusan yang andal, akurat, di sisi lain, untuk memuaskan banyak kasus khusus. Ini berlaku untuk bekerja dengan data, dan ke fitur eksternal dari berbagai kasus penggunaan. Selain itu, semuanya harus dengan mudah diperbarui dan diluncurkan pada proyek.


Komponen seperti itu membutuhkan fleksibilitas yang belum pernah terjadi sebelumnya. Pada saat yang sama, pengaturan tidak dapat dibuat terlalu rumit, karena mereka akan digunakan oleh para senior dan Juni. Mengurangi duplikasi kode adalah salah satu tugas pustaka komponen. Oleh karena itu, konfigurasi tidak dapat diubah menjadi kode penyalinan.


bruce lee


Komponen data-agnostik


Katakanlah kita membuat tombol dengan menu drop-down. Apa yang akan menjadi API-nya? Tentu saja, ia membutuhkan beberapa item sebagai input - array item menu. Kemungkinan besar, versi pertama antarmuka akan seperti ini:


interface MenuItem { readonly text: string; readonly onClick(): void; } 

Cukup cepat, disabled: boolean akan menambah ini. Kemudian desainer akan datang dan menggambar menu dengan ikon. Dan orang-orang dari proyek tetangga, mereka akan menggambar ikon di sisi lain. Dan sekarang antarmuka semakin berkembang, perlu untuk mencakup kasus-kasus khusus semakin banyak, dan dari banyaknya bendera komponen mulai menyerupai Majelis PBB.



Generik datang untuk menyelamatkan. Jika Anda mengatur komponen sehingga tidak peduli dengan model data, maka semua masalah ini akan hilang. Alih-alih memanggil item.onClick pada klik, menu hanya akan mengeluarkan item yang diklik keluar. Apa yang harus dilakukan dengannya setelah itu adalah tugas bagi pengguna perpustakaan. Bahkan jika mereka memanggil item.onClick sama. item.onClick .


Dalam kasus negara yang disabled , misalnya, masalah ini diselesaikan menggunakan penangan khusus. Metode disabledItemHandler: (item: T) => boolean diteruskan ke komponen disabledItemHandler: (item: T) => boolean , di mana setiap item dijalankan. Hasilnya mengatakan apakah elemen ini terkunci.



Jika Anda melakukan ComboBox , sebuah antarmuka mungkin muncul di benak Anda yang menyimpan string untuk tampilan dan nilai arbitrer nyata yang digunakan dalam kode. Gagasan ini jelas. Lagi pula, ketika pengguna mengetik teks, ComboBox harus memfilter opsi sesuai dengan baris yang dimasukkan.


 interface ComboBoxItem { readonly text: string; readonly value: any; } 

Tapi di sini juga, keterbatasan pendekatan semacam itu akan muncul - segera setelah desain muncul di mana garis tidak cukup. Selain itu, formulir akan berisi pembungkus alih-alih nilai sebenarnya, pencarian tidak selalu dilakukan secara eksklusif oleh representasi string (misalnya, kita dapat mengemudi di nomor telepon, tetapi nama orang tersebut harus ditampilkan). Dan jumlah antarmuka akan tumbuh dengan munculnya komponen lain, bahkan jika model data di bawahnya sama.


Generik dan penangan juga akan membantu di sini. Mari kita beri fungsi (item: T) => string komponen stringify . Nilai default adalah item => String(item) . Dengan demikian, Anda bahkan dapat menggunakan kelas sebagai opsi dengan mendefinisikan metode toString() di dalamnya. Seperti disebutkan di atas, perlu untuk memfilter opsi tidak hanya dengan representasi string. Ini juga merupakan kasus yang baik untuk menggunakan penangan. Anda dapat memberikan komponen dengan fungsi yang menerima string pencarian dan elemen sebagai input. Ini akan mengembalikan boolean - ini akan memberi tahu jika barang tersebut cocok untuk permintaan tersebut.


Contoh umum lain menggunakan antarmuka adalah id unik, yang cocok dengan salinan objek JavaScript. Ketika kami menerima nilai formulir segera, dan opsi untuk seleksi datang dalam permintaan terpisah dari server - mereka hanya akan memiliki salinan elemen saat ini. Tugas ini ditangani oleh seorang pawang yang menerima dua elemen sebagai input dan mengembalikan kesetaraan mereka. Perbandingan default adalah normal === .

Komponen tampilan tab, pada kenyataannya, tidak perlu tahu dalam bentuk apa tab diteruskan kepadanya: setidaknya dengan teks, setidaknya dengan objek dengan bidang tambahan, bahkan dengan caranya. Pengetahuan format tidak diperlukan untuk implementasi, tetapi banyak yang membuat tautan ke format tertentu. Tidak adanya ikatan berarti bahwa komponen tidak akan memerlukan perubahan melanggar selama penyempurnaan, tidak akan memaksa pengguna untuk menyesuaikan data mereka untuk mereka, dan akan memungkinkan menggabungkan komponen atom satu sama lain seperti kubus lego.


Pilihan barang yang sama cocok untuk menu konteks dan kotak kombo, pilih, banyak pilih, komponen sederhana dengan mudah dimasukkan dalam desain yang lebih kompleks. Namun, Anda harus dapat menampilkan data yang berubah-ubah.



Daftar dapat berisi avatar, warna berbeda, ikon, jumlah pesan yang belum dibaca, dan banyak lagi.


Untuk melakukan ini, komponen harus bekerja dengan penampilan dengan cara yang mirip dengan obat generik.

Komponen desain-agnostik


Angular menyediakan alat yang ampuh untuk menentukan penampilan.


Misalnya, pertimbangkan ComboBox , karena dapat terlihat sangat beragam. Tentu saja, batasan tingkat tertentu akan ditetapkan dalam komponen, karena harus mematuhi keseluruhan desain aplikasi. Ukurannya, warna default, padding - semua ini harus bekerja dengan sendirinya. Kami tidak ingin memaksa pengguna untuk memikirkan segala sesuatu tentang penampilan.



Data sewenang-wenang seperti air: mereka tidak memiliki bentuk, mereka tidak membawa sesuatu yang spesifik dalam diri mereka. Tugas kita adalah memberikan kesempatan untuk menetapkan "kapal" bagi mereka. Dalam hal ini, pengembangan komponen yang disarikan dari penampilan adalah sebagai berikut:



Komponen adalah semacam rak dari ukuran yang diperlukan, dan templat khusus digunakan untuk menampilkan konten, yang "memakai" itu. Metode standar, seperti keluaran representasi string, diletakkan pada komponen awalnya, dan pengguna dapat mentransfer opsi yang lebih kompleks dari luar. Mari kita lihat kemungkinan yang dimiliki Angular untuk ini.


  1. Cara paling sederhana untuk mengubah penampilan adalah interpolasi garis. Tapi garis yang tidak berubah-ubah tidak cocok untuk menampilkan item menu, karena tidak tahu apa-apa tentang setiap item - dan semuanya akan terlihat sama. String statis tidak memiliki konteks . Tetapi sangat cocok untuk mengatur teks "Tidak Ditemukan" jika daftar opsi kosong.


     <div>{{content}}</div> 

  2. Kita sudah bicara tentang representasi string dari data arbitrer. Hasilnya juga berupa string, tetapi ditentukan oleh nilai input. Dalam situasi ini, konteksnya akan menjadi item daftar. Ini adalah opsi yang lebih fleksibel, meskipun tidak memungkinkan penataan hasil - string tidak diinterpolasi dalam HTML - dan bahkan lebih lagi tidak akan mengizinkan penggunaan arahan atau komponen.


     <div>{{content(context)}}</div> 

  3. Angular menyediakan ng-template dan direktif struktural *ngTemplateOutlet untuk *ngTemplateOutlet . Dengan bantuan mereka, kita dapat mendefinisikan sepotong HTML yang mengharapkan beberapa data menjadi input dan meneruskannya ke komponen. Di sana ia akan dipakai dengan konteks tertentu. Kami akan meneruskan elemen kami ke sana tanpa khawatir tentang model. Membuat templat yang tepat untuk objek Anda adalah tugas pengembang-konsumen komponen kami.


     <ng-container *ngTemplateOutlet="content; context: context"></ng-container> 

    Template adalah alat yang sangat kuat, tetapi perlu didefinisikan dalam beberapa komponen yang ada. Ini sangat mempersulit penggunaannya kembali. Kadang-kadang penampilan yang sama diperlukan di berbagai bagian aplikasi dan bahkan di aplikasi yang berbeda. Dalam praktik saya, ini, misalnya, adalah tampilan pemilihan akun dengan tampilan nama, mata uang, dan saldo.


  4. Cara paling kompleks untuk menyesuaikan tampilan yang menyelesaikan masalah ini adalah komponen dinamis. Di Angular, direktif *ngComponentOutlet telah lama ada untuk membuatnya secara deklaratif. Itu tidak memungkinkan transfer konteks, tetapi masalah ini diselesaikan dengan implementasi dependensi. Kita dapat membuat token untuk konteks dan menambahkannya ke Injector yang dengannya komponen dibuat.


     <ng-container *ngComponentOutlet="content; injector: injector"></ng-container> 

    Perlu dicatat bahwa konteksnya tidak hanya elemen yang ingin kami tampilkan, tetapi juga keadaan di mana ia berada:


     <ng-template let-item let-focused="focused"> <!-- ... --> </ng-template> 

    Misalnya, dalam kasus penarikan akun, status fokus item tercermin dalam tampilan - latar belakang ikon berubah dari abu-abu menjadi putih. Secara umum, masuk akal untuk mentransfer ke konteks kondisi yang berpotensi mempengaruhi tampilan template. Poin ini mungkin satu-satunya batasan-antarmuka dari pendekatan ini.




Outlet Universal


Alat yang dijelaskan di atas tersedia dalam Angular dari versi kelima. Tetapi kami ingin dengan mudah beralih dari satu opsi ke opsi lainnya. Untuk melakukan ini, kami akan mengumpulkan komponen yang menerima konten dan konteks sebagai input dan mengimplementasikan cara yang tepat untuk memasukkan konten ini secara otomatis. Secara umum, cukup bagi kita untuk belajar membedakan antara tipe string , number , (context: T) => string | number (context: T) => string | number , TemplateRef<T> dan Type<any> (tetapi ada beberapa nuansa di sini, yang akan kita bahas di bawah).


Templat komponen akan terlihat seperti ini:


 <ng-container [ngSwitch]="type"> <ng-container *ngSwitchCase="'primitive'">{{content}}</ng-container> <ng-container *ngSwitchCase="'function'">{{content(context)}}</ng-container> <ng-container *ngSwitchCase="'template'"> <ng-container *ngTemplateOutlet="content; context: context"></ng-container> </ng-container> <ng-container *ngSwitchCase="'component'"> <ng-container *ngComponentOutlet="content; injector: injector"></ng-container> </ng-container> </ng-container> 

Kode akan mendapatkan tipe pengambil untuk memilih metode yang sesuai. Perlu dicatat bahwa secara umum kita tidak dapat membedakan komponen dari suatu fungsi. Saat menggunakan modul malas, kita membutuhkan Injector yang tahu tentang keberadaan komponen. Untuk melakukan ini, kita akan membuat kelas pembungkus. Ini juga akan memungkinkan untuk menentukannya secara instanceof :


 export class ComponentContent<T> { constructor( readonly component: Type<T>, private readonly injector: Injector | null = null, ) {} } 

Tambahkan metode untuk membuat injektor dengan konteks yang diteruskan:


 createInjectorWithContext(injector: Injector, context: C): Injector { return Injector.create({ parent: this.injector || injector, providers: [ { provide: CONTEXT, useValue: context, }, ], }); } 

Adapun templat, untuk sebagian besar kasus Anda dapat bekerja dengan mereka apa adanya. Tetapi kita harus ingat bahwa templat tunduk pada verifikasi perubahan sebagai ganti definisinya. Jika Anda mentransfernya ke tampilan, yang paralel atau lebih tinggi di pohon dari tempat definisi, maka perubahan yang dipicu di dalamnya tidak akan diambil dalam tampilan asli.


Untuk memperbaiki situasi ini, kami akan menggunakan tidak hanya templat, tetapi arahan yang juga akan memiliki ChangeDetectorRef tempat definisinya. Dengan cara ini kita dapat mulai memeriksa perubahan bila perlu.


Pola Polimorfik


Dalam praktiknya, dapat bermanfaat untuk mengontrol perilaku templat tergantung pada jenis konten yang masuk ke dalamnya.


Misalnya, kami ingin memberikan kesempatan untuk mentransfer templat ke komponen untuk sesuatu yang istimewa. Pada saat yang sama, dalam banyak kasus, Anda hanya perlu ikon. Dalam situasi seperti itu, Anda dapat mengonfigurasi perilaku default dan menggunakannya saat primitif atau fungsi memasukkan input. Kadang-kadang bahkan jenis primitif itu penting: misalnya, jika Anda memiliki komponen lencana untuk menampilkan jumlah pesan yang belum dibaca pada tab, tetapi Anda ingin menyorot halaman yang memerlukan perhatian dengan ikon khusus.



Untuk melakukan ini, Anda perlu menambahkan satu hal lagi - melewati templat untuk menampilkan primitif. Tambahkan @ContentChild ke komponen, yang mengambil TemplateRef dari konten. Jika ditemukan dan fungsi, string, atau angka diteruskan ke konten, kami dapat instantiate dengan primitif sebagai konteks:


  <ng-container *ngSwitchCase="'interpolation'"> <ng-container *ngIf="!template; else child">{{primitive}}</ng-container> <ng-template #child> <ng-container *ngTemplateOutlet="template; context: { $implicit: primitive }" ></ng-container> </ng-template> </ng-container> 

Sekarang kita dapat menata interpolasi atau bahkan meneruskan hasilnya ke beberapa komponen untuk ditampilkan:


  <content-outlet [content]="content" [context]="context"> <ng-template let-primitive> <div class="primitive">{{primitive}}</div> </ng-template> </content-outlet> 

Saatnya mempraktikkan kode kita.


Gunakan


Sebagai contoh, kami menguraikan dua komponen: tab dan ComboBox . Template tab hanya akan terdiri dari outlet konten untuk setiap tab, di mana objek yang dilewati oleh pengguna akan menjadi konteks:


 <content-outlet *ngFor="let tab of tabs" [class.disabled]="disabledItemHandler(tab)" [content]="content" [context]="getContext(tab)" (click)="onClick(tab)" ></content-outlet> 

Anda perlu mengatur gaya default: misalnya, ukuran font, garis bawah di bawah tab saat ini, warna. Tapi kami akan meninggalkan tampilan yang konkret pada konten. Kode komponen akan seperti ini:


 export class TabsComponent<T> { @Input() tabs: ReadonlyArray<T> = []; @Input() content: Content = ({$implicit}) => String($implicit); @Input() disabledItemHandler: (tab: T) => boolean = () => false; @Input() activeTab: T | null = null; @Output() activeTabChange = new EventEmitter<T>(); getContext($implicit: T): IContextWithActive<T> { return { $implicit, active: $implicit === this.activeTab, }; } onClick(tab: T) { this.activeTab = tab; this.activeTabChange.emit(tab); } } 

Kami mendapat komponen yang dapat bekerja dengan array sewenang-wenang, menampilkannya sebagai tab. Anda dapat dengan mudah memberikan string di sana dan mendapatkan tampilan dasar:



Dan Anda dapat mentransfer objek dan templat untuk menampilkannya dan menyesuaikan tampilan agar sesuai dengan kebutuhan Anda, menambahkan HTML, ikon, indikator:



Dalam kasus ComboBox, pertama-tama kita akan membuat dua komponen dasar yang terdiri darinya: bidang input dengan ikon dan menu. Yang terakhir tidak masuk akal untuk melukis secara rinci - sangat mirip dengan tab, hanya secara vertikal dan memiliki gaya dasar lainnya. Dan kolom input dapat diimplementasikan sebagai berikut:


 <input #input [(ngModel)]="value"/> <content-outlet [content]="content" (mousedown)="onMouseDown($event, input)" > <ng-template let-icon> <div [innerHTML]="icon"></div> </ng-template> </content-outlet> 

Jika Anda membuat input benar-benar diposisikan, itu akan memblokir outlet dan semua klik ada di sana. Ini nyaman untuk bidang input sederhana dengan ikon dekoratif, seperti ikon kaca pembesar. Dalam contoh di atas, pendekatan templat polimorfik diterapkan - string yang ditransmisikan akan digunakan sebagai innerHTML untuk menyisipkan ikon SVG. Jika, misalnya, kami perlu menunjukkan avatar pengguna yang dimasukkan, kami dapat mentransfer template di sana.


ComboBox juga membutuhkan ikon, tetapi harus interaktif. Untuk mencegahnya merusak fokus, tambahkan handler onMouseDown ke outlet:


 onMouseDown(event: MouseEvent, input: HTMLInputElement) { event.preventDefault(); input.focus(); } 

Melewati templat sebagai konten akan memungkinkan kami menaikkannya lebih tinggi melalui CSS hanya dengan membuat posisi: ikon relatif . Kemudian Anda dapat berlangganan klik di ComboBox itu sendiri:


 <app-input [content]="icon"></app-input> <ng-template #icon> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="icon" [class.icon_opened]="opened" (click)="onClick()" > <polyline points="7,10 12,15 17,10" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" /> </svg> </ng-template> 

Berkat organisasi seperti itu, kami mendapatkan perilaku yang diinginkan:



Kode komponen, seperti dalam tab, memberikan pengetahuan tentang model data. Itu terlihat seperti ini:


 export class ComboBoxComponent<T> { @Input() items: ReadonlyArray<T> = []; @Input() content: Content = ({$implicit}) => String($implicit); @Input() stringify = (item: T) => String(item); @Input() value: T | null = null; @Output() valueChange = new EventEmitter<T | null>(); stringValue = ''; //          get filteredItems(): ReadonlyArray<T> { return this.items.filter(item => this.stringify(item).includes(this.stringValue), ); } } 

Kode sederhana semacam itu memungkinkan Anda untuk menggunakan objek apa pun di ComboBox dan menyesuaikan tampilan mereka dengan sangat fleksibel. Setelah beberapa perbaikan yang tidak terkait dengan konsep yang dijelaskan, siap digunakan. Penampilan dapat disesuaikan untuk setiap selera:



Kesimpulan


Penciptaan komponen agnostik menghilangkan kebutuhan untuk memperhitungkan setiap kasus tertentu. Pada saat yang sama, pengguna mendapatkan alat sederhana untuk mengonfigurasi komponen untuk situasi tertentu. Solusi ini mudah digunakan kembali. Independensi dari model data menjadikan kode tersebut universal, andal, dan dapat dikembangkan. Pada saat yang sama, kami menulis tidak begitu banyak baris dan terutama menggunakan alat Angular bawaan.


Dengan menggunakan pendekatan yang dijelaskan, Anda akan segera menyadari betapa nyamannya berpikir dalam hal konten daripada garis atau pola tertentu. Menampilkan pesan kesalahan validasi, tooltips, modal windows - pendekatan ini bagus tidak hanya untuk menyesuaikan tampilan, tetapi juga untuk mentransfer konten secara keseluruhan. Membuat sketsa layout dan menguji logika itu mudah! Misalnya, untuk menampilkan munculan, pengguna tidak perlu membuat komponen atau bahkan templat, Anda bisa meneruskan string rintisan dan kembali lagi nanti.


Kami di Tinkoff.ru telah lama berhasil menerapkan pendekatan yang dijelaskan dan memindahkannya ke pustaka sumber terbuka kecil (1 KB gzip) yang disebut ng-polymorpheus .


Kode sumber


paket npm


Demo dan kotak pasir interaktif


Apakah Anda juga memiliki sesuatu yang ingin Anda masukkan ke dalam open source, tetapi apakah Anda takut dengan tugas-tugas terkait? Coba Starter Perpustakaan Open-source Angular , yang kami buat untuk proyek kami. Ini sudah memiliki CI yang dikonfigurasi, memeriksa komit, linter, pembuatan CHANGELOG, cakupan tes dan semua itu.

Source: https://habr.com/ru/post/id473108/


All Articles