Проекция контента с помощью ng-content
Проекция контента — это способ импортировать HTML контент извне компонента и вставить его в шаблон компонента в определенное место. (вольный перевод документации)
Определение довольно сложное, но на деле все гораздо проще. У нас есть какой-то компонент, и все, что находится между его открывающим и закрывающим тегом, является контентом.
<app-parent> <!-- content --> I'm content! <!-- content --> </app-parent>
И Angular позволяет вставлять в шаблон этого компонента любой HTML код (контент) с помощью элемента ng-content.
Давайте попробуем разобраться, зачем это нужно и как это работает на примере. Допустим, у нас есть простой компонент кнопки. Текст этой кнопки мы передаем в шаблон через input property.
// button.component.ts import {Component, Input} from '@angular/core'; @Component({ selector: 'app-button', template: '<button>{{text}}</button>' }) export class ButtonComponent { @Input() text: string; } // app.component.ts import {Component} from '@angular/core'; @Component({ selector: 'app-root', template: `<app-button [text]="'Button'"></app-button>`, }) export class AppComponent { }
Вроде выглядит неплохо. Но вдруг нам понадобилось для некоторых кнопок добавить к тексту иконку. У нас уже есть компонент иконки. Нужно просто добавить его в шаблон кнопки, навесить директиву ngIf и написать еще одно input property для динамического отображение иконки.
// icon.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-icon', template: '☻', }) export class IconComponent { } // button.component.ts import {Component, Input} from '@angular/core'; @Component({ selector: 'app-button', template: `<button> <app-icon *ngIf="showIcon"></app-icon> {{text}} </button>`, }) export class ButtonComponent { @Input() text: string; @Input() showIcon = true; }
Все работает. Но что будет, если нужно поменять расположение иконки относительно текста? Или добавить еще какой-нибудь новый элемент? Придется править существующий код, добавлять новые свойства и т.д.
Всего этого можно избежать с помощью ng-content. Его можно рассматривать как placeholder для контента. Он отображает все, что вы положите между открывающим и закрывающим тегами компонента.
// button.component.ts import {Component} from '@angular/core'; @Component({ selector: 'app-button', template: `<button> <ng-content></ng-content> </button>`, }) export class ButtonComponent { } // app.component.ts import {Component} from '@angular/core'; @Component({ selector: 'app-root', template: `<app-button> <app-icon></app-icon> Button </app-button>`, }) export class AppComponent { }
Теперь, если нам понадобилась кнопка с иконкой, мы просто помещаем компонент иконки между тегами кнопки. Можно добавить что угодно и как угодно. Это ли не рай? Наш компонент кнопки стал гибким и красивым.
Какую роль играет атрибут select для ng-content?
Иногда нам нужно расположить какой-то контент в определенном месте относительно всего остального контента, в этом случае можно использовать атрибут select, который принимает в себя селектор (.some-class, some-tag, [some-attr]).
// button.component.ts import {Component} from '@angular/core'; @Component({ selector: 'app-button', template: `<button> <ng-content></ng-content> <div> <ng-content select="app-icon"></ng-content> </div> </button>`, }) export class ButtonComponent { }
Сейчас иконка показывается всегда снизу независимо от остального контента. Perfecto!
Что такое ngProjectAs?
Атрибут select у ng-content отлично справляется с тегами, которые находятся на первом уровне вложенности родительского компонента. Но что будет, если мы увеличим уровень вложенности для компонента иконки, обернув его в какой-либо тег?
// app.component.ts import {Component} from '@angular/core'; @Component({ selector: 'app-root', template: `<app-button> <ng-container> <app-icon></app-icon> </ng-container> Button </app-button>` }) export class AppComponent {}
Мы увидим, что select не работает, будто его вовсе не существует. Это происходит, потому что <ng-content select="..."> ищет только на первом уровне вложенности контента родителя. Для решения этой проблемы существует атрибут ngProjectAs. Он принимает в себя селектор и «маскирует» весь DOM узел под него.
// app.component.ts import {Component} from '@angular/core'; @Component({ selector: 'app-root', template: `<app-button> <ng-container ngProjectAs="app-icon"> <app-icon></app-icon> </ng-container> Button </app-button>` }) export class AppComponent {}
Случай *ngIf + ng-content
Разберем еще один интересный случай. Предположим, нам нужно по клику на кнопку скрывать/показывать иконку. Добавляем к классу компонента кнопки булевое свойство, отвечающее за отображение иконки, меняем его по клику на кнопку и вешаем ngIf.
// button.component.ts import {Component, Input} from '@angular/core'; @Component({ selector: 'app-button', template: `<button (click)="toggleIcon()"> <ng-content></ng-content> <div *ngIf="showIcon"> <ng-content select="app-icon"></ng-content> </div> </button>`, }) export class ButtonComponent { showIcon = true; toggleIcon() { this.showIcon = !this.showIcon; } }
Иконка скрывается/появляется по клику. Отлично! Но давайте добавим немного логов на хуки OnInit и OnDestroy для компонента иконки. Общеизвестный факт, что директива ngIf при смене условия полностью удаляет/создает элемент, при этом OnDestroy/OnInit должны срабатывать каждый раз соответствующим образом.
// icon.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; @Component({ selector: 'app-icon', template: '☻', }) export class IconComponent implements OnInit, OnDestroy { ngOnInit() { console.log('app-icon init'); } ngOnDestroy() { console.log('app-icon destroy') } }
Пару раз кликнем на кнопку, убедимся что иконка исчезает, а потом появляется. Дальше заходим в консоль разработчика в надежде увидеть наши заветные логи, однако… их нет!
Есть только один лог на создание компонента. Выходит, наш компонент иконки никогда не удаляется, а просто скрывается. Почему же так происходит?
ng-content не создает новый контент, он просто проецирует уже существующий. Поэтому за создание и удаление отвечает компонент, в котором объявлен контент. Для меня это был совсем неочевидный момент. Поправим наше решение, чтобы оно работало, как ожидалось изначально.
// button.component.ts import {Component} from '@angular/core'; @Component({ selector: 'app-button', template: `<button> <ng-content></ng-content> <ng-content select="app-icon"></ng-content> </button>`, }) export class ButtonComponent { } // app.component.ts import { Component } from "@angular/core"; @Component({ selector: 'app-root', template: `<app-button (click)="toggleIcon()"> <div *ngIf="showIcon" ngProjectAs="app-icon"> <app-icon></app-icon> </div> Button </app-button>`, }) export class AppComponent { showIcon = true; toggleIcon() { this.showIcon = !this.showIcon; } }
Открыв логи, мы можем увидеть, что компонент иконки создается и удаляется как положено.
Вместо заключение
Надеюсь, эта статья немного помогла разобраться с проекцией контента в Angular.
Мне категорично непонятно, почему в официальной документации обошли эту тему стороной. В репозитории Angular даже висит issue на это с 2017 года. Видимо, у Angular команды есть более важные дела.
ссылка на оригинал статьи https://habr.com/ru/post/491136/

Добавить комментарий