Skip to content

Stimulus Controllers

The Maho Storefront uses Hotwire for client-side behaviour — Turbo handles SPA-like page transitions (no full reloads between pages), and Stimulus controllers attach interactivity to server-rendered HTML via data-controller attributes. There's no client-side framework runtime.

Architecture

There is no client-side rendering or virtual DOM. The Worker renders complete HTML, and Stimulus controllers add interactivity (cart management, variant selection, filtering, etc.) on top.

How Stimulus Works

Controllers

A controller is a JavaScript class that connects to a DOM element:

html
<div data-controller="product"
     data-product-sku-value="ABC123"
     data-product-variants-value='[...]'>
  <span data-product-target="price">$49.95</span>
  <button data-action="click->product#addToCart">Add to Cart</button>
</div>
javascript
import { Controller } from '../stimulus.js';

export default class extends Controller {
  static targets = ['price'];
  static values = { sku: String, variants: Array };

  addToCart() {
    // Handle add to cart
  }
}

Targets

Named DOM elements the controller can reference:

javascript
static targets = ['price', 'gallery', 'addButton'];

// Access via:
this.priceTarget      // First matching element
this.priceTargets     // All matching elements
this.hasPriceTarget   // Boolean check

Values

Typed data passed from HTML to the controller:

javascript
static values = {
  sku: String,
  variants: Array,
  inStock: { type: Boolean, default: true }
};

// Access via:
this.skuValue         // "ABC123"
this.variantsValue    // [...]
this.inStockValue     // true

Actions

Event bindings declared in HTML:

html
<!-- click event on the element -->
<button data-action="click->product#addToCart">

<!-- input event -->
<input data-action="input->search#query">

<!-- custom event -->
<div data-action="cart:updated->header#refreshBadge">

Controller Inventory

ControllerFileLinesPurpose
productproduct-controller.js~800Product detail page: variant selection, gallery, pricing, add to cart
checkoutcheckout-controller.js~1000Multi-step checkout: shipping, payment, order placement
accountaccount-controller.js~1100Account dashboard: orders, addresses, profile management
category-filtercategory-filter-controller.js~900Category filtering: sidebar filters, price range, sort, pagination
cartcart-controller.js~600Cart page: item management, quantity updates, coupons
cart-drawercart-drawer-controller.js~600Slide-out mini cart drawer
freshnessfreshness-controller.js~600Background freshness checks and revalidation
authauth-controller.js~300Login, registration, password reset
hover-swatchhover-swatch-controller.js~300Color swatch hover → product image swap
searchsearch-controller.js~150Search bar autocomplete and results
reviewreview-controller.js~200Review submission and rating display
wishlistwishlist-controller.js~150Wishlist add/remove
home-carouselhome-carousel-controller.js~150Homepage image carousel
size-guidesize-guide-controller.js~150Size guide modal
auth-stateauth-state-controller.js~70Auth state persistence
mobile-menumobile-menu-controller.js~50Mobile navigation drawer
contactcontact-controller.js~100Contact form submission
newsletternewsletter-controller.js~50Newsletter subscription
order-successorder-success-controller.js~50Post-purchase tracking

Supporting Libraries

FilePurpose
src/js/api.jsAPI client wrapper with cart ID management
src/js/utils.jsformatPrice(), escapeHtml(), updateCartBadge(), ensureCart()
src/js/template-helpers.jsDOM template hydration (hydrateTemplate(), setSlotHtml())
src/js/analytics.jsEvent tracking (Google Analytics integration)

Bundle

All controllers are bundled into a single file via esbuild:

bash
bun run build:js
# → public/controllers.js.txt

The .js.txt extension ensures Cloudflare serves it as a text module (imported into the Worker). The route handler serves it at /controllers.js with proper MIME type and 1-year cache headers.

Source: src/js/controllers/, src/js/app.js