1

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!

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
RVP
  • 2,330
  • 4
  • 23
  • 34
  • Is there something that prevents you from putting the contents of `onBeforeNext` to `set countryOfBirth`? – Estus Flask Oct 22 '16 at 00:39
  • Yeah - I pass the Subject instance to the forms module so the forms module is blissfully unaware of what type of object has created this subject. For this to work in the setter I would have to do `someModel.countryOfBirth = CountryEnum.US` but in my custom forms module I can only do `value$.next("US")`. – RVP Oct 22 '16 at 00:44
  • 1
    Basically my higher-level model uses enums that are useful in my app's logic but when it comes to forms we only care about strings so I would like my model to know how to handle both scenarios since it's not the form module's business to care about these things. – RVP Oct 22 '16 at 00:46
  • 2
    [I've ended up with extending a subject](http://stackoverflow.com/a/39858574/3731501) to provide side effects for `next`. I guess this pretty much suits your case too. – Estus Flask Oct 22 '16 at 00:49
  • Thanks for the suggestion @estus - I wanted to check if there's an existing hook that I've missed but if that's not the case I'll go your route. – RVP Oct 22 '16 at 00:57
  • I'll save you some time, there's none, at least in RxJS 5. The interior of the subjects is really simple. And fortunately, they are easy enough to be comprehended and extended. – Estus Flask Oct 22 '16 at 01:06
  • @estus thanks again - what you suggested worked like charm – RVP Oct 22 '16 at 16:56

1 Answers1

3

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!

RVP
  • 2,330
  • 4
  • 23
  • 34