В этой статье я хочу рассказать об о возможности Angular, которая впечатляет лично меня больше всего - это тег ng-template, который фактически позволяет передавать кусок вёрстки вместе со всеми обработчиками событий из одного компонента в другой. Такие пакеты, как ng-bootstrap активно пользуются этой возможностью для реализации модальных окон, а сегодня мы рассмотрим, как этим воспользоваться самим
За основу возьмём следующий простой шаблон приложения:
app.component.html
<aside class="sidebar">
</aside>
<main class="content">
<router-outlet></router-outlet>
</main>
app.component.scss
:host {
display: flex;
justify-content: space-between;
.sidebar {
width: 200px;
min-height: 100vh;
background: yellow;
}
.content {
flex-grow: 1;
}
}
Ключевой особенностью ng-template является то, что заключённый в нём код не отображается без явной на то команды. Отсюда вытекает самый простой способ его использования (который лично мне не сильно нравится) - в качестве части else в *ngIf:
<div *ngIf="a !== undefined; else undefinedA">a</div>
<ng-template #undefinedA>Undefined a</ng-template>
Код не нуждается в дополнительных объяснениях. Если условие не выполнилось, вместо контента блока с условием будет подставлен контент в именованом ng-template. Но нас интересуют более серьёзные примеры
Код, заключённый в ng-template можно использовать как шаблон для ng-container. Когда это делается внутри одного файла, достаточно указать его идентификатор в атрибуте *ngTemplateOutlet (или [ngTemplateOutlet]):
<ng-container *ngTemplateOutlet="undefinedA"></ng-container>
Что очень важно, при этом могут быть переданы некоторые параметры внутрь ngTemplateOutlet
<ng-container *ngTemplateOutlet="helloBlock; context: {name: 'Петя'}"></ng-container>
<ng-template #helloBlock let-name="name">Привет, {{name}}</ng-template>
Информацию про это можно найти в документации по Angular
И наконец мы закончили со вступлением и подошли к основной цели статьи. Представьте себе, что вам необходимо в приведённый выше шаблон app.component.html добавить в сайтбаре блок, который мог бы меняться из любого компонента системы. Я решал такую задачу, когда мне надо было сделать своеобразный выплывающий справа попап в соответствии с нарисованным дизайном, но для статьи я упростил оформление, чтоб передать только суть
Вот тут вступает в игру то, что каждый ng-template на самом деле представляет собой объект TemplateRef. Мы можем его вытащить из шаблона в typescript часть компонента через декоратор @ViewChild():
import {Component, TemplateRef, ViewChild} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
@ViewChild("undefinedA") undefinedBlockA: TemplateRef<any>;
title = 'ng-template';
}
И как с любой другой переменной, нам никто не мешает передавать/получать такую переменную через службу. Служба нужна, поскольку нам нужно обеспечить общение компонентов независимо от их родственных отношений. И делается это через события, определённые внутри службы. Вот пример кода такой службы:
sidebar.service.ts
import {Injectable, TemplateRef} from '@angular/core';
import {Subject} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class SidebarService {
showSidebarBlock = new Subject<TemplateRef<any>>();
constructor() { }
setSidebarBlock(block: TemplateRef<any>) {
this.showSidebarBlock.next(block);
}
}
Заинжектим эту службу в наш компонент приложения, и поставим элементарный обработчик события:
app.component.ts
import {Component, TemplateRef, ViewChild} from '@angular/core';
import {SidebarService} from './sidebar.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
@ViewChild("undefinedA") undefinedBlockA: TemplateRef<any>;
a;
sidebarBlock: TemplateRef<any>;
constructor(private sidebarService: SidebarService) {
sidebarService.showSidebarBlock.subscribe(
(block: TemplateRef<any>) => {
this.sidebarBlock = block;
}
)
}
}
В шаблоне компонента app.component.html чуть-чуть изменим код сайтбара:
<aside class="sidebar">
<ng-container [ngTemplateOutlet]="sidebarBlock" *ngIf="sidebarBlock"></ng-container>
</aside>
Всё, мы готовы принимать меняющиеся блоки в сайтбаре. Для проверки создадим новый компонент и повесим его на роут /test
test.component.ts
import { Component, OnInit } from '@angular/core';
import {SidebarService} from '../sidebar.service';
@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
constructor(public sidebarService: SidebarService) { }
ngOnInit(): void {
}
}
test.component.html
<ng-template #blockA>
<div class="red-bock"></div>
</ng-template>
<ng-template #blockB>
<div class="blue-bock"></div>
</ng-template>
<button (click)="sidebarService.setSidebarBlock(blockA)">Block A</button>
<button (click)="sidebarService.setSidebarBlock(blockB)">Block B</button>
<button (click)="sidebarService.setSidebarBlock(null)">Clear block</button>
test.component.scss
.red-bock {
width: 100px;
height: 100px;
background: red;
}
.blue-bock {
width: 100px;
height: 100px;
background: blue;
}
Как видите, контент переносится со всеми стилями. И самое главное, если на любой из блоков повесить обработчик внутри компонента TestComponent, то он будет работать даже после переноса контента в sidebar