In Angular 22, a parent component passes data to a nested (child) component through signal inputs, declared with input(), and the child reports back through signal outputs, declared with output(). For a value both sides change, model() gives two-way binding. This article builds a product list (parent) and a product card (child) with all three, and every snippet was compiled against Angular 22.2.
The @Input() and @Output() decorators that older tutorials use are still supported, but they are the legacy way. Angular's reactivity is now fine-grained: a signal knows exactly which parts of a template read it, so only those parts update. Signal inputs and outputs are how a component joins that system, and they are what you should write in new code.
What changed since the decorator days
If you learned nested components from an older article, these are the differences you will see below:
input(),output()andmodel()replace@Input(),@Output()withEventEmitter, and the input-plus-output pair used for two-way binding.- No
standalone: true. Components have been standalone by default since Angular 19, so the flag is noise. NgModules are not needed. - Shorter names. The current style guide drops the
Componentsuffix: the class isProductCardinproduct-card.ts, notProductCardComponentinproduct-card.component.ts. readonlyandprotected. Mark inputs, outputs and queriesreadonly; mark members used only by the templateprotected.OnPushis the default change detection strategy from Angular 22. Signals make that painless, because reading a signal in a template is what tells Angular when to re-render.@forand@ifreplace*ngForand*ngIf, andinject()replaces constructor injection.
What is a nested component?
A nested component is a component used inside another component's template. The outer one is the parent and the inner one is the child. The parent owns the data and hands a slice of it to each child; the child renders that slice and reports what the user did with it. They talk only through the child's declared inputs and outputs, which keeps both small, testable and reusable.
In our example the parent holds a list of products and renders one <app-product-card> per product.
Pass data down with a signal input
The child declares what it accepts with input(). Each call returns a read-only signal, so you read the value by calling it: product(). Use input.required<T>() when the parent must supply a value; the build fails if the binding is missing, which removes a whole class of "undefined at runtime" bugs.
import { Component, input } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
import { Product } from './product.model';
@Component({
selector: 'app-product-card',
imports: [CurrencyPipe],
template: `
<article class="card" [class.featured]="featured()">
<h3>{{ product().name }}</h3>
<p>{{ product().price | currency }}</p>
</article>
`,
})
export class ProductCard {
// required: the parent MUST bind [product]
readonly product = input.required<Product>();
// optional, with a default value
readonly featured = input(false);
}
The parent binds with the familiar [property] syntax:
<app-product-card [product]="p" [featured]="true" />
Note the imports: [CurrencyPipe]. A standalone component must import every pipe, directive and component its template uses.

Send events up with a signal output
To tell the parent about an action, the child declares an output() and calls .emit(). The parent listens with the (event) binding, exactly as it would for a native DOM event.
import { Component, input, output } from '@angular/core';
import { Product } from './product.model';
@Component({
selector: 'app-product-card',
template: `
<h3>{{ product().name }}</h3>
<button type="button" (click)="addToCart.emit(product())">Add to cart</button>
`,
})
export class ProductCard {
readonly product = input.required<Product>();
readonly addToCart = output<Product>();
}
<app-product-card [product]="p" (addToCart)="addProduct($event)" />
Here $event is typed as Product, the type argument given to output<Product>(). Compared with @Output() addToCart = new EventEmitter<Product>(), an output() is lighter and is not an RxJS subject that code inside the component can misuse. Name outputs for what happened in the domain ("add to cart", "removed"), not for the DOM event behind them, and do not prefix them with "on".
Derive values from inputs with computed()
Because an input is a signal, anything calculated from it should be a computed(). It recalculates only when the input it reads changes, and it replaces the ngOnChanges code that decorator inputs needed.
import { computed } from '@angular/core';
// inside ProductCard
protected readonly isBudget = computed(() => this.product().price < 30);
@if (isBudget()) {
<small>Budget pick</small>
}
Two-way binding with model()
Sometimes the child both receives a value and changes it; a quantity stepper is the classic case. model() creates a writable signal that supports the [(value)] syntax, so parent and child stay in sync without a separate input and output.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-qty-stepper',
template: `
<button type="button" (click)="decrease()">-</button>
<span>{{ quantity() }}</span>
<button type="button" (click)="increase()">+</button>
`,
})
export class QtyStepper {
readonly quantity = model(1);
protected increase() { this.quantity.update(q => q + 1); }
protected decrease() { this.quantity.update(q => Math.max(1, q - 1)); }
reset() { this.quantity.set(1); }
}
<app-qty-stepper [(quantity)]="quantity" />
The parent binds a signal of its own (quantity = signal(1)), and Angular keeps the two in step. Use model() only when the child really edits the value; for read-only data a plain input() is clearer.
Transform inputs as they arrive
input() accepts a transform function that runs on every incoming value. Angular ships booleanAttribute and numberAttribute for the common cases, so a bare attribute can act as true, as native HTML attributes do.
import { booleanAttribute, input } from '@angular/core';
// <app-product-card featured> now gives featured() === true
readonly featured = input(false, { transform: booleanAttribute });
Call the child from the parent with viewChild()
Inputs and outputs cover most needs. When the parent has to call a method on a child, use the signal query viewChild() in place of the old @ViewChild decorator.
import { viewChild } from '@angular/core';
// inside the parent
private readonly stepper = viewChild.required(QtyStepper);
protected clearQuantity() {
this.stepper().reset();
}
Project content with ng-content
Inputs pass data, but sometimes the parent should pass markup. <ng-content> defines a slot in the child that the parent fills; add select for named slots.
<!-- product-card template -->
<article class="card">
<ng-content select="[badge]" />
<h3>{{ product().name }}</h3>
<ng-content />
</article>
<app-product-card [product]="p">
<span badge>New</span>
<p>Ships tomorrow.</p>
</app-product-card>
Put it together in the parent
The parent lists its children in imports, renders them with @for, and gets the cart service with inject(). @for requires a track expression and has an @empty block for an empty list.
import { Component, inject, signal, viewChild } from '@angular/core';
import { ProductCard } from './product-card';
import { QtyStepper } from './qty-stepper';
import { CartService } from './cart.service';
import { Product } from './product.model';
@Component({
selector: 'app-product-list',
imports: [ProductCard, QtyStepper],
template: `
@for (p of products(); track p.id) {
<app-product-card [product]="p" (addToCart)="addProduct($event)" />
} @empty {
<p>No products available.</p>
}
<app-qty-stepper [(quantity)]="quantity" />
<p>Quantity: {{ quantity() }} · Items in cart: {{ cart.count() }}</p>
`,
})
export class ProductList {
protected readonly cart = inject(CartService);
protected readonly quantity = signal(1);
protected readonly products = signal<Product[]>([
{ id: 1, name: 'Keyboard', price: 49 },
{ id: 2, name: 'Mouse', price: 25 },
]);
protected addProduct(product: Product) {
this.cart.add(product);
}
}
cart is protected, not private, because the template reads cart.count(); a private member cannot be used in a template. The service itself uses Angular 22's @Service() decorator, shorthand for @Injectable({ providedIn: 'root' }):
import { Service, computed, signal } from '@angular/core';
import { Product } from './product.model';
@Service()
export class CartService {
private readonly items = signal<Product[]>([]);
readonly count = computed(() => this.items().length);
add(product: Product) {
this.items.update(items => [...items, product]);
}
}
Migrating older components
You do not have to rewrite decorator-based components by hand. Angular ships migrations that do it for you:
ng generate @angular/core:signal-input-migration
ng generate @angular/core:output-migration
ng generate @angular/core:signal-queries-migration
The first converts @Input() to input(), the second @Output() to output(), and the third @ViewChild and @ContentChild to signal queries. Run them one at a time, build, and commit after each.
Key takeaways
- Pass data down with
input(); useinput.required<T>()when the value is mandatory. - Send events up with
output<T>()and.emit(). - Use
model()for two-way[(value)]binding when the child edits the value. - Derive values from inputs with
computed(), notngOnChanges. - Use
viewChild()when the parent must call the child directly. - Drop
standalone: trueand theComponentsuffix; mark inputs and outputsreadonly. @Input()and@Output()still work, but they are the legacy API. Migrate with the schematics above.
Frequently asked questions
What replaced @Input() and @Output() in Angular?
Signal inputs and signal outputs: the input() and output() functions. input() returns a read-only signal and input.required<T>() makes a binding mandatory at build time. The decorators still work for existing code, but new code should use the functions.
Are @Input() and @Output() deprecated?
No. They remain fully supported, and decorator-based and signal-based components can be mixed in one application. The Angular team recommends the signal functions for new projects, and the migrations above convert old code.
How do I do two-way binding between parent and child?
Declare the value with model() in the child and bind it in the parent with [(value)]. Angular also exposes a matching valueChange output, which is why the syntax works.
Do I still need NgModules or standalone: true?
No to both. Components are standalone by default, so the parent simply lists its children in the imports array of its @Component metadata.
How is @for different from *ngFor?
@for is built into the template syntax, needs no import, requires a track expression, and offers an @empty block. It replaces the *ngFor directive.
Comments (0)
No comments yet — be the first to share your thoughts.