[Из песочницы] Создаем свой компонент с микро-шаблонами
Всем привет. Все кто так или иначе писал на фреймворке Angular сталкивался или работал с библиотекой Angular Material. Это очень хорошо написанная библиотека компонентов способная к гибкой стилизации, которая реализована через возможность создания различных тем вашего приложения, с большим набором компонентов на все случаи жизни.
В моей повседневной работе ни один проект без нее не обходиться.
Но кроме всех плюсов гибкости этой библиотеки, из нее так же можно подчерпнуть опыт создателей по написанию своих собственных компонентов, а это для меня лучший мануал по best-practice разработке на Angular.
В этой статье я хочу с вами поделиться тем, как можно реализовать подход со сложным шаблоном который реализован в модуле MatTableModule.
В качестве примера, я хочу показать как сделать список карточек с возможностью добавить пагинацию и фильтры, а за основу мы возьмем модель шаблона MatTable компонента.
Шаблон (источник):
No.
{{element.position}}
Name
{{element.name}}
Weight
{{element.weight}}
Symbol
{{element.symbol}}
После изучения шаблона, становиться ясно что мы указываем в тегах ng-container разметку для конкретной колонки таблицы, но как оно работает внутри? Именно этим вопросом я задался когда увидел эту конструкцию, отчасти именно из-за того что с динамическими компонентами не работал. И так, приступим (исходный код).
Структура
Набор сущностей которые нам необходимо создать. В этой блок-схеме наглядно показано их взаимодействие.
Шаг первый
Нам необходим сервис для регистрации наших микро-шаблонов.
@Injectable()
export class RegisterPropertyDef {
// для хранения шаблонов мы будем использовать обычный Map
// в качестве ключа - инстанс компонента, он будет всегда уникальный
// на случай если сервис будет лежать в глобальном модуле
// и вы будите использовать один компонент множество раз
private store = new Map>>();
setTemplateById(cmp: ComponentInstance, id: string, template: TemplateRef): void {
const state = this.store.get(cmp) || new Map();
state.set(id, template);
this.store.set(cmp, state);
}
getTemplate(cmp: ComponentInstance, id: string): TemplateRef {
return this.store.get(cmp).get(id);
}
}
Шаг второй
Создаем директиву для регистрации шаблонов:
@Directive({
selector: '[providePropertyDefValue]'
})
export class ProvidePropertyDefValueDirective implements OnInit {
@Input() providePropertyDefValueId: string;
constructor(
private container: ViewContainerRef,
private template: TemplateRef, // шаблон в котором определена наша разметка
private registerPropertyDefService: RegisterPropertyDefService, // сервис созданый выше
@Optional() private parent: Alias // тут у нас храниться ссылка на компонент в котором используются наши карточки
) {}
ngOnInit(): void {
this.container.clear(); // этот пункт не обязателен, объясню по ходу
this.registerPropertyDefService.setTemplateById(
this.parent as ComponentInstance,
this.providePropertyDefValueId,
this.template
);
}
}
Шаг третий
Создаем компонент:
@Component({
selector: 'lib-card-list',
template: `
-
{{ findColumnByKey(key)?.label }}
`,
styles: [
'mat-card { margin: 10px; }'
]
})
export class CardListComponent implements OnInit, AfterViewInit {
@Input() defaultColumns: DefaultColumn[];
@Input() source$: Observable;
displayedColumns = [];
sources: T[] = [];
constructor(private readonly registerPropertyDefService: RegisterPropertyDefService,
private readonly parent: Alias) { }
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 шаблонами.
В разметке вы могли обратить внимание на следующие строки —
тут используется фича по передаче scope (как в AngularJS) в разметку. Что позволяет комфортно в наших микро шаблонах объявлять переменную через конструкцию let-my-var в которой будут лежать данные.
Использование
// app.component.html
{{ element.id }}
{{ element.title }}
Инициализация нашего свежего компонента, и передача ему параметров.
Определение шаблонов через ng-container и нашу директиву libProvidePropertyDefValue.
Самое важное здесь это
«let element; id: 'id'»
где element это scope шаблона который равен объекту с данными из списка,
id это идентификатор микро-шаблона.
Теперь хочется вернутся к директиве providePropertyDefValue, к методу ngOnInit
ngOnInit(): void {
this.container.clear();
...
}
Вы можете разместить микро-шаблоны так как показано в примере, и в директиве их «чистить», или полностью перенести их определение внутрь компонента lib-card-list, следовательно разметка будет выглядеть вот так:
{{ element.id }}
{{ element.title }}
Объективно — второй вариант использования производительней.
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [{ provide: Alias, useExisting: forwardRef(() => AppComponent) }]
})
export class AppComponent extends Alias {
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
// card-list.component.ts
@ViewChild(MatPaginator) paginator: MatPaginator;
this.paginator.initialized.subscribe(() => {
// обновление данных для рендеринга
});
this.paginator.page.subscribe((pageEvent: PageEvent) => {
// реализация обновления данных при переключении страницы
})
Фильтрацию можно реализовать через mat-form-field и аналогично с переключением страниц при пагинации, обновлять данные.
На этом все. Очень рекомендую периодически заглядывать в исходный код библиотеки angular/material, на мой взгляд это хорошая возможность подтянуть свои знания в создании гибких и производительных компонентов. Спасибо за внимание.