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();
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> helpersThese 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());
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));
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 :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With