In our previous edition we talked about using writeable, computed and input signals. This time we want to cover component output.

When we discussed input, we explored the new syntax for bringing information from parent components to child components. However we often want to bring that state back from the child component up to the parent.

The EventEmitter replacement

Before signals, we would bring up state and changes using an EventEmitter. Classically you could write component code as followed:

public searchValue = "";
@Output()
public onSearch ​= new EventEmitter<string>();
public handleSearch($event: Event): void {
    this.onSearch.emit(this.searchValue);
}

The same functionality can be achieved using an output signal like this:

public searchValue = "";
public onSearch ​= output<string>();
public handleSearch($event: Event): void {
    this.onSearch.emit(this.searchValue);
}

Now the input and output are using similar syntax!

Combining Input and Output

To create two way binding to combine inputs and outputs on a single variable. A trick often implemented would be to create two separate variables to achieve the effect:

This gives us a way of performing two way binding from the parent component.

With signals we can now combine both into on single line:

public searchValue = model<string>("");

This combines the best of both input and output together. You can even mark the model as required with model.required(). And changes to the signal immediately trigger as an event if a parent component is listening.

Parent components can choose whether they go for classic two way binding or opt for separating input and change:

<app-search-component
    [(searchValue)]="query"
></app-search-component>
<app-search-component
    [searchValue]="query()"
    (searchValueChange)="handleQuery($event)"
></app-search-component>

Model also allows you to use ngModel syntax in the component defining it. To return to the search example:

<input type="text" [(ngModel)]="searchValue">

We can leverage the search value model signal directly inside the text box.

Listening for Change

Listening for Change

Now that we have access to output and model signals. By interoperating with RxJS we can write more expressive code. We can use toObservable and toSignal to effectively pipe this state.

Take the search example and lets add the ability to call an external API for results and put them in a results signal:

public readonly searchValue = model<string>("");
private readonly apiResults$ = toObservable(this.searchValue).pipe(
    debounceTime(100),
    distinctUntilChanged(),
    switchMap((value) =>
        this.searchApi.search({ query: value }),
    )
);
public readonly results = toSignal(this.apiResults$, { initialValue: [] });

This example shows we no longer need to use ngOnInit to write this expression. We can declare the variable and observable logic together.