Tracked descriptors not accessible after property initialization
Background
I'm attempting to create a custom decorator to apply to a @tracked property in order to execute a function whenever the property's value is set.
Here's a simplified version of the component and the decorator:
// component.ts
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { log } from './decorators';
export default class Foo extends Component {
@tracked
@log
bar = '';
}
// decorators.ts
export default function log(target: Object, propertyKey: string): ReturnType<PropertyDecorator> {
const trackedDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey); // returns undefined
Object.defineProperty(target, propertyKey, {
enumerable: true,
writable: true,
get() {
const value = trackedDescriptor.get?.call(this);
return value;
},
set(newValue: unknown) {
trackedDescriptor.set?.call(this, newValue);
// custom logic
console.log(newValue);
}
});
}
Problem
When trying to access the tracked descriptor in a subsequent decorator, it will always be undefined. Thus preventing me from creating a custom decorator for a @tracked property. Am I going about this incorrectly? Any help would be appreciated, thanks!
Version
ember-cli: 3.28.6
node: 16.20.0
os: darwin arm64
@glimmer/component: "^1.0.4"
@glimmer/tracking: "^1.0.4"
Would this work for your usecase?
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
/**
* Note that when an app switches to accessor decorators,
* this won't work
*/
function log(target, propertyKey, descriptor) {
let {get, set} = descriptor;
Object.assign(descriptor, {
get() {
console.log('get', propertyKey);
return get.call(this);
},
set(newValue) {
console.log('set', propertyKey, newValue);
return set.call(this, newValue);
}
});
}
export default class extends Component {
@log
@tracked
foo = '';
change = () => this.foo += Math.random().toString().split('').at(-1);
<template>
foo: {{this.foo}}
<br>
<button {{on 'click' this.change}}>change foo</button>
</template>
}
Would this work for your usecase?
import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { on } from '@ember/modifier'; /** * Note that when an app switches to accessor decorators, * this won't work */ function log(target, propertyKey, descriptor) { let {get, set} = descriptor; Object.assign(descriptor, { get() { console.log('get', propertyKey); return get.call(this); }, set(newValue) { console.log('set', propertyKey, newValue); return set.call(this, newValue); } }); } export default class extends Component { @log @tracked foo = ''; change = () => this.foo += Math.random().toString().split('').at(-1); <template> foo: {{this.foo}} <br> <button {{on 'click' this.change}}>change foo</button> </template> }
So technically yes this works, however, the TypeScript compiler is going to error with
Unable to resolve signature of property decorator when called as an expression. The runtime will invoke the decorator with 2 arguments, but the decorator expects 3 since it is expecting log to be PropertyDecorator
oh yes, the TS types for decorators are way wrong for what we do, so when you type a decorator you need all the lies.
This isn't fixed until we move to supporting the spec-decorators coming out of the TC39 process