1. First Steps with Signals

🔀 Branch: For this lab, please switch to the branch 01-signals-starter:

git reset --hard
git checkout 01-signals-starter

1.1 Adding Signals

Let's add some Signals to our Angular application.

  1. Open the file desserts.component.ts (src/app/desserts/desserts.component.ts) and convert each property into a Signal. Adjust the methods to call the Signal's getters and/or setters:

     import { JsonPipe } from '@angular/common';
    -import { Component, OnInit, inject } from '@angular/core';
    +import { Component, OnInit, inject, signal } from '@angular/core';
     import { FormsModule } from '@angular/forms';
     import { Dessert } from '../data/dessert';
     import { DessertFilter } from '../data/dessert-filter';
    
    [...]
    
       #ratingService = inject(RatingService);
       #toastService = inject(ToastService);
     
    -  originalName = '';
    -  englishName = '';
    -  loading = false;
    +  originalName = signal('');
    +  englishName = signal('');
    +  loading = signal(false);
     
    -  desserts: Dessert[] = [];
    +  desserts = signal<Dessert[]>([]);
     
       ngOnInit(): void {
         this.search();
    
    [...]
    
     
       search(): void {
         const filter: DessertFilter = {
    -      originalName: this.originalName,
    -      englishName: this.englishName,
    +      originalName: this.originalName(),
    +      englishName: this.englishName(),
         };
     
    -    this.loading = true;
    +    this.loading.set(true);
     
         this.#dessertService.find(filter).subscribe({
           next: (desserts) => {
    -        this.desserts = desserts;
    -        this.loading = false;
    +        this.desserts.set(desserts);
    +        this.loading.set(false);
           },
           error: (error) => {
    -        this.loading = false;
    +        this.loading.set(false);
             this.#toastService.show('Error loading desserts!');
             console.error(error);
           },
    
    [...]
    
       }
     
       loadRatings(): void {
    -    this.loading = true;
    +    this.loading.set(true);
     
         this.#ratingService.loadExpertRatings().subscribe({
           next: (ratings) => {
    -        const rated = this.toRated(this.desserts, ratings);
    -        this.desserts = rated;
    -        this.loading = false;
    +        const rated = this.toRated(this.desserts(), ratings);
    +        this.desserts.set(rated);
    +        this.loading.set(false);
           },
           error: (error) => {
             this.#toastService.show('Error loading ratings!');
             console.error(error);
    -        this.loading = false;
    +        this.loading.set(false);
           },
         });
       }
     
       updateRating(id: number, rating: number): void {
    -    console.log('rating changed', id, rating);
    +    const ratings = { [id]: rating };
    +    const rated = this.toRated(this.desserts(), ratings);
    +    this.desserts.set(rated);
       }
     }
    
  2. Switch to the file desserts.component.html (src/app/desserts/desserts.component.html) and call the dessert Signal's getter in the for loop:

     type="submit"
           (click)="search()"
           class="btn btn-primary"
    -      [disabled]="loading"
    +      [disabled]="loading()"
         >
           Search
         </button>
    
    [...]
    
           type="button"
           (click)="loadRatings()"
           class="btn ml-2"
    -      [disabled]="loading"
    +      [disabled]="loading()"
         >
           Expert Ratings
         </button>
     
    -    @if (loading) {
    +    @if (loading()) {
           <div class="loader"></div>
         }
       </div>
     </form>
     
     <div class="card-grid">
    -  @for (dessert of desserts; track dessert.id) {
    +  @for (dessert of desserts(); track dessert.id) {
         <app-dessert-card
           [dessert]="dessert"
           (ratingChange)="updateRating(dessert.id, $event)"
         ></app-dessert-card>
       }
    -  @if (desserts.length === 0 && !loading) {
    +  @if (desserts().length === 0 && !loading()) {
         <p class="text-lg">Sorry, no desserts found.</p>
       }
     </div>
    


💾 Please find this lab's solution in the branch 01a-add-signals (get reset --hard && git checkout 01a-add-signals). You don't have to switch to it branch, if you are fine.

1.2 Derive State Using Computed Signals

Our Signal-based solution works so far. However, we need to take care of merging the desserts with the ratings in two places. Using computed signals, we can (and should) simplify our code.

In this lab, you will use computed to derive rated desserts from the loaded desserts and the ratings.

  1. Open the file desserts.component.ts (src/app/desserts/desserts.component.ts) and introduce a Signal for the loaded ratings as well a computed signal combining the desserts with the ratings:

     import { JsonPipe } from '@angular/common';
    -import { Component, OnInit, inject, signal } from '@angular/core';
    +import { Component, OnInit, computed, inject, signal } from '@angular/core';
     import { FormsModule } from '@angular/forms';
     import { Dessert } from '../data/dessert';
     import { DessertFilter } from '../data/dessert-filter';
    
    [...]
    
       loading = signal(false);
     
       desserts = signal<Dessert[]>([]);
    +  ratings = signal<DessertIdToRatingMap>({});
    +  ratedDesserts = computed(() => this.toRated(this.desserts(), this.ratings()));
     
       ngOnInit(): void {
         this.search();
    
    [...]
    
     
         this.#ratingService.loadExpertRatings().subscribe({
           next: (ratings) => {
    -        const rated = this.toRated(this.desserts(), ratings);
    -        this.desserts.set(rated);
    +        this.ratings.set(ratings);
             this.loading.set(false);
           },
           error: (error) => {
    
    [...]
    
       }
     
       updateRating(id: number, rating: number): void {
    -    const ratings = { [id]: rating };
    -    const rated = this.toRated(this.desserts(), ratings);
    -    this.desserts.set(rated);
    +    this.ratings.update((ratings) => ({
    +      ...ratings,
    +      [id]: rating,
    +    }));
       }
     }
    
  2. Switch to the file desserts.component.html (src/app/desserts/desserts.component.html) and use the computed Signal providing the rated desserts in the for loop:

     </form>
     
     <div class="card-grid">
    -  @for (dessert of desserts(); track dessert.id) {
    +  @for (dessert of ratedDesserts(); track dessert.id) {
         <app-dessert-card
           [dessert]="dessert"
           (ratingChange)="updateRating(dessert.id, $event)"
         ></app-dessert-card>
       }
    -  @if (desserts().length === 0 && !loading()) {
    +  @if (ratedDesserts().length === 0 && !loading()) {
         <p class="text-lg">Sorry, no desserts found.</p>
       }
     </div>
    


💾 Please find this lab's solution in the branch 01b-computed-rated (get reset --hard && git checkout 01b-computed-rated). You don't have to switch to it branch, if you are fine.


> Follow us on X (Twitter) > More Trainings (German) > More Trainings (English)