Search code examples
angulartypescriptangular2-observablesangular-decorator

Angular Property Decorator only creates one property instance per Class Type


I'm using a Property Decorator to create an Observable with static getter/setter for every property.

In the end you can use the decorator in this way

class Test {
    @ObservableProperty(DEFAULT_CATS) 
    cats: number;

    @ObservableProperty(DEFAULT_PIGS) 
    pigs: number;
}

The actual code for the decorator is

export function ObservableProperty(defaultValue = null): any {
    return (target, key, descriptor) => {
        const accessor = `${key}$`;
        target[accessor] = new BehaviorSubject(defaultValue);

        return Object.assign({}, descriptor, {
            get: function() {
                return this[accessor].getValue();
            },
            set: function(value: any) {
                this[accessor].next(value);
            },
        });
    };
}

Now everything works fine with one instance of the Test component.

But with two instances this test actually fails.

fdescribe('ObservableProperty Decorator', () => {
    let test: Test;
    let doppleganger: Test;

    beforeEach(() => {
        test = new Test();
        doppleganger = new Test();
    });

    it('should create different observables for each props', () => {
        expect(test['cats$'] === doppleganger['cats$']).toBe(false);
    });
})

Because the decorator works on the prototype of the component instances the created variables are exactly the same one.

How can I get around this issue?

If it cannot be done with a decorator what's an elegant alternative way?


Solution

  • I'm going to answer the question with the solution i found after one day of thinking.

    First of all the main problem for which i couldn't access the instance was in the use of the arrow function in the definition of the decorator. So i changed:

     return (target, key, descriptor) => {
    

    in

    return  function (target, key) {
    

    This way i could access the instance from inside the getter/setter using this.

    Then i had to find a good spot to initialize the BehaviourSubject. Doing it in getter or setter of the main property wasn't going to work (i want to access this.cats$ without accessing first this.cats).

    So i solved with a new getter for cats$. That stores the variable in a secret property and creates it if it doesn't exist.

    Here's the final code!

    export function ObservableProperty(defaultValue = null): any {
        return  function (target, key) {
            const accessor = `${key}$`;
            const secret = `_${key}$`;
    
            Object.defineProperty(target, accessor, {
                get: function () {
                    if (this[secret]) {
                        return this[secret];
                    }
                    this[secret] = new BehaviorSubject(defaultValue);
                    return this[secret];
                },
                set: function() {
                    throw new Error('You cannot set this property in the Component if you use @ObservableProperty');
                },
            });
    
            Object.defineProperty(target, key, {
                get: function () {
                    return this[accessor].getValue();
                },
                set: function (value: any) {
                    this[accessor].next(value);
                },
            });
        };
    }