Блог Михаила Крамера. PHP и JS
Ng-template и передача вёрстки между компонентами

В этой статье я хочу рассказать об о возможности 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

Комментарии