Introducción a comunicación padre-hijo
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
) - Comunicación hijo → padre: El hijo notifica eventos al padre (usando
@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, inspirado en arquitecturas como React, 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 proporciona el decorador @Input
que permite que un componente hijo reciba datos de su padre. En las siguientes secciones veremos cómo declarar propiedades de entrada y cómo el componente padre puede enviar valores a través de property binding.
También existe una alternativa moderna basada en signals llamada input()
, pero la veremos más adelante en el módulo dedicado a Signals. Por ahora nos enfocaremos en el enfoque tradicional con @Input
para establecer una base sólida.
Introducción a @Input
El decorador @Input
es la herramienta fundamental que Angular proporciona para implementar comunicación padre-hijo. Permite que un componente hijo declare qué propiedades pueden recibir datos desde su componente padre, estableciendo así un canal de comunicación unidireccional.
Sintaxis básica de @Input
Para declarar una propiedad como entrada, simplemente aplicamos el decorador @Input()
antes de la declaración de la propiedad en nuestro componente:
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;
}
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 las propiedades @Input
del hijo:
@Component({
selector: 'app-product-list',
template: `
<div class="product-grid">
<app-product-card
[productName]="'iPhone 15'"
[price]="999">
</app-product-card>
<app-product-card
[productName]="'MacBook Pro'"
[price]="2499">
</app-product-card>
</div>
`
})
export class ProductListComponent {
// El padre controla qué datos enviar a cada hijo
}
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',
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 15', price: 999 },
{ id: 2, name: 'MacBook Pro', price: 2499 },
{ id: 3, name: 'iPad Air', price: 649 }
];
}
Tipos de datos en @Input
Las propiedades @Input
pueden recibir cualquier tipo de datos TypeScript:
@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() user!: { name: string; email: string };
@Input() isActive: boolean = false;
@Input() skills: string[] = [];
}
Propiedades requeridas y opcionales
Por defecto, todas las propiedades @Input
son opcionales. Podemos hacerlas requeridas usando el operador de aserción no nulo (!
) o proporcionando valores por defecto:
export class ProductCardComponent {
// Propiedad requerida (el padre debe enviarla)
@Input() productName!: string;
// Propiedad opcional con valor por defecto
@Input() price: number = 0;
@Input() showDiscount: boolean = false;
}
Alias de propiedades
A veces queremos que el nombre interno de la propiedad sea diferente del nombre que usa el padre. Podemos usar un alias:
export class ProductCardComponent {
@Input('productTitle') internalName: string = '';
}
El padre lo usaría así:
<app-product-card [productTitle]="product.name"></app-product-card>
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) {
// Validar que el precio no sea negativo
this._price = value < 0 ? 0 : value;
}
get price(): number {
return this._price;
}
}
Alternativa moderna: input()
Es importante mencionar que Angular introduce una alternativa moderna a @Input
basada en signals llamada input()
. Esta nueva API ofrece mejor rendimiento y integración con el sistema de reactividad de signals:
// Alternativa moderna (la veremos en el módulo de Signals)
export class ProductCardComponent {
productName = input<string>(''); // Signal input
price = input<number>(0);
}
Sin embargo, en esta lección nos centramos en el enfoque tradicional con @Input
porque es fundamental comprender los conceptos base antes de avanzar a las funcionalidades más modernas.
Buenas prácticas con @Input
Al trabajar con @Input
, es recomendable seguir estas buenas prácticas:
- Tipar correctamente: Siempre especifica el tipo de dato esperado
- Valores por defecto: Proporciona valores sensatos para propiedades opcionales
- Inmutabilidad: Trata los datos de entrada como inmutables en el componente hijo
- Validación: Valida los datos de entrada cuando sea necesario
- Documentación: Comenta el propósito y formato esperado de cada
@Input
Con @Input
establecemos la base para crear componentes reutilizables y flexibles que pueden adaptarse a diferentes contextos según los datos que reciben de sus padres. En la siguiente sección veremos funcionalidades más avanzadas como las transformaciones automáticas de datos.
@Input avanzado: transforms
Las funciones de transformación en @Input
representan una funcionalidad avanzada que permite procesar automáticamente los datos antes de asignarlos a la propiedad del componente. Esta característica elimina la necesidad de usar setters personalizados para transformaciones comunes.
¿Qué son las transforms?
Una transform function es una función pura que recibe el valor de entrada y devuelve el valor transformado. Angular aplica esta función automáticamente cada vez que el componente padre envía un nuevo valor:
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 {
@Input({ transform: booleanAttribute }) debugMode: boolean = false;
@Input({ transform: numberAttribute }) itemCount: number = 0;
}
Transformaciones incorporadas
Angular proporciona funciones de transformación predefinidas para casos comunes:
booleanAttribute - Convierte strings y valores truthy a booleanos:
export class ToggleComponent {
@Input({ transform: booleanAttribute }) enabled: boolean = false;
}
<!-- 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 {
@Input({ transform: numberAttribute }) initialValue: number = 0;
}
<!-- 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 {
@Input({ transform: uppercaseTransform }) title: string = '';
@Input({ transform: priceTransform }) formattedPrice: string = '';
}
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 {
@Input({ transform: trimAndCapitalize }) userName: string = '';
@Input({ transform: clampNumber(0, 100) }) percentage: number = 0;
}
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 {
@Input({ transform: parseTagsFromString }) tags: string[] = [];
@Input({ transform: normalizeUser }) user!: { name: string; email: string };
}
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 {
@Input({ transform: validateAndParseDate }) publishDate: Date = new Date();
@Input({ transform: sanitizeHtml }) description: string = '';
}
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.
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.
- Aprender a usar el decorador @Input para pasar datos desde un componente padre a un hijo.
- Conocer cómo enviar datos estáticos y dinámicos mediante property binding.
- Explorar el uso de transformaciones automáticas en propiedades @Input para validar y formatear datos.
- Aplicar buenas prácticas en la definición y uso de propiedades @Input para componentes reutilizables.