Proyección de contenido con ng-content

Intermedio
Angular
Angular
Actualizado: 24/09/2025

Slotting con ng-content

El slotting en Angular es una técnica que permite crear componentes flexibles y reutilizables mediante la proyección de contenido. Esta funcionalidad se implementa utilizando la etiqueta <ng-content>, que actúa como un marcador de posición donde se insertará el contenido pasado desde el componente padre.

¿Qué es ng-content?

La directiva ng-content permite que un componente hijo reciba y renderice contenido proporcionado por su componente padre. Esto es especialmente útil para crear componentes contenedores como tarjetas, modales o layouts que necesitan mostrar contenido dinámico sin conocer su estructura específica.

// card.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <div class="card-body">
        <ng-content></ng-content>
      </div>
    </div>
  `,
  styles: [`
    .card {
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 16px;
      margin: 8px;
    }
  `]
})
export class CardComponent { }

Para usar este componente, simplemente insertamos contenido entre las etiquetas de apertura y cierre del componente app-card donde lo estemos usando:

<app-card>
  <h3>Título de la tarjeta</h3>
  <p>Este contenido se proyectará dentro del ng-content</p>
  <button>Acción</button>
</app-card>

Múltiples slots con selectores

Cuando necesitamos múltiples áreas de contenido en un mismo componente, podemos usar el atributo select para crear slots específicos. Esto nos permite dirigir diferentes partes del contenido a ubicaciones específicas dentro del componente.

// modal.component.ts
@Component({
  selector: 'app-modal',
  template: `
    <div class="modal-overlay">
      <div class="modal">
        <header class="modal-header">
          <ng-content select="[slot=header]"></ng-content>
        </header>
        
        <main class="modal-body">
          <ng-content select="[slot=body]"></ng-content>
        </main>
        
        <footer class="modal-footer">
          <ng-content select="[slot=footer]"></ng-content>
        </footer>
      </div>
    </div>
  `,
  styles: [`
    .modal-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.5);
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .modal {
      background: white;
      border-radius: 8px;
      min-width: 400px;
    }
    .modal-header {
      padding: 16px;
      border-bottom: 1px solid #eee;
    }
    .modal-body {
      padding: 16px;
    }
    .modal-footer {
      padding: 16px;
      border-top: 1px solid #eee;
      text-align: right;
    }
  `]
})
export class ModalComponent { }

El uso del modal con múltiples slots sería:

<app-modal>
  <h2 slot="header">Confirmar acción</h2>
  
  <div slot="body">
    <p>¿Estás seguro de que quieres eliminar este elemento?</p>
    <p>Esta acción no se puede deshacer.</p>
  </div>
  
  <div slot="footer">
    <button class="btn-cancel">Cancelar</button>
    <button class="btn-danger">Eliminar</button>
  </div>
</app-modal>

Selectores avanzados

Los selectores de ng-content son muy flexibles y permiten diferentes tipos de selección:

  • Por atributo: select="[slot=header]"
<app-component>
  <div slot="header">Contenido del header</div>
</app-component>
  • Por clase CSS: select=".highlight"
<app-component>
  <p class="highlight">Texto destacado</p>
</app-component>
  • Por etiqueta HTML: select="h1"
<app-component>
  <h1>Este h1 será seleccionado</h1>
</app-component>

Ejemplo práctico: Componente de tabs

Creemos un componente de pestañas que demuestre el uso práctico de múltiples slots:

// tabs.component.ts
@Component({
  selector: 'app-tabs',
  template: `
    <div class="tabs">
      <div class="tab-headers">
        <ng-content select="[slot=tab-header]"></ng-content>
      </div>
      
      <div class="tab-content">
        <ng-content select="[slot=tab-panel]"></ng-content>
      </div>
    </div>
  `,
  styles: [`
    .tabs {
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    .tab-headers {
      display: flex;
      background: #f5f5f5;
      border-bottom: 1px solid #ddd;
    }
    .tab-content {
      padding: 16px;
    }
  `]
})
export class TabsComponent { }
<app-tabs>
  <!-- Headers de las pestañas -->
  <button slot="tab-header" class="tab-btn active">Perfil</button>
  <button slot="tab-header" class="tab-btn">Configuración</button>
  <button slot="tab-header" class="tab-btn">Notificaciones</button>
  
  <!-- Contenido de las pestañas -->
  <div slot="tab-panel" class="tab-pane active">
    <h3>Información del perfil</h3>
    <p>Datos del usuario y configuración personal</p>
  </div>
</app-tabs>

Slot por defecto

Cuando usamos ng-content sin selector, actúa como un slot por defecto que captura todo el contenido que no coincida con otros selectores específicos:

@Component({
  selector: 'app-layout',
  template: `
    <aside class="sidebar">
      <ng-content select="[slot=sidebar]"></ng-content>
    </aside>
    
    <main class="content">
      <!-- Todo el contenido sin slot específico irá aquí -->
      <ng-content></ng-content>
    </main>
  `
})
export class LayoutComponent { }
<app-layout>
  <!-- Este div irá al slot sidebar -->
  <nav slot="sidebar">
    <a href="/home">Inicio</a>
    <a href="/products">Productos</a>
  </nav>
  
  <!-- Este contenido irá al ng-content por defecto -->
  <h1>Página principal</h1>
  <p>Este contenido va en el área principal</p>
</app-layout>

Esta aproximación de slotting hace que nuestros componentes sean extremadamente flexibles, permitiendo que los componentes padre definan qué contenido mostrar sin que el componente hijo necesite conocer los detalles específicos de ese contenido.

Contenido proyectado

El contenido proyectado es el mecanismo fundamental que permite que el contenido definido en un componente padre se inserte y renderice dentro de un componente hijo. A diferencia de otros métodos de comunicación entre componentes, la proyección de contenido mantiene el contexto del componente padre, lo que significa que el contenido proyectado conserva acceso a las propiedades, métodos y estado del componente que lo define originalmente.

Contexto del componente padre

Una característica fundamental del contenido proyectado es que mantiene su contexto original. Esto significa que cuando proyectamos contenido desde un componente padre hacia un hijo, ese contenido sigue teniendo acceso a las propiedades y métodos del componente padre, no del componente hijo que lo aloja.

// parent.component.ts
@Component({
  selector: 'app-parent',
  template: `
    <div class="parent-container">
      <h2>Componente Padre</h2>
      <p>Usuario actual: {{ currentUser.name }}</p>
      
      <app-card>
        <!-- Este contenido mantiene acceso a las propiedades del padre -->
        <h3>Bienvenido, {{ currentUser.name }}</h3>
        <p>Tienes {{ notifications.length }} notificaciones</p>
        <button (click)="markAllAsRead()">Marcar como leídas</button>
      </app-card>
    </div>
  `
})
export class ParentComponent {
  currentUser = { name: 'Ana García', id: 123 };
  notifications = ['Mensaje 1', 'Mensaje 2', 'Mensaje 3'];
  
  markAllAsRead() {
    console.log('Marcando todas las notificaciones como leídas');
    this.notifications = [];
  }
}

En este ejemplo, aunque el contenido se renderiza dentro del componente app-card, las interpolaciones {{ currentUser.name }} y {{ notifications.length }}, así como el event binding (click)="markAllAsRead()", se ejecutan en el contexto del componente padre.

Diferencias con otras técnicas de comunicación

El contenido proyectado se diferencia significativamente de otras formas de pasar datos entre componentes:

  • @Input properties: Pasan datos específicos del padre al hijo
// Con @Input, el hijo recibe solo datos específicos
@Component({
  selector: 'app-user-info',
  template: `<p>Usuario: {{ userName }}</p>`
})
export class UserInfoComponent {
  @Input() userName!: string;
}

// Uso: <app-user-info [userName]="currentUser.name"></app-user-info>
  • Contenido proyectado: Permite pasar estructura HTML completa con su lógica
<!-- Con proyección, pasamos HTML completo con su comportamiento -->
<app-card>
  <div class="user-profile">
    <img [src]="currentUser.avatar" [alt]="currentUser.name">
    <h3>{{ currentUser.name }}</h3>
    <button (click)="editProfile()">Editar perfil</button>
  </div>
</app-card>

Acceso a variables del template

El contenido proyectado también mantiene acceso a variables de template definidas en el componente padre:

// list-container.component.ts
@Component({
  selector: 'app-list-container',
  template: `
    <div class="list-wrapper">
      <div *ngFor="let item of items; let i = index; let isLast = last" class="item-wrapper">
        <app-item-card>
          <!-- Las variables del *ngFor están disponibles aquí -->
          <div class="item-content">
            <span class="item-number">Elemento #{{ i + 1 }}</span>
            <h4>{{ item.title }}</h4>
            <p>{{ item.description }}</p>
            @if (isLast) {
              <span class="last-item">Último elemento</span>
            }
          </div>
        </app-item-card>
      </div>
    </div>
  `
})
export class ListContainerComponent {
  items = [
    { title: 'Producto A', description: 'Descripción del producto A' },
    { title: 'Producto B', description: 'Descripción del producto B' },
    { title: 'Producto C', description: 'Descripción del producto C' }
  ];
}

En este ejemplo, las variables i, isLast, e item del bucle *ngFor están disponibles dentro del contenido proyectado, permitiendo lógica condicional y acceso a los datos del contexto padre.

Proyección condicional

El contenido proyectado puede combinarse con la sintaxis de control de flujo moderna de Angular para crear proyecciones dinámicas:

// dashboard.component.ts
@Component({
  selector: 'app-dashboard',
  template: `
    <div class="dashboard">
      <app-widget>
        @if (user.isAdmin) {
          <div class="admin-panel">
            <h3>Panel de Administración</h3>
            <button (click)="manageUsers()">Gestionar usuarios</button>
            <button (click)="viewReports()">Ver reportes</button>
          </div>
        } @else {
          <div class="user-panel">
            <h3>Mi Dashboard</h3>
            <p>Bienvenido, {{ user.name }}</p>
            <button (click)="viewProfile()">Ver perfil</button>
          </div>
        }
      </app-widget>
      
      <app-widget>
        @for (notification of notifications; track notification.id) {
          <div class="notification" [class.unread]="!notification.read">
            <span>{{ notification.message }}</span>
            <button (click)="markAsRead(notification.id)">
              {{ notification.read ? 'Leída' : 'Marcar como leída' }}
            </button>
          </div>
        }
      </app-widget>
    </div>
  `
})
export class DashboardComponent {
  user = { name: 'Carlos López', isAdmin: false };
  notifications = [
    { id: 1, message: 'Nueva actualización disponible', read: false },
    { id: 2, message: 'Reunión programada para mañana', read: true }
  ];
  
  manageUsers() { /* lógica de administración */ }
  viewReports() { /* lógica de reportes */ }
  viewProfile() { /* lógica de perfil */ }
  markAsRead(id: number) { /* marcar notificación */ }
}

Ventajas del contenido proyectado

La proyección de contenido ofrece varias ventajas significativas:

  • Reutilización: Los componentes contenedores pueden usarse con diferentes tipos de contenido
<!-- El mismo componente card puede mostrar diferentes contenidos -->
<app-card>
  <form>
    <input type="text" placeholder="Buscar...">
    <button type="submit">Buscar</button>
  </form>
</app-card>

<app-card>
  <img src="product.jpg" alt="Producto">
  <h3>{{ product.name }}</h3>
  <p>{{ product.price | currency }}</p>
</app-card>

Flexibilidad: El componente padre controla completamente la estructura y comportamiento del contenido

Separación de responsabilidades: El componente hijo se encarga del layout y estilos, mientras el padre define el contenido y lógica

Consideraciones importantes

Al trabajar con contenido proyectado, es importante tener en cuenta:

El rendimiento: El contenido proyectado se evalúa en cada ciclo de detección de cambios del componente padre

Debugging: Los errores en el contenido proyectado se rastrean hasta el componente padre, no el hijo que lo renderiza

Accesibilidad: Es responsabilidad del componente padre asegurar que el contenido proyectado mantenga la estructura semántica correcta

// Ejemplo de buena práctica con accesibilidad
@Component({
  selector: 'app-accessible-card',
  template: `
    <article class="card" [attr.aria-labelledby]="titleId">
      <ng-content></ng-content>
    </article>
  `
})
export class AccessibleCardComponent {
  titleId = `card-title-${Math.random().toString(36).substr(2, 9)}`;
}
<app-accessible-card>
  <!-- El padre debe mantener la estructura semántica -->
  <header>
    <h2 [id]="titleId">Título accesible</h2>
  </header>
  <section>
    <p>Contenido principal de la tarjeta</p>
  </section>
</app-accessible-card>

La proyección de contenido es una técnica fundamental que permite crear componentes verdaderamente flexibles y reutilizables, manteniendo la separación clara de responsabilidades entre el contenido (definido por el padre) y la presentación (manejada por el hijo).

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender qué es y cómo funciona la directiva ng-content en Angular.
  • Aprender a usar múltiples slots con selectores para proyectar contenido en diferentes áreas de un componente.
  • Entender cómo el contenido proyectado mantiene el contexto del componente padre.
  • Diferenciar la proyección de contenido de otras técnicas de comunicación entre componentes.
  • Aplicar selectores avanzados y proyección condicional para crear componentes dinámicos y accesibles.