Reputation: 7210
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
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
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
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
this.dispatch
to easily dispatch events that are just a thin wrapper around CustomEvents.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>
infScroll
exists and then adding the jQuery event listener via the infScroll.on('append',...
.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'.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
.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.target
option to the this.dispatch
, this is not required but it does make it clearer which DOM element we are talking about.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