Search code examples
angularrxjsbehaviorsubject

Subjects - apply changes on every next value [BehaviorSubject.onBeforeNext?]


Say you have a simple BehaviorSubject

this.countryOfBirth$ = new BehaviorSubject<CountryEnum>(null);

get countryOfBirth() {
    return this.countryOfBirth$.value;
};

set countryOfBirth(val: CountryEnum) {
    this.countryOfBirth$.next(val);
};

So doing instance.countryOfBirth returns the last value and instance.countryOfBirth = CountryEnum.US pushes the new value to the Subject.

The problem I'm having is that I pass this subject instance to a custom model-driven form module which by default would wrongly pass a string instead of an instance of my CountryEnum class.

I can fix this in the forms module but I would like to keep that as decoupled as possible from any app-specific logic so it would make more sense to implement the fix in the BehaviorSubject instance itself.

My question is: is there hook or any other way to apply some changes to each and every "next" value before it triggers its subscribers? In other words, after my code does

instance.countryOfBirth = CountryEnum.US;

Before any of the subscribers are triggered I would like to check whether the value is a string (ex: US) and if so - I would like to get the corresponding CountryEnum instance and pass that to the "next" call instead of the original "US" string.

In code it would look something like

this.countryOfBirth$.onBeforeNext((val) => {
    if (typeof val == "string") {
        return CountryEnum.getInstance(val);
    } else if (val instanceof CountryEnum) {
        return val;
    } else {
        throw new Error("Unsupported value type");
    }
});

but obviously onBeforeNext doesn't exist and I can't seem to find anything in the dox that would do what I want.

Your help would be much appreciated!


Solution

  • Since apparently there is no readily available method to do what I needed this is how I implemented my solution using the approach that @estus mentioned in the comments:

    // BehaviorSubjectWithValidation.ts
    
    import { BehaviorSubject } from "rxjs/BehaviorSubject";
    
    export class BehaviorSubjectWithValidation<T> extends BehaviorSubject<T> {
    
        private _validator: (val: T) => T;
    
        constructor(defaultValue: T, validator: (val: T) => T) {
            super(defaultValue);
            this._validator = validator;
        }
    
        next(value: T): void {
            if (this._validator) {
                value = this._validator(value);
            }
            super.next(value);
        }
    }
    

    Then in my CountryEnum class I added the following method

    public static parse: (val: any) => CountryEnum = (val: any) => {
        if (val) {
            if (typeof val === "string") {
                return CountryEnum.getInstance(val);
            } else if (val instanceof CountryEnum) {
                return val;
            } else {
                throw new Error("Unsupported CountryEnum value");
            }
        } else {
            throw new Error("Invalid country");
        }
    }
    

    And then I use it in the following way in my main app's logic:

    this.countryOfBirth$ = 
        new BehaviorSubjectWithValidation<CountryEnum>(null, CountryEnum.parse);
    

    So now whatever part of my code adds a new value to this.countryOfBirth$ it will always going to filter it through CountryEnum.parse.

    Hope this helps someone!