'Best practices for handling UIState in StimulusJS
Just starting off with StimulusJS and trying to follow best practices. As I come from VueJs/Nuxt, state management and communication between components is quite different and I am aiming to follow the StimulusJS way.
Case:
I have a Toggle button that toggles a Sidebar in an e-commerce catalog. Most toggles implemented on the site have a simple and close relation from the toggle button to the toggle target. However, the sidebar lives far from the Sidebar HTML structure-wise. What would be the best approach? Both the toggle button and the toggle target will have a class toggled (.is-open)
Move the controller up to simply have all elements to control as children The problem with this approach is that the JS Data hooks are spread over multiple templates and does not feel componenty
Create two controllers that communicate through events A SidebarToggle (which inherits from my base toggle controller) and a SidebarController that listens.
Bubble up an event to window and make all components that need to catch this even on @window
Something else?
Dummy markup below:
<section class="o-product-catalog mb-60px" data-controller="catalog">
<div>
<!-- Header -->
</div>
<section class="c-catalog-navigation container">
<div class="c-catalog-filter-toggle">
<p class="c-catalog-filter-toggle__label"><span ></span>Show/Hide Filters</p>
<button class="c-catalog-filter-toggle__button"></button>
</div>
<div class="flex">
<!-- Pagination.. -->
</div>
</section>
<div class="container flex relative">
<div class="Filters Sidebar">
<div class="Catalog Facet"></div>
<div class="Catalog Facet"></div>
<div class="Catalog Facet"></div>
<div class="Catalog Facet"></div>
</div>
<div class="Catalog Results">
<div class="Catalog Item"></div>
</div>
</div>
</section>
Solution 1:[1]
When building things with Stimulus, I find it is always good to start with the HTML as described in the documentation.
When building accessible HTML, you will find that the answers get a bit clearer. Your 'toggle' button will need to use aria-controls
to advise browsers what sidebar is being controlled via an id
.
From here, you have a built in reference from the button to the sidebar which you can leverage in the Stimulus code.
This also makes it much clearer, when looking at the DOM, what is happening.
As for communication to the other controller, you are on the right track in thinking with Browser events but it is best to scope events to the DOM elements that need them where possible.
Example
HTML
- See below, we have the same basic DOM but are using
aria-controls
to reference the id'catalog-sidebar'
of the sidebar we want to control. - I have added a separate close button within the sidebar to show that we basically just use the same
data-action
method to toggle, whether it is triggered by click or the dispatched event.
<section class="o-product-catalog mb-60px" data-controller="catalog">
<div>
<!-- Header -->
</div>
<section class="c-catalog-navigation container">
<div class="c-catalog-filter-toggle">
<p class="c-catalog-filter-toggle__label">
<span>Show/Hide Filters</span>
</p>
<button
class="c-catalog-filter-toggle__button"
aria-controls="catalog-sidebar"
data-controller="sidebar-toggle"
data-action="sidebar-toggle#toggle"
>
Show
</button>
</div>
<div class="flex">
<!-- Pagination.. -->
</div>
</section>
<div class="container flex relative">
<div
class="Filters Sidebar"
id="catalog-sidebar"
aria-expanded="true"
data-controller="sidebar"
data-action="sidebar-toggle:toggle->sidebar#toggle"
>
<button type="button" data-action="sidebar#toggle">Close</button>
<div class="Catalog Facet"></div>
<div class="Catalog Facet"></div>
<div class="Catalog Facet"></div>
<div class="Catalog Facet"></div>
</div>
<div class="Catalog Results">
<div class="Catalog Item"></div>
</div>
</div>
</section>
JavaScript
- Both controllers are in the same code snippet below.
Sidebar
has a toggle method that will change thearia-expanded
value (so we keep accessibility in mind).SidebarToggle
has a toggle method (naming things is hard) that dispatches an event to the found sidebar in theconnect
method.
import { Controller } from '@hotwired/stimulus';
/**
* A sidebar (expanding menu).
*/
class Sidebar extends Controller {
connect() {
// ...
}
toggle() {
const isExpanded = !!this.element.getAttribute('aria-expanded');
this.element.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
}
}
/**
* A button which will toggle another sidebar elsewhere in the DOM.
*/
class SidebarToggle extends Controller {
connect() {
this.sidebar = document.getElementById(
this.element.getAttribute('aria-controls')
);
if (!this.sidebar && this.application.debug) {
console.error('should find a matching sidebar');
}
}
toggle() {
this.dispatch('toggle', { target: this.sidebar });
}
}
export { Sidebar, SidebarToggle };
Note: I have not run this code locally but it should be pretty close.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | LB Ben Johnston |