1

I have a component with 2 inputs (or mor) and I want to:

  1. Trigger a method X the first time, when both value are set and exists
  2. Trigger a method X each time, if either one of the both value changes
<some-cmp [itemid]="activeItemId$ | async" [userId]="activeUserId$ | async"></some-cmp>

Both values can change at any time, so I figured using rxjs to build a stream lets me control everything. My current solution seems a bit hacky and is difficult to test. I use 2 BehaviourSubjects and combineLatest with a debounceTime.

@Input() set itemId (id){this.itemId$.next(id)};
@Input() set userId (id){this.userId$.next(id)};

itemId$ = new BehaviourSubject$(null);
userId$ = new BehaviourSubbject$(null);

ngOnInt(){
    combineLatest([
        this.itemId$.pipe(filter(item=>item!===null)),
        this.userId$.pipe(filter(item=>item!===null))
    ]).pipe(
        debounceTime(10),
        switchMap(...)
    ).subscribe(...)
}

So my question are

  1. Is there a more elegant way to achieve this behavior?
  2. Is there a way to avoid the debounceTime, which makes testing difficult?

The debounceTime is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.

Han Che
  • 8,239
  • 19
  • 70
  • 116
  • 1
    fyi, https://github.com/angular/angular/issues/5689 or https://github.com/insidewhy/observable-input – martin May 03 '21 at 08:33
  • 1
    You could define one `Subject` that emits an object with properties for the itemId and userId. That might simplify the code. – DeborahK Nov 10 '21 at 19:10

2 Answers2

0

Angular provides ngOnChanges hook which can be used in this scenario. It'll trigger ngOnChanges method whenever there's a change in any of the inputs of the component.

Below is an example of how this can be achieved:

export class SomeComponent implements OnChanges {
    @Input() itemId: any;
  
    @Input() userId: any;
  
    ngOnChanges(changes: SimpleChanges) {
      const change = changes.itemId || changes.userId;
  
      if (change && change.currentValue !== change.previousValue) {
        this.doSomething();
      }
    }
  
    private doSomething() {
      // Your logic goes here
    }
  }

Your HTML will now look clean and you can get rid of async as well:

<some-cmp [itemid]="itemId" [userId]="userId"></some-cmp>
Krantisinh
  • 1,579
  • 13
  • 16
  • Thanks, I knew of ngOnChanges, but all it does is to replace the setters, since it cannot track the state that both values exists. – Han Che May 02 '21 at 15:54
  • @HanChe Actually, `ngOnChanges` is quite powerful. The changes that it receives follow the `SimpleChange` interface (https://angular.io/api/core/SimpleChange) that tracks all the important info like prrevValue, currentValue, isFirstChange, etc for every input prop. Hence, it definitely suits this problem IMO. – Krantisinh May 02 '21 at 16:38
0

You are right in using combineLatest, it will only emit the first time after each source has emitted once and then will emit any time any source emits.

Is there a way to avoid the debounceTime. [It] is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.

Maybe debounceTime isn't necessary due to the initial behavior of combineLatest; it will not emit the first time until all sources emit. However if you typically receive subsequent emissions from both sources that occur within a short timeframe, use of debounceTime may be an appropriate optimization.

Is there a more elegant way to achieve this behavior?

I think your code is fine. However, it may not be necessary to use BehaviorSubject since you aren't really using the default value. You could use plain Subject or ReplaySubject(1).

You could assign the result of your combineLatest to another variable and subscribe to that inside ngOnInit or use the async pipe in the template:

@Input() set itemId (id){ this.itemId$.next(id) };
@Input() set userId (id){ this.userId$.next(id) };

itemId$ = new Subject<string>();
userId$ = new Subject<string>();

data$ = combineLatest([
    this.itemId$.pipe(filter(i => !!i)),
    this.userId$.pipe(filter(i => !!i))
]).pipe(
    debounceTime(10),
    switchMap(...)
);

ngOnInit() {
  this.data$.subscribe(...);
}

BizzyBob
  • 12,309
  • 4
  • 27
  • 51