Proyectar contenido en documentación angular o perdida de contenido ng

Al aprender Angular, muy a menudo se pierden o no prestan suficiente atención a un concepto como la proyección de contenido. Esta es una herramienta muy poderosa para crear componentes flexibles y reutilizables. Pero su documentación solo menciona un par de párrafos en la sección de ganchos de Lifecycle . Intentemos solucionar esta omisión.



Proyectando contenido usando ng-content


La proyección de contenido es una forma de importar contenido HTML desde fuera de un componente y pegarlo en una plantilla de componente en una ubicación específica. (traducción gratuita de documentación)
La definición es bastante complicada, pero de hecho, todo es mucho más simple. Tenemos algún tipo de componente, y todo lo que está entre sus etiquetas de apertura y cierre es contenido.

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

Y Angular le permite incrustar cualquier código HTML (contenido) en la plantilla de este componente utilizando un elemento ng-content.

Tratemos de descubrir por qué esto es necesario y cómo funciona con un ejemplo. Digamos que tenemos un componente de botón simple. El texto de este botón lo pasamos a la plantilla 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 que se ve bien. Pero de repente tuvimos que agregar un ícono al texto para algunos botones. Ya tenemos un componente de icono. Solo necesita agregarlo a la plantilla del botón, adjuntar una directiva ngIfy escribir otra input propertypara la visualización dinámica del icono.

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

Todo esta funcionando. Pero, ¿qué sucede si necesita cambiar la ubicación del icono en relación con el texto? ¿O agregar otro elemento nuevo? Debe editar el código existente, agregar nuevas propiedades, etc.

Todo esto se puede evitar con ng-content. Se puede considerar como un marcador de posición para el contenido. Muestra todo lo que coloca entre las etiquetas de apertura y cierre del 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 de Stackblitz

Ahora, si necesitáramos un botón con un icono, simplemente colocamos el componente del icono entre las etiquetas de los botones. Puedes agregar cualquier cosa y lo que sea. ¿No es eso el cielo? Nuestro componente de botón se ha vuelto flexible y hermoso.

¿Qué papel juega el atributo select para ng-content?


A veces necesitamos organizar algún contenido en un lugar determinado en relación con el resto del contenido, en este caso podemos usar un atributo selectque acepte un selector ( .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

Ahora el icono siempre se muestra a continuación, independientemente del resto del contenido. Perfecto!

¿Qué es ngProjectAs?


El atributo selecty ng-contenthace frente a las etiquetas que se encuentran en el primer nivel de anidamiento del componente principal. Pero, ¿qué sucede si aumentamos el nivel de anidación para el componente de icono envolviéndolo en una etiqueta?

// 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 selectno funciona, como si no existiera en absoluto. Esto sucede porque solo <ng-content select="...">busca en el primer nivel de anidamiento del contenido principal. Hay un atributo para resolver este problema ngProjectAs. Toma el selector y "enmascara" todo el nodo DOM debajo de él.

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


Examinemos otro caso interesante. Supongamos que necesitamos hacer clic en el botón de ocultar / mostrar icono. Agregue una propiedad booleana a la clase de componente de botón responsable de mostrar el icono, cámbiela haciendo clic en el botón y cuélguela 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;
  }
}

El icono está oculto / aparece al hacer clic. ¡Multa! Pero agreguemos algunos registros para ganchos OnInity OnDestroypara el componente de icono. Es un hecho bien conocido que una directiva, ngIfcuando se cambia una condición, elimina / crea completamente un elemento y OnDestroy/ OnInitdebe funcionar en consecuencia 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 en Stackblitz

Un par de veces hacemos clic en el botón, nos aseguramos de que el icono desaparezca y luego aparezca. Luego vamos a la consola del desarrollador con la esperanza de ver nuestros codiciados registros, sin embargo ... ¡no lo son!

Solo hay un registro para crear un componente. Resulta que nuestro componente de icono nunca se elimina, sino que simplemente se oculta. ¿Por qué está pasando esto?

ng-content no crea contenido nuevo, simplemente proyecta los existentes. Por lo tanto, el componente en el que se declara el contenido es responsable de crear y eliminar. Para mí fue un momento completamente no obvio. Arreglemos nuestra solución para que funcione como se esperaba 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 en Stackblitz

Después de abrir los registros, podemos ver que el componente del icono se crea y elimina como debería.

En lugar de una conclusión


Espero que este artículo te haya ayudado un poco con la proyección de contenido en Angular.
Es categóricamente incomprensible para mí por qué la documentación oficial ignoró este tema. El repositorio angular incluso cuelga el problema en esto desde 2017. Aparentemente, el equipo angular tiene cosas más importantes que hacer.

All Articles