Post

Modern Angular 11: Passing Data to Child Components with input()

Modern Angular 11: Passing Data to Child Components with input()

This is lesson 11 of the Modern Angular Course. In the previous lesson, we rendered a list of products using signals and @for. Each card still shows placeholder content. Now it is time to connect real data from the parent component to the child component using Angular’s modern input() signal API.

This post is part of the Modern Angular Course series. Check the course page for the full episode list.

In this post, we cover:

  • The parent-to-child communication pattern
  • Creating a required input with input.required<T>()
  • Rendering input data in the child template
  • Passing data from parent to child with property binding
  • Using optional inputs with default values
  • Common pitfalls to avoid

The Mental Model

Before writing any code, let’s align on the pattern:

  • The parent component owns the data (the product list)
  • The parent loops through products with @for
  • The parent passes one product to each child card
  • The child receives data through input() and renders it

This is classic parent-to-child communication, but using modern signal-based inputs. It is the first form of component communication we cover in this course.

Creating a Required Input

Open product-card.ts and add a required input for the product:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { Product } from '../product';

@Component({
  selector: 'app-product-card',
  imports: [MatCardModule, MatButtonModule],
  templateUrl: './product-card.html',
  styleUrl: './product-card.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCard {
  readonly product = input.required<Product>();
}

The key line:

1
readonly product = input.required<Product>();

Here is what it means:

  1. input.required marks this input as mandatory
  2. <Product> gives us strict typing
  3. The input is signal-based, so we read it with product()

If the parent forgets to pass this input, Angular will warn us at build time and runtime — which is exactly what we want for critical data.

The Legacy @Input() Decorator

If you look at older Angular codebases or tutorials, you will often see inputs written like this:

1
2
3
4
5
6
import { Component, Input } from '@angular/core';
import { Product } from '../product';

export class ProductCard {
  @Input({ required: true }) product!: Product;
}

This is the decorator-based approach that was the standard before Angular v17.

The key differences compared to the modern input() API:

  • @Input() is a decorator — input() is a function that returns a signal
  • With @Input(), the value is a plain class property — with input(), it is a signal you read with product()
  • @Input({ required: true }) requires a non-null assertion (!) — input.required() handles this cleanly without extra syntax
  • @Input() does not integrate with Angular’s signal reactivity system — input() does, which makes it work naturally with computed() and effect()

Both syntaxes work in Angular today, but @Input() is deprecated and the Angular team indicates intent to remove it in v22. The signal-based input() is the current recommended approach for all new code, and that is what we use throughout this course.

Rendering Input Data in the Template

Now let’s replace the placeholder content in the card template with real data.

File: product-card.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mat-card class="product-card">
  <mat-card-header>
    <mat-card-title>{{ product().name }}</mat-card-title>
    <mat-card-subtitle>Product</mat-card-subtitle>
  </mat-card-header>

  <mat-card-content>
    <p>{{ product().description }}</p>
    <p class="price">{{ product().price | currency }}</p>
  </mat-card-content>

  <mat-card-actions>
    <button matButton>Add to Cart</button>
  </mat-card-actions>
</mat-card>

Notice we call product() in the template. That is because input() returns an input signal, and signals are read with parentheses — the same pattern we have been using since lesson 5.

Passing Data from Parent to Child

Now we connect the parent template to the child input.

File: products-grid.html

1
2
3
4
5
6
7
8
9
10
<div class="products-grid">
  @for (product of products(); track product.id) {
    <app-product-card [product]="product"></app-product-card>
  } @empty {
    <div class="empty-state">
      <h3>No products available</h3>
      <p>Check back later for new arrivals!</p>
    </div>
  }
</div>

The binding that connects everything:

1
[product]="product"

The left side is the child input name. The right side is the loop variable from the parent. Now each ProductCard instance gets one product object from the list.

Optional Inputs with Default Values

Required inputs are perfect for must-have data like product. But sometimes we want configurable behavior that can fall back to a default.

Let’s add an optional input to control the button label text.

File: product-card.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { Product } from '../product';

@Component({
  selector: 'app-product-card',
  imports: [MatCardModule, MatButtonModule],
  templateUrl: './product-card.html',
  styleUrl: './product-card.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductCard {
  readonly product = input.required<Product>();
  readonly addButtonLabel = input('Add to Cart');
}

Here, input('Add to Cart') means:

  • This input is optional
  • The default value is 'Add to Cart'
  • The parent can override it when needed

Now update the template to use it:

File: product-card.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mat-card class="product-card">
  <mat-card-header>
    <mat-card-title>{{ product().name }}</mat-card-title>
    <mat-card-subtitle>Product</mat-card-subtitle>
  </mat-card-header>

  <mat-card-content>
    <p>{{ product().description }}</p>
    <p class="price">{{ product().price | currency }}</p>
  </mat-card-content>

  <mat-card-actions>
    <button matButton>{{ addButtonLabel() }}</button>
  </mat-card-actions>
</mat-card>

And if you ever want to override the button text from the parent:

1
<app-product-card [product]="product" [addButtonLabel]="'View Details'"></app-product-card>

This makes the component more reusable without extra complexity.

Product cards displaying real data passed from the parent component with input()

Common Pitfalls

Two common mistakes to avoid:

  1. Forgetting parentheses in the template — writing product.name instead of product().name. Since inputs are signals, you must call them with () to read the value.
  2. Marking important data as optional when it should be required — if the component cannot work without a piece of data, use input.required<T>(). If it is configuration with a sensible default, use input(defaultValue).

A good rule:

  • If the component cannot work without it → input.required<T>()
  • If it is configuration → input(defaultValue)

Source Code

The full source code for the course is available on GitHub: loiane/modern-angular.

Next Step

In the next lesson, we build on this by adding conditional UI in the card using @if and @else to display sale prices and badges.

Watch the Video

This post is part of the Modern Angular Course series. Check the course page for the full episode list.

This post is licensed under CC BY 4.0 by the author.
This site uses cookies. Please choose whether to accept analytics cookies. Privacy Policy