Angular’s Model Function Explored: A Comprehensive Overview

Netanel Basal
Netanel Basal
Published in
4 min readFeb 20, 2024

--

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!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.