Projetando conteúdo na documentação angular ou perdida de conteúdo ng

Ao aprender Angular, muitas vezes eles perdem ou não prestam atenção suficiente a um conceito como a projeção de conteúdo. Esta é uma ferramenta muito poderosa para criar componentes flexíveis e reutilizáveis. Mas sua documentação menciona apenas alguns parágrafos na seção Ganchos do ciclo de vida . Vamos tentar consertar essa omissão.



Projetando conteúdo usando ng-content


A projeção de conteúdo é uma maneira de importar conteúdo HTML de fora de um componente e colá-lo em um modelo de componente em um local específico. (tradução livre da documentação)
A definição é bastante complicada, mas, de fato, tudo é muito mais simples. Temos algum tipo de componente, e tudo o que está entre as tags de abertura e fechamento é conteúdo.

<app-parent>
    <!-- content -->
    I'm content!
    <!-- content -->
</app-parent>

E Angular permite incorporar qualquer código HTML (conteúdo) ao modelo deste componente usando um elemento ng-content.

Vamos tentar descobrir por que isso é necessário e como funciona com um exemplo. Digamos que temos um componente de botão simples. O texto deste botão é passado pelo modelo 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 {
}

Parece ter uma boa aparência. Mas de repente precisamos adicionar um ícone ao texto para alguns botões. Já temos um componente de ícone. Você só precisa adicioná-lo ao modelo de botão, anexar uma diretiva ngIfe escrever outra input propertypara a exibição dinâmica do ícone.

// 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;
}

Tudo está funcionando. Mas o que acontece se você precisar alterar a localização do ícone em relação ao texto? Ou adicionar outro novo item? Você precisa editar o código existente, adicionar novas propriedades etc.

Tudo isso pode ser evitado ng-content. Pode ser considerado um espaço reservado para o conteúdo. Ele exibe tudo o que você coloca entre as tags de abertura e fechamento do componente.

// 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 {
}

Código Stackblitz

Agora, se precisarmos de um botão com um ícone, basta colocar o componente do ícone entre as tags do botão. Você pode adicionar qualquer coisa e qualquer coisa. Aquele não é o paraíso? Nosso componente de botão tornou-se flexível e bonito.

Qual o papel que o atributo select desempenha no ng-content?


Às vezes, precisamos organizar algum conteúdo em um determinado local em relação ao restante do conteúdo; nesse caso, podemos usar um atributo selectque aceite um seletor ( .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 {
}

Código Stackblitz

Agora, o ícone é sempre mostrado abaixo, independentemente do restante do conteúdo. Perfecto!

O que é o ngProjectAs?


O atributo selecty ng-contentlida com tags que estão no primeiro nível de aninhamento do componente pai. Mas o que acontece se aumentarmos o nível de aninhamento do componente de ícone envolvendo-o em uma tag?

// 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 {}

Veremos que selectnão funciona, como se não existisse. Isso acontece porque ele <ng-content select="...">pesquisa apenas no primeiro nível de aninhamento do conteúdo dos pais. Há um atributo para resolver esse problema ngProjectAs. Ele pega o seletor e "mascara" todo o nó DOM abaixo dele.

// 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 {}

Código Stackblitz

Caso * ngIf + ng-content


Vamos examinar outro caso interessante. Suponha que precisamos clicar no botão ocultar / mostrar ícone. Adicione uma propriedade booleana à classe de componente do botão responsável por exibir o ícone, altere-o clicando no botão e desligue-o 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;
  }
}

O ícone está oculto / aparece por clique. Bem! Mas vamos adicionar alguns logs para ganchos OnInite OnDestroypara o componente icon. É um fato bem conhecido que uma diretiva, ngIfquando uma condição é alterada, remove / cria completamente um elemento e OnDestroy/ OnInitdeve funcionar adequadamente a cada vez.

// 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')
  }
}

Código no Stackblitz

Algumas vezes clicamos no botão, verifique se o ícone desaparece e depois aparece. Em seguida, vamos ao console do desenvolvedor na esperança de ver nossos logs cobiçados, no entanto ... eles não são!

Existe apenas um log para criar um componente. Acontece que nosso componente de ícone nunca é excluído, mas simplesmente oculto. Por que isso está acontecendo?

ng-content não cria novos conteúdos, simplesmente projeta os existentes. Portanto, o componente no qual o conteúdo é declarado é responsável por criar e excluir. Para mim, foi um momento completamente não óbvio. Vamos corrigir nossa solução para que ela funcione conforme o esperado inicialmente.

// 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;
  }
}

Código no Stackblitz

Após abrir os logs, podemos ver que o componente do ícone é criado e excluído como deveria.

Em vez de uma conclusão


Espero que este artigo tenha ajudado um pouco na projeção de conteúdo no Angular.
É categoricamente incompreensível para mim por que a documentação oficial ignorou este tópico. O repositório Angular até suspende o problema desde 2017. Aparentemente, a equipe Angular tem coisas mais importantes a fazer.

All Articles