ember.js icon indicating copy to clipboard operation
ember.js copied to clipboard

Element modifier destruction broken in animated-if with Ember 3.17

Open Turbo87 opened this issue 5 years ago • 16 comments
trafficstars

see https://github.com/ember-animation/ember-animated/issues/202

Since I'm not sure if the issue is in ember-animated or in the new Ember release I'm opening an issue here too.

Turbo87 avatar Mar 05 '20 11:03 Turbo87

I noticed a similar issue when using the will-destroy render modifier.

Given two components, one using @ember/component (Foo) and one using @glimmer/component (Bar).

Component impl:

export default class Foo extends Component {
  _didInsert = (element) => {
    console.log('glimmer', '{{did-insert}}', element, document.querySelector('#yield-bar'), document.querySelector('#outside-yield'));
  }
  _willDestroy = (element) => {
    console.log('glimmer', '{{will-destroy}}', element, document.querySelector('#yield-bar'), document.querySelector('#outside-yield'));
  }
  willDestroy(){
    console.log('glimmer', 'willDestroy', document.querySelector('#yield-bar'), document.querySelector('#outside-yield'));
  }
}

with their template:

<div
  {{did-insert this._didInsert}}
  {{will-destroy this._willDestroy}}
>
  <div>{{yield}}</div>
</div>

and using them in an ember template:

{{#if this.renderComponents}}
  <Foo><h3 id="yield-foo">yield Foo</h3></Foo>
  <Bar><h3 id="yield-bar">yield Bar</h3></Bar>
  <h3 id="outside-yield"></h3>
{{/if}}

I can see a different lifecycle of will-destroy:

3.17:

20200309-150358

3.16:

20200309-150338

It seems like the will-destroy modifier lifecycle changed and it's called after the element was already removed from the DOM.

(I know that one shouldn't query the DOM that way but there might be addons that rely on elements existing when will-destroy is invoked)

makepanic avatar Mar 09 '20 15:03 makepanic

@makepanic the change in destroy order there is actually expected. Classic components had used a different destruction queue before the VM upgrade which added a fair amount of complexity, and was actually unnecessary and took them out of the normal destruction flow (and ordering).

The VM upgrade fixed this, and like you said folks shouldn't be relying on the ordering of willDestroy specifically (whereas they should be able to rely on the ordering of willDestroyElement, and that hasn't changed), which is why we felt comfortable making the change.

pzuraq avatar Mar 09 '20 15:03 pzuraq

Is there a hook for glimmer components to execute when it will destroy but is still in DOM?

I noticed that basic-dropdown (maybe cc @cibernox ) (see template ) does manual DOM manipulation in the will-destroy modifier. There are probably others that will break with 3.17 so we might need to migrate them.

makepanic avatar Mar 09 '20 15:03 makepanic

@makepanic ah, sorry, I didn't read your comment closely enough. I thought you were discussing the ordering of the willDestroy hooks, which changed (the Classic component's comes last).

We've been discussing the modifier issue, there isn't a resolution just yet because the new behavior is actually what was spec'd in the original modifier RFC. But, the nature of modifiers implies they should have a willDestroyElement hook of some sort, and it's likely folks were using the destroyModifier hooks in the managers for that purpose. Will prioritize the discussion for that.

pzuraq avatar Mar 09 '20 15:03 pzuraq

This change is indeed surprising. The name willDestroy totally makes one think that whatever you run there will happen before DOM nodes are destroyed.

cibernox avatar Mar 09 '20 16:03 cibernox

Per the modifier manager RFC:

May or May Not

  • be called in the same tick as DOM removal

I think a lot of folks who were writing modifiers did not realize this, but it makes sense given what willDestroy semantically is about. It's really about cleaning up JavaScript, e.g. preventing memory leaks and other work that can be done lazily. Long term, the goal is to begin scheduling this work to happen during idle time, so it does not block rendering, since it shouldn't actually affect rendering.

Modifiers, though, likely need a way to clean up before DOM removal given their purpose. In addition, the {{will-destroy}} modifier actually had a contradiction in it's the spec - it both guaranteed running before removal and had the above language (copy-pasted from the other RFC 😬 )

pzuraq avatar Mar 09 '20 18:03 pzuraq

hmm... given the above, how does the on modifier call removeEventListener on element if that element is potentially not there anymore? 🤔

or is the element still there, but just not attached to any parent node anymore?

Turbo87 avatar Mar 09 '20 18:03 Turbo87

The element is still there, it has just been removed from the DOM. In many cases, this is all that is needed, like for instance with removeEventListener - detaching the component from the DOM will cause the browser to automatically remove and garbage collect it once the element is fully removed.

pzuraq avatar Mar 09 '20 18:03 pzuraq

okay, thanks for the clarification.

it looks like my issue above is slightly different though, because it seems that the will-destroy modifier is not being called at all.

Turbo87 avatar Mar 09 '20 19:03 Turbo87

Yes, absolutely. I haven't had a chance to dig in here, but I'm tracking and will be taking a look when I can.

pzuraq avatar Mar 09 '20 20:03 pzuraq

I also noticed this when trying to access this.element.parentElement inside the willDestroy hook of a modifier.

miguelcobain avatar Apr 03 '20 12:04 miguelcobain

I'm guessing this is related to https://github.com/emberjs/ember.js/issues/18855#issuecomment-607445112

scottmessinger avatar Apr 03 '20 13:04 scottmessinger

@Turbo87 is it still an issue in 3.18/3.19?

andreyfel avatar Jul 10 '20 14:07 andreyfel

@andreyfel I'm seeing this with PaperMenu while upgrading to Ember 3.20.

chbonser avatar Aug 31 '20 17:08 chbonser

I concur this is still happening in 3.20.

LucasHill avatar Sep 24 '20 17:09 LucasHill

I know it is a bit outdated already but stumbled accross that when I was wondering if the willDestroy can be blocked for period of time.

The {{did-insert}} modifier is often exemplified for the addition of a class that handles an animation:

https://github.com/emberjs/ember-render-modifiers#example-adding-a-class-to-an-element-after-render-for-css-animations

That is a nice feature. It would be great to have a symmetrical operation that allows the teardown while still using the css to animate out. I would have liked to see that {{will-destroy}} or the destruction function in custom modifiers support the usage of a promise that would prevent the destruction for as long as the promise remains unresolved.

From a software development perspective it is not the cleanest approach (since you would need the animation duration in two places), but for the user experience it would be nice to use the inverse animation path by using the same CSS animation.

elfin-sbreuers avatar Mar 03 '22 16:03 elfin-sbreuers