Всем привет.
Все кто писал на фреймворке Angular, так или иначе сталкивался (или даже работал) с библиотекой Angular Material. Это очень хорошо написанная библиотека компонентов способная к гибкой стилизации, которая реализована через возможность создания различных тем вашего приложения, с большим набором компонентов на все случаи жизни.
В моей повседневной работе ни один проект без нее не обходится.
Но кроме всех плюсов гибкости этой библиотеки, из нее так же можно подчерпнуть опыт создателей по написанию своих собственных компонентов, а это для меня лучший мануал по best-practice разработке на Angular.
В этой статье я хочу с вами поделиться тем, как можно реализовать подход со сложным шаблоном который реализован в модуле MatTableModule.
В качестве примера, я хочу показать как сделать список карточек с возможностью добавить пагинацию и фильтры, а за основу мы возьмем модель шаблона MatTable компонента.
Шаблон (источник):
После изучения шаблона, становится ясно что мы указываем в тегах ng-container разметку для конкретной колонки таблицы, но как оно работает внутри? Именно этим вопросом я задался когда увидел эту конструкцию, отчасти именно из-за того что с динамическими компонентами не работал. И так, приступим (исходный код).
Набор сущностей которые нам необходимо создать. В этой блок-схеме наглядно показано их взаимодействие.

Нам необходим сервис для регистрации наших микро-шаблонов.
Создаем директиву для регистрации шаблонов:
Создаем компонент:
Немного пояснения, основная работа компонента происходит в обогащении определения структуры данных в методе ngAfterViewInit. Тут после инициализации шаблонов мы обновляем модели defaultColumns шаблонами.
В разметке вы могли обратить внимание на следующие строки —
тут используется фича по передаче scope (как в AngularJS) в разметку. Что позволяет комфортно в наших микро шаблонах объявлять переменную через конструкцию let-my-var в которой будут лежать данные.
Инициализация нашего свежего компонента, и передача ему параметров.
Определение шаблонов через ng-container и нашу директиву libProvidePropertyDefValue.
Самое важное здесь это
где element это scope шаблона который равен объекту с данными из списка,
id это идентификатор микро-шаблона.
Теперь хочется вернутся к директиве providePropertyDefValue, к методу ngOnInit
Вы можете разместить микро-шаблоны так как показано в примере, и в директиве их «чистить», или полностью перенести их определение внутрь компонента lib-card-list, следовательно разметка будет выглядеть вот так:
Объективно — второй вариант использования производительней.
Тут все достаточно элементарно, единственное что следует учесть это:
В нашем сервисе конструктор получит от инжектора экземпляр AppComponent компонента.
В данном примере мы разобрали как сделать компонент, для многократного пере использования в ваших проектах, для которого можно передавать разные шаблоны с данными, в этих шаблонах может быть определенно все что угодно.
Можно добавить пагинацию из Angular Material и фильтрацию.
Фильтрацию можно реализовать через mat-form-field и аналогично с переключением страниц при пагинации, обновлять данные.
На этом все. Очень рекомендую периодически заглядывать в исходный код библиотеки angular/material, на мой взгляд это хорошая возможность подтянуть свои знания в создании гибких и производительных компонентов. Спасибо за внимание.
Все кто писал на фреймворке Angular, так или иначе сталкивался (или даже работал) с библиотекой Angular Material. Это очень хорошо написанная библиотека компонентов способная к гибкой стилизации, которая реализована через возможность создания различных тем вашего приложения, с большим набором компонентов на все случаи жизни.
В моей повседневной работе ни один проект без нее не обходится.
Но кроме всех плюсов гибкости этой библиотеки, из нее так же можно подчерпнуть опыт создателей по написанию своих собственных компонентов, а это для меня лучший мануал по best-practice разработке на Angular.
В этой статье я хочу с вами поделиться тем, как можно реализовать подход со сложным шаблоном который реализован в модуле MatTableModule.
В качестве примера, я хочу показать как сделать список карточек с возможностью добавить пагинацию и фильтры, а за основу мы возьмем модель шаблона MatTable компонента.
Шаблон (источник):
<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>
После изучения шаблона, становится ясно что мы указываем в тегах ng-container разметку для конкретной колонки таблицы, но как оно работает внутри? Именно этим вопросом я задался когда увидел эту конструкцию, отчасти именно из-за того что с динамическими компонентами не работал. И так, приступим (исходный код).
Структура
Набор сущностей которые нам необходимо создать. В этой блок-схеме наглядно показано их взаимодействие.
Шаг первый
Нам необходим сервис для регистрации наших микро-шаблонов.
@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); } }
Шаг второй
Создаем директиву для регистрации шаблонов:
@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 ); } }
Шаг третий
Создаем компонент:
@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) }) ); } }
Немного пояснения, основная работа компонента происходит в обогащении определения структуры данных в методе ngAfterViewInit. Тут после инициализации шаблонов мы обновляем модели defaultColumns шаблонами.
В разметке вы могли обратить внимание на следующие строки —
<ng-container [ngTemplateOutlet]="findColumnByKey(key)?.template || default" [ngTemplateOutletContext]="{ $implicit: source }"></ng-container>
тут используется фича по передаче scope (как в AngularJS) в разметку. Что позволяет комфортно в наших микро шаблонах объявлять переменную через конструкцию let-my-var в которой будут лежать данные.
Использование
// 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>
Инициализация нашего свежего компонента, и передача ему параметров.
Определение шаблонов через ng-container и нашу директиву libProvidePropertyDefValue.
Самое важное здесь это
«let element; id: 'id'»
где element это scope шаблона который равен объекту с данными из списка,
id это идентификатор микро-шаблона.
Теперь хочется вернутся к директиве providePropertyDefValue, к методу ngOnInit
ngOnInit(): void { this.container.clear(); ... }
Вы можете разместить микро-шаблоны так как показано в примере, и в директиве их «чистить», или полностью перенести их определение внутрь компонента lib-card-list, следовательно разметка будет выглядеть вот так:
<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>
Объективно — второй вариант использования производительней.
@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' } ]); }
Тут все достаточно элементарно, единственное что следует учесть это:
providers: [{ provide: Alias, useExisting: forwardRef(() => AppComponent) }]Данная конструкция необходима для связи шаблона и компонента который их использует.
В нашем сервисе конструктор получит от инжектора экземпляр AppComponent компонента.
Дополнительно
В данном примере мы разобрали как сделать компонент, для многократного пере использования в ваших проектах, для которого можно передавать разные шаблоны с данными, в этих шаблонах может быть определенно все что угодно.
Как улучшить?
Можно добавить пагинацию из Angular Material и фильтрацию.
// 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) => { // реализация обновления данных при переключении страницы })
Фильтрацию можно реализовать через mat-form-field и аналогично с переключением страниц при пагинации, обновлять данные.
На этом все. Очень рекомендую периодически заглядывать в исходный код библиотеки angular/material, на мой взгляд это хорошая возможность подтянуть свои знания в создании гибких и производительных компонентов. Спасибо за внимание.
