Телепортация контента в Angular
Angular - это блестяще разработанный фреймворк, и мы (обычно) рады структурировать наше веб-приложение по его правилам.
Каждое приложение Angular похоже на дерево, поэтому родительский шаблон содержит дочерние компоненты, и каждый дочерний элемент отображает свое содержимое только в своем представлении. И это обычно нормально.
Однако что, если бы мы могли позволить дочерним компонентам выходить за пределы своих собственных представлений, чтобы они могли отображать некоторые из своих виджетов на родительской панели инструментов или фрагменты информации в глобальной строке состояния без каких-либо ограничений привязки данных?
Это то, что я называю «телепортацией» контента.
Наш вариант использования
Чтобы продемонстрировать эту идею, давайте представим следующий вариант использования:
...
...
Компонент приложения на корневом уровне реализует простую панель навигации с общими ссылками с одной стороны и панелью инструментов с другой стороны.
Панель инструментов не реализована самим компонентом приложения, так как мы хотим, чтобы каждая страница имела свою собственную панель инструментов. Вместо этого он использует компонент, в котором будет реализовано содержимое панели инструментов wm-portal
.
Тогда каждая страница реализует свою собственную панель инструментов для телепортации:
...
Page 1 - The Counter
Here the counter you can modify from the toolbar
{{ count }}
...
В этом примере наша страница имеет счетчик, который можно увеличивать, уменьшать и очищать с помощью панели инструментов.
Содержимое панели инструментов определяется с помощью ng-template
и директивы wmTeleport
, предназначенной для портала «панель инструментов», поэтому при каждой активации этой страницы содержимое панели инструментов будет телепортироваться обратно в родительское приложение.
Тем не менее, шаблон панели инструментов по-прежнему останется частью этой страницы для ввода данных и событий, которые будут связаны, и это делает эту технику действительно мощной.
TeleportService
Передача контента возможна благодаря TeleportService, соединяющему директивы с порталами:
export interface TeleportInstance {
[target: string]: TemplateRef;
}
@Injectable()
export class TeleportService extends BehaviorSubject {
constructor() { super(null); }
public activate(instance: TeleportInstance) {
this.next(instance);
}
public clearAll() {
this.next(null);
}
}
Сервис реализован в виде инъецируемой потоковой передачи TeleportInstance(ов) BehaviorSubject, где TemplateRef для передачи связан с ключом, указывающим, на какой портал мы хотим передать шаблон.
Метод activate()
толкает новый экземпляр в то время как метод clearAll()
выталкивает нуль для всех порталов, чтобы очистить их все.
TeleportComponent
Каждый портал имеет уникальное имя для адресации:
@Component({
selector: 'wm-portal',
template: ' '
})
export class TeleportComponent {
readonly template$: Observable>;
@Input() context: any;
constructor(@Attribute('name') name: string, private teleport: TeleportService) {
// Builds the template observable
this.template$ = teleport.pipe(
// Filters only those instances targetting this very portal
filter( instance => !instance || (name in instance) ),
// Returns the template or null
map( instance => instance && instance[name] )
);
}
}
Здесь мы используем декоратор @Attribute()
, чтобы получить имя портала, предполагая, что он останется статическим после определения.
Компонент создает наблюдаемый объект из TeleportService, отфильтровывая экземпляры, предназначенные для этого самого портала, и сопоставляя экземпляр с содержащимся в нем TemplateRef.
Затем поступающий шаблон обрабатывается с использованием структурной директивы ngTemplateOutlet, заключенной в область ng-container
, где наблюдаемое template$
разрешается с использованием pipe async
.
Как следует из названия context
, входные данные принимают объект, который будет предоставлен в качестве контекста шаблона, поэтому портал может дополнительно предоставить шаблону переменные, специфичные для получателя.
TeleportDirective
Последний элемент нашего механизма телепортации - это директива, собирающая контент для передачи:
@Directive({
selector: 'ng-template[wmTeleport]'
})
export class TeleportDirective implements OnChanges, OnDestroy {
constructor(private teleport: TeleportService, private template: TemplateRef) {}
@Input('wmTeleport') target: string;
ngOnChanges(changes: SimpleChanges) {
const target = changes.target;
if(!target) { return; }
// Clears the previous target, if any
target.previousValue && this.teleport.activate({ [target.previousValue]: null });
// Teleports the template to the new target portal
target.currentValue && this.teleport.activate({ [target.currentValue]: this.template });
}
ngOnDestroy() {
// Clears the portal on destroy whenever the target is defined
this.target && this.teleport.activate({ [this.target]: null });
}
}
Директива предназначена для работы только с псевдоэлементами ng-template
.
Имя портала для назначения происходит от его первичного ввода, поэтому, потенциально может изменяться во время выполнения. Вот почему мы используем ловушку жизненного цикла ngOnChanges()
для отслеживания изменений ввода, очищая предыдущую цель (если есть) и отправляя шаблон на новый целевой портал для рендеринга.
И последнее, но не менее важное: мы очищаем целевой портал в ловушке жизненного цикла ngOnDestroy()
, поэтому телепортируемый контент исчезает вместе с его исходным контейнером.
Выводы
И вот как мы создали шаблон, позволяющий вам определять контент, который будет отображаться где-то еще. Это действительно похоже на телепортацию как мне кажется.
У этого подхода много преимуществ:
- Виджеты определяются внутри тех самых компонентов, с которыми они должны взаимодействовать.
- Рендеринг может происходить везде в приложении, независимо от иерархии.
- Angular позаботится обо всем для нас, от обнаружения изменений до обновления представлений, несмотря на то, что мы как бы обманываем его модель компонентов / представлений.
Попробуй сам
Код доступен в полнофункциональном живом демо на StackBlitz, с которым вы можете поиграть: