Proyección de contenido

<< Reactive Forms View/Content queries >>

Angular permite proyectar contenido desde el componente padre a un componente hijo. Esto es útil cuando tenemos componentes cuyo contenido no está predefinido en su plantilla sino que actuan como contenedores genéricos de cualquier tipo de contenido, como puede ser una ventana modal, por ejemplo.

Proyección simple

Por defecto, todo el contenido que metamos dentro del selector del componente hijo, se proyectará en un elemento especial de Angular llamado ng-content que no se renderiza en el DOM, pero sirve para indicarle a Angular donde tiene que insertar el contenido proyectado.

Vamos a ver un ejemplo donde creamos una card cuyo contenido (card-body) se le pasa por proyección. Para dar algo más de contexto, vamos a utilizar Bootstrap (previa instalación) para gestionar la apariencia de la card.

<div class="container">
  <div class="mb-4 mt-4 row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
    <my-card bgClass="bg-primary" textClass="text-white">
      <h3>Proyección de título</h3>
      <p>Proyección de contenido</p>
    </my-card>
  </div>
</div>
<div [class]="['card', 'shadow', bgClass(), textClass()]">
  <div class="card-body">
    <ng-content><!-- Aquí se proyecta el contenido --></ng-content>
  </div>
</div>

Este sería el DOM renderizado para la card una vez el contenido ha sido proyectado:

<my-card bgclass="bg-primary" textclass="text-white">
  <div class="bg-primary card shadow text-white">
    <div class="card-body">
      <h3>Proyección de título</h3>
      <p>Proyección de contenido</p>
    </div>
  </div>
</my-card>

Ejemplo: Botón con contenido personalizado y animación de carga

Para el siguiente ejemplo crearemos un componente llamado load-button que contendrá un botón con el contenido que proyectemos en el componente. Además, tendrá un parámetro de entrada (loading) que será un booleano. Cuando esté a true dicho booleano, se mostrará una animación con un icono y se deshabilitará el botón.

<button [class]="['btn', colorClass()]" [disabled]="loading()">
  @if(loading()) {
    <div class="load-overlay">
      <svg class="load-icon" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26.349 26.35">
          <g>
            <circle cx="13.792" cy="3.082" r="3.082"/>
            <circle cx="13.792" cy="24.501" r="1.849"/>
            <circle cx="6.219" cy="6.218" r="2.774"/>
            <circle cx="21.365" cy="21.363" r="1.541"/>
            <circle cx="3.082" cy="13.792" r="2.465"/>
            <circle cx="24.501" cy="13.791" r="1.232"/>
            <path d="M4.694,19.84c-0.843,0.843-0.843,2.207,0,3.05c0.842,0.843,2.208,0.843,3.05,0c0.843-0.843,0.843-2.207,0-3.05
              C6.902,18.996,5.537,18.988,4.694,19.84z"/>
            <circle cx="21.364" cy="6.218" r="0.924"/>
          </g>
      </svg>
    </div>
  }
  <ng-content></ng-content>
</button>
<!-- ... -->
<load-button [loading]="loading" (click)="loading = !loading">
  Loading button
</load-button>
<!-- ... -->

Proyección múltiple

Los elementos proyectados, se pueden colocar en diferentes partes de la plantilla del componente. Para ello utilizamos varios elementos ng-content y añadimos el atributo select (a todos, o todos menos uno). El valor será un selector (como en CSS) y ahí se proyectarán los elementos que coincidan con dicho selector. Si dejamos un elemento ng-content sin atributo select, ahí se proyectan los elementos que no coincidan con ningún selector.

Ejemplo: Ventana modal

Vamos a crear un componente que represente una ventana modal, y aunque nos apoyaremos en alguna variable y clase de Bootstrap, prácticamente todo el CSS será desde cero. Esta ventana modal tendrá, además de un overlay para tapar el contenido de la página, 3 secciones. El encabezado (modal-header) donde pondremos el título, el contenido (modal-body) y el pie (modal-footer) donde irán los botones para interactuar con la modal.

ng g c my-modal

El contenido que va en el encabezado tendrá un atributo llamado header→select="[header]". El contenido del pie serán botones con el atributo footer →select="button[footer]",  y en el cuerpo de la modal irá todo lo demás.

@if(show()) {
  <div class="modal-overlay">
    <div class="modal-content">
      <button class="modal-close" (click)="show.set(false)">X</button>
      <div class="modal-header">
        <ng-content select="[header]"></ng-content>
      </div>
      <div class="modal-body">
        <ng-content></ng-content>
      </div>
      <div class="modal-footer">
        <ng-content select="button[footer]"></ng-content>
      </div>
    </div>
  </div>
}

Para crear contenido para el encabezado utilizaremos ng-container. Este elemento de Angular nos permite en este caso establecer el atributo header para que la proyección del texto funcione, pero a diferencia de otros elementos, desaparece cuando se renderiza, dejando solo su contenido interno.

<!-- ... -->
<my-modal [(show)]="showModal">
  <ng-container header>Ejemplo modal</ng-container>
  <p>Soy un texto</p>
  <p>Soy otro texto</p>
  <button footer class="btn btn-success">Sí</button>
  <button footer class="btn btn-danger">No</button>
</my-modal>

Contenido por defecto

Se puede establecer contenido por defecto en los elementos ng-content, simplemente añadiéndolo dentro. Cuando haya contenido que proyectar este será sustituido por el contenido proyectado.

@if(show()) {
  <div class="modal-overlay">
    <div class="modal-content">
      <button class="modal-close" (click)="show.set(false)">X</button>
      <div class="modal-header">
        <ng-content select="[header]">Encabezado</ng-content>
      </div>
      <div class="modal-body">
        <ng-content>
          <p>Contenido por defecto</p>
        </ng-content>
      </div>
      <div class="modal-footer">
        <ng-content select="button[footer]">
          <button class="btn btn-danger" (click)="show.set(false)">Ok</button>
        </ng-content>
      </div>
    </div>
  </div>
}
<!-- ... -->
<my-modal [(show)]="showModal">
</my-modal>

<< Reactive Forms View/Content queries >>