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?
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