We use cookies to enhance your experience on the site
CodeWorlds

Signal Inputs - How a Samurai Receives Orders

A samurai card component receives its data from a parent: a name, a level, a clan. It used to take them through

@Input()
, today it takes them through signals. This lesson covers three kinds of such inputs: a plain
input()
(an order travelling one way),
model()
(a conversation running both ways) and signal queries for child elements. One thing ties them all together - you read every one of them like any other signal, by calling
()
.

input() - an order from above

input()
turns data coming from the parent into a read-only signal. In the template you read it exactly like a normal signal:
name()
.

1import { Component, input } from '@angular/core';
2
3@Component({
4  selector: 'app-samurai-card',
5  template: `<h2>{{ name() }}</h2><p>Level: {{ level() }}</p>`
6})
7export class SamuraiCardComponent {
8  name = input('Unknown');                 // optional, with a default value
9  level = input.required<number>();         // required
10  clan = input('Ronin', {
11    transform: (v: string) => v.toUpperCase()   // transform it on the way in
12  });
13}

Three variants cover most of what you will ever need.

input('Unknown')
carries a default value for the case where the parent passes nothing.
input.required<number>()
does the opposite - Angular reports an error when the parent forgets to pass it, which protects you from a silent mistake. Look closely at the shape of that call:
required
is a property on
input
, not a separate function and not a decorator, so
requiredInput<string>()
and
@RequiredInput()
are names that do not exist, and
@Input({ required: true })
is the old decorator this API replaces.
transform
runs the value through a function right at the entrance - here every clan is stored in upper case, with no manual processing inside the component. One more option is worth knowing,
alias
: written as
samuraiId = input(0, { alias: 'id' })
it lets the parent bind
[id]
while the class keeps the longer name. The parent binds all of them as always:
<app-samurai-card name="Musashi" [level]="10">
.

model() - a conversation both ways

Sometimes the child should not only read a value but also change it - a health slider, for example.

model()
gives you two-way binding: the parent supplies the value, the child updates it, and the parent sees the change.

1import { Component, model } from '@angular/core';
2
3@Component({
4  selector: 'app-health-bar',
5  template: `
6    <input type="range" [value]="health()"
7           (input)="health.set(+$any($event.target).value)">
8  `
9})
10export class HealthBarComponent {
11  health = model(100);   // works like [(ngModel)]
12}

The difference from

input()
is a single one, but it is fundamental: a
model()
can be set (
health.set(...)
), and the change travels back to the parent. That is why the parent binds it in both directions:
<app-health-bar [(health)]="currentHealth">
. Move the slider inside the child and
currentHealth
in the parent updates on its own. This is the signal-based heir of the old
[(ngModel)]
, and it is the only one of these functions that supports the
[(value)]
syntax -
input()
is read-only,
computed()
derives a value you can never set, and
output()
merely pushes events out of the component through calls like
selected.emit(name())
, with nothing flowing back in.

Signal queries for children

The last kind of input is not data from the parent but a handle on your own children in the template. The old

@ViewChild
decorators now have signal-based counterparts.

1import { viewChild, viewChildren, contentChild } from '@angular/core';
2
3export class ParentComponent {
4  childComponent = viewChild(ChildComponent);     // one child from the template
5  allCards = viewChildren(CardComponent);          // every matching child
6  projectedHeader = contentChild('header');        // a child passed in via <ng-content>
7}

The names tell you where to look:

viewChild
finds an element in the component's own template, while
contentChild
finds an element passed in from outside through content projection. The plural forms (
viewChildren
,
contentChildren
) return every match instead of the first. Notice the naming rule - each function simply repeats the name of its decorator in lower camel case, which is why
querySignal()
,
childSignal()
and
selectChild()
appear nowhere in Angular. A query can also reach a plain DOM element through its template reference, as in
viewChild<ElementRef>('myInput')
. The gain from the signal-based version is the same as everywhere else: the result is a signal, so you can build
computed
and
effect
on top of it instead of waiting for lifecycle hooks.

Take one shared trait away from this lesson: all of these inputs are signals, and you read them by calling

()
.
input()
receives an order from above,
model()
holds a conversation in both directions, and
viewChild
/
contentChild
hand you signal-based handles on your own children.

Go to CodeWorlds