Description
👉 I implemented this as
ember-render-helpers
RFC #415 Render Element Modifiers introduced the following render modifiers which, are available via the official @ember/render-modifiers
package:
{{did-insert fn ...args}}
: Activated only when the element is inserted in the DOM.{{did-update fn ...args}}
: Activated only on updates to its arguments (both positional and named). It does not run during or after initial render, or before element destruction.{{will-destroy fn ...args}}
: Activated immediately before the element is removed from the DOM
These helpers allowed to reduce the API surface area of the new @glimmer/component
. The commonly used classic Ember Component
hooks (didInsertElement
, didReceiveAttrs
, willDestroyElement
) are replaced with the respective render modifiers.
In my opinion, this works extremely well for {{did-insert}}
(didInsertElement
) and {{will-destroy}}
(willDestroyElement()
), which are used to setup and teardown element related state.
There also are very valid applications for {{did-update}}
, like this example from the RFC:
export default class Component {
@action
setScrollPosition(element, [scrollPosition]) {
element.scrollTop = scrollPosition;
}
}
Another big advantage of {{did-update}}
over a generic didReceiveAttrs()
/ didUpdate()
hook is that you explicitly list the arguments you want to observe, whereas the generic hook would be re-evaluated whenever any argument changes. With {{did-update}}
you can also observe any other property on the component, whereas the hook only gets called when arguments to the component change.
However, besides all the things {{did-update}}
has going for it, I believe that it will often only be used as a workaround for the missing didReceiveAttrs()
/ didUpdate()
hook and that users will not actually use the element
that is passed to the fn
then. For these scenarios, {{did-update}}
as an element modifier is just a hack. It also forces you to render elements to the DOM, which is not an option for "renderless" components that only {{yield}}
state.
To better support these scenarios, I think we should provide complimentary template helpers, that behave exactly the same way, except for that they don't pass an element
to fn
.
For {{did-insert}}
and {{did-update}}
this should be easily achieved with the public classic Ember Helper
API. For {{will-destroy}}
I don't think that it'll be possible with the public API.
{{did-insert}}
import Helper from '@ember/component/helper';
import { assert } from '@ember/debug';
export default class DidInsertHelper extends Helper {
didRun = false;
compute(positional: any[], named: Record<string, any>): void {
const fn = positional[0] as (positional: any[], named: typeof named) => void;
assert(
`\`{{did-insert fn}}\` expects a function as the first parameter. You provided: ${fn}`,
typeof fn === 'function'
);
if (this.didRun) return;
this.didRun = true;
fn(positional.slice(1), named);
}
}
{{did-update}}
import Helper from '@ember/component/helper';
import { assert } from '@ember/debug';
export default class DidUpdateHelper extends Helper {
didRun = false;
compute(positional: any[], named: Record<string, any>): void {
const fn = positional[0] as (positional: any[], named: typeof named) => void;
assert(
`\`{{did-insert fn}}\` expects a function as the first parameter. You provided: ${fn}`,
typeof fn === 'function'
);
if (!this.didRun) {
this.didRun = true;
return;
}
fn(positional.slice(1), named);
}
}
{{will-destroy}}
Assuming that willDestroy
is called for instances of Helper
, which I am not sure about.
import Helper from '@ember/component/helper';
import { assert } from '@ember/debug';
export default class DidUpdateHelper extends Helper {
fn?: (positional: any[], named: typeof named) => void;
positional?: any[];
named?: Record<string, any>;
compute(positional: any[], named: Record<string, any>): void {
const fn = positional[0] as ;
assert(
`\`{{did-insert fn}}\` expects a function as the first parameter. You provided: ${fn}`,
typeof fn === 'function'
);
this.fn = fn;
this.positional = positional;
this.named = named;
}
willDestroy() {
if (this.fn && this.positional && this.named)
this.fn.call(null, this.positional, this.named);
super.willDestroy();
}
}
I personally don't care much for {{did-insert}}
and {{will-destroy}}
, but I think {{did-update}}
is crucial.
For instance, I cannot update my ember-link
addon to the Octane programming model, if I want to keep asserting the arguments that were provided to the <Link>
component. It was based on sparkles-component
, which still had an didUpdate
hook, which I used like:
didUpdate() {
super.didUpdate();
assert(
`You provided '@queryParams', but the argument you mean is just '@query'.`,
!('queryParams' in this.args)
);
// more assertions here...
}
I can't use the {{did-update}}
element modifier, since the template contains no DOM tags and only {{yield}}
s some state.