Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Boolean algebra helpers for composition of RxJS Observables

How can I avoid writing crazy combineLatest statements for calculation of simple boolean logic expressions?

eg. this simple expression barely even fits in the stackoverflow code control, and if you accidentally reorder the parameters you'll have a really hard time debugging!

this.showPlayButton = combineLatest(this.playPending, this.isReady, this.showOverlay)
  .pipe(
   map(([playPending, isReady, showOverlay]) => isReady && !playPending && showOverlay),
   distinctUntilChanged();
like image 754
Simon_Weaver Avatar asked Dec 14 '25 04:12

Simon_Weaver


1 Answers

Ok so I've been quite surprised that I couldn't find an existing library for this, so I've started collecting together a few helper observable creation functions.

Observable<boolean> helpers

These are the 'purest' helpers, take in and also output Observable<boolean>. I've added distinctUntilChanged() to each, which prevents multiple unnecessary emissions. This is a very inert operator without the more complex consequences of share or shareReplay(1) - but it's important to be aware it has been applied.

export const allTrue = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.every(v => v == true) ), distinctUntilChanged());
export const allFalse = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.every(v => v == false) ), distinctUntilChanged());
export const anyTrue = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.find(v => v == true) != undefined ), distinctUntilChanged());
export const anyFalse = (...observables: Array<ObservableInput<boolean>> ) => combineLatest(observables).pipe(map(values => values.find(v => v == false) != undefined), distinctUntilChanged());

export const not = (observable: Observable<boolean> ) => observable.pipe(map(value => !value), distinctUntilChanged());

Boolean converters for truthy / falsey

Some other 'helpers' in the same category, such as ifTruthy or ifFalsy are intended to be used with the above helpers (since they require true boolean values). These currently use != as opposed to !==, so isDefined(of(null)) and isDefined(of(undefined)) both product a true Observable. Also isEqual won't be a deep comparison.

 export const isTruthy = <T>(observable: Observable<T>) => observable.pipe(map(obsValue => !!obsValue), distinctUntilChanged());
 export const isFalsey = <T>(observable: Observable<T>) => observable.pipe(map(obsValue => !obsValue), distinctUntilChanged());

 export const isDefined = <T>(observable: Observable<T>) => observable.pipe(map(obsValue => obsValue != undefined), distinctUntilChanged());
 export const isEqual = <T>(observable: Observable<T>, value: T) => observable.pipe(map(obsValue => obsValue == value), distinctUntilChanged());
 export const notEqual = <T>(observable: Observable<T>, value: T) => observable.pipe(map(obsValue => obsValue != value), distinctUntilChanged());

There's also a third category such as iff which is equivalent to SQL Server's iff statment. The output of these is either A or B (the type of whatever your parameters are). This is like a very simple if statement.

export const iff = <A, B>(ifObs: Observable<boolean>, trueValue: A, falseValue: B) => ifObs.pipe(map(value => value ? trueValue : falseValue));

Composition

With RxJS it's super easy to compose them together and create very readable combinations. I often use these in Angular components for UI specific properties, and it's much easier to debug. These below are real code examples and should be pretty self explanatory.

loading$ = not(this.loaded$);
hasSelectedOrder$ = isTruthy(orderId$);  // Note: this wouldn't work for something zero
hasSelectedOrder$ = isDefined(orderId$);  // uses != undefined 

layout$ = iff(deviceType.isDesktop$, 'horizontal', 'vertical');
isMobileOrTablet$: anyTrue(this.isMobile$, this.isTablet);
isTabletOrDesktop$: anyTrue(this.isTablet, this.isDesktop$);

isBusy$ = anyTrue(this.hasBusyTask$, this.busyService.isBusy$);

// very useful for UI (using async pipe)
isAddNewCreditCardSelected$ = isEqual(selectedPaymentMoniker$, 'NEW');
showSavedPayments$ = allTrue(showAvailablePaymentMethods$, hasVault$, not(isAddNewCreditCardSelected$));
showAddCreditCardButton$ = allTrue(showPaymentButtons$, not(showAddCreditCardPanel$), not(showVault$))
showDefaultFooter$ = allTrue(not(this.isWebapp$), this.showDefaultFooter$);
showBusyIndicator$ = allTrue(not(this.pageService.handlesBusyIndicator$), this.busyService.isBusy$) ;

And as for the original question, it simply becomes this:

this.showPlayButton$ = allTrue(this.isReady$, not(this.playPending$), this.showOverlay$)

I will add to this list as I come up with new ones. These have covered most situations I've come across so far. This could certainly be made into a library, but I'm not able to formalize this right now to be able to do that. If something already exists would love to compare :-)

like image 161
Simon_Weaver Avatar answered Dec 16 '25 09:12

Simon_Weaver



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!