API output() y OutputEmitterRef
La función output() representa la evolución moderna de los outputs en Angular, integrándose perfectamente con el ecosistema de signals que ya hemos estudiado. Esta nueva API mantiene la filosofía reactiva que caracteriza a los signal inputs, proporcionando una alternativa más limpia y consistente al decorador @Output()
tradicional.
Definición básica de outputs
Para crear un signal output, utilizamos la función output()
importada desde @angular/core
. A diferencia de los signal inputs que son de solo lectura dentro del componente, los outputs nos permiten emitir eventos hacia el componente padre:
import { Component, output } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="increment()">{{ count }}</button>
`
})
export class CounterComponent {
count = 0;
// Signal output básico
countChanged = output<number>();
increment() {
this.count++;
this.countChanged.emit(this.count);
}
}
La función output()
retorna un objeto de tipo OutputEmitterRef, que actúa como emisor de eventos tipado. Este tipo garantiza la seguridad de tipos en tiempo de compilación, evitando errores comunes en la comunicación entre componentes.
El tipo OutputEmitterRef
El OutputEmitterRef es la interfaz que encapsula la funcionalidad de emisión de eventos. Proporciona un método emit()
que permite enviar datos al componente padre de forma segura:
import { Component, output, OutputEmitterRef } from '@angular/core';
interface UserAction {
type: 'create' | 'update' | 'delete';
data: any;
}
@Component({
selector: 'app-user-form',
standalone: true,
template: `
<form (ngSubmit)="handleSubmit()">
<input [(ngModel)]="username" placeholder="Username">
<button type="submit">Save User</button>
<button type="button" (click)="handleCancel()">Cancel</button>
</form>
`
})
export class UserFormComponent {
username = '';
// Output tipado con interfaz personalizada
userAction: OutputEmitterRef<UserAction> = output<UserAction>();
cancelled = output<void>();
handleSubmit() {
this.userAction.emit({
type: 'create',
data: { username: this.username }
});
}
handleCancel() {
this.cancelled.emit();
}
}
Outputs opcionales y configuración
Los signal outputs pueden definirse como opcionales, lo que resulta útil cuando no todos los componentes padre necesitan suscribirse a determinados eventos:
import { Component, output } from '@angular/core';
@Component({
selector: 'app-product-card',
standalone: true,
template: `
<div class="card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency }}</p>
<button (click)="addToCart()">Add to Cart</button>
<button (click)="toggleFavorite()" [class.active]="isFavorite">
♥
</button>
</div>
`
})
export class ProductCardComponent {
product = { name: 'Laptop', price: 999 };
isFavorite = false;
// Output requerido - siempre debe ser manejado
productAdded = output<{ id: number; quantity: number }>();
// Output opcional - puede o no ser manejado
favoriteToggled = output<{ id: number; isFavorite: boolean }>({ alias: 'onFavoriteChange' });
addToCart() {
this.productAdded.emit({ id: 1, quantity: 1 });
}
toggleFavorite() {
this.isFavorite = !this.isFavorite;
this.favoriteToggled.emit({ id: 1, isFavorite: this.isFavorite });
}
}
Comparación con @Output() tradicional
La nueva sintaxis simplifica significativamente la definición de outputs. Mientras que con @Output()
necesitábamos crear un EventEmitter
manualmente, output()
encapsula esta funcionalidad de manera más elegante:
Sintaxis tradicional con @Output():
// Enfoque tradicional (no recomendado en Angular 20+)
@Output() dataChanged = new EventEmitter<string>();
@Output() actionCompleted = new EventEmitter<void>();
Sintaxis moderna con output():
// Enfoque moderno recomendado
dataChanged = output<string>();
actionCompleted = output<void>();
Uso en el componente padre
El componente padre puede suscribirse a estos eventos utilizando la sintaxis de binding de eventos habitual. Los signal outputs mantienen la misma compatibilidad con templates que los outputs tradicionales:
@Component({
selector: 'app-parent',
standalone: true,
imports: [ProductCardComponent],
template: `
<app-product-card
(productAdded)="handleProductAdded($event)"
(onFavoriteChange)="handleFavoriteChange($event)">
</app-product-card>
`
})
export class ParentComponent {
handleProductAdded(product: { id: number; quantity: number }) {
console.log('Product added:', product);
}
handleFavoriteChange(favorite: { id: number; isFavorite: boolean }) {
console.log('Favorite changed:', favorite);
}
}
Esta integración perfecta con el sistema de templates existente garantiza que la migración de @Output()
a output()
sea gradual y sin interrupciones, manteniendo toda la funcionalidad mientras se aprovechan las ventajas del nuevo ecosistema de signals.
Emisión de eventos reactivos
Los signal outputs van más allá de ser simples emisores de eventos: forman parte del ecosistema reactivo de Angular, permitiendo crear flujos de datos que responden automáticamente a cambios en el estado. Esta naturaleza reactiva se manifiesta especialmente cuando combinamos outputs con signals, computed signals y effects.
Emisión basada en cambios de signals
Los signal outputs brillan cuando se integran con el sistema de signals. En lugar de emitir eventos manualmente en respuesta a acciones del usuario, podemos crear outputs que reaccionen automáticamente a cambios en signals:
import { Component, output, signal, effect } from '@angular/core';
@Component({
selector: 'app-search-input',
standalone: true,
template: `
<input
[value]="searchTerm()"
(input)="searchTerm.set($event.target.value)"
placeholder="Buscar productos...">
<span class="result-count">{{ resultCount() }} resultados</span>
`
})
export class SearchInputComponent {
searchTerm = signal('');
resultCount = signal(0);
// Output reactivo que se emite cuando cambia el término de búsqueda
searchChanged = output<{ query: string; timestamp: number }>();
constructor() {
// Effect que emite automáticamente cuando cambia searchTerm
effect(() => {
const term = this.searchTerm();
if (term.length >= 2) {
this.searchChanged.emit({
query: term,
timestamp: Date.now()
});
}
});
}
updateResultCount(count: number) {
this.resultCount.set(count);
}
}
Emisión condicional con computed signals
Los computed signals pueden determinar cuándo y qué datos emitir, creando lógica de emisión más sofisticada:
import { Component, output, signal, computed, effect } from '@angular/core';
interface FilterState {
category: string;
priceRange: { min: number; max: number };
inStock: boolean;
}
@Component({
selector: 'app-product-filter',
standalone: true,
template: `
<div class="filter-panel">
<select (change)="updateCategory($event.target.value)">
<option value="">Todas las categorías</option>
<option value="electronics">Electrónicos</option>
<option value="clothing">Ropa</option>
</select>
<div class="price-range">
<input type="range" [value]="minPrice()"
(input)="minPrice.set(+$event.target.value)">
<input type="range" [value]="maxPrice()"
(input)="maxPrice.set(+$event.target.value)">
</div>
<label>
<input type="checkbox" [checked]="inStock()"
(change)="inStock.set($event.target.checked)">
Solo en stock
</label>
</div>
`
})
export class ProductFilterComponent {
category = signal('');
minPrice = signal(0);
maxPrice = signal(1000);
inStock = signal(false);
// Computed que crea el estado completo del filtro
filterState = computed((): FilterState => ({
category: this.category(),
priceRange: { min: this.minPrice(), max: this.maxPrice() },
inStock: this.inStock()
}));
// Computed que determina si el filtro es válido
isValidFilter = computed(() => {
const state = this.filterState();
return state.priceRange.min < state.priceRange.max;
});
// Output que se emite solo cuando el filtro es válido
filterChanged = output<FilterState>();
constructor() {
effect(() => {
if (this.isValidFilter()) {
this.filterChanged.emit(this.filterState());
}
});
}
updateCategory(category: string) {
this.category.set(category);
}
}
Patrones de debounce y throttle reactivos
Los signal outputs permiten implementar patrones de control de flujo de manera más elegante que con EventEmitters tradicionales:
import { Component, output, signal, effect } from '@angular/core';
@Component({
selector: 'app-auto-save-form',
standalone: true,
template: `
<form>
<textarea
[value]="content()"
(input)="content.set($event.target.value)"
placeholder="Escribe tu contenido...">
</textarea>
<div class="status">
@if (isSaving()) {
<span class="saving">Guardando...</span>
} @else {
<span class="saved">Guardado</span>
}
</div>
</form>
`
})
export class AutoSaveFormComponent {
content = signal('');
isSaving = signal(false);
private saveTimeout: any;
// Output que implementa debounce automático
contentSaved = output<{ content: string; savedAt: Date }>();
constructor() {
effect(() => {
const currentContent = this.content();
// Limpiar timeout anterior
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
// Solo guardar si hay contenido
if (currentContent.trim().length > 0) {
this.isSaving.set(true);
// Debounce de 2 segundos
this.saveTimeout = setTimeout(() => {
this.contentSaved.emit({
content: currentContent,
savedAt: new Date()
});
this.isSaving.set(false);
}, 2000);
}
});
}
}
Emisión en cadena y composición de eventos
Los signal outputs facilitan la creación de cadenas de eventos reactivos, donde un output puede desencadenar otros outputs en componentes relacionados:
import { Component, output, signal, input } from '@angular/core';
interface ValidationResult {
field: string;
isValid: boolean;
errors: string[];
}
@Component({
selector: 'app-form-field',
standalone: true,
template: `
<div class="field-group">
<label>{{ label() }}</label>
<input
[value]="value()"
(input)="handleInput($event.target.value)"
[class.invalid]="!isValid()">
@if (!isValid()) {
<div class="errors">
@for (error of validationErrors(); track error) {
<span class="error">{{ error }}</span>
}
</div>
}
</div>
`
})
export class FormFieldComponent {
label = input.required<string>();
value = signal('');
validationErrors = signal<string[]>([]);
// Computed que determina validez
isValid = computed(() => this.validationErrors().length === 0);
// Outputs reactivos para diferentes eventos
valueChanged = output<string>();
validationChanged = output<ValidationResult>();
constructor() {
// Effect que emite cuando cambia el valor
effect(() => {
const currentValue = this.value();
this.valueChanged.emit(currentValue);
});
// Effect que emite cuando cambia la validación
effect(() => {
this.validationChanged.emit({
field: this.label(),
isValid: this.isValid(),
errors: this.validationErrors()
});
});
}
handleInput(newValue: string) {
this.value.set(newValue);
this.validateField(newValue);
}
private validateField(value: string) {
const errors: string[] = [];
if (value.length < 3) {
errors.push('Debe tener al menos 3 caracteres');
}
if (!/^[a-zA-Z0-9\s]+$/.test(value)) {
errors.push('Solo se permiten caracteres alfanuméricos');
}
this.validationErrors.set(errors);
}
}
Integración con el componente padre reactivo
El componente padre puede aprovechar la naturaleza reactiva de los signal outputs para crear flujos de datos complejos:
@Component({
selector: 'app-reactive-form',
standalone: true,
imports: [FormFieldComponent],
template: `
<form>
<app-form-field
label="Nombre"
(valueChanged)="handleFieldChange('name', $event)"
(validationChanged)="handleValidationChange($event)">
</app-form-field>
<app-form-field
label="Email"
(valueChanged)="handleFieldChange('email', $event)"
(validationChanged)="handleValidationChange($event)">
</app-form-field>
<button [disabled]="!isFormValid()">
Enviar formulario
</button>
</form>
`
})
export class ReactiveFormComponent {
formData = signal<Record<string, string>>({});
fieldValidations = signal<ValidationResult[]>([]);
// Computed que determina si todo el formulario es válido
isFormValid = computed(() => {
return this.fieldValidations().every(validation => validation.isValid);
});
handleFieldChange(fieldName: string, value: string) {
this.formData.update(data => ({
...data,
[fieldName]: value
}));
}
handleValidationChange(validation: ValidationResult) {
this.fieldValidations.update(validations => {
const filtered = validations.filter(v => v.field !== validation.field);
return [...filtered, validation];
});
}
}
Esta aproximación reactiva a la emisión de eventos crea aplicaciones más predecibles y fáciles de mantener, donde los cambios de estado se propagan automáticamente a través de la jerarquía de componentes sin necesidad de gestión manual de suscripciones.

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 función output() y su integración con signals en Angular.
- Aprender a definir outputs tipados y opcionales usando OutputEmitterRef.
- Diferenciar la nueva sintaxis output() del decorador @Output() tradicional.
- Implementar emisión reactiva de eventos basada en signals, computed y effects.
- Aplicar patrones avanzados como debounce, throttle y composición de eventos con signal outputs.