hotwired / stimulus

A modest JavaScript framework for the HTML you already have
https://stimulus.hotwired.dev/
MIT License
12.73k stars 426 forks source link

Proposal: Add Stimulus event to listen to addition/removal of targets #200

Closed ernsheong closed 6 years ago

ernsheong commented 6 years ago

I am looking for a way to listen to events for addition or removal of controller targets.

Use case: When new targets appear in the DOM, I want to trigger an action/DOM manipulation.

Why: My controller sits at the top level (document body). I suppose the recommended use case is to scope it to a specific DOM subtree, but in this case it is meant to manage DOM everywhere so it resides in the body

skyksandr commented 6 years ago

My approach would be to create a controller for this specific targets and let them emits an event in connect. Also, you can pass any required data in CustomEvent.

ernsheong commented 6 years ago

Yes, but event emitted in connect() is only the first time, what if more targets are added later on, just targets mind you, no new controllers.

skyksandr commented 6 years ago

What I was saying in other words - let targets emit events.

sstephenson commented 6 years ago

Thanks for the suggestion! This is something that's on our radar.

For now, I would echo @skyksandr's recommendation to add a new data-controller annotation to each target element you want to observe. Those elements can still serve as targets for your outer controller. For example:

<body data-controller="outer">
  ...
  <!-- Some newly inserted targets: -->
  <div data-target="outer.thing" data-controller="inner">1...</div>
  <div data-target="outer.thing" data-controller="inner">2...</div>
</body>

Each target has its own inner controller, which emits an inner-connected event when connected:

// inner_controller.js
export default class extends Controller {
  connect() {
    const event = document.createEvent("CustomEvent")
    event.initCustomEvent("inner-connected", true, true, null)
    this.element.dispatchEvent(event)
  }
}

Add a data-action to the body element to call the thingConnected() method in response to those events:

<body data-controller="outer" data-action="inner-connected->outer#thingConnected">
   ...
// outer_controller.js
export default class extends Controller {
  static targets = [ "thing" ]

  thingConnected(event) {
    console.log(event.target) // the element that was just connected
  }
}

Closing this issue for now, but please feel free to continue discussing it over on the community forum.

fiznool commented 5 years ago

@sstephenson I really like the above approach, and feel that its a very valuable Stimulus pattern. Perhaps it could become part of the official documentation? There are many places where custom events make a lot of sense.

Or perhaps it could be considered for inclusion into the core library, on the base Controller class?

  protected emit(evtType: string, evtData?: object) {
    let evt;
    if (typeof CustomEvent === 'function') {
      // Modern browsers
      evt = new CustomEvent(evtType, {
        bubbles: true,
        detail: evtData,
      });
    } else {
      // IE
      evt = document.createEvent('CustomEvent');
      evt.initCustomEvent(evtType, true, false, evtData);
    }

    this.element.dispatchEvent(evt);
  }
brendon commented 4 years ago

I found that the outer listener doesn't catch the event if it's fired as part of the connect() function of the inner. The event gets caught just fine later on though.

brendon commented 4 years ago

This is why it wasn't catching the event. Wrapping the dispatch in a promise fixed it. Feels dirty :)

https://github.com/stimulusjs/stimulus/issues/201#issuecomment-435285227

brendon commented 3 years ago

This (https://github.com/stimulusjs/stimulus/issues/222) fixed my particular problem and I was able to remove the resolved promise.

benbonnet commented 1 year ago

as of today

export default class extends Controller {
  static targets = [ "thing" ]

  thingTargetConnected(event) {
    console.log(event.target) // the element that was just connected
  }
}