Angular’s Model Function Explored: A Comprehensive Overview
The newest addition to Angular is the model
function, which enhances two-way data binding using signals. This feature simplifies data management by providing writable signals accessible through input/output pairs within directives and components.
Two-Way Binding to a Signal
Utilizing the model
function in Angular opens the door to seamless two-way binding capabilities, particularly when signals are involved. Let’s illustrate this with a practical example involving a pagination component and a consumer component:
@Component({
selector: 'app-pagination',
standalone: true,
template: `{{ page() }}`,
})
export class PaginationComponent {
page = model(1);
}
In this scenario, the PaginationComponent
exposes a page
model using the model
function. It defaults to page 1.
@Component({
selector: 'app-todos',
standalone: true,
template: `
<app-pagination [(page)]="currentPage" />
`,
})
export class TodosComponent {
currentPage = signal(1);
}
The TodosComponent
then incorporates the PaginationComponent
and binds its currentPage
signal to the page
model of the PaginationComponent
using the banana-in-a-box syntax ([(page)]="currentPage"
).
What’s remarkable here is the bidirectional synchronization between the PaginationComponent’s page
signal and the TodosComponent's currentPage
signal. When either signal updates, Angular ensures that the corresponding value in the other component is updated accordingly.
Two-Way Binding to a Non-Signal Value
The model
function seamlessly integrates with non-signal values, maintaining the familiar two-way binding experience:
@Component({
selector: 'app-todos',
standalone: true,
template: `
<app-pagination [(page)]="currentPage" />
`,
})
export class TodosComponent {
currentPage = 1;
}
Despite currentPage
being a conventional number property, Angular effortlessly manages bidirectional data binding between components.
Two-Way Binding with Custom Implementations
An intriguing aspect is its compatibility with custom two-way binding implementations we’re familiar with. Consider the following example:
@Component({
selector: 'app-pagination',
standalone: true,
template: `{{ page }}`,
})
export class PaginationComponent {
@Input() page = 1;
@Output() pageChange = new EventEmitter<number>();
}
@Component({
selector: 'app-todos',
standalone: true,
template: `
<app-pagination [(page)]="currentPage" />
`,
})
export class TodosComponent {
currentPage = signal(1);
}
In this setup, PaginationComponent
employs custom two-way binding with page
as its input and pageChange
as its output. Despite this custom approach, Angular seamlessly integrates with it. The currentPage
signal in TodosComponent
seamlessly binds with PaginationComponent's
page
property, maintaining bidirectional data flow.
One-Way Property Binding to a Model
When the need arises, opt for one-way non-signal binding:
@Component({
selector: 'app-todos',
standalone: true,
template: `
<app-pagination [page]="currentPage" />
`,
})
export class TodosComponent {
currentPage = 1;
}
Responding to Model Changes
To capture model changes, subscribe to the corresponding event emitted by the component. The event name follows a convention where it’s derived from the model
property name with the addition of “Change” postfix. For instance, if your model
property is named page
, the corresponding event would be pageChange
:
@Component({
selector: 'app-todos',
standalone: true,
template: `
<app-pagination [page]="currentPage"
(pageChange)="onChange($event)" />
`,
})
export class TodosComponent {
currentPage = 1;
onChange(page: number) {}
}
Leveraging Alias and Required Options
Similar to signal inputs, the model
function supports advanced options like alias
and required
:
@Component({
selector: 'app-pagination',
standalone: true,
template: `{{ page() }}`,
})
export class PaginationComponent {
page = model(1, { alias: 'currentPage' });
id = model.required<string>();
}
@Component({
selector: 'app-todos',
standalone: true,
template: `
<app-pagination [(currentPage)]="currentPage" />
<app-pagination [currentPage]="currentPage()"
(currentPageChange)="onChange()" />
`,
})
export class TodosComponent {
currentPage = signal(1);
}
Updating Models with Directives
In my previous article titled “Why Directives are the Go-To Choice for Select Component Options Reuse in Angular,” we explored the use of directives. Now, let’s revisit the approach and refactor it to leverage models:
@Component({
selector: 'app-select',
standalone: true,
templateUrl: './select.component.html',
})
export class SelectComponent {
placeholder = model('Select an option');
options = model<string[]>([]);
}
@Directive({
selector: '[appUsersOptions]',
standalone: true,
})
export class UsersOptionsDirective {
select = inject(SelectComponent);
ngOnInit() {
this.select.placeholder.set('Select a user');
setTimeout(() => {
this.select.options.set(['John', 'Jane', 'Doe']);
}, 1000);
}
}
In the SelectComponent
, we utilize the model
function to define a placeholder and an options array. Meanwhile, the UsersOptionsDirective
sets the placeholder to ‘Select a user’ and updates the options array after a brief delay.
Additional Features
- Inheritance support
- Compatibility with
ngOnChanges
hook
Modeling Differences: Comparing model() and input() Functions
In Angular, both the input()
and model()
functions serve as means to define signal-based inputs, yet they exhibit distinct characteristics:
Output Definition
The model
function establishes both an input and an output.
Signal Type
ModelSignal
, employed by model()
, is a WritableSignal
allowing value modifications through methods like set and update from any location. In contrast, InputSignal
used by input()
is read-only and can only be altered via the template.
Input Transforms
Model inputs lack support for input transforms, a feature available with signal inputs.
Understanding these distinctions aids in selecting the most suitable approach for handling data flow within your Angular application.
🙏 Support ngneat & Netanel Basal: Get Featured!
Are you passionate about the ngneat open source libraries for Angular or find Netanel Basal’s blog posts invaluable for your learning journey? Show your support by sponsoring my work!
Follow me on Medium or Twitter to read more about Angular and JS!