Signal inputs with the input() function
The input() API serves as the modern counterpart to the @Input() decorator, offering a signal-based approach to defining component inputs in Angular.
It's important to note that the existing @Input() decorator remains fully supported and is not deprecated. The introduction of input() provides an additional, more streamlined option rather than a replacement that removes existing functionality.
When you use input(), the value received from a parent component is exposed as a Signal. This signal always reflects the most recent value assigned to the input property by the parent.
Consider this example where we define a book input on a BookComponent:
import { Component, input } from "@angular/core";
@Component({...})
class BookComponent {
book = input<Book>()
}
In this instance, input() is used to define an input field named book. The returned value is a read-only Signal of type InputSignal<Book>.
This means book is no longer a plain Book object as it would be with the traditional @Input() decorator. Instead, it's a signal that holds the current value of the input as provided by the parent.
Despite this change on the component's end, the parent component's experience remains unchanged. You can still pass data to BookComponent using the book input property name, just as you would with @Input():
<book [book]="angularCoreDeepDiveBook" />
angularBook = {
title: "Angular Core Deep Dive",
synopsis: "A deep dive into Angular core concepts",
};
Here, the angularCoreDeepDiveBook object is passed to BookComponent through the book property.
Accessing input() values
From the parent's perspective, everything works as before. But how does the component itself retrieve the input's value?
To read the value, you invoke the book input signal, just like any other signal in Angular:
book();
This invocation returns the latest value of the book signal, which is the angularBook object.
It's worth noting that signals always hold a value. The result of book() will either be a book object or undefined.
Let's update our component to display the book's title and synopsis 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>();
}
In the template, we call the book() signal and then access the title and synopsis properties. We use the ?. optional chaining operator to safely handle the possibility that book might be undefined.
This approach works but can become cumbersome. What if we can guarantee that the book value will never be undefined?
This brings us to the two types of signal inputs available in Angular:
- Optional inputs
- Required inputs
Optional signal inputs
By default, inputs created with input() are optional. This means a parent component is not required to provide a value for them.
Our earlier BookComponent example used an optional input. This has a couple of implications:
First, you can use BookComponent without supplying a value for the book input:
<book />
Second, in this scenario, the book signal will hold undefined as its value.
If you'd prefer a different default instead of undefined, you can pass an initial value to your optional input():
const age = input<number>(0);
Here, the initial value of the age input signal becomes 0 rather than undefined.
age(); // 0
Making an input required
In some cases, you'll want an input to be mandatory. Here's how you can achieve that:
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>();
}
When using input.required(), there are a few important notes:
- You cannot provide an initial value to the input signal. The signal's value will be whatever the parent assigns to the input.
- You can no longer omit the
bookproperty in the parent component:
<book />
This will trigger a compilation error because the book input is now required.
To resolve this, you must pass the book property to BookComponent:
<book [book]="angularBook" />
This covers the fundamentals of optional and required inputs. Now let's explore some extra configuration options you might need for your signal inputs.
Setting an alias for an input property
Typically, you'll want the input property name to match the input signal's name. However, there are occasions where a different name is preferable.
While this is rarely needed, it can be useful in certain situations. Here's how to create an alias for both optional and required inputs:
book = input<Book>(null, {
alias: "bookInput",
});
book = input.required<Book>({
alias: "bookInput",
});
Here's how you would use the alias in a parent component:
<book [bookInput]="angularBook" />
If you try to use the original property name instead of the alias:
<book [book]="angularBook" />
It won't work and will produce an error:
NG8002: Can't bind to 'book' since it isn't a known property of 'book'.
Transforming input values
In rare cases, you might need to transform an input value before it's stored in the input signal. This can be accomplished using an input transform.
Here's how to define input transforms for both 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 transform property expects a pure function with no side effects. Inside this function, you write your transformation logic and must return a value.
Deriving values from signal inputs
Since input() returns a signal, you can perform any operation on it that you would on any other signal, including creating derived signals.
Here's how to 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);
}
bookLength is a derived signal based on the book input signal. Whenever the book value changes, bookLength will be recalculated automatically.
You could also use effect() on the book signal to monitor its changes. Remember, an input signal is just a read-only signal — there's nothing special about it. You can use it with all the standard signal operations.
Eliminating the OnChanges lifecycle hook
Let's look at a hidden advantage of using signal inputs over the @Input() decorator.
Previously, if you wanted to be notified when a component input changed, you'd use 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);
}
}
}
With the signal-based component format, the OnChanges hook is no longer necessary. Instead, the effect() API can be used to react to 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 needed — a simple effect() call is sufficient.
This covers the essentials of component inputs. Now let's move on to outputs and the less commonly used model() two-way binding API.
Angular component outputs with output()
The output() function is the new alternative to the traditional @Output() decorator.
The @Output decorator is not deprecated and remains supported. However, since inputs now use input(), it makes sense to have a corresponding approach for outputs.
Angular introduced output() as a more type-safe way to define component outputs, with better RxJs integration compared to the traditional @Output and EventEmitter pattern.
Here's how to define a component output using output():
import { Component, output } from "@angular/core";
@Component({...})
class BookComponent {
deleteBook = output<Book>()
}
The output function returns an OutputEmitterRef. The generic type <Book> in output<Book>() specifies that this output will only emit values of type Book.
From the parent component's perspective, listening to the deleteBook output is done using standard event binding syntax:
<book (deleteBook)="deleteBookEvent($event)" />
deleteBookEvent(book: Book) {
console.log(book);
}
As you can see, from the parent's viewpoint, using output() is indistinguishable from the traditional @Output() decorator.
Now, let's look at how to emit values from 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",
});
}
}
The onDelete method emits a book object via the deleteBook output.
This covers the basics of output(). Let's now discuss the configuration options and RxJs integration.
Setting an alias on an output()
Just like with signal inputs, you can also define an alias for an output():
deleteBook = output<Book>({
alias: "deleteBookOutput",
});
The parent component will then use deleteBookOutput to bind to the output event:
<book (deleteBookOutput)="deleteBookEvent($event)" />
RxJs interoperability with outputFromObservable()
It's worth noting that output() is not signal-based; it's simply more type-safe than the traditional @Output() decorator.
However, one of its key advantages is its superior integration with RxJs.
For instance, you can easily create an output that emits values from an observable using 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 this example, we created an Observable that emits a book object. We then created an output called deleteBook that emits the observable's values as component output events.
Connecting output() with RxJs through outputToObservable()
The reverse direction is also possible: we can transform an output into an observable stream.
This is achieved by invoking the outputToObservable function with a component output as its argument:
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);
});
}
}
Notice how straightforward it is to derive a new observable, deleteBookObservable$, from the deleteBook output signal.
The values pushed through this observable will mirror exactly what the deleteBook output emits.
With that, we have thoroughly examined how to declare component outputs with the modern output() API.
Now let's shift our focus to another API that is closely tied to both input() and
output()—the newly introduced model() API.
Understanding the model() API
In addition to input() and output(), Angular's signal-based components come with a third API called model(), which is used to define what are referred to as model inputs.
A model input is fundamentally a writeable input!
Model inputs enable us to establish a two-way data binding relationship between a parent component and a child component.
With model(), not only can the parent send data down to the child through the input, but the child can also send data back up to the parent.
Implementing two-way binding with model()
Let's see how model() functions in practice.
We begin by using it in a manner similar to 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;
});
}
}
Observe that we've created a Model signal by invoking the model function, instead of opting for
input().
Consequently, the book signal is now of type ModelSignal, not InputSignal.
So what sets them apart?
The key distinction is that, unlike a typical input, the book input is now a writeable signal, as illustrated in the changeTitle method.
You can conceptualize it as a signal that functions both as an input and an output.
This implies that we can now push new values to it from the child component!
The child BookComponent can both accept new values through book and also send new values for that same signal.
Thus, a contract is now in place between the child and parent components, with both utilizing signals for direct, two-way communication.
Here's what this looks like from the perspective 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;
});
}
}
Here is what's happening in this scenario.
In this case, we've defined a standard writeable signal called book within the BookListComponent.
We then bound this book signal to the book model input of the BookComponent using the [()] syntax, commonly known as the "banana-in-a-box" syntax.
This action effectively sets up a two-way binding.
We have formed a bidirectional agreement between the parent and child component, where both sides commit to exchanging data by emitting values through a shared model input.
So, when the changeSynopis method gets called in the BookListComponent, a new value is emitted to the book model input of the BookComponent.
Conversely, when the changeTitle method is invoked in the BookComponent, a new value is emitted back to the book signal of the
BookListComponent.
Determining when to use model()
In essence, model() serves as a two-way communication channel, implemented through a writeable signal that is shared between the parent and child components.
This feature can be beneficial in specific scenarios.
Think of a date picker component that has a primary input called value.
This value property is a prime candidate for two-way binding—the parent needs to set the initial value, but it also needs to receive updated values as the user interacts with the picker.
However, as a general rule, unless there's a compelling reason, it's often better to stick with traditional inputs and outputs. They tend to be more explicit and easier to reason about.
Overusing model can lead to code that is difficult to understand and debug.
Consider passing a model input through multiple layers of nested components: tracing the source of a value during debugging can become a real challenge.
My advice is to use model() judiciously, if you use it at all, and only when you have a clear-cut justification.
Don't feel pressured to adopt model() simply because it's the latest feature.
And don't worry about missing out if you don't find yourself using model() frequently, or even at all.
No framework feature is used with equal frequency, and that's perfectly acceptable.
Two-Way Binding with Non-Signal Values
The primary purpose of model inputs is to facilitate two-way data binding via a writeable signal.
However, it's also possible to pass a simple, non-signal value to a model input.
Let's modify the BookListComponent to use a plain value instead:
@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 += "!";
}
}
Notice that the book class field is now just a standard, non-signal value!
Even so, it remains two-way bound to the book input of the BookComponent.
Any modifications made by the parent will be automatically reflected in the child component.
Similarly, any changes the child makes to the book model input will be propagated back to the parent component's book property.
Reacting to model() updates
It's worth noting that we can also define an event handler to respond whenever a model input gets a new value.
We can listen for these model changes in the parent component by subscribing to the bookChange event in the following manner:
@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 shown, the name of the event we need to subscribe to follows a specific convention:
It's simply the name of the model input, book, followed by the suffix Change.
This naming rule isn't exclusive to model inputs; it's a standard convention that applies to all bi-directional data binding scenarios.
Wherever you can use the [()] syntax, you can also listen for a corresponding Change event.
Applying aliases to model()
Similar to inputs and outputs, we have the option to assign an alias to our model input:
book = model<Book>(
{
title: "The Avengers",
synopsis: "Loki is back to take over the world!",
},
{
alias: "bookInput",
}
);
Here's how you would bind to the book model input from the parent component:
<book [(bookInput)]="book" />
Enforcing required models
Also mirroring inputs and outputs, we can enforce that a model input is mandatory:
book = model.required<Book>();
Now, the parent component is required to pass a value to the book input of the BookComponent.
<book [(book)]="book" />
Furthermore, when using required, there's no need to provide an initial value in the model function call, since the parent is obligated to supply one.
Wrapping Up
Throughout this guide, we have taken a deep dive into the modern, signal-based primitives for component authoring: input(), output(), and model().
As you can see, these new APIs offer a much cleaner approach compared to their older counterparts—@Input(), @Output(), and [(ngModel)].
They are more concise, boast a cleaner syntax, and significantly minimize the reliance on lifecycle hooks.
This gives us the full power and flexibility of signals when building our Angular components.
So, we encourage you to experiment with these new authoring primitives. Feel free to submit any questions you might have in the comments section below.
We're here to help!
