Search code examples
angulartypescriptdependency-injectionangular2-changedetectionangular-decorator

How to pass changeDetectorRef to decorator?


I'm creating my own decorator for the property that will automatically synchronize value of the property with firebase db. My decorator is preety simple and looks like this:

export function AutoSync(firebasePath: string) {
    return (target: any, propertyKey: string) => {
        let   _propertyValue = target[propertyKey];

        // get reference to db
        const _ref           = firebase.database().ref(firebasePath);

        // listen for data from db
        _ref.on('value', snapshot => {
            _propertyValue = snapshot.val();
        });

        Object.defineProperty(target, propertyKey, {
            get: () => _propertyValue,
            set: (v) => {
                _propertyValue = v;
                _ref.set(v);
            },
        });
    }
}

I'm using it like this:

@AutoSync('/config') configuration;

And it (almost) works like a charm. My configuration property is automatically synchronised with firebase db object on the path /config. Property setter automatically updates the value in db - that works great!

Now the problem: when value in database is being updated in firebase db by some other app than we get the snapshow and value of _propertyValue is being correcly updated but changeDetection is not triggered as the configuration property is not changed directly.

So I need to do that manually. I was thinking of triggering change detector from the function in decorator but I'm not sure how to pass an instance of change detector to the decorator.

I have come with a walkaround: In app.component's constructor I save reference to change detector instance in global window object:

constructor(cd: ChangeDetectorRef) {
    window['cd'] = cd;

    (...)
}

Now I can use it in my AutoSync decorator like this:

// listen for data from db
_ref.on('value', snapshot => {
    _propertyValue = snapshot.val();
    window['cd'].detectChanges();
});

but this is kind of hacky and dirty solution. What is the right way?


Solution

  • There is no good way to pass class property value to a decorator. Saving provider reference to a global variable is not just hacky but wrong solution, because there may be more than one class instance, and thus more than one provider instance.

    Property decorator is evaluated once on class definition, where target is class prototype, and it is expected that propertyKey property is defined there. _ref and _propertyValue are same for all instances, _ref.on is called and listened even if the component was never instantiated.

    An workaround is to expose desired provider instance on class instance - or injector if several providers should be accessed in a decorator. Since each component instance should set up its own _ref.on listener, it should be performed in class constructor or ngOnInit hook. It's impossible to patch constructor from property decorator, but ngOnInit should be patched:

    export interface IChangeDetector {
      cdRef: ChangeDetectorRef;
    }
    
    export function AutoSync(firebasePath: string) {
      return (target: IChangeDetector, propertyKey: string) => {
        // same for all class instances
        const initialValue = target[propertyKey];
    
        const _cbKey = '_autosync_' + propertyKey + '_callback';
        const _refKey = '_autosync_' + propertyKey + '_ref';
        const _flagInitKey = '_autosync_' + propertyKey + '_flagInit';
        const _flagDestroyKey = '_autosync_' + propertyKey + '_flagDestroy';
        const _propKey = '_autosync_' + propertyKey;
    
        let ngOnInitOriginal = target['ngOnInit'];
        let ngOnDestroyOriginal = target['ngOnDestroy']
    
        target['ngOnInit'] = function () {
          if (!this[_flagInitKey]) {
            // wasn't patched for this key yet
            this[_flagInitKey] = true;
    
            this[_cbKey] = (snapshot) => {
              this[_propKey] = snapshot.val();
            };
    
            this[_refKey] = firebase.database().ref(firebasePath);
            this[_refKey].on('value', this[_cbKey]);
          }
    
          if (ngOnInitOriginal)
            return ngOnInitOriginal.call(this);
        };
    
        target['ngOnDestroy'] = function () {
          if (!this[_flagDestroyKey]) {
            this[_flagDestroyKey] = true;
            this[_refKey].off('value', this[_cbKey]);
          }
    
          if (ngOnDestroyOriginal)
            return ngOnDestroyOriginal.call(this);
        };
    
        Object.defineProperty(target, propertyKey, {
          get() {
            return (_propKey in this) ? this[_propKey] : initialValue;
          },
          set(v) {
            this[_propKey] = v;
            this[_refKey].set(v);
            this.cdRef.detectChanges();
          },
        });
    
      }
    }
    
    class FooComponent implements IChangeDetector {
      @AutoSync('/config') configuration;
    
      constructor(public cdRef: ChangeDetectorRef) {}
    }
    

    It is considered a hack