Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event not bubbling to parent controller #363

Open
ryanb opened this issue Jan 7, 2021 · 5 comments
Open

Event not bubbling to parent controller #363

ryanb opened this issue Jan 7, 2021 · 5 comments

Comments

@ryanb
Copy link

ryanb commented Jan 7, 2021

If I have two of the same controllers nested, the action does not bubble up to the parent controller.

<div data-controller="gallery" data-action="click->gallery#next">
  <div data-controller="gallery">
    <button>Click me!</button>
  </div>
</div>

Here the gallery#next action is not triggered even though the event bubbles up to this DOM element.

Here is a CodePen example.

There you can see that if the parent controller is different from the child controller it triggers the action correctly. I expect nesting the same controllers to behave the same way.

This is using Stimulus 2.0.0.

@adrienpoly
Copy link
Member

I had some other issues with nested controllers couldn't really find a solution.
I opened a PR #364 with 2 failing tests, yours and mine, this is as far as I could get for now.....

Not sure if they are related.

Hopefully someone can propose a nice solution for both

@tdegrunt
Copy link

tdegrunt commented Jan 8, 2021

Interestingly I have the reverse experience with a stimulus 1.1.1 controller, whereby an event bubbles even though I ask for it not to do so:

export default class extends Controller {
  remove(event) {
    console.log("remove")
    event.cancelBubble = true
    // this.element.remove() - // This causes the event to bubble
    setTimeout(() => {
      this.element.remove() // This setTimeout tricks makes it not bubble
    }, 0)
  }
}

Clicking the inner button deletes the outer group, but displays 'remove' twice in the console.

<div data-controller="group">
  <button data-action="rules-group#remove"></button>
  <div data-controller="group">
    <button data-action="rules-group#remove"></button>
  </div>
</div>

@sstephenson
Copy link
Contributor

Hey Ryan, thanks for opening the issue! This is intentional behavior, but I admit we've done a poor job communicating it in the documentation.

The main concept to understand is scopes. A scope is the set of elements known to a controller. Specifically, the controller's scope says which elements are eligible to be targets and which elements can connect actions.

In general, a controller's scope is the controller element plus all its children:

Screen Shot 2021-01-09 at 2 00 32 PM

As you've discovered, Stimulus treats nested scopes differently. A scope's elements are exclusive with respect to the controller identifier. When we add a new controller with the same identifier as another controller on a parent element, we create a new scope, and the outer scope no longer contains its elements:

Screen Shot 2021-01-09 at 2 00 52 PM

The final piece to note: An action connects to a controller through a binding, which is responsible for receiving events and invoking the corresponding action method. Bindings filter out events whose target element is not part of the controller's scope.

I would be curious to hear more about how your gallery controller is set up. I suspect the best solution is to use two separate controllers.

@ryanb
Copy link
Author

ryanb commented Jan 12, 2021

Thanks for the detailed response. Scoping for the most part works intuitively. I like that targets and actions only apply to the closest controller. My biggest issue is with this:

Bindings filter out events whose target element is not part of the controller's scope.

I think events should follow the normal JS bubble behavior. If we are listening to that event on a DOM element, that element becomes the currentTarget as the event bubbles up. At that point it feels like that event has escaped the inner scope. One can call event.stopPropagation() if they don't want it to bubble up to a parent controller.

Here's an example of why I find it unintuitive:

<div data-controller="alt-gallery" data-action="click->alt-gallery#next">
  <div data-controller="gallery" data-action="click->gallery#next">
    <div data-controller="gallery">
      <button>Click me!</button>
    </div>
  </div>
</div>

The gallery#next is not triggered but alt-gallery#next is triggered. The event basically jumps over the middle controller.


Here's my use case. I have a nested set of lists with a stimulus controller on each item to expand the nested list (think folders in a file system list view). I have another button on each item to expand the deepest part of the tree which is collapsed. The tricky part is I want to visually disable that button when there are no further descendants to expand. This means the parent button needs to update when a descendant expands/collapses.

I attempted to do this by emitting a custom event whenever expanding/collapsing the list. I expected the event to bubble up to each ancestor controller so I can update the button which manages the deep expansion. My work around is re-triggering the event on the parent element so it can escape the scope. The downside is we lose context of the target, but that can be passed through event detail if needed.

I suspect the best solution is to use two separate controllers.

This may be difficult to do since the nested lists can be arbitrarily deep. I could have a separate controller which wraps everything to manage updating the buttons, however I think this will be more complex than my current work around since the logic of what needs to be updated nicely follows the DOM tree and you'd lose that context.

I would be interested to hear counter examples where the current behavior is desirable and intuitive.

@janko
Copy link

janko commented May 26, 2021

I've encountered this issue as well, and agree with @ryanb.

In my case, I'm using a "reveal" controller in a nested list. One use of this controller is to open a list item to reveal its children. Another use is during sorting, where I display modal for saving changes when sorting is changed.

So, I have the following:

<div data-controller="reveal">
  <div data-action="sort->show#reveal">
    <!-- top-level list -->
    <ul data-controller="sortable">
      <li>
        <!-- ... item content ... -->
        <div data-controller="reveal">
          <!-- nested list -->
          <ul data-controller="sortable">
            <li>
              <!-- ... item content ... -->
            </li>
          </ul>
        </div>
      </li>
    </ul>
  <div>

  <div data-reveal-target="item">
     <!-- modal for saving changes -->
  </div>
</div>

When my sorting library triggers the sort event on the top-level ul, show#reveal is correctly called. But when the sort event is triggered on the ul inside the nested reveal Stimulus controller, the top-level show#reveal is not being called.

The explanation @sstephenson provided does a great job at clarifying why show#reveal wasn't called in this example. Are there cases where this behaviour is desired? It doesn't feel right that a child controller takes over the parent controller just because they have the same name. If I remove the inner reveal controllers, then it works, so the fact that there is a sortable controller nested doesn't seem to trigger this issue (presumably because it's a controller with a different name).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

5 participants