Runloop with did-update modifier

Hi, I am trying to figure out how did-update works. Let’s assume I have a component with:

<ul {{did-update this.didUpdate @someParam}}>
  {{#each this.myArray as |item|}}
     <li>{{item}}</li>
  {{/each}}
</ul>

and

@action
didUpdate() {
    this.myArray = [1, 2, 3, 4, 5, 6, 7];
    scheduleOnce('afterRender', () => {
        // .. get the height of ul or do some dom manipulation
       // The items are not rendered yet
    }
}

So my question is, why aren’t the items rendered yet? Is this the expected behavior? If yes, then how can I do some DOM manipulation, after a did-update function call, in which I want to modify some tracked attributes, that will alter the DOM element I am trying to manipulate. Thanks.

in the ender-render-modifier docs it says:

One key thing to know about {{did-update}} is it will not rerun whenever the contents or attributes on the element change. For instance, {{did-update}} will not rerun when @type changes here:

<div {{did-update this.setupType}} class="{{@type}}"></div>

If {{did-update}} should rerun whenever a value changes, the value should be passed as a parameter to the modifier. For instance, a textarea which wants to resize itself to fit text whenever the text is modified could be setup like this:

<textarea {{did-update this.resizeArea @text}}> {{@text}} </textarea>

In this case it’s obviously the contents that are changing but not causing a re-render. Also I think in general it’s best to avoid the runloop if possible. I think part of the problem is that the parent element is what you’re “watching” but the child elements are actually what you’re interested in. So obviously you could move the did-update (or did-insert) modifier to the children. You’d have to debounce or use some other mechanism to only run the handler once per “update” but that’s closer to what you’re actually trying to accomplish anyway. You could also consider writing a custom modifier (maybe using ResizeObserver?) and still using it on the parent.

I can’t say definitively what the best solution is but IIRC the did-update modifier was more intended to be a fill-in for component lifecycle hooks and arg updates rather than DOM changes per-se. That said a modifier still feels like the right abstraction for this.

1 Like

Hi, thanks for the info. I am actually using the did-update modifier more as a lifecycle hook. My actual component does this:

  • it receives a @profile of the user
  • when the @profile changes, I want to update a list of items from the new profile
  • then, after the list is updated, I want to update the scroller (I use perfect-scrollbar library for this), so the scroller sees the new height of the content

So, I used to have an observer on the @profile . As I want to refactor away from this, I has hoping I can use a did-update modifier. I will have a look how I can modify my component with your advice, but if you guys have any better ideas for the task I am trying to accomplish, it would be really helpful for me, understanding how I should migrate away from these observers.

I have very few observers, very very few, but they do multiple things, all at once:

  • they set state on the component, but on multiple properties
  • they also trigger async state fetching from the server
  • they apply updates on third party dom manipulation libraries ( like updating a progress circle plugin, or a scroller)

Edit: So, following your advice, this is another version:

<ul {{did-update this.didUpdate @someParam}}>
  {{#each this.myArray as |item|}}
     <li {{did-insert this.didInsertItem}}>{{item}}</li>
  {{/each}}
</ul>

and then

import { debounce } from '@ember/runloop';

@action
didUpdate() {
    this.myArray = [1, 2, 3, 4, 5, 6, 7];
}

@action
didInsertItem() {
    debounce(this, this.updateScroller, 100);
}

The 100ms are pretty arbitrary, I might run into trouble here, right?

did-update and did-insert are very generic modifiers that are really just a crutch for when people are trying to literally translate their classic components into glimmer components.

What I would suggest is to write a perfect-scrollbar modifier. Modifiers are designed for this case – they have direct access to the DOM, they have the lifecycle hooks you need, and they can take arguments from the rest of your app and react when those arguments change. Scrolling is even the example used in the README for ember-modifier.

If you need to wait to be sure a data change has already flushed out to the DOM, you can schedule for afterRender.

Here’s a sketch of what the modifier would probably look like:

import Modifier from 'ember-modifier';
import { schedule } from '@ember/runloop';
import PerfectScrollbar from 'perfect-scrollbar';

export default class PerfectScrollbarModifier extends Modifier {
  didInstall() {
    this.ps = new PerfectScrollbar(this.element);
  }
  didUpdateArguments() {
    schedule('afterRender', () => this.ps.update());
  }
}

You could use it like:

<ul {{perfect-scrollbar this.myArray.length }}>
  {{#each this.myArray as |item|}}
    <li>{{item}}</li>
  {{/each}}
</ul>
2 Likes

That is pretty cool, thanks.

thanks for the awesome information.