简体   繁体   中英

Stimulus access action in another controller

Here is one of my Stimulus controllers:

import { Controller } from "@hotwired/stimulus"
import InfiniteScroll from 'infinite-scroll'

export default class extends Controller {
    static get targets() {
        return ["next", "grid", "footer", "infinitescrollelement"]
    }

    connect() {
        let infScroll;

        if (this.hasNextTarget) {
            infScroll = new InfiniteScroll(this.gridTarget, {
                path: '.next_page a',
                append: '[data-infinitescroll-target="infinitescrollelement"]',
                // append: `.${this.data.get("object")}-top-level`,
                scrollThreshold: false,
                status: '.page-load-status',
                button: '.view-more-button'
            })

            this.footerTarget.querySelector('.view-more-button').style.display = 'inline-flex'
        } else {
            this.footerTarget.querySelector('.view-more-button').style.display = 'none'
        }

        // When new content is appended, re-layout the gallery to ensure new photos position correctly
        ***infScroll.on('append', (event, response, path, items) => {
            ***layoutGallery(galleryElement)
        ***})
    }
}

The three lines that start with *** are where my problem is.

Basically, when new content is appended by Infinite Scroll, I need an action in my Gallery Controller to be run. How can I do this? It can't be run at the same time, it must only be run when that infinite scroll event is called.

Any ideas?

As per the Stimulus documentation, the recommended way to communicate across controllers is via browser events .

A few things to note about this

  • jQuery events are not browser events and by default will not be picked up by Stimulus event, or any non jQuery event listeners (by the looks of the code, I assume the infinite scroll is a jQuery util)
  • Stimulus controllers have a convenience method this.dispatch to easily dispatch events that are just a thin wrapper around CustomEvent s.

Start with the HTML

  • Starting with the HTMl we can use the event listener feature of Stimulus called actions .
  • data-action="infinite-scroll:append->gallery#updateLayout" -> this says that the gallery controller should listen to the event 'infinite-scroll:append' and call the gallery's updateLayout method (I just made up this name, call it whatever you want).
<main>
  <h1>Gallery with infinite scroll</h1>
  <section
    class="gallery"
    data-controller="gallery"
    data-action="infinite-scroll:append->gallery#updateLayout"
  >
    <div
      class="scroll-container"
      data-controller="infinite-scroll"
      data-infinite-scroll-target="grid"
    >
      <img src="/image-1" />
      <img src="/image-2" />
      <img src="/image-3" />
      <img src="/image-4" />
    </div>
  </section>
</main>

Trigger a non-jQuery event from the jQuery event 'append'

  • In the updated controller code below we are first checking if infScroll exists and then adding the jQuery event listener via the infScroll.on('append',... .
  • Here we fire a real browser event using the this.dispatch we give it the name 'append' which will be auto-prefixed by the controller name (thanks Stimulus!) so the actual event will be 'infinite-scroll:append' , assuming your controller is registered as 'infinite-scroll'.
  • We pass in everything the listener might need via the detail object, we also add cancelable: false , while this is not critical it is nice to be clear about this and the default in Stimulus event dispatching is true .
  • Note that we are also passing in the event we get from the jQuery listener, this may not be needed but it is good to know that this event and the event that will be dispatched are different events.
  • Note that we are adding the target option to the this.dispatch , this is not required but it does make it clearer which DOM element we are talking about.
  • Events with this.dispatch will bubble by default, so they will be picked up by the parent elements.
import { Controller } from '@hotwired/stimulus';
import InfiniteScroll from 'infinite-scroll';

class InfiniteScrollController extends Controller {
  static get targets() {
    return ['next', 'grid', 'footer', 'item'];
  }

  connect() {
    let infScroll;

    if (this.hasNextTarget) {
      infScroll = new InfiniteScroll(this.gridTarget, {
        path: '.next_page a',
        append: '[data-infinite-scroll-target="item"]',
        // append: `.${this.data.get("object")}-top-level`,
        scrollThreshold: false,
        status: '.page-load-status',
        button: '.view-more-button',
      });

      this.footerTarget.querySelector('.view-more-button').style.display =
        'inline-flex';
    } else {
      this.footerTarget.querySelector('.view-more-button').style.display =
        'none';
    }

    // When new content is appended, re-layout the gallery to ensure new photos position correctly
    if (infScroll) {
      infScroll.on('append', (event, response, path, items) => {
        // note: the 'event' here is the jQuery event, the dispatch below will also dispatch with its own event
        // passing the original jQuery event (which is not strictly a DOM event) in the detail as it may be used

        const detail = { event, response, path, items };

        this.dispatch('append', {
          cancelable: false,
          detail,
          target: event.target,
        });
      });
    }
  }
}

export default InfiniteScrollController;

I found this technique helpful: https://dev.to/leastbad/the-best-one-line-stimulus-power-move-2o90 -- the article builds up to a big reveal but here it is:

Controllers have access to the global Stimulus application scope, which has getControllerForElementAndIdentifier as a member function. If you have a reference to the element with the controller attached and the name of the controller, you can get a reference to any controller on your page. Still, this doesn't offer any solutions to developers working outside of a Stimulus controller.

Here's what we should all do instead.

In your controller's connect() method, add this line:

this.element[this.identifier] = this

Boom. This hangs a reference to the Stimulus controller instance off the DOM element that has the same name as the controller itself, Now, if you can get a reference to the element. you can access element.controllerName anywhere you need it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM