One of two actions on same element not being called


#1

First of all, StimulusJS is fantastic and I love everyone who has worked on it.

That said, I’ve spent the last two hours banging my head against the wall trying to figure this out, and I finally made a couple of JS Fiddles to share with the group here and get some much-needed help.

Background
I have two general-purpose controllers, an addable controller and a removable controller. The removable controller just allows anything within it to call removable#remove in an action and delete the element. The addable controller takes some content in a template (think, a form), and copies it onto the page.

Exhibit A
Here’s the first JS Fiddle. You can only add one new box at a time. I control this by disabling the button when a new box is present, and then re-enabling the button when the box is removed. Note line 23, where I’m calling two actions, one to remove the element and one to re-enable the button. But note that when you add and then remove a box, the button stays disabled and only the removable#remove action is called (check the console logs)!

Exhibit B
Here’s the same JS Fiddle, slightly modified. The only thing I’ve changed is that I have removed the call to removable#remove on line 23, and now the action to re-enable the button is called!

Any ideas?


#2

Tricky one! removable#remove removes the element, which in turn uninstalls all of the actions. So, addable#boxRemoved never gets called.

Here a couple ideas:

1 - Dispatch a custom event before removing the element and add an action for that event:

class RemovableController extends Stimulus.Controller {
  remove() {
    const event = new CustomEvent("box:remove", { bubbles: true })
    this.element.dispatchEvent(event)
    this.element.parentNode.removeChild(this.element)
  }
}
<div data-controller="addable" data-action="box:remove->addable#boxRemoved">
  …
</div>

2 - Defer removing the element, allowing addable#boxRemoved to be invoked before it’s removed:

class RemovableController extends Stimulus.Controller {
  remove() {
    setTimeout(() => {
      this.element.parentNode.removeChild(this.element)
    }, 1)
  }
}

#3

Thank you very much for your response, @javan!

I guess I’m just confused as to why this JS Fiddle functions as intended, then. Here, all I’ve done is allowed the initial box to trigger both removable#remove and addable#boxRemoved. If you first click the “Add Another Box” button, then remove the initial box element, note that is is both removed and the button is re-enabled.

If removing an element uninstalls all of the actions, how is this newly-modified original box element executing both actions?


#4

I haven’t looked too closely, but I think there are subtle timing issues at play with nested controllers and dynamically added / removed content (Stimulus uses MutationObserver under the hood to detect when elements are added and removed).

I’m happy to report that your original implementation will work as-is in Stimulus 1.1 (coming soon) thanks to the changes in https://github.com/stimulusjs/stimulus/pull/149.