Skip to content

Instantly share code, notes, and snippets.

@pratheeshrussell
Last active August 25, 2023 07:38
Show Gist options
  • Save pratheeshrussell/7ae0ef1ba05ee2c4a26d2705eb012151 to your computer and use it in GitHub Desktop.
Save pratheeshrussell/7ae0ef1ba05ee2c4a26d2705eb012151 to your computer and use it in GitHub Desktop.
An Angular Decorator to call a function on value change
// This is a modified version of property-watch-decorator to support objects
// Refer: https://github.com/zhaosiyang/property-watch-decorator
// might work but not optimal for deeply nested structures
export interface ChangeDetails<T> {
propertyKey: PropertyKey;
firstChange: boolean | null;
previousValue: T;
currentValue: T | undefined;
isFirstChange: () => boolean | null;
}
export type CallBackFunction<T> = (
value: T | undefined,
change?: ChangeDetails<T>
) => void;
function createObjectProxy<T = any>(
obj: any,
callBackFn: CallBackFunction<T>,
path: any[] = []
) {
return new Proxy(obj, {
get(target, key): any {
if (typeof target[key] === 'object' && target[key] !== null) {
return createObjectProxy(target[key], callBackFn, path.concat(key));
}
return target[key];
},
set(target, key, value) {
const oldValue = target[key];
const simpleChange: ChangeDetails<T> = {
propertyKey: [...path, key].join('.'),
firstChange: null,
previousValue: oldValue,
currentValue: value,
isFirstChange: () => null,
};
target[key] = value;
callBackFn.call(this, value, simpleChange);
return true;
},
deleteProperty(target, key) {
const oldValue = target[key];
const simpleChange: ChangeDetails<T> = {
propertyKey: [...path, key].join('.'),
firstChange: null,
previousValue: oldValue,
currentValue: undefined,
isFirstChange: () => null,
};
delete target[key];
callBackFn.call(this, undefined, simpleChange);
return true;
},
});
}
export function OnChange<T = any>(callback: CallBackFunction<T> | string) {
const cachedValueKey = Symbol();
const isFirstChangeKey = Symbol();
return (target: any, key: PropertyKey) => {
const callBackFn: CallBackFunction<T> =
typeof callback === 'string' ? target[callback] : callback;
if (!callBackFn) {
throw new Error(
`Cannot find method ${callback} in class ${target.constructor.name}`
);
}
// handle change to main variable
Object.defineProperty(target, key, {
set: function (value) {
// change status of "isFirstChange"
this[isFirstChangeKey] = this[isFirstChangeKey] === undefined;
// No operation if new value is same as old value
if (!this[isFirstChangeKey] && this[cachedValueKey] === value) {
return;
}
const that = this;
const oldValue = this[cachedValueKey];
//this[cachedValueKey] = value;
if (typeof value === 'object') {
this[cachedValueKey] = createObjectProxy(value, callBackFn, [key]);
} else {
this[cachedValueKey] = value;
}
const simpleChange: ChangeDetails<T> = {
propertyKey: key,
firstChange: this[isFirstChangeKey],
previousValue: oldValue,
currentValue: this[cachedValueKey],
isFirstChange: () => this[isFirstChangeKey],
};
callBackFn.call(this, this[cachedValueKey], simpleChange);
},
get: function () {
return this[cachedValueKey];
},
});
};
}
// https://stackblitz.com/edit/stackblitz-starters-fylgaw?file=src%2Fmain.ts
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule],
template: `
<h1>{{ value }}</h1>
<button (click)="incNo()"> increase </button>
`,
})
export class App extends AppMixin {
name = 'Angular';
@OnChange('valueChanged')
value = 0;
@OnChange('valueChanged')
value2 = {
number: 0,
nested: {
valuex: 2,
},
};
incNo() {
this.value += 1;
this.value2.number += 5;
this.value2.nested.valuex += 2;
}
valueChanged(value: number, change: ChangeDetails<number>) {
console.log(
`${change!.propertyKey.toString()} is changed from ${change!.previousValue} to ${value}`
);
// Output will be::
// value is changed from 0 to 1
// value2.number is changed from 0 to 5
// value2.nested.valuex is changed from 2 to 4
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment