sudonull.com Open in urlscan Pro
185.65.244.20  Public Scan

URL: https://sudonull.com/post/27319-Create-your-component-with-micro-templates
Submission: On March 16 via manual from ES — Scanned from ES

Form analysis 0 forms found in the DOM

Text Content

 * Home
 * Random Post


CREATE YOUR COMPONENT WITH MICRO-TEMPLATES

 * From the sandbox
 * Tutorial

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 component
library capable of flexible styling, which is implemented through the ability to
create various themes for your application, with a large set of components for
all occasions.

In my daily work, not a single project can do without it.

But besides all the pluses of this library’s flexibility, it’s also possible to
draw on the experience of the creators in 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 the approach with
a complex template that is implemented in the moduleMatTableModule .

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

Template ( source ):



<tablemat-table [dataSource]="dataSource"class="mat-elevation-z8"><ng-containermatColumnDef="position"><thmat-header-cell *matHeaderCellDef> No. </th><tdmat-cell *matCellDef="let element"> {{element.position}} </td></ng-container><ng-containermatColumnDef="name"><thmat-header-cell *matHeaderCellDef> Name </th><tdmat-cell *matCellDef="let element"> {{element.name}} </td></ng-container><ng-containermatColumnDef="weight"><thmat-header-cell *matHeaderCellDef> Weight </th><tdmat-cell *matCellDef="let element"> {{element.weight}} </td></ng-container><ng-containermatColumnDef="symbol"><thmat-header-cell *matHeaderCellDef> Symbol </th><tdmat-cell *matCellDef="let element"> {{element.symbol}} </td></ng-container><trmat-header-row *matHeaderRowDef="displayedColumns"></tr><trmat-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 column of the table, but how does it
work inside? It was this question that I asked when I saw this design, partly
due to the fact that I did not work with dynamic components. So, let's get
started (source code) .




STRUCTURE


A set of entities that we need to create. This block diagram illustrates their
interaction.






STEP ONE


We need a service to register our micro-templates.



@Injectable()
exportclassRegisterPropertyDef<T> {
  // для хранения шаблонов мы будем использовать обычный Map // в качестве ключа - инстанс компонента, он будет всегда уникальный// на случай если сервис будет лежать в глобальном модуле// и вы будите использовать один компонент множество раз
  private store = newMap<ComponentInstance, Map<string, TemplateRef<T>>>();
  setTemplateById(cmp: ComponentInstance, id: string, template: TemplateRef<any>): void {
    const state = this.store.get(cmp) || newMap();
    state.set(id, template);
    this.store.set(cmp, state);
  }
  getTemplate(cmp: ComponentInstance, id: string): TemplateRef<T> {
    returnthis.store.get(cmp).get(id);
  }
}




SECOND STEP


Create a directive for registering templates:



@Directive({
  selector: '[providePropertyDefValue]'
})
exportclassProvidePropertyDefValueDirective<T> implementsOnInit{
  @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
    );
  }
}




THIRD STEP


Create the 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; }'
  ]
})
exportclassCardListComponent<T> implementsOnInit, 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 {
    returnthis.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 is to enrich the definition
of the data structure in the ngAfterViewInit method . Here, after initializing
the templates, we update the defaultColumns models with templates.

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



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


here a feature is used to pass scope (as in AngularJS) to the markup. That
allows you to comfortably declare a variable in our micro templates through the
let-my-var construct in which the data will lie.




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.

Template definition via ng-container and our libProvidePropertyDefValue
directive .

The most important thing here is

> "Let element; id: 'id' »


where element is the scope of the template which is equal to the object with the
data from the list,
id is the identifier of the micro-template.

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



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



You can place micro-templates as shown in the example, and “clean” them in the
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) }]
})
exportclassAppComponentextendsAlias<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'
    }
  ]);
}



Everything is quite elementary here, the only thing to consider is:


> providers: [{provide: Alias, useExisting: forwardRef (() => AppComponent)}]

This design is necessary for linking 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 looked at how to make a component, for repeated use in your
projects, for which you can transfer different templates with data, these
templates can definitely be anything.




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 mat-form-field and, similarly to switching
pages during pagination, update 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 enhance
your knowledge in creating flexible and productive components. Thanks for
attention.
Tags:
 * angular
 * angular-material
 * guide
 * tutorial
 * dynamic-components

Add tags


--------------------------------------------------------------------------------

ALSO POPULAR NOW:

 * The main thing is people
 * Game frameworks: JavaScript trends in 2019
 * Watching user
 * Smartphone Zuk Z1. ZUK / JD Blog
 * Read or buy?
 * Programming LibreOffice Base. Part 1
 * Sourcery to automatically convert to Realm object structures
 * Urho3D: Serious Games
 * People on the moon. Sources
 * Not "Kepler" one

--------------------------------------------------------------------------------

Copyright © Sudo Null company 2019
sudonull@yahoo.com