Ember.js: [QUEST] Glimmer Components in Ember

Created on 28 Feb 2018  Ā·  23Comments  Ā·  Source: emberjs/ember.js

This quest issue tracks the implementation of Glimmer components in Ember.js.

The Plan

Glimmer.js components have the following features:

  1. ā€œOuter HTMLā€ templates (no tagName, attributeBindings, etc.)
  2. Arguments in templates are @ prefixed, like {{@firstName}}
  3. Arguments are set on the component as this.args
  4. Component classes use JavaScript class syntax
  5. Mutable component state is annotated with @tracked properties
  6. Components are invoked via <AngleBracket /> syntax
  7. Attributes can be ā€œsplattedā€ via ā€¦attributes

In keeping with Emberā€™s spirit of incrementalism, we want to land this functionality piece by piece via an addon. This allows the community to start using features and providing feedback early in the process.

In fact, weā€™ve already started down the road to Glimmer components in Ember. The first two features have already started to land:

  1. In template-only components, templates are ā€œouter HTMLā€ (assuming the template-only-glimmer-components optional feature has been enabled.)
  2. Arguments can be accessed in the template via the @ prefix (e.g. {{@firstName}}).

This issue proposes finishing the process of bringing Glimmer components to Ember by allowing addons to provide alternate component implementations, then transforming the @glimmer/component package into an Ember addon that implements the Glimmer.js component API.

Weā€™ll break that work into phases, each one unlocking benefits for existing Ember apps and addons. Phase 0 is about adding the necessary primitives to Ember.js to support alternate component implementations. Phases 1, 2 and 3 are about incrementally enabling Glimmer.js component API.

While we go into depth on Phases 0 and 1, we will defer exploring the technical details of later phases until the first phases are closer to completion.

Phase 0: Customizing Component Behavior

TL;DR: Add a CustomComponentManager API to Ember.js to allow addons to implement custom component API.

Currently, all components in an Ember app are assumed to be subclasses of Ember.Component. In order to support alternate component APIs in Ember, we need some way to tell Ember when and how component behavior should change.

When we say ā€œcustom component behavior,ā€ we specifically mean:

  1. How component instances are created.
  2. How component instances are destroyed.
  3. How arguments are provided to the component instance.
  4. How the component is notified of these lifecycle changes.

While Glimmer VM introduces the concept of a ā€œcomponent manager,ā€ an object that makes these decisions, this API is very low level. It would be premature to adopt them directly as public API in Ember because they are difficult to write, easy to author in a way that breaks other components, and not yet stable.

Instead, we propose a new Ember API called CustomComponentManager that implements a delegate pattern. The CustomComponentManager provides a smaller API surface area than the full-fledged ComponentManager Glimmer VM API, which allows addon authors to fall into a ā€œpit of success.ā€

So how does Ember know which component manager to use for a given component? The original iteration of the Custom Components RFC introduced the concept of a ComponentDefinition, a data structure that was eagerly registered with Ember and specified which component manager to use.

One of the major benefits of the ComponentDefinition approach is that component manager resolution can happen at build time. Unfortunately, that means we have to design an API for exactly how these get registered, and likely means some sort of integration with the build pipeline.

Instead, we propose an API for setting a componentā€™s manager at runtime via an annotation on the component class. This incremental step allows work on custom component managers to continue while a longer term solution is designed.

Custom Component Manager Discovery

In this iteration, components must explicitly opt-in to alternate component managers. They do this via a special componentManager function, exported by Ember, that annotates at runtime which component manager should be used for a particular component class:

import { componentManager } from '@ember/custom-component-manager';
import EmberObject from '@ember/object';

export default componentManager(EmberObject.extend({
  // ...
}), 'glimmer');

Eventually, this could become a class decorator:

import { componentManager } from '@ember/custom-component-manager';

export default @componentManager('glimmer') class {
  // ...
}

The first time this component is invoked, Ember inspects the class to see if it has a custom component manager annotation. If so, it uses the string value to perform a lookup on the container. In the example above, Ember would ask the container for the object with the container key component-manager:glimmer.

Addons can thus use normal resolution semantics to provide custom component managers. Our Glimmer component addon can export a component manager from addon/component-managers/glimmer.js that will get automatically discovered through normal resolution rules.

While this API is verbose and not particularly ergonomic, apps and addons can abstract it away by introducing their own base class with the annotation. For example, if an addon called turbo-component wanted to provide a custom component manager, it could export a base class like this:

// addon/index.js
import EmberObject from '@ember/object';
import { componentManager } from '@ember/custom-component-manager';

export default componentManager(EmberObject.extend({
  // ...
}), 'turbo');

Users of this addon could subclass the TurboComponent base class to define components that use the correct component manager:

import TurboComponent from 'turbo-component';

export default TurboComponent.extend({
  didInsertElementQuickly() {
    // ...
  }
});

Custom Component API

No component is an island, and for backwards compatibility reasons, itā€™s important that the introduction of new component API donā€™t break existing components.

One example of this is the existing view hierarchy API. Ember components can inspect their parent component via the parentView property. Even if the parent is not an Ember.Component, the child Ember components should still have a non-null parentView property.

Currently, the CurlyComponentManager in Ember is responsible for maintaining this state, as well as other ambient ā€œscope stateā€ like the target of actions.

To prevent poorly implemented component managers from violating invariants in the existing system, we use a compositional pattern to customize behavior while hiding the sharp corners of the underlying API.

import CustomComponentManager from "@ember/custom-component-manager";

export default new CustomComponentManager({
  // major and minor Ember version this manager targets
  version: "3.1",
  create({ ComponentClass, args }) {
    // Responsible for instantiating the component class and passing provided
    // component arguments.
    // The value returned here is passed as `component` in the below hooks.
  },
  getContext(component) {
    // Returns the object that serves as the root scope of the component template.
    // Most implementations should return `component`, so the component's properties
    // are looked up in curly expressions.
  },
  update(component, args) {
    // Called whenever the arguments to a component change.
  },
  destroy(component) {
  }
});

Phase 1: Ember Object Glimmer Components

The Component base class in Ember supports a long list of features, many of which are no longer heavily used. These features can impose a performance cost, even when they are unused.

As a first step, we want to provide a way to opt in to the simplified Glimmer.js component API via the @glimmer/component package. To ease migration, we will provide an implementation of the Glimmer Component base class that inherits from Ember.Object at @glimmer/component/compat.

Hereā€™s an example of what an ā€œEmber-Glimmer componentā€ looks like:

// src/ui/components/user-profile/component.js
import Component from '@glimmer/component/compat';
import { computed } from '@ember/object';

export default Component({
  fullName: computed('args.firstName', 'args.lastName', function() {
    let { firstName, lastName } = this.args
    return `${firstName} ${lastName}`;
  })

  isAdmin: false,

  toggleAdmin() {
    this.set('isAdmin', !this.isAdmin);
  }
});
{{!-- src/ui/components/user-profile/template.hbs --}}
<h1>{{fullName}}</h1>
<p>
  Welcome back, {{@firstName}}!
  {{#if isAdmin}}
    <strong>You are an admin.</strong>
  {{/if}}
</p>
<button {{action toggleAdmin}}>Toggle Admin Status</button>

Notable characteristics of these components:

  • Templates are outer HTML. Because the example template above does not have a single root element, this renders as a ā€œtagless componentā€.
  • Actions are just functions on the component and donā€™t need to be nested in the actions hash.
  • Arguments are set on the args property rather than setting individual properties on the component directly.
  • They use the Ember object model. This means tools like computed properties and mixins that Ember developers are already familiar with continue to work.

Just as important is what is not included:

  • @tracked properties will not be supported until Phase 3.
  • No layout or template properties on the component.
  • No methods or properties relating to the view hierarchy, such as childViews, parentView, nearestWithProperty, etc.
  • No tagName, attributeBindings, or other custom JavaScript DSL for modifying the root element.
  • No send or sendAction for dispatching events.
  • No mandatory ember-view class or auto-generated guid element ID.
  • No manual rerender() method.
  • No attrs property (use args instead).
  • Passed arguments are ā€œunidirectionalā€ and donā€™t create two-way bindings.
  • Passed arguments are not set as properties on the component instance, avoiding the possibility of hard-to-debug naming collisions.
  • No this.$() to create a jQuery object for the component element.
  • No manual appendTo of components into the DOM.
  • No support for the following lifecycle events:

    • willInsertElement

    • didRender

    • willRender

    • willClearRender

    • willUpdate

    • didReceiveAttrs

    • didUpdateAttrs

    • parentViewDidChange

  • No on() event listener for component lifecycle events; hooks must be implemented as methods.

One interesting side effect of this set of features is that it dovetails with the effort to enable JavaScript classes. In conjunction with the design proposed in the ES Classes RFC, we can provide an alternate implementation of the above component:

// src/ui/components/user-profile/component.js
import Component from '@glimmer/component/compat';
import { computed } from 'ember-decorators/object';

export default class extends Component {
  isAdmin = false;

  @computed('args.firstName', 'args.lastName')
  get fullName() {
    let { firstName, lastName } = this.args;
    return `${firstName} ${lastName}`;
  })

  toggleAdmin() {
    this.set('isAdmin', !this.isAdmin);
  }
});

Phase 2 - Angle Bracket Syntax

Phase 2 enables invoking components via angle brackets (<UserAvatar @user={{currentUser}} />) in addition to curlies ({{my-component user=currentUser}}). Because this syntax disambiguates between component arguments and HTML attributes, this feature also enables ā€œsplattingā€ passed attributes into the component template via ā€¦attributes.

{{! src/ui/components/UserAvatar/template.hbs }}
<div ...attributes> {{! <-- attributes will be inserted here }}
  <h1>Hello, {{@firstName}}!</h1>
</div>
<UserAvatar @user={{currentUser}} aria-expanded={{isExpanded}} />

This would render output similar to the following:

<div aria-expanded="true">
  <h1>Hello, Steven!</h1>
</div>

Phase 3 - Tracked Properties

Phase 3 enables tracked properties via the @tracked decorator in Ember. The details of the interop between Emberā€™s object model and tracked properties is being worked out. Once tracked properties land, users will be able to drop the @glimmer/component/compat module and use the normal, non-Ember.Object component base class.

In tandem with the recently-merged ā€œautotrackā€ feature (which infers computed property dependencies automatically), this should result in further simplification of application code:

import Component, { tracked } from '@glimmer/component';

export default class extends Component {
  @tracked isAdmin = false;

  @tracked get fullName() {
    let { firstName, lastName } = this.args;
    return `${firstName} ${lastName}`;
  }

  toggleAdmin() {
    this.isAdmin = !this.isAdmin;
  }
}

Q&A

Can I add back things like the ember-view class name or auto-generated id attribute to Glimmer components for compatibility with existing CSS?

Yes. Example:

<div class="ember-view" id="{{uniqueId}}">
  Component content goes here.
</div>
import Component from '@glimmer/component';
import { guidFor } from '@ember/object/internals';

export default class extends Component {
  get uniqueId() {
    return guidFor(this);
  }
}

Resources

Tasks

Weā€™ll use the lists below to track ongoing work. As we learn more during implementation, we will add or remove items on the list.

Custom Component Manager API

  • [x] Update Custom Component RFC to reflect changes above (@chancancode)
  • [x] Add glimmer-custom-component-manager feature flag
  • [x] Implement componentManager function

    • [x] Expose as { componentManager } from '@ember/custom-component-manager'

  • [x] Resolver needs to detect annotated classes
  • [x] Resolver needs to look up specified component manager

    • [ ] Guidance for addon authors about where to put component managers so they are discovered

    • [ ] How does this work with Module Unification? (@mixonic)

  • [x] Implement CustomComponentManager API

    • [x] Define the CustomComponentManagerDelegate interface

    • [x] version

    • [x] create()

    • [x] getContext()

    • [x] update()

    • [x] destroy?()

    • [x] didCreate?()

    • [x] didUpdate?()

    • [x] getView?()

    • [x] Validate version property upon creation

    • [x] Internals

    • [x] Preserve childViews and parentView in existing components

    • [ ] Instrument for rendering performance

    • [ ] Instrument for compatibility with Ember Inspector

Glimmer Component Addon

  • [x] Support using sparkles-components addon

    • [x] Base class should import and add componentManager annotation when consumed from Ember

    • [x] CustomComponentManager implementation should be discoverable via Emberā€™s container

  • [ ] Support using @glimmer/component as an Ember addon

    • [ ] Preserve existing behavior when consumed from Glimmer.js

    • [ ] import Component from '@glimmer/component' provides plain JavaScript base class

    • [ ] import Component from '@glimmer/component/compat' provides Ember.Object base class

      lookup

  • [ ] Lifecycle Hooks

    • [x] static create(injections)

    • [x] didInsertElement()

    • [x] willDestroy()

    • [x] didUpdate()

    • [x] Event delegation-invoked methods (click() etc.) should not be triggered

  • [ ] Element Access

    • [ ] this.bounds

    • [ ] this.element computed property alias to this.bounds

    • [ ] Element modifier-based API for exposing elements

  • [ ] Arguments

    • [x] this.args is available in the constructor

    • [x] this.args is updated before didUpdate is called

    • [ ] this.args should not trigger an infinite re-render cycle (need to verify)

  • [ ] Documentation

    • [ ] How to install

    • [ ] Caveats

    • [ ] Canary-only

    • [ ] Pre-1.0

    • [ ] Ember-Glimmer ā€œcompatā€ components

    • [ ] Outer HTML templates

    • [ ] Lifecycle hooks

    • [ ] Defining computed properties



      • [ ] How to depend on args



    • [ ] Migration Guide / Glimmer Components forā€¦

    • Guides for writing effective Glimmer components for people familiar with other libraries

    • [ ] Glimmer Components for Ember Developers

    • [ ] Glimmer Components for React Developers

    • [ ] Glimmer Components for Angular Developers

    • [ ] Glimmer Components for COBOL Developers

Open Questions

  1. What are the best practices for actions? Is this something we need to allow component managers to hook into?
  2. How do you use bare JavaScript classes (those with no static create() method) as component classes? The protocol for initializing a component seems simple but is surprisingly tricky.
  3. Where is the best place to document ā€œaddon APIsā€ like CustomComponentManager?
  4. How do we handle CustomComponentManager versioning?
Quest

Most helpful comment

@dbbk The biggest reason we don't require {{...attributes}} is because it's not syntactically ambiguous with a normal attribute, like the other cases where we require curlies, and four fewer characters seemed easier to type and less visually noisy.

I think it may also cause people to assume attributes is a property in scope, but it's notā€”you can't do <SomeComponent something={{attributes}} />, for example, which that syntax would suggest. It also more strongly implies that spread syntax works in other positions when it doesn't.

There has been some suggestion of adopting both @arguments as a special template binding and adding ... spread syntax in Handlebars, but that needs design and implementation that is not on the immediate roadmap. I wouldn't be totally surprised if in the future we replaced ...attributes with {{...@attributes}} or something like it.

@Gaurav0 The Glimmer component API would go through the RFC process before being enabled by default in Ember apps, should we ever want to do that. One nice thing about the custom component manager approach is that we don't canonize a "next generation" API but can let different designs compete on their merits via the addon ecosystem.

@Turbo87 Great suggestion, I'll add a migration guide to the list. We should show existing patterns and how to approach solving the same problem with the new API.

I'll address the case of didReceiveAttrs() specifically since you brought it up. Usually people use this hook to do some initialization of component state based on passed arguments, but in practice this means you can do a fair bit of unnecessary in these hooks, e.g. to initialize values that never end up getting used. And of course people do end up doing shocking things in these hooks that are on the rendering hot path.

The alternative is to switch from "push-based" initialization to "pull-based" initialization. For example, if you wanted to do some computation to generate or initialize the value of a component property, you would instead use a tracked computed property. This ensures the work is only done for values that actually get used. Example:

Instead of this:

import Component from "@ember/component";
import { computed } from "@ember/object";

export default Component.extend({
  didReceiveAttrs() {
    this.set('firstName', this.attrs.firstName || 'Tobias');
    this.set('lastName', this.attrs.lastName || 'Bieniek');
  },

  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

Do this:

import Component, { tracked } from "@glimmer/component";

export default class extends Component {
  @tracked get firstName() {
    return this.args.firstName || 'Tobias';
  }

  @tracked get lastName() {
    return this.args.lastName || 'Bieniek';
  }

  @tracked get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

I'll admit I was skeptical of this at first, but @wycats persuaded me, and in my experience building a big Glimmer.js app over the last year or so, we never ran into a use case for didReceiveAttrs that couldn't be modeled with more-efficient computed properties.

All 23 comments

We'll be coordinating work in the #st-glimmer-components channel on the Ember community Slack if you're interested in helping out! Lurkers welcome too if you're just curious how the sausage gets made.

Regarding this;

{{! src/ui/components/UserAvatar/template.hbs }}
<div ...attributes> {{! <-- attributes will be inserted here }}
  <h1>Hello, {{@firstName}}!</h1>
</div>

I don't understand why curly brackets are used to denote dynamic variables ({{@firstName}}), but not the splatted attributes? To me visually it looks like <div ...attributes> is what's going to be rendered. For consistency wouldn't this be better as <div {{...attributes}}>?

Are there RFCs for glimmer components where we can discuss how they should look and work?

@tomdale the issue above mentions documenting the component compat mode, but it does not seem to mention documenting a migration guide. for example I was quite surprised that didReceiveAttrs() was not a thing anymore, and it would be great to have some documentation that shows how migrate common use cases to new best practice patterns.

@dbbk The biggest reason we don't require {{...attributes}} is because it's not syntactically ambiguous with a normal attribute, like the other cases where we require curlies, and four fewer characters seemed easier to type and less visually noisy.

I think it may also cause people to assume attributes is a property in scope, but it's notā€”you can't do <SomeComponent something={{attributes}} />, for example, which that syntax would suggest. It also more strongly implies that spread syntax works in other positions when it doesn't.

There has been some suggestion of adopting both @arguments as a special template binding and adding ... spread syntax in Handlebars, but that needs design and implementation that is not on the immediate roadmap. I wouldn't be totally surprised if in the future we replaced ...attributes with {{...@attributes}} or something like it.

@Gaurav0 The Glimmer component API would go through the RFC process before being enabled by default in Ember apps, should we ever want to do that. One nice thing about the custom component manager approach is that we don't canonize a "next generation" API but can let different designs compete on their merits via the addon ecosystem.

@Turbo87 Great suggestion, I'll add a migration guide to the list. We should show existing patterns and how to approach solving the same problem with the new API.

I'll address the case of didReceiveAttrs() specifically since you brought it up. Usually people use this hook to do some initialization of component state based on passed arguments, but in practice this means you can do a fair bit of unnecessary in these hooks, e.g. to initialize values that never end up getting used. And of course people do end up doing shocking things in these hooks that are on the rendering hot path.

The alternative is to switch from "push-based" initialization to "pull-based" initialization. For example, if you wanted to do some computation to generate or initialize the value of a component property, you would instead use a tracked computed property. This ensures the work is only done for values that actually get used. Example:

Instead of this:

import Component from "@ember/component";
import { computed } from "@ember/object";

export default Component.extend({
  didReceiveAttrs() {
    this.set('firstName', this.attrs.firstName || 'Tobias');
    this.set('lastName', this.attrs.lastName || 'Bieniek');
  },

  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

Do this:

import Component, { tracked } from "@glimmer/component";

export default class extends Component {
  @tracked get firstName() {
    return this.args.firstName || 'Tobias';
  }

  @tracked get lastName() {
    return this.args.lastName || 'Bieniek';
  }

  @tracked get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

I'll admit I was skeptical of this at first, but @wycats persuaded me, and in my experience building a big Glimmer.js app over the last year or so, we never ran into a use case for didReceiveAttrs that couldn't be modeled with more-efficient computed properties.

@tomdale I agree that didReceiveAttrs isn't necessary for most things. But sometimes, it has saved me from needing to write an observer on one of the attributes passed in from the parent. For example, what if we wanted to kick off an ajax request if one of the attributes changed? I'd hate to add that kind of side effect to a getter.

yep, I'm thinking of similar use cases. one example is animation. say I want to kick off an animation when collapsed changes from true to false and a different one for the other direction. how would that work with glimmer components?

@turbo87 @Gaurav0 Ah okay, so the system is working because those are all performance foot guns that should not be performed in didReceiveAttrs which is synchronous! Anything with side effects like animations or network requests should be scheduled in async hooks like didInsertElement or didUpdate.

didInsertElement doesn't seem to fit well in that example case above. didUpdate only seems to trigger when the component is rerendered, correct? but what if I don't actually use that property for rendering? then the component wouldn't rerender when changing the argument, so didUpdate would never be called. šŸ¤”

Also didInsertElement throws a hard error in Glimmer if we set anything and cause a rerender.

@Turbo87 There might be a misconception around when didUpdate is called. This hook is invoked when a tracked property used in the template _or a passed argument_ changes. See this example: http://tinyurl.com/y7osxpy2. When the parent component passes in an argument, the child component gets its didUpdate hook called even if it never "used" that argument.

@Gaurav0 You're moving the goalposts. ;) Your example was kicking off an ajax request in response to an argument change, which would not cause an error to be thrown if it happened in didInsertElement. If you want to synchronously set a property at the same time, you should be able to model the same behavior with lazy getters. If you need to set a property asynchronously after the ajax, for example, that won't trigger the error because it will happen in a different event loop.

alright, that seems good enough for me. I didn't want to expand on this specific thing anyway, just wanted to highlight that thorough migration guides will be needed :)

@tomdale is this still up to date? seems to be inactive for some time.

@tomdale Ok, the scenario is that we need to kick off an ajax request when a particular attribute has changed, and only that attribute. We don't want to write an observer. Can we still do this in didInsertElement?

@Gaurav0 What is causing the attribute to change? Following DDAU, I think whatever code is responsible for changing the data that causes the attribute to change should also be responsible for kicking off the AJAX request.

The attribute is changing from the parent component / controller. DDAU.

@Gaurav0 @tomdale Another example of this use case is a (mostly) "DOM-less" set of components which apply changes to some other "object". A current example would be ember-leaflet/ember-composability-tools which allows to declaratively construct the contents of a leaflet map. To me this does not seem possible in Glimmer components right now due to the lack of any kind of didReceiveAttrs or observer functionality. How would/could this be tackled?

@nickschot I think I can take this one, we actually did an audit during the design process of various community addons and use cases, and specifically covered ember-leaflet.

If you dig into ember-leaflet/ember-composability-tools, you'll actually find that it doesn't ever actually use didReceiveAttrs or didUpdate or any lifecycle hooks like this. It actually only needs the init and willDestroy hooks, in order for components to register/deregister themselves on their parent (and ultimately, the root rendering parent). This can be done in the constructor and willDestroy hooks in Glimmer components. The root parent component _does_ need the ability to use didInsertElement, but it can use it via the {{did-insert}} modifier.

In the general case, you can use {{did-insert}}/{{did-update}} to reflect any number of attributes onto DOM elements. These are autotracked, so consuming a value in them will cause them to rerun if the values change. You can also write your own custom modifiers if you want, and they will be able to do this as well.

There is a use case that is somewhat related, which is not possible with Glimmer components - a "render detector" like this one in the ember-google-maps addon. This is something that is much less common (we only found this one use), and which can either be addressed via a MutationObserver to watch the DOM, or via a custom component manager that opts-into the ability to run the didUpdate/didRender hooks. I actually think a general purpose <RenderDetector> component would be a good way to handle all of these use cases, since it would isolate the ability to watch the component subtree for updates in a single, easy to find component.

@pzuraq its been a while, but small update in this area. For my use case I cant use modifiers as there is no DOM at all within these components. What I've currently done is implement a custom component manager w/ glimmer semantics which re-enables the didUpdate hook.

@nickschot our recommendation there is to actually use a helper. Helpers can return nothing and side-effect, similar to useEffect in React if you're familiar (whereas modifiers are more similar to useLayoutEffect).

The class based helper API does need some work, ideally it should match the final user facing modifier API I think (plus we need to introduce helper managers in order to make it possible to redesign at all), but the current API should give you all the abilities you need to accomplish any side effects you're after.

@pzuraq How does that work? Do we need to create a custom helper or any helper will do?

Is the syntax like this?

{{concat (did-insert this.myAction)}}

It feels like passing an argument and not "modifying".

Or are you suggesting that we should use helpers instead of modifiers? Something like:

{{my-did-insert this.myAction}}

If yes, please post some links to documentation and examples. Current public API of the class helper only has compute and recompute, nothing about component lifecycle.

Please excuse stupid questions, but to regular Ember users this matter is quite cryptic, documentation is lacking and scattered.

Or are you suggesting that we should use helpers instead of modifiers?

Right, exactly. You can see an example of this in ember-render-helpers for instance, which mimics the @ember/render-modifiers API.

In general, compute triggers the first time a helper is inserted into the template/computed, and re-triggers every time any of its arguments change. It also autotracks in 3.13+.

If you need something along the lines of {{did-insert}}, for instance, you could do:

export default class didInsert extends Helper {
  compute(fn) {
    if (!this.rendered) {
      fn();
      this.rendered = true;
    }
  }
}

And no worries, I understand that it may seem a bit frustrating and cryptic. That's the nature of being on the cutting edge/canary side of things, we haven't gotten all the new patterns documented yet and we're working on it šŸ™‚

I believe this issue can be closed now that Octane is out šŸ˜„

Was this page helpful?
0 / 5 - 0 ratings