Output moderno con output()

Avanzado
Angular
Angular
Actualizado: 25/09/2025

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 - 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 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.