📜 ⬆️ ⬇️

Create your component with micro templates

Hello.
Everyone who wrote on the Angular framework, somehow came across (or even worked) with the Angular Material library. This is a very well-written library of components capable of flexible styling, which is realized through the ability to create various themes for your application, with a large set of components for all occasions.

In my daily work, no project is complete without it.

But apart from all the advantages of the flexibility of this library, one can also learn from the experience of the creators of writing their own components, and this is for me the best manual on best-practice development on Angular .
')
In this article I want to share with you how you can implement an approach with a complex pattern that is implemented in the MatTableModule module.

As an example, I want to show how to make a list of cards with the ability to add pagination and filters , and we take as a basis the model pattern of the MatTable component.

Template ( source ):

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> <ng-container matColumnDef="position"> <th mat-header-cell *matHeaderCellDef> No. </th> <td mat-cell *matCellDef="let element"> {{element.position}} </td> </ng-container> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef> Name </th> <td mat-cell *matCellDef="let element"> {{element.name}} </td> </ng-container> <ng-container matColumnDef="weight"> <th mat-header-cell *matHeaderCellDef> Weight </th> <td mat-cell *matCellDef="let element"> {{element.weight}} </td> </ng-container> <ng-container matColumnDef="symbol"> <th mat-header-cell *matHeaderCellDef> Symbol </th> <td mat-cell *matCellDef="let element"> {{element.symbol}} </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table> 

After studying the template, it becomes clear that we indicate in the ng-container tags the markup for a specific table column, but how does it work inside? It was this question that I wondered when I saw this design, in part because it did not work with dynamic components. And so, let's get started (source code) .

Structure


The set of entities we need to create. In this flowchart, their interaction is illustrated.

image

Step one


We need a service for registering our micro templates.

 @Injectable() export class RegisterPropertyDef<T> { //        Map //    -  ,     //          //         private store = new Map<ComponentInstance, Map<string, TemplateRef<T>>>(); setTemplateById(cmp: ComponentInstance, id: string, template: TemplateRef<any>): void { const state = this.store.get(cmp) || new Map(); state.set(id, template); this.store.set(cmp, state); } getTemplate(cmp: ComponentInstance, id: string): TemplateRef<T> { return this.store.get(cmp).get(id); } } 

Step Two


Create a directive for registering templates:

 @Directive({ selector: '[providePropertyDefValue]' }) export class ProvidePropertyDefValueDirective<T> implements OnInit { @Input() providePropertyDefValueId: string; constructor( private container: ViewContainerRef, private template: TemplateRef<any>, //       private registerPropertyDefService: RegisterPropertyDefService<any>, //    @Optional() private parent: Alias<T[]> //             ) {} ngOnInit(): void { this.container.clear(); //    ,    this.registerPropertyDefService.setTemplateById( this.parent as ComponentInstance, this.providePropertyDefValueId, this.template ); } } 

Step Three


Create a component:

 @Component({ selector: 'lib-card-list', template: ` <mat-card *ngFor="let source of sources"> <ul> <li *ngFor="let key of displayedColumns"> <span>{{ findColumnByKey(key)?.label }}</span> <span> <ng-container [ngTemplateOutlet]="findColumnByKey(key)?.template || default" [ngTemplateOutletContext]="{ $implicit: source }" ></ng-container> </span> </li> </ul> </mat-card> <ng-template #default></ng-template> `, styles: [ 'mat-card { margin: 10px; }' ] }) export class CardListComponent<T> implements OnInit, AfterViewInit { @Input() defaultColumns: DefaultColumn[]; @Input() source$: Observable<T[]>; displayedColumns = []; sources: T[] = []; constructor(private readonly registerPropertyDefService: RegisterPropertyDefService<T>, private readonly parent: Alias<T[]>) { } ngOnInit() { this.source$.subscribe((data: T[]) => this.sources = data); this.displayedColumns = this.defaultColumns.map(c => c.id); } findColumnByKey(key: string): DefaultColumn { return this.defaultColumns.find(column => column.id === key); } ngAfterViewInit(): void { this.defaultColumns = this.defaultColumns.map(column => Object.assign(column, { template: this.registerPropertyDefService.getTemplate(this.parent as ComponentInstance, column.id) }) ); } } 

A little explanation, the main work of the component occurs in enriching the definition of the data structure in the ngAfterViewInit method. Here, after initializing the templates, we update the model with defaultColumns templates.

In the markup, you could pay attention to the following lines -

 <ng-container [ngTemplateOutlet]="findColumnByKey(key)?.template || default" [ngTemplateOutletContext]="{ $implicit: source }"></ng-container> 

it uses a feature on the transfer of scope (as in AngularJS) in the markup. This allows us to comfortably declare a variable in our micro templates through the let-my-var construction in which the data will be located.

Using


 // app.component.html <lib-card-list [defaultColumns]="defaultColumns" [source$]="sources$"></lib-card-list> <ng-container *libProvidePropertyDefValue="let element; id: 'id'"> {{ element.id }} </ng-container> <ng-container *libProvidePropertyDefValue="let element; id: 'title'"> {{ element.title }} </ng-container> 

Initializing our fresh component and passing parameters to it.

Defining templates through ng-container and our libProvidePropertyDefValue directive.

The most important thing here is
“Let element; id: 'id' "

where element is the scope of the template that is equal to the object with the data from the list,
id is a micro template id .

Now I want to return to the directive providePropertyDefValue , to the method ngOnInit

  ngOnInit(): void { this.container.clear(); ... } 

You can place micro-templates as shown in the example, and in the “clean” directive, or completely transfer their definition inside the lib-card-list component, therefore the markup will look like this:

 <lib-card-list [defaultColumns]="defaultColumns" [source$]="sources$"> <ng-container *libProvidePropertyDefValue="let element; id: 'id'"> {{ element.id }} </ng-container> <ng-container *libProvidePropertyDefValue="let element; id: 'title'"> {{ element.title }} </ng-container> </lib-card-list> 

Objectively - the second use case is more productive.

 @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [{ provide: Alias, useExisting: forwardRef(() => AppComponent) }] }) export class AppComponent extends Alias<any> { title = 'card-list-example'; defaultColumns: DefaultColumn[] = [ { id: 'id', label: 'ID' }, { id: 'title', label: 'Title' } ]; sources$ = of([ { id: 1, title: 'Hello' }, { id: 2, title: 'World' } ]); } 

It's all quite elementary, the only thing that should be considered is:
Providers: [{provide: Alias, useExisting: forwardRef (() => AppComponent)}]
This construction is necessary to connect the template and the component that uses them.

In our service, the constructor will receive an instance of the AppComponent component from the injector.

Additionally


In this example, we have disassembled how to make a component, for repeated reuse in your projects, for which you can transfer different templates with data, anything can be definitely in these templates.

How to improve?


You can add pagination from Angular Material and filtering.

 // card-list.component.html <mat-paginator [pageSize]="5"showFirstLastButton></mat-paginator> 

 // card-list.component.ts @ViewChild(MatPaginator) paginator: MatPaginator; this.paginator.initialized.subscribe(() => { //     }); this.paginator.page.subscribe((pageEvent: PageEvent) => { //       }) 

Filtering can be implemented through the mat-form-field and similarly with page switching during pagination, you can update the data.

That's all. I highly recommend periodically looking into the source code of the angular / material library, in my opinion this is a good opportunity to pull up your knowledge in creating flexible and productive components. Thanks for attention.

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


All Articles