| |

Angular 13 ๐Ÿ…ฐ๏ธ Component Communication โ€” @Input and @Output

Components don’t live in isolation. A parent renders a child, passes it data, and listens for events coming back. That’s component communication โ€” the flow of information up and down the component tree. Angular’s two primitives for this are @Input (parent โ†’ child data) and @Output (child โ†’ parent events). Every UI you build in Angular is a tree of components passing data through these two decorators. Getting them right is the foundation of every component you’ll write.

Key point: Data flows down through @Input, events flow up through @Output. A child never reaches into its parent โ€” it emits an event and lets the parent decide what to do. This one-way discipline keeps components reusable and testable. Break it by mutating inputs or reaching into parent state, and the component tree becomes a tangled mess.


The two directions

Component communication has exactly two directions:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Parent                              โ”‚
โ”‚                                      โ”‚
โ”‚  [input]        (output)             โ”‚
โ”‚     โ”‚              โ–ฒ                 โ”‚
โ”‚     โ–ผ              โ”‚                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Child                       โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  • @Input โ€” parent sends data down to child
  • @Output โ€” child sends events up to parent

Parent template:

<app-child
  [data]="parentData"
  (changed)="handleChange($event)">
</app-child>

[data] is a property binding to the child’s @Input. (changed) is an event binding to the child’s @Output. The syntax is identical to binding DOM properties and events โ€” because the same machinery handles both.

The rules:

  • A child receives data via @Input
  • A child emits events via @Output
  • A child never reads or writes parent state directly
  • A parent never calls methods on the child to get data (use outputs instead)

Why this discipline matters: Components are reusable only if their dependencies are explicit. If a child reached into its parent, it would only work inside that parent. By requiring inputs and outputs, Angular forces components to declare what they need and what they produce โ€” making them portable.

Why one-way flow: Bidirectional data flow creates tangles. If a child can both read and write parent state, reasoning about where changes originate becomes impossible. Angular’s discipline โ€” data down, events up โ€” keeps the flow predictable. It’s the same idea as unidirectional data flow in Redux, applied at the component level.


@Input โ€” receiving data

An @Input property is a doorway from the parent into the child.

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

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

The parent binds to those inputs:

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

Angular assigns the bound values to the child’s properties. When the parent’s values change, Angular updates the child’s inputs.

Inputs are set after construction. The constructor runs before Angular assigns inputs. If you need to react to input values, use ngOnInit or ngOnChanges.

export class UserCardComponent implements OnInit {
  @Input() userId!: number;
  user?: User;

  ngOnInit(): void {
    // userId is now set
    this.loadUser();
  }
}

The ! non-null assertion tells TypeScript that userId will be set before ngOnInit runs. Angular guarantees this.

Input types:

@Input() label = '';                    // string with default
@Input() count = 0;                     // number
@Input() disabled = false;              // boolean
@Input() items: string[] = [];          // array
@Input() user!: User;                   // required, set by parent
@Input() config: Config | null = null;  // nullable

Naming conventions: The input name is the property name unless aliased. @Input() name is bound as [name]. @Input('firstName') name is bound as [firstName] but the property is name.

Required inputs: Angular 16+ supports @Input({ required: true }):

@Input({ required: true }) name!: string;

The compiler errors if the parent doesn’t provide it. This catches mistakes at build time.

Why required inputs matter: Without them, a child could be used without providing a critical input, and the error would only appear at runtime. required: true moves the check to compile time. It’s a small win that compounds in larger codebases.


Aliasing inputs

An input’s template name can differ from its property name.

@Input('userName') name = '';

Bound as:

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

Inside the class, it’s this.name. In templates, it’s [userName].

Why alias:

  • Avoid collisions with HTML attributes
  • Shorten names for readability
  • Rename internally without breaking templates
  • Match a public API established by convention

Common pattern:

@Input('ariaLabel') label = '';

Now [ariaLabel] in templates maps to label in the class.

Aliases and required inputs:

@Input({ required: true, alias: 'userId' }) id!: number;

[userId] is required; the property is id.

Why aliases are useful: They decouple the template API from the class implementation. You can rename the internal property without touching templates, or match an existing naming convention without renaming the class. It’s a small flexibility that pays off over time.


@Output โ€” emitting events

An @Output property is an EventEmitter. The child calls .emit() to send a value up to the parent.

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="increment()">+1</button>
    <button (click)="reset()">Reset</button>
  `
})
export class CounterComponent {
  @Output() countChange = new EventEmitter<number>();
  @Output() reset = new EventEmitter<void>();

  private count = 0;

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

  resetCount(): void {
    this.count = 0;
    this.reset.emit();
  }
}

The parent listens:

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

$event is the value passed to .emit().

EventEmitter is a Subject: It extends RxJS Subject, so you can subscribe to it programmatically if needed. In practice, you use it in templates via (output).

Emitting no value:

@Output() closed = new EventEmitter<void>();
// ...
this.closed.emit();

Parent: (closed)="handleClose()".

Emitting complex values:

interface UserEvent {
  id: number;
  action: 'created' | 'updated' | 'deleted';
}

@Output() userChanged = new EventEmitter<UserEvent>();
// ...
this.userChanged.emit({ id: 1, action: 'created' });

Required outputs: Angular doesn’t have a required option for outputs โ€” a child may legitimately have no listener. Use outputs freely.

Why EventEmitter and not a callback: EventEmitter integrates with Angular’s template syntax, supports multiple subscribers, and works with RxJS operators. Passing a callback function would work but wouldn’t compose with the rest of Angular’s binding syntax. EventEmitter is the idiomatic tool.


Two-way communication with the Change convention

A child can expose an input and an output with a naming convention that enables [(value)] two-way binding.

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

  setRating(value: number): void {
    this.rating = value;
    this.ratingChange.emit(value);
  }
}

The output name must be the input name with Change appended.

Parent usage:

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

Angular expands this to:

<app-rating [rating]="userRating" (ratingChange)="userRating = $event"></app-rating>

The [( )] syntax is sugar โ€” it’s just an input plus an output with a specific name.

Common examples:

  • value + valueChange
  • checked + checkedChange
  • selected + selectedChange
  • open + openChange

Why the naming convention: Angular’s compiler recognizes <input>Change as the companion output for a <input> two-way binding. It’s a simple rule that enables a lot of ergonomics.

Why two-way binding is powerful: It reduces boilerplate โ€” the parent writes [(value)] instead of [value] and (valueChange). Under the hood it’s still one-way down and one-way up. The convention just bundles them.


ngOnChanges โ€” reacting to input changes

When an input changes, Angular calls ngOnChanges with a SimpleChanges object.

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

export class UserCardComponent implements OnChanges {
  @Input() userId!: number;
  @Input() theme: 'light' | 'dark' = 'light';

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userId']) {
      const { previousValue, currentValue, firstChange } = changes['userId'];
      console.log('userId changed', previousValue, 'โ†’', currentValue);
      if (firstChange) this.loadUser();
    }
    if (changes['theme']) {
      // theme changed
    }
  }
}

SimpleChanges structure:

{
  [inputName]: {
    previousValue: T;
    currentValue: T;
    firstChange: boolean;
  }
}
  • firstChange โ€” true on the first assignment
  • previousValue โ€” the previous value (undefined on first change)
  • currentValue โ€” the new value

When to use ngOnChanges:

  • You need to react to specific inputs changing
  • You want to distinguish first change from subsequent changes
  • You need the previous value

When to use ngOnInit instead:

  • You only need the initial value
  • The setup logic is one-time

Order: ngOnChanges runs before ngOnInit on the first change, then again on subsequent changes.

Under OnPush change detection: ngOnChanges fires when the input reference changes. If the input is a mutable object that’s mutated in place, it won’t fire.

Why ngOnChanges and not a setter: A setter runs every time the input is assigned, including when the reference is the same. ngOnChanges gives you the previous value and a firstChange flag, and groups all changes into one call. It’s a better fit for reacting to input changes in a controlled way.


Inputs and the change detection model

Inputs are checked during change detection. When the parent’s binding expression changes, Angular updates the child’s input.

Reference equality: Angular compares the new input value to the old using ===. For primitives, that’s value equality. For objects and arrays, it’s reference equality โ€” a new object with the same contents is a change; a mutated object with the same reference is not.

// Parent
items = [1, 2, 3];

addItem(): void {
  this.items.push(4);          // โŒ same reference โ€” child won't see it
  this.items = [...this.items, 4];  // โœ… new reference โ€” child updates
}

With OnPush change detection, the child only checks inputs when the reference changes or when marked for check. Mutating inputs (arrays, objects) won’t trigger an update.

Under Default change detection, Angular checks every component on every CD cycle, so mutated inputs still propagate eventually โ€” but it’s inefficient and hides bugs.

The rule: Treat inputs as immutable. Never mutate them. If the parent wants to change an input, assign a new value.

Why reference equality: Angular needs a cheap way to detect changes. Reference comparison is O(1) and works for any type. Deep equality checks would be expensive. The trade-off is that you must produce new references when values change โ€” which is also how signals, Redux, and most modern state systems work.


Common patterns

Container/presentational split:

Container components hold state, fetch data, and pass it down. Presentational components receive inputs, render, and emit outputs.

// Container
@Component({
  selector: 'app-users-page',
  standalone: true,
  imports: [UserListComponent],
  template: `
    <app-user-list
      [users]="users"
      (selected)="onSelect($event)">
    </app-user-list>
  `
})
export class UsersPageComponent {
  users = signal<User[]>([]);
  onSelect(user: User) { /* ... */ }
}

// Presentational
@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    @for (user of users; track user.id) {
      <button (click)="selected.emit(user)">{{ user.name }}</button>
    }
  `
})
export class UserListComponent {
  @Input() users: User[] = [];
  @Output() selected = new EventEmitter<User>();
}

Why this split: Container components know about services and state. Presentational components only know about their inputs and outputs. The presentational ones are reusable, testable, and easy to reason about.

Passing a callback vs an output: Prefer outputs. Callbacks are harder to test and don’t compose with the template syntax.

Reusable form control: A child input + output pair that reports value changes.

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

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

Usage: [(value)]="rating" in the parent.

Why these patterns matter: They keep components focused. Presentational components are pure โ€” given the same inputs, they render the same output. That makes them easy to test and safe to reuse. The container/presentational split is the oldest pattern in frontend architecture, and it still works.


A full example

A product list with selection.

// product-card.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CurrencyPipe } from '@angular/common';

export interface Product {
  id: number;
  name: string;
  price: number;
}

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [CurrencyPipe],
  template: `
    <div class="card" [class.selected]="selected">
      <h3>{{ product.name }}</h3>
      <p>{{ product.price | currency:'EUR' }}</p>
      <button (click)="select.emit(product)">
        {{ selected ? 'Selected' : 'Select' }}
      </button>
    </div>
  `,
  styles: `
    .card { border: 1px solid #ccc; padding: 1rem; }
    .card.selected { border-color: green; }
  `
})
export class ProductCardComponent {
  @Input({ required: true }) product!: Product;
  @Input() selected = false;
  @Output() select = new EventEmitter<Product>();
}
// product-list.component.ts
import { Component, signal } from '@angular/core';
import { ProductCardComponent, Product } from './product-card.component';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ProductCardComponent],
  template: `
    <h2>Products</h2>
    @for (product of products(); track product.id) {
      <app-product-card
        [product]="product"
        [selected]="selectedId() === product.id"
        (select)="onSelect($event)">
      </app-product-card>
    }
    <p>Total selected: {{ selectedId() }}</p>
  `
})
export class ProductListComponent {
  products = signal<Product[]>([
    { id: 1, name: 'Keyboard', price: 79.99 },
    { id: 2, name: 'Mouse', price: 39.5 },
    { id: 3, name: 'Monitor', price: 249 }
  ]);
  selectedId = signal<number | null>(null);

  onSelect(product: Product): void {
    this.selectedId.set(product.id);
  }
}

The parent holds the list and the selection. The child receives a product and its selected state, renders it, and emits a select event when clicked. The parent handles the event and updates state.

What this shows:

  • @Input for product and selected state
  • @Output for the selection event
  • The child never mutates parent data โ€” it emits and lets the parent decide
  • The child is reusable โ€” it doesn’t know the list exists

Why this shape: The card component knows nothing about the list or the selection logic โ€” it just renders a product and reports a click. The list owns the state and decides what a click means. That separation is what makes the card reusable in another context without changes.


Complete Example Session

# ============================================
# PART 1: GENERATE THE CHILD
# ============================================

ng generate component product-card
# [ CREATE src/app/product-card/product-card.component.ts ]
# [ CREATE src/app/product-card/product-card.component.html ]
# [ CREATE src/app/product-card/product-card.component.css ]

# ============================================
# PART 2: WRITE THE CHILD
# ============================================

cat > src/app/product-card/product-card.component.ts << 'EOF'
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CurrencyPipe } from '@angular/common';

export interface Product {
  id: number;
  name: string;
  price: number;
}

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [CurrencyPipe],
  template: `
    <div class="card" [class.selected]="selected">
      <h3>{{ product.name }}</h3>
      <p>{{ product.price | currency:'EUR' }}</p>
      <button (click)="select.emit(product)">
        {{ selected ? 'Selected' : 'Select' }}
      </button>
    </div>
  `
})
export class ProductCardComponent {
  @Input({ required: true }) product!: Product;
  @Input() selected = false;
  @Output() select = new EventEmitter<Product>();
}
EOF

# ============================================
# PART 3: WRITE THE PARENT
# ============================================

cat > src/app/product-list/product-list.component.ts << 'EOF'
import { Component, signal } from '@angular/core';
import { ProductCardComponent, Product } from '../product-card/product-card.component';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ProductCardComponent],
  template: `
    <h2>Products</h2>
    @for (p of products(); track p.id) {
      <app-product-card
        [product]="p"
        [selected]="selectedId() === p.id"
        (select)="onSelect($event)">
      </app-product-card>
    }
    <p>Selected ID: {{ selectedId() }}</p>
  `
})
export class ProductListComponent {
  products = signal<Product[]>([
    { id: 1, name: 'Keyboard', price: 79.99 },
    { id: 2, name: 'Mouse', price: 39.5 },
    { id: 3, name: 'Monitor', price: 249 }
  ]);
  selectedId = signal<number | null>(null);

  onSelect(product: Product): void {
    this.selectedId.set(product.id);
  }
}
EOF

# ============================================
# PART 4: TWO-WAY BINDING
# ============================================

cat > src/app/rating/rating.component.ts << 'EOF'
import { Component, Input, Output, EventEmitter } 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 {
  @Input() value = 0;
  @Output() valueChange = new EventEmitter<number>();

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

# ============================================
# PART 5: USE TWO-WAY BINDING
# ============================================

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

@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [RatingComponent],
  template: `
    <h2>Rate us</h2>
    <app-rating [(value)]="rating"></app-rating>
    <p>You rated: {{ rating }}</p>
  `
})
export class DemoComponent {
  rating = 0;
}
EOF

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

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

Every pattern is exercised โ€” inputs, outputs, required inputs, two-way binding via the Change convention, and the container/presentational split.

Why this exercise covers everything: It’s the whole communication story: parent passes data down, child emits events up, two-way binding ties a child control to parent state. Once these patterns are familiar, every component you build follows the same shape.


Quick Reference

@Input

FormMeaning
@Input() name = 'x'Input with default
@Input() name!: TRequired, set by parent
@Input({ required: true })Required โ€” compile error if missing
@Input('alias') nameAliased input
@Input({ alias: 'x' }) nameModern alias syntax

@Output

FormMeaning
@Output() evt = new EventEmitter<T>()Typed emitter
@Output() evt = new EventEmitter<void>()No payload
this.evt.emit(value)Send event

Template Syntax

SyntaxDirection
[input]="expr"Parent โ†’ child
(output)="handler($event)"Child โ†’ parent
[(value)]="prop"Two-way
[value]="p" (valueChange)="p = $event"Expanded two-way

Two-Way Convention

InputOutput
valuevalueChange
checkedcheckedChange
openopenChange
selectedselectedChange

Lifecycle

HookWhen
constructorBefore inputs set
ngOnChangesBefore ngOnInit, then on changes
ngOnInitAfter first ngOnChanges
ngOnDestroyOn destroy

SimpleChanges

FieldMeaning
previousValueOld value (undefined first time)
currentValueNew value
firstChangeTrue on first assignment

Required Inputs

SyntaxBehavior
@Input({ required: true })Compile error if missing
@Input() name!: TTypeScript-level requirement
@Input() name = defaultOptional, safe default

Input Mutability

ActionEffect
Reassign input valueโœ… triggers change
Mutate input object/arrayโŒ no change detected under OnPush
Use immutable patternsโœ… recommended

Container vs Presentational

AspectContainerPresentational
StateOwnsReceives
ServicesUsesDoesn’t
InputsFewMany
OutputsFewMany
ReusabilityLowHigh

Best Practices

โœ… Do This:

// Type your inputs and outputs
@Input() user!: User;                                  // โœ…
@Output() select = new EventEmitter<User>();           // โœ…

// Mark critical inputs as required
@Input({ required: true }) userId!: number;            // โœ…

// Use ngOnChanges to react to input changes
ngOnChanges(changes: SimpleChanges): void { }          // โœ…

// Emit new values; don't mutate inputs
this.valueChange.emit(newValue);                       // โœ…

// Use the Change naming convention for two-way
@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();    // โœ…

// Treat inputs as immutable
this.items = [...this.items, newItem];                 // โœ…

// Use container/presentational split
// Container holds state; child renders             // โœ…

// Emit specific, typed values
this.selected.emit(user);                              // โœ…

โŒ Don’t Do This:

// Don't read inputs in the constructor
constructor() {
  console.log(this.userId);  // โŒ undefined              // โŒ
}

// Don't mutate inputs
this.items.push(newItem);  // โŒ same reference            // โŒ

// Don't call parent methods directly
// (no direct parent reference)                          // โŒ

// Don't emit on every keystroke without debounce
(input)="valueChange.emit($event.target.value)"          // โš ๏ธ  consider debounce

// Don't use EventEmitter for anything other than outputs
@Input() stream = new EventEmitter<T>();                 // โŒ

// Don't forget to handle undefined in ngOnChanges
if (changes['x']) { }  // โœ… always check                    // โœ…

// Don't assume inputs are set in the constructor
class Child { @Input() x!: number; constructor() { this.x.toString(); } } // โŒ

// Don't skip required inputs when the child needs them
@Input() user!: User;  // โš ๏ธ  use required: true when possible

Common Pitfalls

PitfallProblemSolution
Reading input in constructorUndefinedUse ngOnInit/ngOnChanges
Mutating input objectsNo change detectedReassign new values
Missing EventEmitter importCompile errorImport from @angular/core
Wrong output name for two-way[(x)] doesn’t workUse xChange
Forgetting requiredRuntime errorUse @Input({ required: true })
Emitting same referenceChange not detectedEmit new values
Mutating input arrayChild doesn’t updateCreate new array
Not checking changesErrors on unset inputsCheck if (changes['x'])

Real-World Examples

1. Basic input

@Input() label = '';

2. Required input

@Input({ required: true }) user!: User;

3. Input with default

@Input() count = 0;

4. Aliased input

@Input('ariaLabel') label = '';

5. Basic output

@Output() clicked = new EventEmitter<void>();

6. Typed output

@Output() selected = new EventEmitter<User>();

7. Emit an event

this.selected.emit(user);

8. Listen in parent

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

9. Two-way binding

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

10. Custom two-way

@Input() value = 0;
@Output() valueChange = new EventEmitter<number>();

11. React to input changes

ngOnChanges(c: SimpleChanges): void {
  if (c['userId']) this.loadUser();
}

12. Check first change

if (c['x']?.firstChange) { /* first time */ }

13. Input array with immutable update

this.items = [...this.items, item];

14. Pass data down

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

15. Send events up

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

16. Container component

@Component({ template: `<app-list [items]="items()" (pick)="onPick($event)">` })

17. Presentational component

@Component({ template: `@for (i of items; track i.id) { <button (click)="pick.emit(i)">{{ i.name }}</button> }` })

18. Output with payload

@Output() changed = new EventEmitter<{ id: number; value: string }>();

19. Required input with alias

@Input({ required: true, alias: 'userId' }) id!: number;

20. Test an output

let emitted: User | undefined;
component.selected.subscribe(u => emitted = u);
component.selected.emit(user);
expect(emitted).toBe(user);

Visual: Data Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Parent Component                            โ”‚
โ”‚                                              โ”‚
โ”‚  user = { name: 'Alice' }                    โ”‚
โ”‚                                              โ”‚
โ”‚  <app-card                                   โ”‚
โ”‚    [user]="user"          โ† input            โ”‚
โ”‚    (selected)="onPick($event)">  โ† output    โ”‚
โ”‚  </app-card>                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚                                โ–ฒ
       โ”‚ [user]                    (selected)
       โ–ผ                                โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Child Component                             โ”‚
โ”‚                                              โ”‚
โ”‚  @Input() user!: User                        โ”‚
โ”‚  @Output() selected = new EventEmitter()     โ”‚
โ”‚                                              โ”‚
โ”‚  onPick() { this.selected.emit(this.user); } โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Two-Way Expansion

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  You write:                                  โ”‚
โ”‚                                              โ”‚
โ”‚  <app-rating [(value)]="rating"></app-rating>โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Angular expands to:                         โ”‚
โ”‚                                              โ”‚
โ”‚  <app-rating                                 โ”‚
โ”‚    [value]="rating"                          โ”‚
โ”‚    (valueChange)="rating = $event">          โ”‚
โ”‚  </app-rating>                               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Lifecycle Order

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  constructor                                 โ”‚
โ”‚  (no inputs yet)                             โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  ngOnChanges (first)                         โ”‚
โ”‚  (inputs now set)                            โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  ngOnInit                                    โ”‚
โ”‚  (safe to use inputs)                        โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  ngOnChanges (subsequent)                    โ”‚
โ”‚  (input changed)                             โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  ngOnDestroy                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: SimpleChanges

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ngOnChanges(changes)                        โ”‚
โ”‚                                              โ”‚
โ”‚  changes = {                                 โ”‚
โ”‚    userId: {                                 โ”‚
โ”‚      previousValue: undefined,  โ† first time โ”‚
โ”‚      currentValue: 42,                       โ”‚
โ”‚      firstChange: true                       โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  On next change:                             โ”‚
โ”‚                                              โ”‚
โ”‚    previousValue: 42,                        โ”‚
โ”‚    currentValue: 99,                         โ”‚
โ”‚    firstChange: false                        โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Container / Presentational

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Container                                   โ”‚
โ”‚  โ”€ owns state                                โ”‚
โ”‚  โ”€ uses services                             โ”‚
โ”‚  โ”€ passes data down                          โ”‚
โ”‚  โ”€ handles events                            โ”‚
โ”‚                                              โ”‚
โ”‚  @Component({                                โ”‚
โ”‚    template: `                               โ”‚
โ”‚      <app-list                               โ”‚
โ”‚        [items]="items()"                     โ”‚
โ”‚        (select)="onSelect($event)">          โ”‚
โ”‚      </app-list>                             โ”‚
โ”‚    `                                         โ”‚
โ”‚  })                                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  inputs / outputs
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Presentational                              โ”‚
โ”‚  โ”€ pure inputs                               โ”‚
โ”‚  โ”€ emits outputs                             โ”‚
โ”‚  โ”€ no services                               โ”‚
โ”‚  โ”€ reusable                                  โ”‚
โ”‚                                              โ”‚
โ”‚  @Component({                                โ”‚
โ”‚    template: `                               โ”‚
โ”‚      @for (i of items; track i.id) {         โ”‚
โ”‚        <button (click)="select.emit(i)">     โ”‚
โ”‚          {{ i.name }}                        โ”‚
โ”‚        </button>                             โ”‚
โ”‚      }                                       โ”‚
โ”‚    `                                         โ”‚
โ”‚  })                                          โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Input Mutability

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Reassign (recommended)                      โ”‚
โ”‚                                              โ”‚
โ”‚  this.items = [...this.items, newItem];      โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ new reference                             โ”‚
โ”‚  โ†’ child sees change                         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Mutate (avoid)                              โ”‚
โ”‚                                              โ”‚
โ”‚  this.items.push(newItem);                   โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ same reference                            โ”‚
โ”‚  โ†’ child doesn't update under OnPush         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Output Naming for Two-Way

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Child:                                      โ”‚
โ”‚                                              โ”‚
โ”‚  @Input()  value                             โ”‚
โ”‚  @Output() valueChange                       โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”‚  same base name + "Change"           โ”‚
โ”‚       โ–ผ                                      โ”‚
โ”‚  Parent:                                     โ”‚
โ”‚                                              โ”‚
โ”‚  [(value)]="prop"                            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Communication Checklist

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Parent passes data?   โ†’ @Input              โ”‚
โ”‚  Child sends events?   โ†’ @Output             โ”‚
โ”‚  Both?                 โ†’ [(value)]           โ”‚
โ”‚  React to input?       โ†’ ngOnChanges         โ”‚
โ”‚  Use input on init?    โ†’ ngOnInit            โ”‚
โ”‚  Mark as required?     โ†’ @Input required     โ”‚
โ”‚  Alias for template?   โ†’ @Input('name')      โ”‚
โ”‚  Reusable component?   โ†’ container/present.  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
@InputParent โ†’ child data
@OutputChild โ†’ parent events
EventEmitterEmits output values
ngOnChangesReacts to input changes
SimpleChangesDiff of input changes
Two-way binding[(value)] sugar
xChange conventionEnables two-way
Required input@Input({ required: true })
Alias@Input('name')
Container/presentationalSplit of state and display

Key takeaways:

  • @Input receives data from the parent; @Output emits events to the parent
  • The template syntax mirrors DOM bindings โ€” [input], (output), [(twoWay)]
  • Inputs are set after the constructor โ€” use ngOnInit or ngOnChanges to react
  • ngOnChanges gives you previous and current values, plus a firstChange flag
  • Treat inputs as immutable โ€” reassign, don’t mutate, especially under OnPush
  • Two-way binding uses the x + xChange naming convention
  • Mark critical inputs required with @Input({ required: true })
  • Alias inputs when you want a different template name
  • EventEmitter is a Subject โ€” typed, testable, composable
  • Split components into containers (state) and presentational (display)
  • Children never reach into parents โ€” they emit events and let the parent decide
  • Typed inputs and outputs make components self-documenting

Remember: Component communication is one-way down, one-way up. @Input brings data in; @Output sends events out. The parent owns the state and decides what events mean. The child receives inputs, renders, and emits โ€” nothing more. That discipline is what keeps components reusable, testable, and easy to reason about. Every Angular app is a tree of components passing data through these two decorators. Get them right and the rest of the framework follows.


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!