Autotracking on a Set()s or Arrays?

Hey Folks, I’m trying to wrap my brain around some of the autotracking changes in Octane and having a hard time. Does autotracking work on a Set?

I want to calculate the height of a list of elements and I figured a Set would make the unique tracking easy. Take a look at the code below. I have a Set of dom elements, which is tracked. The height() method isn’t being called again when the elements changes.

  @tracked elements = new Set();

  get height() {
    if (this.elements.size === 0) {
      return 'auto';
    }
    let height = [...this.elements].reduce((sum, el) => sum + el.offsetHeight);

    return `${height}px`;
  }

  @action
  addEl(el) {
    this.elements.add(el);
  }

  @action
  removeEl(el) {
    this.elements.delete(el);
  }

Then I’m referencing height in a template:

<div
  style="flex-basis: {{this.height}}"
>
  ...

This is my first attempt at using a Set as well, so it’s possible I’m heading down the wrong path before the autotracking confusion. I’ve tried this with elements as an array and height is still not called.

Thanks for the help!

Out of the box, no, it doesn’t: because you’re tracking the Set instance, not its internals. However, you can use the tracked-built-ins addon, which supplies auto-tracked versions of Map, WeakMap, Set, and WeakSet.

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from 'tracked-built-ins'; // NOTE THE IMPORT!

export default DemoIt extends Component {
  @tracked elements = new Set();

  get height() {
    if (this.elements.size === 0) {
      return 'auto';
    }
    let height = [...this.elements].reduce((sum, el) => sum + el.offsetHeight);

    return `${height}px`;
  }

  @action
  addEl(el) {
    this.elements.add(el);
  }

  @action
  removeEl(el) {
    this.elements.delete(el);
  }
}
1 Like

Oooooo. That is very nice. Thanks @chriskrycho. I’ll give that a try.

Thanks again @chriskrycho, that worked for me.

Here is what I ended up with:

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { next } from '@ember/runloop';
import { tracked } from 'tracked-built-ins';

export default class AccordionSectionComponent extends Component {
  @tracked elements = new Set();

  get height() {
    if (this.elements.size === 0) {
      return 'auto';
    }
    let height = [...this.elements].reduce((sum, el) => {
      return sum + el.offsetHeight;
    }, 0);

    return `${height}px`;
  }

  @action
  addEl(el) {
    this.elements.add(el);
  }

  @action
  removeEl(el) {
    next(() => this.elements.delete(el));
  }
}
2 Likes

I wonder if the original usage would be suitable for a lint rule.

1 Like

Explicitly notifying glimmer about the change would be another option, wouldn’t it?

this.elements.add(el);
this.elements = this.elements;

As Set.add() returns the set instance this would also work and may look less confusing:

this.elements = this.elements.add(el);

But this does not work for Set.delete(), which returns true if the item was in the set and false otherwise.

I think an notifyChange utility function is the best solution for know. It’s readable but does not come with limitations and drawbacks of tracked-built-ins.

I’m looking forward to Tracked Maps and Sets RfC, which provides best long-term solution in my opinion.

Unfortunately, this wouldn’t help with getting specific items from the Set (or a Map) in a template. It would not notify that the item within the collection had been changed. In fact, that’s what the tracked-built-in’s library and the RFC you linked account for.

I’m a little bit confused. What’s the difference to Ticker class example given in Tracked Properties RFC for manual invalidation?

The difference, if I recall correctly, is that get, which template invocations use when resolving their values, is aware of properties (as used in that example) but doesn’t know how to get the values from Set or Map. It can be “taught” to, and that’s part of what the RFC for native tracked Map and Set proposes, but today it doesn’t. (@pzuraq can correct me if I’ve got that wrong in some way!)