glimmer.js icon indicating copy to clipboard operation
glimmer.js copied to clipboard

Tracked descriptors not accessible after property initialization

Open dill-larson opened this issue 1 year ago • 3 comments

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"

dill-larson avatar Sep 11 '24 21:09 dill-larson

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

NullVoxPopuli avatar Sep 11 '24 21:09 NullVoxPopuli

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

dill-larson avatar Sep 12 '24 13:09 dill-larson

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

NullVoxPopuli avatar Sep 12 '24 22:09 NullVoxPopuli