With the rise of signals in the Angular ecosystem, Angular has now available a completely new way of authoring components, directives, and pipes.
This new authoring format is called Signal Components, and it's way simpler and more powerful than the traditional one.
Signal components are a completely new alternative to the traditional @Input()
and @Output()
decorators, and they also provide a new way of doing two-way binding.
They are now the recommended way to author new components in Angular going forward, and just to be clear they are ready to be used today.
This new signal-based authoring format is characterized by the absence of decorators, as well as by the absence of most lifecycle hooks, and by its deep integration with signals.
Signal components are way better than the old format, as you will learn in this post.
To be able to benefit from Signal Components, you will need to upgrade to Angular 17.3 or beyond.
In this guide, I will explain the input
, output
, and model
component authoring primitives, and show how to use them to author signal-based Angular components.
Table of Contents
- Signal inputs with
input()
- Reading
input()
values - Optional inputs
- Making an input to be required
- Setting a custom public name: alias
- Value transformation: transform
- Deriving values from input signals
- No more need for the OnChanges lifecycle hook
- What is
output()
? - Alias on output()
- output() RxJs interoperability using outputFromObservable()
- output() RxJs interoperability using outputToObservable()
- What is
model()
? - Two-Way Binding to a Signal Value
- When to use model()?
- Two-Way Binding to a Non-Signal Value
- Responding to model changes
- Using alias on model()
- Making model() to be required
- Summary
To understand how Signal components work, you need to first understand the basics of signals in Angular.
So if you haven't done so already, I recommend that you read first this other guide: Angular Signals: The Complete Guide.
Note that there are also new signal-based alternatives to view queries and content queries, but those will be covered in a separate guide:
Angular Signal Queries: viewChild, contentChild, viewChildren, contentChildren (Complete Guide)
Signal inputs with input()
The input()
API function is a direct replacement for the @Input()
decorator.
This is the new signal-based way of defining component inputs in Angular.
But notice that with the arrival of input()
, the @Input()
decorator is not deprecated, it will still be supported for the foreseeable future.
With input()
, the input values of an input property are exposed as a Signal.
The signal always holds the latest value of the input that is bound from the parent.
For example, here is how we create an input property book
in a BookComponent
:
import { Component, input } from "@angular/core";
@Component({...})
class BookComponent {
book = input<Book>()
}
Here we used input()
to create an input field book
.
The input
function returns a read-only Signal of type InputSignal<Book>
.
So book
is not a plain Book
object anymore, like it would be with the traditional @Input()
decorator.
Instead, book
now becomes a signal that holds the latest value of the input that is bound from the parent.
But even though we are now using a signal input, from the point of view of the parent component, nothing changes.
You can still send data from a parent component to the BookComponent
via a
book
input property name, just like with the @Input() decorator:
<book [book]="angularCoreDeepDiveBook" />
angularBook = {
title: "Angular Core Deep Dive",
synopsis: "A deep dive into Angular core concepts",
};
We passed the angularCoreDeepDiveBook
book object to the BookComponent
via the book
property.
Reading input()
values
So from the point of view of the parent component, everything remains the same.
But what about the component itself, how can we get the input value?
To read the value, we need to call or invoke the book
input signal, just like with any other signal in Angular:
book();
The above invocation will return the latest value of the book
signal, which contains the value of the angularBook
object.
Notice that signals always have a value, so the return value of book()
will either be undefined or a book object.
Let's now edit our component to display the title and synopsis of the book in its template:
import { Component, input } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
</div> `,
})
class BookComponent {
book = input<Book>();
}
Notice that in the template, we invoked the book()
signal and then accessed the title
and synopsis
properties.
We were careful to use the ?.
optional chaining operator because the book
value can potentially be undefined.
This works, but it's potentially cumbersome.
What if we are sure and can give the guarantee that the book
value will never be undefined?
That brings us to the two different types of signal inputs we can have in Angular:
- Optional inputs
- Required inputs
Optional signal inputs
By default, inputs created via input()
are considered optional.
This means that we don't have to provide a value for the input when using the component from the parent component.
The example that we used above with the BookComponent
was an optional input.
This means a couple of important things:
First, we can use the BookComponent
without providing a value for the book
input:
<book />
Second of all, in this situation, the book
signal will have its value as undefined
.
If we don't want to have the default value of undefined, we can also pass an initial value to our optional input()
like this:
const age = input<number>(0);
The initial value of the age
input signal will now become 0, instead of undefined.
age(); // 0
Making an input to be required
There are some scenarios in which we want our input properties to be required, instead of optional. Here is how we do it:
import { Component, input, required }
from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book().title }}</b>
<div>{{ book().synopsis }}</div>
</div> `,
styles: ``,
})
class BookComponent {
book = input.required<Book>();
}
A couple of important things when we use input.required()
:
-
We can no longer provide an initial value to the input signal. The initial value will be the value assigned to the input.
-
We can't no longer omit the
book
property in the parent component:
<book />
This will now throw a compilation error because the book
input field is no longer optional!
To solve this, we need to pass the book
property to the BookComponent
.
<book [book]="angularBook" />
And so this covers the fundamentals of how optional and required inputs work.
Let's now talk about some common extra configuration options that you might need for your signal inputs.
Setting an input property alias
Most of the time, we want the name of our input property to be the same as the name of the input signal.
But sometimes, we might want the input property to have a different name.
This should be rarely needed, but it might be useful in some cases.
If you run into one of those situations, here is how you create an input alias both for optional and required inputs:
book = input<Book>(null, {
alias: "bookInput",
});
book = input.required<Book>({
alias: "bookInput",
});
And this is how you use the input alias in the parent component:
<book [bookInput]="angularBook" />
Notice that if you try to use the input property name instead of the alias, like this:
<book [book]="angularBook" />
It won't work, you will get an error:
NG8002: Can't bind to 'book' since it isn't a known property of 'book'.
Input value transformation: the transform function
There are some rare situations where we might want to transform the input value before it is assigned to the input signal.
We can transform the raw data coming in from the parent component by using an input transform.
Here is how we can define input transforms, both for optional and required inputs:
book = input(null, {
transform: (value: Book | null) => {
if (!value) return null;
value.title += " :TRANSFORMED";
return value;
},
});
book = input.required({
transform: (value: Book | null) => {
if (!value) return null;
value.title += " :TRANSFORMED";
return value;
},
});
The value of the transform property should be a pure function, with no side effects.
Inside this function is where we write our value transformation logic, and we must return a value from the function.
Deriving values from signal inputs
Since an input()
is just a signal, we can do anything with it that we can do with any signal, including calculating a derived signal from it.
Here is how we can create a derived signal from an input signal using the computed() API:
import { Component, input, computed }
from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
<div>{{ bookLength() }}</div>
</div> `,
styles: ``,
})
class BookComponent {
book = input.required<Book>();
bookLength = computed(() => this.book().title.length);
}
The bookLength
is our derived signal from the book
signal.
Whenever the book
input signal value changes, then the bookLength
signal will also be recalculated.
We could also apply an effect() on top of a book signal to monitor changes to it.
Remember, an input signal is just a read-only signal, there is nothing special about it. You can do with it all the usual operations that you can do with any other signal.
No more need for the OnChanges lifecycle hook
Let's talk about a hidden benefit of using signal inputs instead of the @Input()
decorator.
Imagine that we would like to get notified when a component input changes. Traditionally, this was done using the OnChanges
lifecycle hook:
import { OnChanges } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
</div> `,
})
class BookComponent implements OnChanges {
@Input() book: Book;
ngOnChanges(changes: SimpleChanges) {
if (changes[book]) {
console.log("Book changed: ",
changes.book.currentValue);
}
}
}
But with the new signal-based component authoring format, we no longer need to use the OnChanges
lifecycle hook.
Instead, we can use the effect()
API to get notified whenever an input signal changes:
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
</div> `,
styles: ``,
})
class BookComponent {
book = input.required<Book>();
constructor() {
effect(() => {
console.log("Book changed: ", this.book());
});
}
}
As you can see, no special lifecycle hook is required, just a plain effect() call will do the trick!
And this essentially covers everything that we need to know about component inputs.
Let's now talk about outputs, and cover also the less commonly used model()
two-way binding API.
Angular component outputs with output()
The output()
API is a direct replacement for the traditional @Output()
decorator.
Notice that the @Output
decorator is not deprecated, as it will still be supported for the foreseeable future.
But it would be weird to have inputs with input()
and outputs with @Output()
, right?
So Angular has added output()
as a new way to define component outputs in Angular, in a way that is more type-safe and better integrated with RxJs than the traditional @Output
and EventEmitter
approach.
Here is how we can use the new output()
API to define a component output in Angular:
import { Component, output } from "@angular/core";
@Component({...})
class BookComponent {
deleteBook = output<Book>()
}
Notice that the output
function returns an OutputEmitterRef
.
The <Book>
generic type in output<Book>()
indicates that this output will only emit values of type Book
.
The parent component can then listen to the deleteBook
output event using the usual event handling syntax:
<book (deleteBook)="deleteBookEvent($event)" />
deleteBookEvent(book: Book) {
console.log(book);
}
So as you can see, from the point of view of the parent component, the use of output()
is indistinguishable from the traditional @Output()
decorator.
Let's now see how we can emit values using an output()
:
import { Component, output } from "@angular/core";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
<button (click)="onDelete()">Delete Book</button>
</div>`,
})
class BookComponent {
deleteBook = output<Book>();
onDelete() {
this.deleteBook.emit({
title: "Angular Deep Dive",
synopsis: "A deep dive into Angular core concepts",
});
}
}
As we can see,the onDelete
method will then emit a book object via the deleteBook
output.
And this covers the basics for output()
. Let's now cover the configuration options we have available as well as the new RxJs integration.
Setting an alias on an output()
Just like in the case of signal inputs, we can also define an alias for an output()
like so:
deleteBook = output<Book>({
alias: "deleteBookOutput",
});
The parent component will then use deleteBookOutput
to listen to the output event:
<book (deleteBookOutput)="deleteBookEvent($event)" />
output() RxJs Interoperability using outputFromObservable()
As we can see, output()
is not signal-based, it's just more type-safe than the traditional @Output()
decorator.
But besides that, one of the advantages is that it provides a much better interoperability with RxJs.
For example, we can very easily create an output signal that emits values from an observable.
We can do this by calling the outputFromObservable
function:
import { Component } from "@angular/core";
import { outputFromObservable }
from "@angular/core/rxjs-interop";
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
</div>`,
})
class BookComponent {
deleteBook = outputFromObservable<Book>(
of({
title: "Angular Core Deep Dive",
synopsis: "A deep dive into the core features of Angular.",
})
);
}
In the above example, we created an Observable that emits a book Object, just as an example.
Then we created an output signal deleteBook
that emits as a component output the values emitted by the Observable.
output() RxJs interoperability with outputToObservable()
We can go the other way around and convert an output into an observable.
We do this by calling the outputToObservable
function and passing a component output to it:
import {
outputToObservable,
} from "@angular/core/rxjs-interop";
@Component({...})
class BookComponent {
deleteBook = output<Book>();
deleteBookObservable$ =
outputToObservable(this.deleteBook);
constructor() {
this.deleteBookObservable$.subscribe((book: Book) => {
console.log("Book emitted: ", book);
});
}
}
As you can see, we have easily created a new observable deleteBookObservable$
from the deleteBook
output signal.
The values emitted by the Observable will be the same as the values emitted by the deleteBook
output.
And with this, we have covered in detail how to define component outputs using the new output()
API.
Let's now talk about another API that is strongly related to both input()
and
output()
: the new model()
API.
What is the model()
API?
Besides input()
and output()
, Angular signal components also include a new API called model()
for creating what is known as model inputs.
A Model input is essentially a writeable input!
Model inputs allow us to specify a two-way data binding contract between the parent component and the child component.
With model()
, not only the parent component can pass data through it to child components, but child components can also emit data back to the parent component.
Two-way data binding using model()
Here is how model()
works.
We start by using it just like we would use a regular input()
:
@Component({
selector: "book",
standalone: true,
template: `<div class="book-card">
<b>{{ book()?.title }}</b>
<div>{{ book()?.synopsis }}</div>
<button (click)="changeTitle()">
Change title
</button>
</div> `,
styles: `
`,
})
export class BookComponent {
book = model<Book>();
changeTitle() {
this.book.update((book) => {
if (!book) return;
book.title = "New title";
return book;
});
}
}
See that we created a Model signal by calling the model
function, instead of
input()
.
So now, the book
signal is a ModelSignal
So what's the difference?
The main difference is that unlike with a normal input, the book input is now a writeable signal, as we can see on the changeTitle
method.
You can think of it as a writeable input/output signal.
This means that we can now emit values to it from the child component!
The child BookComponent
can both receive new values using book
, but it can also emit new values for that signal.
So there is now a contract established between the child and the parent component, they are both using signals for direct two-way communication.
Here is how this works from the point of view of the parent component:
@Component({
selector: "booklist",
standalone: true,
template: `
<div>
<book [(book)]="book" />
</div>
<div>
<b>Parent</b> <br />
<div>{{ book().title }}</div>
<div>{{ book().synopsis }}</div>
<button (click)="changeSynopis()">
Change Synopsis
</button>
</div>
`,
styles: ``,
imports: [BookComponent],
})
export class BookListComponent {
book = signal<Book>({
title: "Angular Core Deep Dive",
synopsis: "Deep dive to advanced features of Angular",
});
changeSynopis() {
this.book.update((book) => {
book.synopsis += "Updated synopis!!";
return book;
});
}
}
So here is what is going on.
Here, we created a plain writeable signal called book
in the BookListComponent
.
We then passed the book
signal to the book
model input of the BookComponent
using the [()]
syntax, which is often called the "banana-in-a-box" syntax.
This essentially means that we have created a two-way binding.
We have created a bi-directional contract between the parent and the child component, where they both agree to share data by emitting values via a shared model input.
So when the changeSynopis
method is called in the BookListComponent
, it will emit a new value to the book
model input of the BookComponent
.
And the other way around is also true: when the changeTitle
method is called in the BookComponent
, it will emit a new value to the book
signal of the
BookListComponent
.
When to use model()?
So in a nutshell, model()
is a two-way communication mechanism that is implemented by a writeable signal that is shared between the parent and the child component.
This could be useful for certain situations.
For example, imagine a date picker component with a main input value
.
We can see how the value property would benefit from two-way binding. The parent wants to set the initial value, but also receive new values back.
In general though, unless we have a very specific reason for doing so, we should prefer the use of regular inputs and outputs, as they are more explicit and easier to understand.
The use of model
can easily degenerate in code that is potentially hard to understand and debug.
Imagine that you pass a model input through several levels of nesting: it would be hard to know from where a certain value is coming from during debugging.
So my recommendation is, to use model()
sparingly, if at all, and only when you have a very specific reason for doing so.
So don't feel forced to use model()
just because it's new and shiny.
And don't fear missing out on something if you don't use model()
often, or even at all.
Not every feature in a framework will have the same frequency of use, and that's perfectly fine.
Two-Way Binding to a Non-Signal Value
The main use case for model inputs is two-way data binding via a writeable signal.
But it's also possible to pass to a model input a plain non-signal value as well.
Let's edit the BookListComponent
to use a non-signal input value:
@Component({
selector: "booklist",
standalone: true,
template: `
<div>
<book [(book)]="book"></book>
</div>
<div>
<b>Parent</b> <br />
<div>{{ book.title }}</div>
<i>{{ book.synopsis }}</i
><br />
<button (click)="changeSynopis()">
Change Synopsis
</button>
</div>
`,
styles: ``,
imports: [BookComponent],
})
export class BookListComponent {
book = {
title: "Angular Core Deep Dive",
synopsis: "Deep dive into advanced Angular features",
};
changeSynopis() {
this.book.synopsis += "!";
}
}
See that the book
class field is now a plain non-signal value!
Yet, it is still two-way data bound to the book
input of the BookComponent
.
Any updates that the parent component does will be reflected in the child component.
Likewise, any updates that the child component does to the book
input model will be reflected in the parent component book
property.
Responding to model() changes
Notice that we can also define an event handler that will be triggered whenever a model input emits a new value.
We can subscribe to model changes in the parent component by subscribing to the bookChange
event like so:
@Component({
selector: "booklist",
standalone: true,
template: `
<div>
<book [book]="book"
(bookChange)="bookChangeEvent($event)"/>
</div>
<div>
<b>Parent</b> <br />
<div>{{ book.title }}</div>
<i>{{ book.synopsis }}</i
><br />
</div>
`,
styles: ``,
imports: [BookComponent],
})
export class BookListComponent {
book = {
title: "Angular Core Deep Dive",
synopsis: "Deep dive into advanced Angular features",
};
bookChangeEvent(book: Book) {
console.log("Book changed");
}
}
As you can see, the event that we want to subscribe to follows a particular naming convention:
It's just the name of the model input book
, followed by the suffix Change
.
Notice that this naming convention is not specific to model inputs only, this is a general convention that is always available in bi-directional data binding scenarios.
Anywhere where you can apply the [()]
syntax, you can also subscribe to the corresponding Change
event.
Using alias on model()
Just like in the case of inputs and outputs, we can set an alias to our model input as well:
book = model<Book>(
{
title: "The Avengers",
synopsis: "Loki is back to take over the world!",
},
{
alias: "bookInput",
}
);
Here is how we can bind to the book model input from the parent component:
<book [(bookInput)]="book" />
Making model to be required
Just like in the case of inputs and outputs, we can also make the model
to be required:
book = model.required<Book>();
Now the parent component must pass a value to the book
input of the BookComponent
.
<book [(book)]="book" />
Also, when we use required
we do not pass an initial value to the model
function call because a value must be supplied by the parent.
Summary
In this guide, we have explored in detail the new input()
, output()
, and model()
signal-based component authoring primitives.
As you can see, the new primitives are way cleaner than their traditional counterparts @Input()
, @Output()
, and [(ngModel)]
.
They are less verbose, have a cleaner syntax, and reduce a lot the need for lifecycle hooks.
We now have all the flexibility and power of signals at our disposal when authoring our Angular components.
So go ahead and try out the new component authoring primitives, and let me know in the comments below of any questions you might have.
I'll be happy to help!