Introducción a comunicación padre-hijo
flowchart TD
Root[AppComponent root] --> Header[Header]
Root --> Main[Main]
Root --> Footer[Footer]
Main --> Sidebar[Sidebar]
Main --> Content[Content]
Content --> List[ProductList]
List --> Card1[ProductCard 1]
List --> Card2[ProductCard 2]
List --> CardN[ProductCard N]
Root -.input signal.-> Header
List -.input product.-> Card1
Card1 -.output buy.-> List
List -.output filter.-> Content
Content -.event bus<br/>service signal.-> Sidebar
style Root fill:#fff3e0,stroke:#ef6c00
style Card1 fill:#e3f2fd,stroke:#1565c0
En aplicaciones Angular reales, raramente trabajamos con un único componente aislado. En su lugar, construimos interfaces de usuario complejas dividiendo la funcionalidad en múltiples componentes que cooperan entre sí. Esta arquitectura modular hace que nuestras aplicaciones sean más mantenibles y reutilizables.
La comunicación entre componentes es fundamental para crear aplicaciones Angular efectivas. Los componentes necesitan compartir datos, notificarse sobre eventos y coordinar su comportamiento para ofrecer una experiencia de usuario cohesiva.
Jerarquía de componentes
Angular organiza los componentes en una estructura jerárquica similar a un árbol familiar. Cuando un componente incluye otro componente en su plantilla, establece una relación padre-hijo:
<!-- Componente padre: AppComponent -->
<div class="app-container">
<app-header></app-header>
<app-product-list></app-product-list>
<app-footer></app-footer>
</div>
En este ejemplo, AppComponent actúa como componente padre de HeaderComponent, ProductListComponent y FooterComponent, que son sus componentes hijos.
Patrones de comunicación
Existen varios patrones para la comunicación entre componentes en Angular:
- Comunicación padre -> hijo: El padre envía datos al hijo (usando
input()o@Input) - Comunicación hijo -> padre: El hijo notifica eventos al padre (usando
output()o@Output) - Comunicación entre hermanos: A través del padre común o servicios compartidos
- Comunicación global: Usando servicios con estado compartido
Comunicación padre-hijo en detalle
La comunicación padre-hijo es el patrón más común y fundamental. Permite que el componente padre controle y configure el comportamiento de sus componentes hijos enviándoles datos.
Casos de uso típicos:
- Pasar configuración a componentes reutilizables
- Enviar datos dinámicos que el hijo debe mostrar
- Controlar el estado o apariencia de componentes hijos
- Personalizar el comportamiento de componentes genéricos
Imaginemos un componente ProductCard que debe mostrar información de diferentes productos:
// Componente padre
@Component({
selector: 'app-product-list',
template: `
<div class="product-grid">
<app-product-card></app-product-card>
<app-product-card></app-product-card>
<app-product-card></app-product-card>
</div>
`
})
export class ProductListComponent {
// ¿Cómo le decimos a cada tarjeta qué producto mostrar?
}
Sin comunicación padre-hijo, cada ProductCard sería idéntica y estática. Necesitamos una forma de que el padre envíe datos específicos a cada instancia del componente hijo.
Beneficios del patrón padre-hijo
Este patrón de comunicación ofrece múltiples ventajas arquitectónicas:
- Reutilización: Un componente hijo puede ser usado en diferentes contextos
- Separación de responsabilidades: El padre gestiona los datos, el hijo se encarga de la presentación
- Mantenibilidad: Cambios en la lógica del padre no afectan la implementación del hijo
- Testabilidad: Componentes independientes son más fáciles de probar
Flujo de datos unidireccional
Angular implementa un flujo de datos unidireccional donde los datos fluyen desde componentes padre hacia componentes hijos. Este patrón hace que el comportamiento de la aplicación sea más predecible y fácil de depurar.
Los datos siempre van "hacia abajo" en la jerarquía, desde el padre hacia los hijos, mientras que los eventos van "hacia arriba", desde los hijos hacia el padre. Esta consistencia direccional evita bucles de dependencias y hace más sencillo rastrear el origen de los cambios en la aplicación.
Preparación para input()
Para implementar comunicación padre-hijo, Angular 21 proporciona la función input() basada en signals como enfoque principal. Esta función permite que un componente hijo declare qué datos puede recibir de su padre, devolviendo un signal de solo lectura que se integra perfectamente con el sistema de reactividad de Angular.
También existe el decorador clásico @Input que sigue funcionando en aplicaciones existentes. En las siguientes secciones veremos primero el enfoque moderno con input() y después el enfoque clásico con @Input.
Enfoque moderno: input()
La función input() es la forma recomendada en Angular 21 para declarar inputs en componentes. Forma parte del sistema de signals y devuelve un InputSignal de solo lectura que se actualiza automáticamente cuando el componente padre envía nuevos valores.
Sintaxis básica de input()
Para declarar un input, usamos la función input() importada desde @angular/core:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<h3>{{ productName() }}</h3>
<p>Precio: {{ price() }}€</p>
</div>
`
})
export class ProductCardComponent {
productName = input<string>('');
price = input<number>(0);
}
Observa que en el template se accede al valor llamando al signal como una función: {{ productName() }}. Con esta declaración, nuestro componente ProductCard puede recibir valores para productName y price desde su componente padre.
Enviando datos desde el padre
El componente padre utiliza property binding con corchetes para enviar datos a los inputs del hijo, exactamente igual que con @Input:
@Component({
selector: 'app-product-list',
imports: [ProductCardComponent],
template: `
<div class="product-grid">
<app-product-card
[productName]="'iPhone 16'"
[price]="999">
</app-product-card>
<app-product-card
[productName]="'MacBook Pro'"
[price]="2499">
</app-product-card>
</div>
`
})
export class ProductListComponent { }
Datos dinámicos con propiedades del padre
En aplicaciones reales, los datos suelen venir de propiedades dinámicas del componente padre en lugar de valores estáticos:
@Component({
selector: 'app-product-list',
imports: [ProductCardComponent],
template: `
<div class="product-grid">
@for (product of products; track product.id) {
<app-product-card
[productName]="product.name"
[price]="product.price">
</app-product-card>
}
</div>
`
})
export class ProductListComponent {
products = [
{ id: 1, name: 'iPhone 16', price: 999 },
{ id: 2, name: 'MacBook Pro', price: 2499 },
{ id: 3, name: 'iPad Air', price: 649 }
];
}
Inputs requeridos con input.required()
Para declarar que un input es obligatorio y el padre debe proporcionarlo, usamos input.required():
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<div class="user-profile">
<h2>{{ user().name }}</h2>
<p>Activo: {{ isActive() ? 'Sí' : 'No' }}</p>
<ul>
@for (skill of skills(); track skill) {
<li>{{ skill }}</li>
}
</ul>
</div>
`
})
export class UserProfileComponent {
// Input requerido - el padre debe proporcionarlo
user = input.required<{ name: string; email: string }>();
// Inputs opcionales con valor por defecto
isActive = input<boolean>(false);
skills = input<string[]>([]);
}
Si el padre no proporciona un input.required(), Angular lanzará un error en tiempo de ejecución, lo que facilita la detección de errores.
Alias de inputs
Podemos definir un alias para que el nombre público del input sea diferente del nombre interno:
export class ProductCardComponent {
// El padre usa [productTitle], internamente se llama internalName
internalName = input<string>('', { alias: 'productTitle' });
}
<app-product-card [productTitle]="product.name"></app-product-card>
Integración con computed() y effect()
Una de las grandes ventajas de input() es su integración nativa con el sistema de signals. Podemos crear valores derivados con computed() o ejecutar efectos con effect():
import { Component, input, computed, effect } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<h3>{{ productName() }}</h3>
<p>Precio: {{ price() }}€</p>
<p>Precio con IVA: {{ precioConIva() }}€</p>
</div>
`
})
export class ProductCardComponent {
productName = input.required<string>();
price = input<number>(0);
// Valor derivado que se recalcula automáticamente
precioConIva = computed(() => (this.price() * 1.21).toFixed(2));
constructor() {
// Efecto que reacciona a cambios en el input
effect(() => {
console.log(`Precio actualizado: ${this.price()}€`);
});
}
}
Buenas prácticas con input()
Al trabajar con input(), es recomendable seguir estas buenas prácticas:
- Usar input.required() para inputs obligatorios en lugar de aserción no nula
- Tipar correctamente: Siempre específica el tipo genérico
input<string>() - Valores por defecto: Proporciona valores sensatos para inputs opcionales
- Usar computed() para valores derivados en lugar de cálculos en el template
- Inmutabilidad: Los InputSignal son de solo lectura, lo que refuerza la inmutabilidad
Enfoque clásico: @Input
El decorador @Input es el enfoque clásico que sigue funcionando en Angular 21 y es ampliamente utilizado en aplicaciones existentes. Permite que un componente hijo declare propiedades que pueden recibir datos desde su componente padre.
Sintaxis básica de @Input
Para declarar una propiedad como entrada, aplicamos el decorador @Input() antes de la declaración de la propiedad:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<div class="product-card">
<h3>{{ productName }}</h3>
<p>Precio: {{ price }}€</p>
</div>
`
})
export class ProductCardComponent {
@Input() productName: string = '';
@Input() price: number = 0;
}
Propiedades requeridas y opcionales
Por defecto, todas las propiedades @Input son opcionales. Podemos hacerlas requeridas usando la opción required:
export class ProductCardComponent {
// Propiedad requerida
@Input({ required: true }) productName!: string;
// Propiedad opcional con valor por defecto
@Input() price: number = 0;
@Input() showDiscount: boolean = false;
}
Alias de propiedades
export class ProductCardComponent {
@Input('productTitle') internalName: string = '';
}
Validación básica en el setter
Podemos usar setters para validar o procesar los datos que llegan a través de @Input:
export class ProductCardComponent {
private _price: number = 0;
@Input()
set price(value: number) {
this._price = value < 0 ? 0 : value;
}
get price(): number {
return this._price;
}
}
Comparativa: input() vs @Input
| Característica | input() (moderno) | @Input (clásico) |
|---|---|---|
| Tipo de valor | InputSignal (solo lectura) | Propiedad mutable |
| Acceso en template | {{ miInput() }} | {{ miInput }} |
| Inputs requeridos | input.required<T>() | @Input({ required: true }) |
| Integración con signals | Nativa (computed, effect) | Requiere conversión manual |
| Detección de cambios | Reactiva con signals | Basada en ngOnChanges |
Se recomienda usar input() en código nuevo, pero @Input sigue siendo completamente válido y no requiere migración inmediata en aplicaciones existentes.
Inputs avanzados: transforms
Las funciones de transformación permiten procesar automáticamente los datos antes de asignarlos al input del componente. Esta característica funciona tanto con input() como con @Input y elimina la necesidad de lógica de conversión manual.
Transforms con input()
La función input() acepta una opción transform que procesa el valor antes de almacenarlo en el signal:
import { Component, input, booleanAttribute, numberAttribute } from '@angular/core';
@Component({
selector: 'app-config-panel',
template: `
<div class="config-panel">
<p>Modo debug: {{ debugMode() ? 'Activado' : 'Desactivado' }}</p>
<p>Número de elementos: {{ itemCount() }}</p>
</div>
`
})
export class ConfigPanelComponent {
debugMode = input(false, { transform: booleanAttribute });
itemCount = input(0, { transform: numberAttribute });
}
Transformaciones incorporadas
Angular proporciona funciones de transformación predefinidas para casos comunes:
booleanAttribute - Convierte strings y valores truthy a booleanos:
export class ToggleComponent {
// Enfoque moderno con input()
enabled = input(false, { transform: booleanAttribute });
}
<!-- Todos estos se convierten a true -->
<app-toggle enabled></app-toggle>
<app-toggle enabled="true"></app-toggle>
<app-toggle enabled=""></app-toggle>
<app-toggle [enabled]="someVariable"></app-toggle>
numberAttribute - Convierte strings a números:
export class CounterComponent {
// Enfoque moderno con input()
initialValue = input(0, { transform: numberAttribute });
}
<!-- Se convierte automáticamente a number -->
<app-counter initialValue="42"></app-counter>
<app-counter [initialValue]="stringNumber"></app-counter>
Funciones de transformación personalizadas
Podemos crear transformaciones personalizadas para casos específicos de nuestra aplicación:
// Función para convertir texto a mayúsculas
function uppercaseTransform(value: string): string {
return value?.toString().toUpperCase() || '';
}
// Función para formatear precios
function priceTransform(value: number): string {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR'
}).format(value || 0);
}
@Component({
selector: 'app-product-display',
template: `
<div class="product">
<h3>{{ title() }}</h3>
<p class="price">{{ formattedPrice() }}</p>
</div>
`
})
export class ProductDisplayComponent {
title = input('', { transform: uppercaseTransform });
formattedPrice = input('', { transform: priceTransform });
}
Transformaciones con múltiples parámetros
Las funciones de transformación pueden ser más complejas y manejar diferentes tipos de entrada:
function trimAndCapitalize(value: string | null | undefined): string {
if (!value) return '';
const trimmed = value.toString().trim();
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1).toLowerCase();
}
function clampNumber(min: number, max: number) {
return (value: number): number => {
const num = Number(value) || 0;
return Math.min(Math.max(num, min), max);
};
}
export class UserInputComponent {
userName = input('', { transform: trimAndCapitalize });
percentage = input(0, { transform: clampNumber(0, 100) });
}
Transformaciones con arrays y objetos
También podemos transformar estructuras de datos complejas:
function parseTagsFromString(value: string | string[]): string[] {
if (Array.isArray(value)) return value;
if (!value) return [];
return value.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
}
function normalizeUser(value: any): { name: string; email: string } {
return {
name: value?.name || 'Usuario desconocido',
email: value?.email || 'email@ejemplo.com'
};
}
export class TaggedContentComponent {
tags = input<string[]>([], { transform: parseTagsFromString });
user = input.required<{ name: string; email: string }>({ transform: normalizeUser });
}
Combinando transforms con validación
Las transformaciones pueden incluir lógica de validación para garantizar datos consistentes:
function validateAndParseDate(value: string | Date): Date {
if (value instanceof Date) return value;
const parsed = new Date(value);
if (isNaN(parsed.getTime())) {
console.warn(`Fecha inválida recibida: ${value}`);
return new Date(); // Fecha actual por defecto
}
return parsed;
}
function sanitizeHtml(value: string): string {
return value
?.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
?.replace(/<[^>]*>/g, '') || '';
}
export class ContentComponent {
publishDate = input(new Date(), { transform: validateAndParseDate });
description = input('', { transform: sanitizeHtml });
}
Ventajas de las transformaciones
El uso de transform functions ofrece múltiples beneficios:
- Automatización: Las transformaciones se aplican automáticamente sin código adicional
- Reutilización: Las funciones de transformación se pueden compartir entre componentes
- Consistencia: Garantizan que los datos siempre lleguen en el formato esperado
- Simplicidad: Eliminan la necesidad de setters personalizados complejos
- Performance: Son más eficientes que las transformaciones manuales en templates
Casos de uso prácticos
Las transformaciones son especialmente útiles en estos escenarios comunes:
- Formularios: Normalizar entrada de usuario (trim, mayúsculas, formato)
- APIs: Convertir formatos de datos entre frontend y backend
- Componentes de UI: Estandarizar propiedades de configuración
- Validación: Asegurar tipos y formatos correctos
- Localización: Adaptar datos a formatos locales
Consideraciones de rendimiento
Las funciones de transformación deben ser funciones puras sin efectos secundarios para garantizar un comportamiento predecible:
// ✅ Correcto - Función pura
function formatCurrency(value: number): string {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: 'EUR'
}).format(value || 0);
}
// ❌ Incorrecto - Tiene efectos secundarios
function logAndFormat(value: number): string {
console.log('Valor recibido:', value); // Efecto secundario
return formatCurrency(value);
}
Las transform functions representan una herramienta avanzada que mejora significativamente la robustez y mantenibilidad de nuestros componentes, proporcionando un mecanismo elegante para garantizar que los datos siempre lleguen en el formato correcto.
Transforms con el enfoque clásico @Input
Las transformaciones también funcionan con el decorador @Input clásico, usando la misma sintaxis de opciones:
export class ConfigPanelComponent {
@Input({ transform: booleanAttribute }) debugMode: boolean = false;
@Input({ transform: numberAttribute }) itemCount: number = 0;
}
La funcionalidad de transforms es idéntica en ambos enfoques; la diferencia es que con input() el resultado es un signal reactivo.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Angular
Documentación oficial de Angular
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 la arquitectura de comunicación padre-hijo en Angular 21.
- Aprender a usar la función input() basada en signals como enfoque principal para recibir datos del componente padre.
- Conocer cómo enviar datos estáticos y dinámicos mediante property binding.
- Explorar el uso de input.required() y opciones de transformación.
- Conocer el enfoque clásico con @Input que sigue funcionando en aplicaciones existentes.
- Aplicar buenas prácticas en la definición y uso de inputs para componentes reutilizables.