How to Simplify Angular Component Communication with model() and Signals

Dany ParedesDany Paredes
6 min read

I was very busy in September, but now I'm working on creating a workshop about signals and developing ideas on how signals can simplify our code. Recently, I've been using @Input() and @Output for component communication. However, Angular 17 introduced input signals for communication, which work well but the Angular Team is also working on model signals, a great alternative for bidirectional communication between components with less code. Let's explore it!

Note: The model is in developer preview but we can play with it today!

But you might ask, why do I need another way to communicate if I have input and output? That's a good question. Let me show you with a real-world example.

We want to show a list of movies and rate each one. To make it easy to rate my movies, I will use Kendo UI for Angular Rating and implementing input/output signals for bidirectional data binding, then migrate to model(), an easy approach.

Let’s do it!

Setup Project

I will use the latest version of Angular. The easiest way is by running the following command in the terminal. Answer yes for default questions.

npx -p @angular/cli signal-model-rating-kendo
Need to install the following packages:
@angular/cli@18.2.6
Ok to proceed? (y) y
.....

Next, move to the project folder cd signal-model-rating-kendo and use Kendo UI Schematics to install Kendo Input Rating.


ng add @progress/kendo-angular-inputs
✔ Determining Package Manager
  › Using package manager: npm
✔ Searching for compatible package version
  › Found compatible package version: @progress/kendo-angular-inputs@16.10.0.
✔ Loading package information from registry
✔ Confirming installation
✔ Installing package
UPDATE package.json (1656 bytes)
UPDATE angular.json (3071 bytes)

The Rating Using Input/Output Signals

First, let's create a custom component to rate movies using Kendo UI's kendo-rating component. We'll bind the movie's rating using input/output signals to demonstrate bidirectional data binding.

ng g c movie-rating
signal-model-rating-kendo % ng g c componentes/movie-rating
CREATE src/app/componentes/movie-rating/movie-rating.component.scss (0 bytes)
CREATE src/app/componentes/movie-rating/movie-rating.component.html (27 bytes)
CREATE src/app/componentes/movie-rating/movie-rating.component.spec.ts (628 bytes)
CREATE src/app/componentes/movie-rating/movie-rating.component.ts (258 bytes)

Let’s use the kendo-rating component and set the value by using the input signal that will receive and emits the new rating value using the output when user updates the rating

Open the movie-rating.component.ts file. First, import the RatingComponent from @progress/kendo-angular-inputs. Next, declare the rating property as input to get the value and ratingChange as output to emit the event.


import { Component, input, output } from '@angular/core';
import { RatingComponent } from '@progress/kendo-angular-inputs';

@Component({
  selector: 'app-movie-rating',
  standalone: true,
  imports: [RatingComponent],
  templateUrl: './movie-rating.component.html',
  styleUrl: './movie-rating.component.scss',
})
export class MovieRatingComponent {
  rating = input(4);
  ratingChange = output<number>();

  onRatingChange(newRating: number) {
    this.ratingChange.emit(newRating);
  }
}

In the template, we use the kendo-rating component, bind the value property with the rating() signal, and listen to the valueChange event to trigger the onRatingChange event. The final code looks like:

<kendo-rating [value]="rating()" (valueChange)="onRatingChange($event)">
</kendo-rating>

Perfect, now it's time to connect the movie list with our movie-rating component. Let's do it!

Communicate The Movie Rating

Open the app.component.ts, declare a type Movie with the following properties:

interface Movie {
  title: string;
  rating: number;
}

We create a mock list of movies to use in the component.

movies = signal<Movie[]>([
    { title: 'Inception', rating: 4 },
    { title: 'Interstellar', rating: 5 },
    { title: 'The Dark Knight', rating: 5 },
    { title: 'Dunkirk', rating: 3 },
  ]);

The most important step is to import the MovieRatingComponent into the app.component.ts. The final code looks like:

import { Component, signal } from '@angular/core';
import { RouterOutlet this } from '@angular/router';
import { MovieRatingComponent } from './componentes/movie-rating/movie-rating.component';

interface Movie {
  title: string;
  rating: number;
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, MovieRatingComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {
  movies = signal<Movie[]>([
    { title: 'Inception', rating: 4 },

    { title: 'Interstellar', rating: 5 },

    { title: 'The Dark Knight', rating: 5 },

    { title: 'Dunkirk', rating: 3 },
  ]);
}

In the app.component.html we use the @for to iterate over movies list, show the movie title, rate and the movie-rating component to allow the user vote.

Using [(rating)] two way binding we can get the value when it emits.

@for (movie of movies(); track movie) {
  <h3>{{ movie.title }}</h3>

  <app-movie-rating [(rating)]="movie.rating"></app-movie-rating>

  <p>Current rating: {{ movie.rating }} stars</p>
}

Save changes and run ng serve to have our app show the list of movies and their ratings. Go to http://localhost:4200 in your browser, and you'll see the list of movies with the rating feature working.

We successfully implemented bidirectional binding using the input/output signals, but this requires manually handling the input and emitting the output.

If I told you we can do it with less code using model()? 😎

Moving to Model

Before we start, why do I want to move to model()? Well, model() automatically creates both an input and an output, making it easier to manage two-way bindings without manually emitting events. So, with a small change in MovieRatingComponent to use model() instead of the input/output, we can achieve the same result without using output signals.

Let’s do it, first import model from @angular/core

import { Component, model } from '@angular/core';
import { RatingComponent } from '@progress/kendo-angular-inputs';

Change the type of input to model. The model is a writable signal, so we can update it using the update method.

import { Component, model } from '@angular/core';
import { RatingComponent } from '@progress/kendo-angular-inputs';

@Component({
  selector: 'app-movie-rating',
  standalone: true,
  imports: [RatingComponent],
  templateUrl: './movie-rating.component.html',
  styleUrl: './movie-rating.component.scss',
})
export class MovieRatingComponent {
  rating = model(4);

  onRatingChange(newRating: number) {
    this.rating.update(() => newRating);
  }
}

It automatically emits the value when the signal changes. We no longer need the output for bidirectional communication because the model creates an output for us with the change appended to the name. In our case, it is ratingChange.

<app-movie-rating [(rating)]="movie.rating" (ratingChange)="onRatingChange($event)"></app-movie-rating>

Open http://localhost:4200 in your browser to see the list of movies with their ratings. It works the same way with less code! Yeah!

WAIT A SECOND! Want to dive even deeper into Angular Signals?

I highly recommend the Angular Signals Masterclass eBook by Kevin Kreuzer. It’s the best way to learn everything about Signals in one place, with clear examples to guide you.

Grab a free preview to get a taste of the awesome insights inside! And when you're ready to go all in, unlock the full version for the complete Signals deep dive.

Now you know my secret to learning about Signals. 😎 Let’s continue!

Ok but Why we have input and model are not the same ?

Well, you can use input() and model() to communicate with your components, but the magic of model() is that it creates an input and an output for our property.

Before pick one keep in mind the following points:

  • Model(): This automatically creates both an input and an output for a property, allowing for bidirectional data binding with far less code. It’s a writable signal, meaning it can be updated and emits changes to subscribers without needing manual event handling.

  • Input/Output Signals: These require manual handling of input values and emitting output events to get two-way binding.

Remember: The model() simplifies communication but doesn’t support transformation functions, which might be necessary in more complex scenarios. For these cases, using input/output signals may be a better option.

Recap

We learned how to implement bidirectional data binding using input/output signals in Angular and then saw how the new model() feature can simplify this process with less code.

We started by building a movie rating component using input/output signals, where we manually handled data binding and event emissions. While this approach works well, it requires more boilerplate code to manage the communication between components, then, we switched to the model() approach, which simplifies the code by automatically handling both input and output bindings.

Happy coding!

0
Subscribe to my newsletter

Read articles from Dany Paredes directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dany Paredes
Dany Paredes

I'm passionate about front-end development, specializing in building UI libraries and working with technologies like Angular, NgRx, Accessibility, and micro-frontends. In my free time, I enjoy writing content for the Google Dev Library, This Is Angular Community, Kendo UI, and sharing insights here.