Search code examples
angularsignals

angular 17 - How too use the new input with declartive with toSignal


I'm trying to use the new toSignal and input with the new angular 17.x. But when it comes to "input" i can only get the value on "OnInit" how can i solve this because I'm trying to filter the observable with the incoming value from input declarative.

Hope you guys understand what I mean.

  transferId = input.required<string>();
  progressSteamType = input.required<string>();
  progressBarHeightSmall = input<boolean>(true);

  progress$ = this._progressService.progress$;

  private _subs = new Subscription();
  progress = toSignal<IProgress | null>(this.progress$);

  ngOnInit(): void {
    this.progress$.pipe(
      filter(p => p.transferId.toLowerCase() === this.transferId()?.toLowerCase() && p.type === this.progressSteamType()) <--- this won't work because i already declared it above. The only way i can get the transferId is in ngOnit. because its an input
    )
  }

What I want to achieve is something like this. But this wont work. because transferId hasn't initiated

  progress$ = this._progressService.progress$.pipe(
    filter(p => p.transferId.toLowerCase() === this.transferId()?.toLowerCase() && p.type === this.progressSteamType())
  )

Solution

  • We can use effect to watch for new signal changes and trigger the pipe update! Only when transferId is available we trigger the update!

    Then we take the effectRef which can be used to destroy the signal once we are done with updating the pipe, we can use effectRef.destroy() for this!

    import { Component, effect, input } from '@angular/core';
    import { Subscription, of, interval } from 'rxjs';
    import { toSignal } from '@angular/core/rxjs-interop';
    import { filter, map } from 'rxjs/operators';
    import { CommonModule } from '@angular/common';
    @Component({
      selector: 'app-child',
      standalone: true,
      imports: [CommonModule],
      template: `{{progress$ | async | json}}`,
    })
    export class ChildComponent {
      transferId = input.required<string>();
      progressSteamType = input.required<string>();
      progressBarHeightSmall = input<boolean>(true);
    
      progress$ = interval(1000).pipe(
        map(() => ({ transferId: 'test', type: 'qwerty' }))
      );
    
      private _subs = new Subscription();
      progress = toSignal<any | null>(this.progress$);
    
      constructor() {
        const effectRef = effect(() => {
          const transferId = this.transferId();
          if (transferId) {
            console.log('time to cleanup', transferId, this.progressSteamType());
            this.progress$ = this.progress$.pipe(
              filter(
                (p: any) =>
                  p.transferId.toLowerCase() === transferId?.toLowerCase() &&
                  p.type === this.progressSteamType()
              )
            );
            effectRef.destroy();
          }
        });
      }
    
      ngOnInit(): void {}
    }
    

    PARENT:

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { ChildComponent } from './app/child/child.component';
    
    @Component({
      selector: 'app-root',
      imports: [ChildComponent],
      standalone: true,
      template: `
        <app-child [transferId]="'test'" [progressSteamType]="'qwerty'"/>
      `,
    })
    export class App {}
    
    bootstrapApplication(App);
    

    Stackblitz Demo