Glimmer not updating correctly with plain array of objects - is this the best way of doing it?

I often stumble into the case where I have a collection of things which you should be able to toggle on and off for instance. In pre octane we often solved more complex cases like that with ObjectProxy and made a decorator around the object that should toggle for instance.

In octane I haven’t found a good solution yet. The problem is that if I just use a new array that updates when the selection or the collection updates all DOM nodes are replaced which aborts any transitions.

The two solutions I found are:

  • Use a custom includes helper (feels dirty, since you’re not supposed to have logic in templates)
  • Use sub components (lots of passing props around, and lots of files)

I’ve made a twiddle with three different ways of doing it explaining it better: Ember Twiddle

Which way is the most ‘octane way’ of doing it? Is there another way of solving it? Thanks!

2 Likes

Hi @jede, good question! Let me answer a bunch of different pieces, but in slightly different order:

  • You can still use a new array if that’s the clearest thing for the data structure! You’ll just want to use the key= named param to the {{each}} helper. That tells Ember how to understand the thing you’re operating on with {{each}}. If you want it to be keyed to a specific property of the objects, just pass the name of that property. If you want it to be keyed to the index in the array, use key="@index" (note the quotes!). See Specifying Keys in the docs for {{each}} for more details.

  • Using an includes helper seems totally fine to me! I’m not sure where the idea that having any logic in your templates is bad came from, though it’s common in the Ember community. I think it’s probably just an overcorrection to having the wrong kind of logic in your templates. It’s actually unavoidable to have some logic in your templates though: if and each are logic!

    Using something like your includes helper in a template can actually be a great way of making it clear that this component doesn’t have any state or behavior of its own, but is just a “pure function”-style component which derives the UI from the arguments it gets. No need for a backing class at all! In fact, I expect many, many more components to look like this in the years ahead. (Certainly many more of ours at LinkedIn are looking like this!)

Two other bonus notes:

  • I would push all of the logic for constructing the updated array up to the owner of the array (in your Gist, the controller), because I find it much clearer to have all of that kind of logic in one place. Part of the reason that the object proxy approach you used before felt better, I suspect, is precisely because you’re looking to make these changes locally in these components, rather than back up at the owner of the data. If you push it back up to the owner of the data, you end up with a pretty simple structure: the owner and a single component and a single helper to manage it

  • this.args.categories in a template is quite unidiomatic, and only works when you have a backing class! If you switch to a template-only component, it’ll break. Prefer @categories etc.!

If I were writing this, I’d probably end up with something like this, using TrackedSet from tracked-built-ins:

// application.js
import Controller from '@ember/controller';
import { action } from '@ember/object'
import { tracked } from '@glimmer/tracking'
import { TrackedSet } from 'tracked-built-ins'

export default class ApplicationController extends Controller {
  @tracked categories = [{id: 1, name: 'Food'}, {id: 2, name: 'Drinks'}, {id: 3, name: 'Snacks'}];
  selectedCategoryIds = new TrackedSet();

  @action toggle(categoryId) {
    if (this.selectedCategoryIds.has(categoryId)) {
      this.selectedCategoryIds.delete(categoryId);
    } else {
      this.selectedCategoryIds.add(categoryId);
    }
  }
}
{{! application.hbs }}
<CategoriesFilter
  @categories={{this.categories}}
  @selectedIds={{this.selectedCategoryIds}}
  @toggle={{this.toggle}}
/>
{{! categories-filter.hbs }}
{{#each @categories as |category|}}
  <button
    class={{if (includes category.id) @selected 'selected'}}
    {{on 'click' (fn @toggle category.id)}}
  >
    {{category.name}}
  </button>
{{/each}}
1 Like

Hi @chriskrycho ,

Thank you for a great answer, it helped me a lot!

key="id" is probably what I was looking for. I had no idea! And I assure you I will not miss ObjectProxy a bit! I really like the octane way of doing things! :smiley:

As for the notion that you shouldn’t have logic in the templates I think it stems from the early days of Handlebars being described as “logicless templating”. Also Ember 2.x docs stated that " Ember gives you the ability to write your own helpers, to bring a minimum of logic into Ember templating." (Handlebars Basics - Templates - Ember Guides). Thats of course vary vague (whats a “a minimum of logic” really?) and as you say both if and each are of course logic. However the lack of native common helpers like includes, at least for me, implies that you shouldn’t do it, but rather solve it with a component.

I thought @categories just was sugar for this.args.categories, thanks for pointing it out!

TrackedSet looks very useful, I’ll definitely try it out. Makes sense to have the controller handle the logic in this case when there isn’t much else happening there and make a template-only component. In more complex cases I think it’s sometimes useful to push some of that logic to the components to keep the controller size manageable.

Glad to help!

Here I would suggest a slightly different tack: build a small class that can encapsulate a given bucket of state and delegate to it from whoever owns that state. That keeps the ownership and responsibilities clear and lets the children components stay “dumber” about how (or even whether!) their arguments change. That decreases the coupling in your system substantially: those child components don’t need to know if you swap our an array for a set for different performance characteristics at that point; they just care that you gave them the same data. But it also helps keep the surface are of the controller small to delegate that particular responsibility elsewhere! Once you’ve done that you can even just pass methods from the delegate directly into the template as long as they’re properly bound to the instance (this is really all @action does in Octane).

Where I would push the responsibility down to a component is when the component itself is responsible for managing the data: selection, here, for example, could conceivably be a thing that the controller doesn’t need to manage and which could be a function of just the filtering component. But in that case I would move the corresponding data structure down as well.

1 Like