All articles

Angular Nested Components with input() and output()

In Angular 22, a nested (child) component receives data from its parent through the signal-based input() function and sends events back up with output(). You…

0 · log in to like, save & follow Share on LinkedIn Share on X

In Angular 22, a nested (child) component receives data from its parent through the signal-based input() function and sends events back up with output(). You pass data down by binding to the child's inputs in the parent template, and you send actions up by subscribing to the child's outputs. This article builds a product-list parent and a product-card child so you can see both directions, plus two-way binding with model(), content projection, and the new control-flow syntax.

Angular Nested Components with input() and output()

Angular 22 makes standalone components the default and treats signal-based input(), output(), and model() as the standard way to wire components together. The older @Input() and @Output() decorators still work, but the signal functions are what you should reach for in new code.

What is a nested component?

A nested component is simply a component used inside another component's template. The outer one is the parent, the inner one is the child. The parent owns the data and hands slices of it to each child; each child stays focused on rendering one thing and reporting what the user did with it. This keeps components small, testable, and reusable.

In our example the parent holds a list of products and renders one <app-product-card> per product. Each card knows how to display a single product and emit an "add to cart" event. The parent never reaches inside the child, and the child never reaches out to the parent — they communicate only through the explicit inputs and outputs on the child's public surface. That contract is what makes nested components predictable: you can read a component's @Component metadata and its input/output declarations and know exactly how it talks to the rest of the app.

Because everything here is signal-based, values flow through the change-detection system automatically. When the parent updates a signal that feeds an input, the child re-renders the affected expression and nothing else. This is also what makes Angular 22's optional zoneless mode practical: signals track their own dependencies, so the framework no longer needs Zone.js to guess what changed.

Pass data down with input()

The child declares what it accepts using 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 — Angular reports an error at build time if the binding is missing, which removes a whole class of "undefined at runtime" bugs.

import { Component, input } from '@angular/core';
import { Product } from './product.model';

@Component({
  selector: 'app-product-card',
  standalone: true,
  template: `
    <article class="card">
      <h3>{{ product().name }}</h3>
      <p>{{ product().price | currency }}</p>
    </article>
  `,
})
export class ProductCardComponent {
  // required: the parent MUST bind [product]
  product = input.required<Product>();
  // optional with a default value
  featured = input(false);
}

The parent binds to these inputs with the familiar [property] syntax:

<app-product-card [product]="p" [featured]="true" />

Angular product-card child component using input and output

Emit events up with output()

To notify the parent of an action, the child exposes an output() and calls .emit(). An output() returns an emitter you can type; the parent listens with the (event) binding, exactly like a native DOM event.

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

@Component({
  selector: 'app-product-card',
  standalone: true,
  template: `
    <article class="card">
      <h3>{{ product().name }}</h3>
      <button (click)="addToCart.emit(product())">Add to cart</button>
    </article>
  `,
})
export class ProductCardComponent {
  product = input.required<Product>();
  addToCart = output<Product>();
}

The parent reacts to the emitted event:

<app-product-card [product]="p" (addToCart)="onAddToCart($event)" />

Here $event is strongly typed as Product, because that is the type argument you gave to output<Product>(). This is a real improvement over the old @Output() addToCart = new EventEmitter<Product>() pattern: output() is lighter, cannot accidentally be subscribed to inside the component like a full observable, and reads as a first-class part of the component's public API rather than a stream you happen to expose.

Keep outputs coarse and intentional. A child should emit meaningful domain events — "add to cart", "quantity changed", "removed" — not leak low-level DOM events upward. The parent decides what those events mean, so the same product-card can live in a catalog page, a wishlist, or a search result without change.

Two-way binding with model()

Sometimes the child both receives a value and needs to change it — a quantity stepper is the classic case. model() creates a writable signal that supports the [(value)] banana-in-a-box syntax, so parent and child stay in sync without a separate input and output pair.

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

@Component({
  selector: 'app-qty-stepper',
  standalone: true,
  template: `
    <button (click)="dec()">-</button>
    <span>{{ quantity() }}</span>
    <button (click)="inc()">+</button>
  `,
})
export class QtyStepperComponent {
  quantity = model<number>(1);
  inc() { this.quantity.update(q => q + 1); }
  dec() { this.quantity.update(q => Math.max(1, q - 1)); }
}

The parent binds both directions in one shot:

<app-qty-stepper [(quantity)]="cartQty" />

Under the hood model() is a signal that Angular also exposes as an implicit quantityChange output, which is why the banana-in-a-box syntax works. You can still bind one direction only — [quantity]="cartQty" — when the parent wants to seed a value without listening for changes. Reach for model() only when the child genuinely owns edits to 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, which is handy for coercion or normalization. Angular ships booleanAttribute and numberAttribute for the two most common cases; booleanAttribute lets a bare attribute act as true, matching how native HTML boolean attributes behave. The transform runs before the signal is updated, so every reader of the input always sees the normalized value.

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

@Component({ /* ... */ })
export class ProductCardComponent {
  // <app-product-card featured> becomes featured() === true
  featured = input(false, { transform: booleanAttribute });
  // uppercase the label as it comes in
  badge = input('', { transform: (v: string) => v.toUpperCase() });
}

Project content with ng-content

Inputs pass data, but sometimes you want the parent to pass markup. Content projection with <ng-content> lets the child define a slot the parent fills. Use the select attribute for multiple named slots.

@Component({
  selector: 'app-product-card',
  standalone: true,
  template: `
    <article class="card">
      <ng-content select="[header]"></ng-content>
      <ng-content></ng-content>
    </article>
  `,
})
export class ProductCardComponent {}
<app-product-card>
  <h3 header>Wireless Mouse</h3>
  <p>Ships tomorrow.</p>
</app-product-card>

Render the list in the parent with @for and @if

Angular 22's built-in control flow replaces *ngFor and *ngIf. Use @for with a mandatory track expression for efficient list rendering and @if for conditionals — no imports, no NgModule. The parent also uses inject() for dependency injection instead of a constructor parameter.

import { Component, inject, signal } from '@angular/core';
import { ProductCardComponent } from './product-card.component';
import { CartService } from './cart.service';
import { Product } from './product.model';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ProductCardComponent],
  template: `
    @if (products().length) {
      @for (p of products(); track p.id) {
        <app-product-card
          [product]="p"
          (addToCart)="onAddToCart($event)" />
      }
    } @else {
      <p>No products available.</p>
    }
    <p>Items in cart: {{ cart.count() }}</p>
  `,
})
export class ProductListComponent {
  private cart = inject(CartService);
  protected products = signal<Product[]>([
    { id: 1, name: 'Keyboard', price: 49 },
    { id: 2, name: 'Mouse', price: 25 },
  ]);

  onAddToCart(product: Product) {
    this.cart.add(product);
  }
}

The inject(CartService) call retrieves the shared service; cart.count() is a signal the template reads reactively.

Key takeaways

  • Use input() to pass data down; use input.required<T>() when the value is mandatory so Angular enforces it at build time.
  • Use output<T>() plus .emit() to send events up, and listen with the (event) binding.
  • Use model() for two-way [(value)] binding when the child edits a value the parent owns.
  • Pass a transform function to input() to coerce or normalize incoming values.
  • Use <ng-content> to project markup from the parent into the child.
  • In Angular 22, standalone components, @for/@if, and inject() are the defaults — reach for signal APIs over the legacy @Input()/@Output() decorators.

Frequently asked questions

What replaced @Input() and @Output() in Angular 22?

The signal-based input() and output() functions. @Input() and @Output() still work for backward compatibility, but input() returns a read-only signal and input.required<T>() enforces mandatory bindings, so they are preferred in new Angular 22 code.

How do I do two-way binding between parent and child?

Declare the value with model() in the child, then bind it in the parent with [(value)]. Angular wires up the read and write automatically, so you no longer need a matching input/output pair for the common "value plus valueChange" pattern.

Do I still need NgModules for nested components?

No. Standalone components are the default in Angular 22. The parent lists its child in the imports array of its own @Component metadata, and you bootstrap the app with bootstrapApplication and an app.config.ts instead of an AppModule.

How is @for different from *ngFor?

@for is built into the template syntax, needs no imports, and requires a track expression for stable, efficient DOM updates. It is faster and less error-prone than the structural *ngFor directive it replaces, and it pairs with @if, @else, and @switch.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.