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 dependencycomputedderives values from inputs automaticallyeffectreacts to input changes withoutngOnChangesOnPushcomponents 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 acomputedoreffectautomatically tracks it โ when the input changes, dependent computations re-run. That’s what replacesngOnChanges. 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 anOutputEmitterRef, not aSubject- 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.
OutputEmitterRefcan’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
ratingin - Output โ the child’s
.set()emitsvalueChangeto updaterating
How it works:
model()returns aModelSignal<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 theChangenaming convention โ an@Input() valueplus an@Output() valueChange.model()bundles them into one, removes the naming requirement, and integrates with signals. The parent still writes[(value)]="prop". The child writesthis.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 = defaultbecomesx = input(default)@Input({ required: true }) x!: Tbecomesx = input.required<T>()@Output() y = new EventEmitter<T>()becomesy = output<T>()@Input() v; @Output() vChangebecomesv = model(default)- Reading an input changes from
this.xtothis.x() - Writing a model changes from
this.v = y; this.vChange.emit(y)tothis.v.set(y) ngOnChangesis replaced bycomputedoreffect
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:
| Task | ngOnChanges | Signals |
|---|---|---|
| React to input change | ngOnChanges(changes) | computed or effect |
| Get previous value | changes.x.previousValue | Not available โ use linkedSignal if needed |
| First change check | changes.x.firstChange | Constructor runs before inputs โ use effect |
| Multiple inputs | Iterate SimpleChanges | Read multiple signals |
| Side effects | In ngOnChanges | In effect |
| Derived state | Manual re-compute | computed |
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
computedbeatsngOnChanges: You declare the derivation, not the reaction.fullName = computed(() => ...)is a formula; Angular figures out when to re-run it.ngOnChangesmakes 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 (likeeffect) may not give the bound value yet.effectruns 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
computedthat reads@Inputvalues 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 defaultsinput.required()would be used if placeholder had no defaultmodel()for two-wayoutput()for eventscomputed()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, noEventEmitter. This is the template for every new Angular component.
Quick Reference
API Comparison
| Old | New |
|---|---|
@Input() x = default | x = input(default) |
@Input({ required: true }) x!: T | x = input.required<T>() |
@Input('alias') x | x = input(d, { alias: 'alias' }) |
@Output() y = new EventEmitter<T>() | y = output<T>() |
@Input() v; @Output() vChange | v = model(default) |
ngOnChanges(changes) | computed / effect |
Reading and Writing
| Operation | Syntax |
|---|---|
| Read input | this.name() |
| Read required input | this.userId() |
| Emit output | this.selected.emit(v) |
| Read model | this.value() |
| Write model | this.value.set(v) |
| Update model | this.value.update(v => ...) |
Declaration Forms
| Form | Meaning |
|---|---|
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
| Binding | Direction |
|---|---|
[input]="x" | Parent โ child |
(output)="handler($event)" | Child โ parent |
[(model)]="x" | Both |
input (no brackets) | Static string value |
Signal Inputs in Templates
| Use | Syntax |
|---|---|
| Direct read | {{ name() }} |
| With pipe | {{ name() | uppercase }} |
| With condition | @if (user()) { } |
| With loop | @for (item of items(); track item.id) { } |
| In computed | computed(() => this.x() * 2) |
| In effect | effect(() => console.log(this.x())) |
Reacting to Input Changes
| Need | Tool |
|---|---|
| Derived value | computed |
| Side effect | effect |
| Writable, resets on input change | linkedSignal |
| One-time setup | ngOnInit |
Migration Cheatsheet
| Old | New |
|---|---|
this.x | this.x() |
this.x = y | this.x.set(y) |
this.x = y; this.xChange.emit(y) | this.x.set(y) |
ngOnChanges(c) | computed/effect |
changes.x.firstChange | effect in constructor |
new EventEmitter<T>() | output<T>() |
Import
| API | From |
|---|---|
input | @angular/core |
output | @angular/core |
model | @angular/core |
computed | @angular/core |
effect | @angular/core |
signal | @angular/core |
Availability
| API | Since |
|---|---|
input() | 17.1 |
output() | 17.3 |
model() | 17.2 |
linkedSignal() | 19.0 |
input.required() | 17.1 |
Parent vs Child Views
| Aspect | Parent | Child |
|---|---|---|
| Binding syntax | [input], (output), [(model)] | Same as always |
| Reads input | N/A | this.input() |
| Emits output | N/A | this.output.emit(v) |
| Updates model | N/A | this.model.set(v) |
| Two-way state | [(value)]="x" | model(d) |
When to Use Which
| Need | API |
|---|---|
| Read-only input | input() |
| Required input | input.required() |
| Emit events | output() |
| Two-way binding | model() |
| Derived state | computed() |
| Side effect from input change | effect() |
| Writable state from input | linkedSignal() |
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
| Pitfall | Problem | Solution |
|---|---|---|
Forgetting () | Reads the function, not the value | Call the signal |
Writing to input() | Not assignable | Use model() |
Using EventEmitter with output() | Not the right type | output<T>() |
Subscribing to output() | Not an observable | Use template binding |
ngOnChanges with signals | Doesn’t fire | Use computed/effect |
| Reading signal in constructor | Default value | Use effect |
| Mixing decorators and signals | Confusing | Commit to one |
Missing input.required | Runtime error | Use input.required<T>() |
Forgetting alias in migration | Template mismatch | Use { alias: 'x' } |
| Template with static string | Missing brackets | name="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
| Concept | Meaning |
|---|---|
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 |
OutputEmitterRef | Output emitter type |
ModelSignal<T> | Two-way signal |
InputSignal<T> | Input signal type |
Key takeaways:
input()declares a signal input โ read it withthis.x()input.required<T>()makes an input required at compile timeoutput<T>()replacesEventEmitterfor template-only eventsmodel()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 parentcomputed()derives state from inputs โ re-runs automaticallyeffect()runs side effects when inputs change โ replacesngOnChangeslinkedSignal()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!