Signal Inputs & Outputs (v18)

📚 Signal-based Inputs & Outputs

Angular 18 made signal inputs and outputs stable:

  • input(): Creates signal input (optional, can have default)
  • input.required(): Required signal input
  • output(): Creates output emitter (replaces @Output)
  • model(): Two-way binding signal (input + output combined)
  • Signal-based: Inputs are signals, use () to read

🎯 Interview Questions

  • Q1: What is the difference between input() and @Input()?
  • A: input() returns a signal that tracks changes automatically. @Input is a property that requires manual change detection.
  • Q2: How do you create a required signal input?
  • A: Use input.required<T>(). The returned signal is guaranteed to have a value (no undefined).
  • Q3: What is the model() function?
  • A: Creates two-way binding. Combines input signal with automatic output for changes. Parent uses [(model)]="value".
  • Q4: Can you transform input values?
  • A: Yes, use transform option: input(0, { transform: numberAttribute }) to convert string attributes to numbers.

🔧 Live Demo - Parent Component

Parent Controls

5

Child Component:

Hello World

Count: 42

Select a value (two-way bound):

Event Log:

  • No events yet

📋 API Comparison

Old (Decorator)New (Function)Type
@Input() name: stringname = input<string>()InputSignal<string | undefined>
@Input() name = 'default'name = input('default')InputSignal<string>
@Input({ required: true }) name!: stringname = input.required<string>()InputSignal<string>
@Output() click = new EventEmitter()click = output<MouseEvent>()OutputEmitterRef<MouseEvent>
@Input() + @Output() valuevalue = model(0)ModelSignal<number>

💻 Signal Inputs/Outputs Code


import { Component, input, output, model } from '@angular/core';

@Component({
  selector: 'app-product-card',
  template: `
    <div>
      <h2>{{ name() }}</h2>
      <p>Price: ${{ price() }}</p>
      <p>Quantity: {{ quantity() }}</p>
      <button (click)="addToCart.emit(name())">Add</button>
      
      <!-- Two-way binding with model -->
      <input 
        type="number" 
        [value]="quantity()" 
        (input)="quantity.set(+$event.target.value)"
      />
    </div>
  `
})
export class ProductCardComponent {
  // Required input - must be provided
  name = input.required<string>();
  
  // Optional input with default
  price = input(0);
  
  // Optional input without default
  description = input<string>();
  
  // Input with transform (string attribute to number)
  maxItems = input(10, { transform: numberAttribute });
  
  // Input with alias
  productId = input.required<string>({ alias: 'id' });
  
  // Output
  addToCart = output<string>();
  
  // Two-way binding with model()
  quantity = model(1);
}

// ===== Usage in parent template =====

<app-product-card
  [name]="product.name"
  [price]="product.price"
  [id]="product.id"
  [maxItems]="5"
  (addToCart)="handleAddToCart($event)"
  [(quantity)]="selectedQuantity"
/>

// ===== Reading signal inputs =====

// In component class:
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);

// inputs are signals, so they work with computed/effect
effect(() => {
  console.log('Name changed:', this.name());
});

// ===== Input transforms =====
import { booleanAttribute, numberAttribute } from '@angular/core';

@Component({...})
export class MyComponent {
  // Transform "true"/"false" string to boolean
  disabled = input(false, { transform: booleanAttribute });
  
  // Transform string to number
  count = input(0, { transform: numberAttribute });
  
  // Custom transform
  tags = input<string[]>([], {
    transform: (value: string) => value.split(',')
  });
}

// Usage: <my-comp disabled="true" count="5" tags="a,b,c" />