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

[Bug] DX: due to destruction happening "at any time" / asynchronously, we cannot ergonomically use a conditional `on` modifier (aka conditional `on` modifiers are not possible if the condition goes from true to true))

Open NullVoxPopuli opened this issue 2 years ago • 2 comments

🐞 Describe the Bug

Repro

Code
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import * as eModifier from 'ember-modifier';

const lifecycleTest = eModifier.modifier((element, [counter]) => {
  console.log('setting up', counter);
  return () => {
    console.log('removing', counter);
  }
})

class Demo extends Component {
  @tracked count = 0;

  get isEven() {
    return this.count % 2 === 0;
  }

  double = () => {
    console.log('eh?');
    this.count = this.count * 2;
  }
  increment = () => this.count++;

  <template>
    <button 
      {{ (if this.isEven (modifier on 'click' this.double))}}
      {{ (if this.isEven (modifier lifecycleTest this.count))}}
      style="{{if this.isEven "opacity: 1;" "opacity: 0.5; cursor: not-allowed"}}"
    >
      Can only click me when even
    </button>
    <button {{on 'click' this.increment}}>
      Can always click
    </button>

    <div>
      count: {{this.count}}<br>
      isEven: {{this.isEven}}
    </div>
    <br>
  </template>
}

<template>
  <Demo />
</template>

😕 Actual Behavior

teardown happens after setup, but because the same reference to the function that handles the 'click' event is passed, the event listener is removed.

when clicking the double button (can only click me when even),

  • this.isEven is dirtied twice, even though the value remains the same
  • when isEven is dirtied, the modifier destroys itself and re-sets itself up

🤔 Expected Behavior

Event listener is not removed.

🌍 Environment

  • Ember: - 5.5

➕ Additional Context

As a work-around, or maybe ... the solution? we need to create a new reference to the function every time the condition changes so that each addEventListener / removeEventListener pair has a unique reference to the function to handle the event.

atm, I'm trying to figure out how to do that tho

here is a solve:

had to do a custom on modifier that always made its own local ref of the passed function:

Repro/fix


const myOn = eModifier.modifier((element, [eventName, handler]) => {
  let ownRef = (...args) => handler(...args);
  element.addEventListener(eventName, ownRef);
  return () => {
    element.removeEventListener(eventName, ownRef);
  }
})

NullVoxPopuli avatar Feb 13 '24 19:02 NullVoxPopuli

Candidates in case this is regression:

  • https://github.com/glimmerjs/glimmer-vm/commit/e3cd32de907870548bd7f450bd8cbfd81599e52c#diff-301b4fe274bd34f5552c35fc1f58078575896ba487df721bed939eb1d0fe9891
  • https://github.com/glimmerjs/glimmer-vm/commit/64eb186f5a176aa9c70c48db1989fe2da21dbb25#diff-301b4fe274bd34f5552c35fc1f58078575896ba487df721bed939eb1d0fe9891R1

NullVoxPopuli avatar Feb 13 '24 19:02 NullVoxPopuli

Gonna try to pair with @chancancode on some of this.

I'm getting hung up on @glimmer-workspaces made up testing environment :sweat_smile:

NullVoxPopuli avatar Feb 13 '24 21:02 NullVoxPopuli