Search code examples
typescriptrxjs

Implementing Register-like behaviour with rxjs


I'm very new to rxjs and declarative programming in general, so apologies if this is a foolish question!

I am, in Typescript, trying to implement behaviour similar to that of a digital register, in that I have two input boolean observables, let's say A and B, and an output register O (also boolean)

Functionality:

  1. O should start with default value "false"
  2. When B goes from FALSE to TRUE, O should update itself with the current value of A
  3. In all other cases, the current value of O should remain static

What I am trying to do is, when B becomes true, save the value of A, so that when B becomes false again I can then use the value of A

I've seen this question, but I'm not able to bridge the gap from its answer to my requirement

I think that I can use pairwise() on B to detect the case when B goes from FALSE to TRUE (previousValue === false && currentValue === true) in conjunction with withLatestFrom to query the current value of A and update the value of O.

What I do not understand is how, in all other cases, I can re-emit/retain the current value of O. As far as I can tell, there is no way to tell, within a pipe operator when defining the Observable O, to reference its current value


Solution

  • The quickest way I could find to fulfil your requirements is to come up with the following:

    1. Input observables a$ and b$ as BehaviorSubject and ReplaySubject respectively.
      • a$: BehaviorSubject<string>: Emit either default value or the last value emitted to it (using eg. a$.next()) to it's subscribers including future subscribers. If you wish for a different behavior you could change it either with ReplaySubject with buffer 1 (emit the last emitted value but without a default value) or Subject (does not emit previous emission to future subscribers).
      • b$: ReplaySubject<boolean>(1): Emit the last emitted value to it's future subscribers.
    2. Observable bTransformer$ that takes care of your requirements using the following operators:
      • distinctUntilChanged - will only emit when the values of b$ changes i.e. true -> false or false -> true.
      • pairwise - emits previous and current emissions as array either [true, false] or [false, true].
      • combineLatestWith(a$) - combine with the value of a$ eg. [[true, false], 'value of a'].
      • filter - emit only when b$ changes from false -> true.
      • map - emit the value of a$.
    3. Output observable o$ - starts with value false using the operator startWith and emit whenever bTransformer$ emits.

    Note that with the current implementation, o$ will also emit whenever a value is emitted to a$ since it's wired it using the combineLatestWith$ operator.

    Try the following:

    const { BehaviorSubject, ReplaySubject, combineLatestWith, distinctUntilChanged, pairwise, filter, startWith, map } = rxjs;
    
    const a$ = new BehaviorSubject('value of a');
    const b$ = new ReplaySubject(1);
    const bTransformer$ = b$.pipe(
      distinctUntilChanged(),                // emit only when b$ changes
      pairwise(),                            // emit previous and new values: either [true, false] or [false, true]
      combineLatestWith(a$),                 // combine with the value of a$: eg. [[true, false], 'value of a']
      filter(([[bValueOld]]) => !bValueOld), // destructure the pairwise value of b and emit only if it's [false, true]
      map(([_, a]) => a)                     // emit the value of a
    );
    
    const o$ = bTransformer$.pipe(
      startWith(false),                      // always start the subscription to o$ with 'false'
    );
    
    function onClickB(value) {
      b$.next(value);
    }
    
    function emitValueA() {
      const value = document.getElementById('a-input').value;
      a$.next(value);
    }
    
    o$.subscribe(console.log);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.8.1/rxjs.umd.min.js"></script>
    
    <button onclick="onClickB(true)">Emit true to b</button>
    <button onclick="onClickB(false)">Emit false to b</button>
    <br>
    <br>
    <input id="a-input"/>
    <button onclick="emitValueA()">Emit value to a</button>