Description
Problem
there is a lot of boilerplate for creating vanilla classes that are properly wired up to the container.
For example:
import { cached } from '@glimmer/tracking';
import { getOwner, setOwner } from '@ember/owner';
import { associateDestroyableChild } from '@ember/destroyable';
class MyClass { /* ... */ }
export default class Demo extends Component {
@cached
get myInstance() {
let instance = new MyClass();
associateDestroyableChild(this, instance);
let owner = getOwner(this);
if (owner) {
setOwner(instance, owner);
}
return instance;
}
}
Solution
this would be a new feature to ember in my ideal world, and not in a user-land package (even though it could live there)
Usage:
import { link } from '@ember/owner'; // proposed;
class MyClass { /* ... */ }
export default class Demo extends Component {
@link myInstance = new MyClass();
// alternatively, suggestion by Runspired
@link(MyClass) declare myInstance: MyClass;
}
- One import instead of 3
- Two lines instead of 13
- This is a common task -- and something that's easy to mess up as the order of params swaps between
associateDestroyableChild
andsetOwner
Implementation:
function link(_prototype: object, key: string, descriptor?: Descriptor): void {
if (!descriptor) return;
assert(`@link can only be used with string-keys`, typeof key === 'string');
let { initializer } = descriptor;
assert(
`@link may only be used on initialized properties. For example, ` +
`\`@link foo = new MyClass();\``,
initializer
);
let caches = new WeakMap<object, any>();
// https://github.com/pzuraq/ember-could-get-used-to-this/blob/master/addon/index.js
return {
get(this: object) {
let child = caches.get(this);
if (!child) {
child = initializer.call(this);
associateDestroyableChild(this, child);
let owner = getOwner(this);
if (owner) {
setOwner(child, owner);
}
caches.set(this, child);
assert(`Failed to create cache for internal resource configuration object`, child);
}
return child;
},
} as unknown as void /* Thanks TS. */;
}
Disclaimer: I've given up with "properly typing" Stage 1 decorators, and instead assert
my way in to the shapes I need.
Thoughts?
Some may wonder how you'd reactively manage args passed to MyClass
, and the answer there is a Resource.
Reason being is that you can't use this pattern:
@cached
get myInstance() {
let instance = new MyClass(this.args.foo);
link(this, instance); // overloaded
return instance;
}
because you only get constructions of MyClass
, and no destructions or updates when the args change.
This may be perfectly fine for some cases, but Resources are specifically for managing this destruction-or-update scenario where as the @cached
getter is constructions only, until the parent class is destroyed (then all relevant destructors still remaining in memory would be called)
Sure, you could manage an "update" pattern via a local un-tracked variable such as:
_myInstance;
@cached
get myInstance() {
if (this._myInstance) {
this._myInstance.update(this.args.foo);
return this._myInstance;
}
let instance = new MyClass(this.args.foo);
link(this, instance); // overloaded
this._myInstance = instance;
return instance;
}
But wouldn't it feel much better to do this instead:
@use myInstance = MyClass.from(() => [this.args.foo]);
// or
myInstance = MyClass.from(this, () => [this.args.foo]);
ember-resources supports both of these APIs (with and without the decorator), and both are 100% TS safe / make sense to consumers. I need more feedback before one could be decided on -- and no matter what happens, there will be codemods <3