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;