rctneil
rctneil

Reputation: 7210

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?

Upvotes: 5

Views: 13780

Answers (3)

Danny
Danny

Reputation: 4114

While the official documentation now mentions using events, with a fallback to getControllerForElementAndIdentifier, I found the following technique helpful and simple to understand. From the same discussion https://github.com/hotwired/stimulus/issues/35, it results in element.controllerName returning the stimulus-connected element.

The author writes the details in their blog and 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.

This approach offers the ease of use of querying for a DOM element and running a function or accessing a property. My simple reptile brain likes it better than dispatching a custom event to be picked up within another controller.

The drawback is potential property collission, so you want to be careful that you aren't overriding a property already defined on the controller. Perhaps give it a namespace beyond this.identifier.

Upvotes: 4

Nicolas Facciolo
Nicolas Facciolo

Reputation: 306

From https://github.com/hotwired/stimulus/issues/35

From your StimulusController you can get the controller:

this.application.getControllerForElementAndIdentifier(window.document.body, 'my-controller-name');

The first parameter is in order to deal with cases you have call several time the same controller on the same page, you have to tell exactly which instance of your controller you want to get

Then you can use parameters and method as you wish

Upvotes: 0

LB Ben Johnston
LB Ben Johnston

Reputation: 5186

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 CustomEvents.

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;

Upvotes: 7

Related Questions