Как стать автором
Обновить

Angular компоненты со встраиваемым контентом

Время на прочтение 4 мин
Количество просмотров 9.9K
Когда создаешь новые компоненты при разработке на Angular, стремишься их создать таким образом, чтобы их можно было переиспользовать много раз. Точно так же, как и при разработке ПО, программист стремится сделать свой код максимально переиспользуемым. При этом хочется иметь компоненты гибкие, но не слишком сложные.



Как правило, компоненты необходимо делать максимально «тупыми», чтобы их было проще поддерживать и тестировать. Но при этом настраиваемыми и гибкими, чтобы можно было их многократно переиспользовать, а не заниматься клонированием компонентов, которые похожи визуально и функционально, но всё-таки чем-то отличаются. Или не добавлять в компоненты кучу параметров, на основе которых включать или отключать какое-либо поведение компонента, но которые по факту будут использоваться в 1 из 100 случаев.

Создавать подводную лодку, которая должна погружаться на самое дно синего моря без риска для жизни экипажа, а потом доработать ее до полетов по голубому небу – задача не из легких.

Angular-компонент – это минимальная единица пользовательского интерфейса, которая должна выполнять одну простую задачу и ничего более. Создавать компоненты на все случаи жизни – это плохая практика. Поэтому приходится искать компромисс между гибкостью и простотой.

Предположим, необходимо создать компонент, состоящий из некоторой обертки, внутри которой есть заголовок и какой-то переключатель, а под ними какой-то контент. Что-то вроде этого:



Зеленым цветом обведен весь компонент, который будет называться виджетом. Переключатель с заголовком выделен красным, а контент – синим. Angular – невероятно мощный фреймворк, создать компонент, похожий на тот, который на картинке, — весьма тривиальная задача для Angular.

А что, если данный компонент сделать более гибким, например, чтобы контент менялся в зависимости от параметров компонента виджета?

Разрулить внешнее представление контента на основе параметра виджета можно, например, при помощи структурной директивы ngIf. Можно будет переключаться между разными представлением. А если представлений будет слишком много, можно воспользоваться еще одной структурной директивой ngSwitch.

А если разные представления будут по-разному реагировать на действия пользователя? Тогда и код компонента будет иметь кучу условных операторов. Но код, в котором много условных операторов, со временем становится тяжело поддерживать, устранять баги или расширять функциональность…

Можно создать компонент виджета таким образом, чтобы он получал любой контент, но при этом пробрасывал выбранные значения панели фильтров в компоненты контентной области. А также хотелось бы иметь возможность пробрасывать параметры из родительского компонента виджета в контент, при этом не затрагивая сам компонент виджета. Для решение этой задачи можно воспользоваться замечательной директивой ngTemplateOutlet, которая позволит «разрулить» визуальное представление компонента. За дополнительной информацией можно обратиться к документации, хотя на мой взгляд там не показана вся ее мощь.

В этом случае компонент виджета можно представить следующим образом:

// widget.component.ts
…
private widgetContentTemplate: TemplateRef<any>;

// Получение шаблона контентной области виджета
@ContentChild(WidgetContentTemplateDirective, { read: TemplateRef })
set WidgetContentTemplate(value: TemplateRef<any>) {
	this.widgetContentTemplate = value;
}

get WidgetContentTemplate(): TemplateRef<any> {
	return this.widgetContentTemplate;
}
…

<!-- widget.component.html -->
…
 <div class="widget__content">
	<!-- Сюда будет вставлен шаблон с контентом, а параметры
	передадутся через объект context. Дополнительно обрабатывается случай,
	если шаблон контента не задан-->
	<ng-container *ngTemplateOutlet="WidgetContentTemplate || DefaultWidgetContentTemplate;
		context: {
			post: SelectedPost
		}">
	</ng-container>
</div>

<ng-template #DefaultWidgetContentTemplate>
  <div>[ Контент не задан ]</div>
</ng-template>
…

Директива ngTemplateOutlet принимает объект TemplateRef, который считывается в компоненте при помощи @ContentChild. Для этого потребуется создать простую директиву. Вот ее код:

// widget-content-template.directive.ts
import { Directive, TemplateRef } from '@angular/core';

@Directive({
    selector: '[appWidgetContentTemplate]'
})
export class WidgetContentTemplateDirective {
    get Template(): TemplateRef<any> {
        return this.template;
    }

    constructor(private readonly template: TemplateRef<any>) {}
}

Параметры из переключателя виджета можно передать в контент при помощи объекта context. Чтобы параметры из context были корректно считаны компонентом, необходимо в родительском компоненте, в нашем случае это AppComponent, не забыть описать их следующим образом:

<!-- app.component.html -->
<app-widget-component Title="Просмотр поста (с дополнительной информацией)">
    <!-- let-post="post" добавляется, чтобы компонент виджета смог передать параметр в контент -->
    <ng-template appWidgetContentTemplate let-post="post">
        <app-post-view [Post]="post"></app-post-view>
    </ng-template>
</app-widget-component>
<br>
<br>
<app-widget-component Title="Просмотр поста (без дополнительной информацией)">
    <!-- Значение let-post="post" будет также передано из виджета, а ShowPostInfo из AppComponent -->
    <ng-template appWidgetContentTemplate let-post="post">
        <app-post-view [Post]="post" [ShowPostInfo]="ShowPostInfo"></app-post-view>
    </ng-template>
</app-widget-component>

Таким образом, получился компонент виджета, который принимает и отображает любой контент, при этом сам компонент виджета ничего не обязан знать о контенте. Он просто пробрасывает параметры переключателя. Считывать их или нет – дело контента.



Полную версию кода можно посмотреть здесь.
Теги:
Хабы:
+6
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн