How to convert HTTP Response from Observable to Angular signal with toSignal

Connie Leung - Jun 10 '23 - - Dev Community

Introduction

I extended my Pokemon application to call an API to retrieve a Pokemon by id. The HTTP request returned an Observable that required ngIf and async pipe to resolve in order to render the results in inline template. In this blog post, I want to demonstrate how to convert HTTP response to Signal with toSignal. toSignal returns T | undefined but we can provide an initial value to the function to get rid of the undefined type.

Old Pokemon Component with RxJS codes

// pokemon.component.ts

...omitted import statements for brevity...

const retrievePokemonFn = () => {
  const httpClient = inject(HttpClient);
  return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
    .pipe(
      map((pokemon) => ({
        id: pokemon.id,
        name: pokemon.name,
        height: pokemon.height,
        weight: pokemon.weight,
        back_shiny: pokemon.sprites.back_shiny,
        front_shiny: pokemon.sprites.front_shiny,
      }))
    );
}

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [AsyncPipe, FormsModule, NgIf, NgTemplateOutlet],
  template: `
    <h1>
      Display the first 100 pokemon images
    </h1>
    <div>
      <ng-container *ngIf="pokemon$ | async as pokemon">
        <div class="pokemon-container">
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Id: ', value: pokemon.id }"></ng-container>
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Name: ', value: pokemon.name }"></ng-container>
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Height: ', value: pokemon.height }"></ng-container>
          <ng-container *ngTemplateOutlet="details; context: { $implicit: 'Weight: ', value: pokemon.weight }"></ng-container>
        </div>
        <div class="container">
          <img [src]="pokemon.front_shiny" />
          <img [src]="pokemon.back_shiny" />
        </div>
      </ng-container>
    </div>
    <div class="container">
      <button class="btn" #btnMinusTwo>-2</button>
      <button class="btn" #btnMinusOne>-1</button>
      <button class="btn" #btnAddOne>+1</button>
      <button class="btn" #btnAddTwo>+2</button>
      <form #f="ngForm" novalidate>
        <input type="number" [(ngModel)]="searchId" [ngModelOptions]="{ updateOn: 'blur' }" 
          name="searchId" id="searchId" />
      </form>
    </div>
    <ng-template #details let-name let-value="value">
      <label><span style="font-weight: bold; color: #aaa">{{ name }}</span>
        <span>{{ value }}</span>
      </label>
    </ng-template>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent implements OnInit {
  @ViewChild('btnMinusTwo', { static: true, read: ElementRef })
  btnMinusTwo: ElementRef<HTMLButtonElement>;

  @ViewChild('btnMinusOne', { static: true, read: ElementRef })
  btnMinusOne: ElementRef<HTMLButtonElement>;

  @ViewChild('btnAddOne', { static: true, read: ElementRef })
  btnAddOne: ElementRef<HTMLButtonElement>;

  @ViewChild('btnAddTwo', { static: true, read: ElementRef })
  btnAddTwo: ElementRef<HTMLButtonElement>;

  @ViewChild('f', { static: true, read: NgForm })
  myForm: NgForm;

  pokemon$!: Observable<FlattenPokemon>;
  searchId = 1;
  retrievePokemon = retrievePokemonFn();

  ngOnInit() {
    const btnMinusTwo$ = this.createButtonClickObservable(this.btnMinusTwo, -2);
    const btnMinusOne$ = this.createButtonClickObservable(this.btnMinusOne, -1);
    const btnAddOne$ = this.createButtonClickObservable(this.btnAddOne, 1);
    const btnAddTwo$ = this.createButtonClickObservable(this.btnAddTwo, 2);

    const inputId$ = this.myForm.form.valueChanges
      .pipe(
        debounceTime(300),
        distinctUntilChanged((prev, curr) => prev.searchId === curr.searchId),
        filter((form) => form.searchId >= 1 && form.searchId <= 100),
        map((form) => form.searchId),
        map((value) => ({
          value,
          action: POKEMON_ACTION.OVERWRITE,
        }))
      );

    const btnPokemonId$ = merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$, inputId$)
      .pipe(
        scan((acc, { value, action }) => { 
          ... derive pokemon id....
        }, 1),
        startWith(1),
        shareReplay(1),
      );

      this.pokemon$ = btnPokemonId$.pipe(switchMap((id) => this.retrievePokemon(id)));
  }

  createButtonClickObservable(ref: ElementRef<HTMLButtonElement>, value: number) {
    return fromEvent(ref.nativeElement, 'click').pipe(
      map(() => ({ value, action: POKEMON_ACTION.ADD }))
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

retrievePokemonFn() returns a function that accepts an id to retrieve a Pokemon. When btnPokemonId$ Observable emits an id, the stream invokes this.retrievePokemon and assigns the results to this.pokemon$ Observable. Then, this.pokemon$ is resolved in inline template to display image URLs and details. My goals are to refactor ngOnInit and convert this.pokemon$ Observable to Angular signal. Then, inline template renders the signal value instead of the resolved Observable.

Store Reactive results into signal

First, I create a signal to store current Pokemon id

// pokemon-component.ts

pokemonId = signal(1);
Enter fullscreen mode Exit fullscreen mode

Then, I modify inline template to add click event to the button elements to update pokemonId signal.

Before (RxJS)

<div class="container">
   <button class="btn" #btnMinusTwo>-2</button>
   <button class="btn" #btnMinusOne>-1</button>
   <button class="btn" #btnAddOne>+1</button>
   <button class="btn" #btnAddTwo>+2</button>
</div>
Enter fullscreen mode Exit fullscreen mode
After (Signal)

<button class="btn" *ngFor="let delta of [-2, -1, 1, 2]" (click)="updatePokemonId(delta)">{{delta < 0 ? delta : '+' + delta }}</button>
Enter fullscreen mode Exit fullscreen mode

In signal version, I remove template variables such that the component does not require ViewChild to query HTMLButtonElement

readonly min = 1;
readonly max = 100;

updatePokemonId(delta: number) {
    this.pokemonId.update((value) => {
      const potentialId = value + delta;
      return Math.min(this.max, Math.max(this.min, potentialId));
    });
}
Enter fullscreen mode Exit fullscreen mode

When button is clicked, updatePokemonId updates pokemonId to a value between 1 and 100.

In Imports array, I include NgFor to use ngFor directive

imports: [..., NgFor],
Enter fullscreen mode Exit fullscreen mode

Now, I declare searchId signal to react to changes to number input field

// pokemon.component.ts

searchId = signal(1);
Enter fullscreen mode Exit fullscreen mode

searchId emits search value, streams to subsequent RxJS operators and subscribes to update pokemonId signal.

Before (RxJS)

ngOnInit() {
    const inputId$ = this.myForm.form.valueChanges
       .pipe(
         debounceTime(300),
         distinctUntilChanged((prev, curr) => prev.searchId === curr.searchId),
         filter((form) => form.searchId >= 1 && form.searchId <= 100),
         map((form) => form.searchId),
         map((value) => ({
           value,
           action: POKEMON_ACTION.OVERWRITE,
         }))
       );
}
Enter fullscreen mode Exit fullscreen mode
After (Signal)

<input type="number" [ngModel]="searchId()"
   (ngModelChange)="searchId.set($event)"
   name="searchId" id="searchId" />
Enter fullscreen mode Exit fullscreen mode

[(ngModel)] is decomposed to [ngModel] and (ngModelChange) to get my solution to work. NgModel input is bounded to searchId() and (ngModelChange) updates the signal when input value changes.

constructor() {
    toObservable(this.searchId)
      .pipe(
        debounceTime(300),
        distinctUntilChanged(),
        filter((value) => value >= this.min && value <= this.max),
        map((value) => Math.floor(value)),
        takeUntilDestroyed(),
      ).subscribe((value) => this.pokemonId.set(value));
}
Enter fullscreen mode Exit fullscreen mode

Angular 16 introduces takeUntilDestroyed that completes Observable; therefore, I donโ€™t have to implement OnDestroy interface to unsubscribe subscription manually.

Convert Observable to Angular Signal with toSignal

import { toSignal } from '@angular/core/rxjs-interop';

const initialValue: DisplayPokemon = {
  id: 0,
  name: '',
  height: -1,
  weight: -1,
  back_shiny: '',
  front_shiny: '',
};

pokemon = toSignal(
    toObservable(this.pokemonId).pipe(switchMap((id) => this.retrievePokemon(id))), { initialValue });
Enter fullscreen mode Exit fullscreen mode

When the codes update pokemonId, toObservable(this.pokemonId) emits the id to switchMap operator to retrieve the specific Pokemon. The result of the stream is a Pokemon Observable that is passed to toSignal to convert to an Angular signal.

pokemon is a signal, I use it to compute rowData signal and pass that signal value to the context object of ngTemplateOutlet.

rowData = computed(() => {
    const { id, name, height, weight } = this.pokemon();
return [
    { text: 'Id: ', value: id },
    { text: 'Name: ', value: name },
    { text: 'Height: ', value: height },
    { text: 'Weight: ', value: weight }, 
  ] 
});

<div class="pokemon-container">
    <ng-container 
*ngTemplateOutlet="details; context: { $implicit: rowData() }"></ng-container>
</div>
Enter fullscreen mode Exit fullscreen mode

I modify ngTemplate to iterate the rowData array to display the label and actual value.

<ng-template #details let-rowData>
   <label *ngFor="let data of rowData">
      <span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
      <span>{{ data.value }}</span>
  </label>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

I remove NgIf and AsyncPipe from the imports array because the inline template does not need them anymore. The final array is consisted of FormsModule, NgTemplateOutlet and NgFor.

imports: [FormsModule, NgTemplateOutlet, NgFor],
Enter fullscreen mode Exit fullscreen mode

New Pokemon Component using toSignal

// retrieve-pokemon.ts
// ...omitted import statements due to brevity...

export const retrievePokemonFn = () => {
  const httpClient = inject(HttpClient);
  return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
    .pipe(
      map((pokemon) => pokemonTransformer(pokemon))
    );
}

const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => ({
  id: pokemon.id,
  name: pokemon.name,
  height: pokemon.height,
  weight: pokemon.weight,
  back_shiny: pokemon.sprites.back_shiny,
  front_shiny: pokemon.sprites.front_shiny,
});

// pokemon.component.ts
...omitted import statements for brevity...

const initialValue: DisplayPokemon = {
  id: 0,
  name: '',
  height: -1,
  weight: -1,
  back_shiny: '',
  front_shiny: '',
};

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [FormsModule, NgTemplateOutlet, NgFor],
  template: `
    <h2>
      Display the first 100 pokemon images
    </h2>
    <div>
      <ng-container>
        <div class="pokemon-container">
          <ng-container *ngTemplateOutlet="details; context: { $implicit: rowData() }"></ng-container>
        </div>
        <div class="container">
          <img [src]="pokemon().front_shiny" />
          <img [src]="pokemon().back_shiny" />
        </div>
      </ng-container>
    </div>
    <div class="container">
      <button class="btn" *ngFor="let delta of [-2, -1, 1, 2]" (click)="updatePokemonId(delta)">
        {{delta < 0 ? delta : '+' + delta }}
      </button>
      <input type="number" [ngModel]="searchId()" (ngModelChange)="searchId.set($event)"
          name="searchId" id="searchId" />
    </div>
    <ng-template #details let-rowData>
      <label *ngFor="let data of rowData">
        <span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
        <span>{{ data.value }}</span>
      </label>
    </ng-template>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  readonly min = 1;
  readonly max = 100;

  searchId = signal(1);
  retrievePokemon = retrievePokemonFn();
  pokemonId = signal(1);

  pokemon = toSignal(
    toObservable(this.pokemonId).pipe(switchMap((id) => this.retrievePokemon(id))), { initialValue });

  rowData = computed(() => {
    const { id, name, height, weight } = this.pokemon();
    return [
      { text: 'Id: ', value: id },
      { text: 'Name: ', value: name },
      { text: 'Height: ', value: height },
      { text: 'Weight: ', value: weight }, 
    ] 
  });

  updatePokemonId(delta: number) {
    this.pokemonId.update((value) => {
      const potentialId = value + delta;
      return Math.min(this.max, Math.max(this.min, potentialId));
    });
  }

  constructor() {
    toObservable(this.searchId)
      .pipe(
        debounceTime(300),
        distinctUntilChanged(),
        filter((value) => value >= this.min && value <= this.max),
        map((value) => Math.floor(value)),
        takeUntilDestroyed(),
      ).subscribe((value) => this.pokemonId.set(value));
  }
}
Enter fullscreen mode Exit fullscreen mode

The new version uses toSignal function to convert Pokemon Observable to Pokemon signal with an initial value. After the conversion, I can use computed to derive rowData signal and pass the signal value to the inline template to render. Thus, the inline template and logic is less verbose than the previous RxJS version.

This is it and I have enhanced the Pokemon application to make HTTP request and convert the HTTP response to signal. Then, signal functions are called within the inline template to render the results.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .