| |

Angular 14 ๐Ÿ…ฐ๏ธ Modern Inputs and Outputs โ€” input(), output(), model()

Angular 17.1 introduced signal-based inputs, outputs, and two-way bindings โ€” the modern replacement for @Input, @Output, and the Change naming convention. The new APIs are input(), output(), and model(), and they integrate with Angular’s signal system. Inputs become signal reads instead of plain properties, outputs become plain emitters, and two-way bindings use model() โ€” a single function that produces both the input and output. The old decorators still work; the new functions are what new code should use.

Key point: input() returns a signal, not a value. You read it by calling the signal (this.name()), and the compiler tracks dependencies automatically. output() replaces EventEmitter with a simpler emitter. model() combines input and output into one two-way binding. All three integrate with signals โ€” meaning computed, effect, and OnPush change detection work naturally.


Why signal-based inputs exist

The decorator API has real problems that signals solve.

The problems with @Input:

  • Inputs are plain properties โ€” no reactivity, no automatic dependency tracking
  • You react to changes with ngOnChanges, which is verbose and only fires on reference change
  • Under OnPush, you must remember to update when inputs change
  • Inputs can be read before they’re set (constructor problem)
  • Required inputs need a separate decorator option

How signals fix them:

  • input() returns a signal โ€” reading it tracks the dependency
  • computed derives values from inputs automatically
  • effect reacts to input changes without ngOnChanges
  • OnPush components update when signals change
  • Inputs are always available at read time โ€” no constructor issue
  • Required inputs are enforced at compile time

The comparison:

// Decorator (old)
@Input() name = '';

// Signal (new)
name = input('');

Both declare an input. But the signal version gives you reactivity, and reading it (this.name()) participates in the reactive graph.

Why signals are the future: Angular is moving toward signal-based reactivity across the framework โ€” change detection, forms, HTTP, and more. Signal inputs are the foundation. Learning them now means your components are ready for the rest of the signal migration. The decorator API remains supported but won’t get new features.


input() โ€” signal-based inputs

input() declares an input as a signal.

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

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <h3>{{ name() }}</h3>
    <p>{{ email() }}</p>
  `
})
export class UserCardComponent {
  name = input('');
  email = input('');
}

Read the input by calling the signal:

this.name();     // current value

The parent uses the same binding syntax:

<app-user-card [name]="user.name" [email]="user.email"></app-user-card>

Nothing changes on the parent side โ€” inputs are still inputs. What changes is how the child reads them.

With a type:

name = input<string>('');
count = input(0);
items = input<string[]>([]);
user = input<User | null>(null);

TypeScript infers the type from the default. Pass an explicit type parameter when the default is null or the type is wider than the default.

Required inputs:

userId = input.required<number>();

input.required() has no default. The compiler errors if the parent doesn’t provide it. That’s the equivalent of @Input({ required: true }) โ€” but enforced at compile time with no runtime check.

Aliased inputs:

name = input('', { alias: 'userName' });

The template binds [userName] but the property is name.

Transforms: input() accepts a transform function that runs on the incoming value.

count = input(0, { transform: (v: string | number) => Number(v) });

Useful when a parent might pass a string or a number โ€” the input coerces to the expected type.

Reading an input: Always call it as a function.

// โœ…
const currentName = this.name();

// โŒ
const currentName = this.name;  // this is the signal, not the value

The signal itself is a function โ€” reading it returns the current value and registers a dependency.

Why input() returns a signal: A signal is a reactive reference. Reading it inside a computed or effect automatically tracks it โ€” when the input changes, dependent computations re-run. That’s what replaces ngOnChanges. A plain property can’t do this; a signal can.


output() โ€” signal-based outputs

output() replaces EventEmitter with a simpler emitter.

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `<button (click)="increment()">+</button>`
})
export class CounterComponent {
  countChange = output<number>();
  private count = 0;

  increment(): void {
    this.count++;
    this.countChange.emit(this.count);
  }
}

The API mirrors EventEmitter โ€” .emit(value) sends a value to the parent. The parent listens the same way:

<app-counter (countChange)="onCount($event)"></app-counter>

Differences from EventEmitter:

  • output() returns an OutputEmitterRef, not a Subject
  • It’s not an observable โ€” you can’t .subscribe() to it
  • It’s not used for anything other than emitting to the template
  • It’s simpler and lighter than EventEmitter

No payload:

closed = output<void>();
// ...
this.closed.emit();

With payload:

selected = output<User>();
// ...
this.selected.emit(user);

Required outputs? No such thing โ€” outputs are optional by nature. A child may emit events that no parent listens to, and that’s fine.

Why not EventEmitter: EventEmitter is an RxJS Subject โ€” an observable you can subscribe to. That flexibility was rarely used for outputs; outputs are almost always template-only. output() narrows the API to what’s actually needed: a typed emitter.

Why a new emitter type: Fewer capabilities, fewer gotchas. OutputEmitterRef can’t be subscribed to or piped โ€” but it also can’t accidentally be used as an observable. It’s the right tool for the job: emit values to the template. The name says what it does.


model() โ€” two-way bindings

model() declares a two-way binding โ€” an input and its matching output in one function.

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

@Component({
  selector: 'app-rating',
  standalone: true,
  template: `
    @for (n of [1, 2, 3, 4, 5]; track n) {
      <button (click)="setValue(n)" [class.active]="n <= value()">
        โ˜…
      </button>
    }
  `
})
export class RatingComponent {
  value = model(0);

  setValue(n: number): void {
    this.value.set(n);     // updates the model and emits
  }
}

The parent uses two-way binding:

<app-rating [(value)]="rating"></app-rating>

model() handles both directions:

  • Input โ€” the parent passes rating in
  • Output โ€” the child’s .set() emits valueChange to update rating

How it works:

  • model() returns a ModelSignal<T>
  • Reading it: this.value()
  • Writing it: this.value.set(newValue)
  • Updating it: this.value.update(v => v + 1)

Calling .set() or .update() both updates the local signal and emits the Change output โ€” the parent’s bound property is automatically updated.

Required models:

value = model.required<number>();

The parent must provide [(value)].

Models with transforms:

count = model(0, { transform: (v: string | number) => Number(v) });

Aliased models:

value = model(0, { alias: 'rating' });

The parent binds [(rating)]; the property is value.

Models vs inputs: A model() is essentially an input plus an output that stay in sync. If you only need one direction, use input() or output(). If you need the child to modify the value, use model().

Why model() matters: Two-way binding used to require the Change naming convention โ€” an @Input() value plus an @Output() valueChange. model() bundles them into one, removes the naming requirement, and integrates with signals. The parent still writes [(value)]="prop". The child writes this.value.set(x) instead of emitting manually.


Migrating from decorators

The decorator API still works. Migrating is optional but recommended.

Before:

export class UserCardComponent {
  @Input() name = '';
  @Input({ required: true }) userId!: number;
  @Output() selected = new EventEmitter<User>();

  onSelect(user: User): void {
    this.selected.emit(user);
  }
}

After:

export class UserCardComponent {
  name = input('');
  userId = input.required<number>();
  selected = output<User>();

  onSelect(user: User): void {
    this.selected.emit(user);
  }
}

Before (two-way):

export class RatingComponent {
  @Input() value = 0;
  @Output() valueChange = new EventEmitter<number>();

  setValue(n: number): void {
    this.value = n;
    this.valueChange.emit(n);
  }
}

After:

export class RatingComponent {
  value = model(0);

  setValue(n: number): void {
    this.value.set(n);
  }
}

Key differences in the migration:

  • @Input() x = default becomes x = input(default)
  • @Input({ required: true }) x!: T becomes x = input.required<T>()
  • @Output() y = new EventEmitter<T>() becomes y = output<T>()
  • @Input() v; @Output() vChange becomes v = model(default)
  • Reading an input changes from this.x to this.x()
  • Writing a model changes from this.v = y; this.vChange.emit(y) to this.v.set(y)
  • ngOnChanges is replaced by computed or effect

The most common mistake in migration: Forgetting to call the signal.

// After migration
template: `<p>{{ name() }}</p>`  // โœ…
template: `<p>{{ name }}</p>`    // โŒ renders the function itself

name is a function; name() is the value.

Why migrate now: New Angular APIs โ€” resources, signal forms, signal queries โ€” assume signal inputs. Old decorators work but don’t participate in the signal graph. Migrating unlocks the modern toolset and simplifies change detection. The migration is largely mechanical and can be done incrementally.


Reacting to inputs without ngOnChanges

The signal API replaces ngOnChanges with computed and effect.

With computed:

export class UserCardComponent {
  firstName = input('');
  lastName = input('');

  fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

fullName re-computes whenever either input changes โ€” automatically, with dependency tracking.

With effect:

export class UserCardComponent {
  userId = input.required<number>();
  user = signal<User | null>(null);

  constructor() {
    effect(() => {
      const id = this.userId();
      this.loadUser(id);
    });
  }

  private loadUser(id: number): void {
    // fetch
  }
}

The effect runs whenever userId changes โ€” no ngOnChanges, no SimpleChanges object, no manual tracking.

With input + computed for derived state:

count = input(0);
doubled = computed(() => this.count() * 2);
isEven = computed(() => this.count() % 2 === 0);

Every derived signal updates automatically when count changes.

Comparison to ngOnChanges:

TaskngOnChangesSignals
React to input changengOnChanges(changes)computed or effect
Get previous valuechanges.x.previousValueNot available โ€” use linkedSignal if needed
First change checkchanges.x.firstChangeConstructor runs before inputs โ€” use effect
Multiple inputsIterate SimpleChangesRead multiple signals
Side effectsIn ngOnChangesIn effect
Derived stateManual re-computecomputed

When linkedSignal is needed: If you want a writable signal that resets when an input changes, linkedSignal (Angular 19+) provides that.

userId = input.required<number>();
selectedId = linkedSignal(() => this.userId());

selectedId starts as the input value and can be updated locally โ€” but resets when userId changes.

Why computed beats ngOnChanges: You declare the derivation, not the reaction. fullName = computed(() => ...) is a formula; Angular figures out when to re-run it. ngOnChanges makes you manually detect which input changed and what to do. The signal approach is declarative; the decorator approach is imperative.


Inputs in templates and lifecycle

Signal inputs change how you read inputs in the template and when they’re available.

Templates:

<h3>{{ name() }}</h3>
<p>{{ count() + 1 }}</p>
@if (user()) {
  <p>{{ user()!.email }}</p>
}

Read like any other signal. Since signals participate in change detection, OnPush components update when inputs change.

In the constructor: Signal inputs are available in the constructor. Unlike decorator inputs, you don’t have to wait for ngOnInit.

export class ChildComponent {
  userId = input.required<number>();

  constructor() {
    // This works with signals โ€” the input is available.
    // With decorators, userId would be undefined here.
    effect(() => {
      console.log(this.userId());
    });
  }
}

Actually โ€” reading a signal input in the constructor is allowed, but the value is only meaningful inside an effect (which runs after change detection). Direct reads in the constructor give the default value (or throw for required inputs). Use effect for anything that reacts to input values.

In ngOnInit:

ngOnInit(): void {
  const id = this.userId();     // โœ… value is available
  this.loadUser(id);
}

ngOnInit runs after inputs are set, so the signal returns the actual value.

In computed:

fullName = computed(() => `${this.first()()} ${this.last()()}`);

computed tracks the input signals automatically.

In effect:

constructor() {
  effect(() => {
    console.log('userId:', this.userId());
  });
}

The effect re-runs whenever userId changes.

Why the constructor behaves differently: With decorators, inputs aren’t set until after the constructor โ€” reading them there gives undefined. Signals are different: the input signal exists immediately, but reading it outside a reactive context (like effect) may not give the bound value yet. effect runs after inputs are set, so it’s the safe place to react.


Mixing decorators and signals

You can use @Input and input() in the same component โ€” but avoid it.

export class MixedComponent {
  @Input() legacyName = '';       // decorator
  modernCount = input(0);          // signal
}

Both work. The decorator input can be read directly (this.legacyName), the signal input needs a call (this.modernCount()). Templates handle both.

Why you might mix temporarily:

  • During a migration
  • When integrating with code that uses decorators
  • When a library requires @Input

Why you shouldn’t mix long-term:

  • Two mental models to keep track of
  • Signal-based derivations don’t track decorator inputs
  • Change detection behaves differently for each
  • Migration is simpler if you commit to one

Migration tip: Migrate component by component. Convert inputs first, then outputs, then two-way bindings. Test each step. Don’t try to convert everything at once.

Why consistency matters: The signal graph only tracks signal reads. A computed that reads @Input values won’t re-run when they change โ€” because decorator inputs aren’t signals. Mixing gives you the worst of both worlds: you keep the old mental model and lose the new benefits. Commit to signals for new code.


A full example

A search box with signal inputs, outputs, and two-way binding.

// search-box.component.ts
import { Component, input, output, model, computed } from '@angular/core';

@Component({
  selector: 'app-search-box',
  standalone: true,
  template: `
    <div class="search">
      <input
        [value]="query()"
        (input)="onInput($event)"
        [placeholder]="placeholder()"
        [disabled]="disabled()">

      @if (query()) {
        <button (click)="clear()">โœ•</button>
      }

      <small>{{ charCount() }} chars</small>
    </div>
  `
})
export class SearchBoxComponent {
  // inputs
  placeholder = input('Search...');
  disabled = input(false);

  // two-way binding
  query = model('');

  // output
  submitted = output<string>();

  // derived from input
  charCount = computed(() => this.query().length);

  onInput(event: Event): void {
    const value = (event.target as HTMLInputElement).value;
    this.query.set(value);
  }

  clear(): void {
    this.query.set('');
  }

  submit(): void {
    this.submitted.emit(this.query());
  }
}

Parent:

@Component({
  selector: 'app-page',
  standalone: true,
  imports: [SearchBoxComponent],
  template: `
    <app-search-box
      [(query)]="searchQuery"
      placeholder="Find products..."
      (submitted)="onSearch($event)">
    </app-search-box>

    <p>Searching for: {{ searchQuery() }}</p>
  `
})
export class PageComponent {
  searchQuery = signal('');
  onSearch(q: string): void {
    console.log('Searching:', q);
  }
}

The child declares three signal inputs, one two-way binding, and one output. The parent binds [(query)], listens to (submitted), and shares the same searchQuery state.

What this shows:

  • input() with defaults
  • input.required() would be used if placeholder had no default
  • model() for two-way
  • output() for events
  • computed() deriving from the model
  • Signals integrate with template bindings

Why this shape: It’s how a real search box would be built. Inputs configure the box. A model exposes the current query to the parent. An output notifies the parent when a search is submitted. Derived state (character count) is a computed. Everything is signals.


Complete Example Session

# ============================================
# PART 1: GENERATE A COMPONENT
# ============================================

ng generate component rating
# [ CREATE src/app/rating/rating.component.ts ]

# ============================================
# PART 2: SIGNAL INPUTS
# ============================================

cat > src/app/user-card/user-card.component.ts << 'EOF'
import { Component, input, computed } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <h3>{{ fullName() }}</h3>
    <p>{{ email() }}</p>
  `
})
export class UserCardComponent {
  firstName = input('');
  lastName = input('');
  email = input('');

  fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}
EOF

# ============================================
# PART 3: SIGNAL OUTPUTS
# ============================================

cat > src/app/counter/counter.component.ts << 'EOF'
import { Component, output, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="increment()">+</button>
    <span>{{ count() }}</span>
  `
})
export class CounterComponent {
  count = signal(0);
  countChange = output<number>();

  increment(): void {
    this.count.update(c => c + 1);
    this.countChange.emit(this.count());
  }
}
EOF

# ============================================
# PART 4: MODEL โ€” TWO-WAY BINDING
# ============================================

cat > src/app/rating/rating.component.ts << 'EOF'
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-rating',
  standalone: true,
  template: `
    @for (n of [1, 2, 3, 4, 5]; track n) {
      <button (click)="setValue(n)" [class.active]="n <= value()">
        โ˜…
      </button>
    }
  `
})
export class RatingComponent {
  value = model(0);

  setValue(n: number): void {
    this.value.set(n);
  }
}
EOF

# ============================================
# PART 5: USE THEM
# ============================================

cat > src/app/demo/demo.component.ts << 'EOF'
import { Component, signal } from '@angular/core';
import { UserCardComponent } from '../user-card/user-card.component';
import { CounterComponent } from '../counter/counter.component';
import { RatingComponent } from '../rating/rating.component';

@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [UserCardComponent, CounterComponent, RatingComponent],
  template: `
    <app-user-card
      firstName="Alice"
      lastName="Johnson"
      email="alice@example.com">
    </app-user-card>

    <app-counter (countChange)="onCount($event)"></app-counter>
    <p>Count from child: {{ count() }}</p>

    <app-rating [(value)]="rating"></app-rating>
    <p>You rated: {{ rating() }} stars</p>
  `
})
export class DemoComponent {
  count = signal(0);
  rating = signal(0);

  onCount(value: number): void {
    this.count.set(value);
  }
}
EOF

# ============================================
# PART 6: SERVE
# ============================================

ng serve
# [ Local:   http://localhost:4200/ ]

Every modern API is exercised โ€” input(), output(), model(), computed(), and signal integration.

Why this shape: It shows the full modern component contract: inputs for configuration, outputs for events, models for two-way, computed for derived state. No decorators, no ngOnChanges, no EventEmitter. This is the template for every new Angular component.


Quick Reference

API Comparison

OldNew
@Input() x = defaultx = input(default)
@Input({ required: true }) x!: Tx = input.required<T>()
@Input('alias') xx = input(d, { alias: 'alias' })
@Output() y = new EventEmitter<T>()y = output<T>()
@Input() v; @Output() vChangev = model(default)
ngOnChanges(changes)computed / effect

Reading and Writing

OperationSyntax
Read inputthis.name()
Read required inputthis.userId()
Emit outputthis.selected.emit(v)
Read modelthis.value()
Write modelthis.value.set(v)
Update modelthis.value.update(v => ...)

Declaration Forms

FormMeaning
input()Required input (inferred type)
input(default)Optional input with default
input.required<T>()Required input, explicit type
input(d, { alias })Aliased input
input(d, { transform })Input with transform
output<T>()Typed emitter
output()Untyped emitter
model(d)Two-way binding
model.required<T>()Required two-way
model(d, { alias })Aliased two-way

Template Bindings

BindingDirection
[input]="x"Parent โ†’ child
(output)="handler($event)"Child โ†’ parent
[(model)]="x"Both
input (no brackets)Static string value

Signal Inputs in Templates

UseSyntax
Direct read{{ name() }}
With pipe{{ name() | uppercase }}
With condition@if (user()) { }
With loop@for (item of items(); track item.id) { }
In computedcomputed(() => this.x() * 2)
In effecteffect(() => console.log(this.x()))

Reacting to Input Changes

NeedTool
Derived valuecomputed
Side effecteffect
Writable, resets on input changelinkedSignal
One-time setupngOnInit

Migration Cheatsheet

OldNew
this.xthis.x()
this.x = ythis.x.set(y)
this.x = y; this.xChange.emit(y)this.x.set(y)
ngOnChanges(c)computed/effect
changes.x.firstChangeeffect in constructor
new EventEmitter<T>()output<T>()

Import

APIFrom
input@angular/core
output@angular/core
model@angular/core
computed@angular/core
effect@angular/core
signal@angular/core

Availability

APISince
input()17.1
output()17.3
model()17.2
linkedSignal()19.0
input.required()17.1

Parent vs Child Views

AspectParentChild
Binding syntax[input], (output), [(model)]Same as always
Reads inputN/Athis.input()
Emits outputN/Athis.output.emit(v)
Updates modelN/Athis.model.set(v)
Two-way state[(value)]="x"model(d)

When to Use Which

NeedAPI
Read-only inputinput()
Required inputinput.required()
Emit eventsoutput()
Two-way bindingmodel()
Derived statecomputed()
Side effect from input changeeffect()
Writable state from inputlinkedSignal()

Best Practices

โœ… Do This:

// Use signal inputs for new code
name = input('');                                         // โœ…

// Mark required inputs with input.required
userId = input.required<number>();                        // โœ…

// Use model() for two-way bindings
value = model(0);                                         // โœ…

// Read inputs as functions
const n = this.name();                                    // โœ…

// Use computed for derived state
fullName = computed(() => `${this.first()} ${this.last()()}`);  // โœ…

// Use effect for side effects from inputs
constructor() {
  effect(() => this.load(this.userId()));
}                                                         // โœ…

// Update models with set or update
this.value.set(5);
this.value.update(v => v + 1);                            // โœ…

// Type your inputs and outputs
selected = output<User>();                                // โœ…

// Mix old and new only during migration
// (commit to signals for new code)                       // โœ…

โŒ Don’t Do This:

// Don't forget to call signal inputs
template: `{{ name }}`  // โŒ renders function                   // โŒ

// Don't write to signal inputs directly
this.name = 'x';  // โŒ not assignable                      // โŒ

// Don't use ngOnChanges with signal inputs
ngOnChanges() { }  // โš ๏ธ  won't fire properly                // โš ๏ธ

// Don't use EventEmitter with output()
selected = new EventEmitter<User>();  // โŒ use output()        // โŒ

// Don't subscribe to output()
this.selected.subscribe()  // โŒ not an observable           // โŒ

// Don't mix decorators and signals in the same input
@Input() a = ''; b = input('');  // โš ๏ธ  pick one              // โš ๏ธ

// Don't read signal inputs in the constructor directly
constructor() { this.name(); }  // โš ๏ธ  default value only   // โš ๏ธ

// Don't forget to import the new APIs
import { input } from '@angular/core';                    // โœ…

Common Pitfalls

PitfallProblemSolution
Forgetting ()Reads the function, not the valueCall the signal
Writing to input()Not assignableUse model()
Using EventEmitter with output()Not the right typeoutput<T>()
Subscribing to output()Not an observableUse template binding
ngOnChanges with signalsDoesn’t fireUse computed/effect
Reading signal in constructorDefault valueUse effect
Mixing decorators and signalsConfusingCommit to one
Missing input.requiredRuntime errorUse input.required<T>()
Forgetting alias in migrationTemplate mismatchUse { alias: 'x' }
Template with static stringMissing bracketsname="value" for literals

Real-World Examples

1. Basic signal input

name = input('');

2. Input with default

count = input(0);

3. Required input

userId = input.required<number>();

4. Aliased input

name = input('', { alias: 'userName' });

5. Input with transform

count = input(0, { transform: (v: string | number) => Number(v) });

6. Basic output

selected = output<User>();

7. Emit an output

this.selected.emit(user);

8. No-payload output

closed = output<void>();
this.closed.emit();

9. Two-way binding

value = model(0);

10. Write a model

this.value.set(5);

11. Update a model

this.value.update(v => v + 1);

12. Required model

value = model.required<number>();

13. Computed from input

fullName = computed(() => `${this.first()} ${this.last()()}`);

14. Effect from input

constructor() {
  effect(() => console.log(this.userId()));
}

15. Use in template

<p>{{ name() }}</p>

16. Use in condition

@if (user()) { <p>{{ user()!.email }}</p> }

17. Use in loop

@for (item of items(); track item.id) { }

18. Pass input from parent

<app-card [user]="currentUser"></app-card>

19. Listen to output

<app-card (selected)="onSelect($event)"></app-card>

20. Two-way from parent

<app-rating [(value)]="rating"></app-rating>

Visual: Signal Input Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Parent                                      โ”‚
โ”‚                                              โ”‚
โ”‚  <app-card [name]="'Alice'">                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚ [name]
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Child                                       โ”‚
โ”‚                                              โ”‚
โ”‚  name = input('')                            โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  this.name()  โ†’  'Alice'                     โ”‚
โ”‚                                              โ”‚
โ”‚  computed(() => this.name().toUpperCase())   โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ automatically re-computes          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: model() Two-Way

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Parent                                      โ”‚
โ”‚                                              โ”‚
โ”‚  <app-rating [(value)]="rating">             โ”‚
โ”‚                                              โ”‚
โ”‚  rating = signal(0)                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚                                โ–ฒ
       โ”‚ [value]                  (valueChange)
       โ–ผ                                โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Child                                       โ”‚
โ”‚                                              โ”‚
โ”‚  value = model(0)                            โ”‚
โ”‚                                              โ”‚
โ”‚  setValue(n) {                               โ”‚
โ”‚    this.value.set(n)  โ”€โ”€โ–บ emits valueChange  โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: input() vs @Input

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  @Input()                                    โ”‚
โ”‚                                              โ”‚
โ”‚  @Input() name = '';                         โ”‚
โ”‚                                              โ”‚
โ”‚  Template: {{ name }}                        โ”‚
โ”‚  Class:    this.name                         โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ plain property                            โ”‚
โ”‚  โ†’ no reactivity                             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  input()                                     โ”‚
โ”‚                                              โ”‚
โ”‚  name = input('');                           โ”‚
โ”‚                                              โ”‚
โ”‚  Template: {{ name() }}                      โ”‚
โ”‚  Class:    this.name()                       โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ signal                                    โ”‚
โ”‚  โ†’ reactive, computed-friendly               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Reacting to Input Change

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Old: ngOnChanges                            โ”‚
โ”‚                                              โ”‚
โ”‚  ngOnChanges(changes) {                      โ”‚
โ”‚    if (changes['x']) {                       โ”‚
โ”‚      const newVal = changes['x'].currentValueโ”‚
โ”‚      this.doThing(newVal);                   โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Verbose, manual                             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  New: effect                                 โ”‚
โ”‚                                              โ”‚
โ”‚  constructor() {                             โ”‚
โ”‚    effect(() => {                            โ”‚
โ”‚      const val = this.x();                   โ”‚
โ”‚      this.doThing(val);                      โ”‚
โ”‚    });                                       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Declarative, automatic                      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Signal APIs in a Component

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  @Component({ ... })                         โ”‚
โ”‚  export class SearchBoxComponent {           โ”‚
โ”‚                                              โ”‚
โ”‚    // inputs                                 โ”‚
โ”‚    placeholder = input('Search...');         โ”‚
โ”‚    disabled    = input(false);               โ”‚
โ”‚                                              โ”‚
โ”‚    // two-way                                โ”‚
โ”‚    query = model('');                        โ”‚
โ”‚                                              โ”‚
โ”‚    // outputs                                โ”‚
โ”‚    submitted = output<string>();             โ”‚
โ”‚                                              โ”‚
โ”‚    // derived                                โ”‚
โ”‚    charCount = computed(() => this.query().length);โ”‚
โ”‚                                              โ”‚
โ”‚    // methods                                โ”‚
โ”‚    clear() { this.query.set(''); }           โ”‚
โ”‚    submit() { this.submitted.emit(this.query()); }โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Migration Path

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 1: Convert inputs                      โ”‚
โ”‚                                              โ”‚
โ”‚  @Input() x = ''  โ†’  x = input('')           โ”‚
โ”‚  Update template: {{ x }} โ†’ {{ x() }}        โ”‚
โ”‚  Update class: this.x โ†’ this.x()             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 2: Convert outputs                     โ”‚
โ”‚                                              โ”‚
โ”‚  @Output() y = new EventEmitter<T>()         โ”‚
โ”‚         โ†’ y = output<T>()                    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 3: Convert two-way bindings            โ”‚
โ”‚                                              โ”‚
โ”‚  @Input() v; @Output() vChange               โ”‚
โ”‚         โ†’ v = model(default)                 โ”‚
โ”‚                                              โ”‚
โ”‚  Update: this.v = n; this.vChange.emit(n)    โ”‚
โ”‚         โ†’ this.v.set(n)                      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 4: Replace ngOnChanges                 โ”‚
โ”‚                                              โ”‚
โ”‚  ngOnChanges โ†’ computed / effect             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Choosing the Right API

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Parent โ†’ child only      โ†’  input()         โ”‚
โ”‚  Child โ†’ parent only      โ†’  output()        โ”‚
โ”‚  Both directions          โ†’  model()         โ”‚
โ”‚  Derived state            โ†’  computed()      โ”‚
โ”‚  Side effect from change  โ†’  effect()        โ”‚
โ”‚  Writable + resets        โ†’  linkedSignal()  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
input()Signal-based input
input.required<T>()Required signal input
output<T>()Signal-based output
model()Two-way binding signal
computed()Derived reactive value
effect()Side effect from signals
linkedSignal()Writable derived signal
OutputEmitterRefOutput emitter type
ModelSignal<T>Two-way signal
InputSignal<T>Input signal type

Key takeaways:

  • input() declares a signal input โ€” read it with this.x()
  • input.required<T>() makes an input required at compile time
  • output<T>() replaces EventEmitter for template-only events
  • model() combines input and output for two-way binding
  • Read inputs as functions โ€” {{ name() }} in templates, this.name() in classes
  • model().set() updates the value and emits the change to the parent
  • computed() derives state from inputs โ€” re-runs automatically
  • effect() runs side effects when inputs change โ€” replaces ngOnChanges
  • linkedSignal() creates a writable signal that resets when an input changes
  • Templates and parents use the same binding syntax โ€” [input], (output), [(model)]
  • Migration is incremental โ€” convert inputs, then outputs, then two-way, then ngOnChanges
  • Signal inputs work with OnPush โ€” change detection updates automatically
  • New code should use signal APIs โ€” decorators remain for compatibility

Remember: input(), output(), and model() are the modern way to communicate between components. They integrate with signals โ€” meaning computed, effect, and OnPush change detection work together. Read inputs as functions, emit with output, and use model() for two-way. Migrate component by component, and let the signals do the tracking. Everything else โ€” reactivity, change detection, derived state โ€” falls out of the design.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!