marten_stimulus

Version, currently 0.1.12 versions

github.com/treagod/marten-stimulus

No description declared in shard.yml.

4 stars
0 dependents
License: MIT

Installation

# Add this to your shard.yml
dependencies:
  marten_stimulus:
    github: treagod/marten-stimulus
    version: ~> 0.1.1

Then run:

shards install

shard.yml

Crystal
>= 1.13.0
License
MIT
Author
Marvin Ahlgrimm

Dependencies

Runtime Dependencies

  • marten_importmap~> 0.1github: treagod/marten-importmap

Development Dependencies

  • sqlite3*github: crystal-lang/crystal-sqlite3dev

README

MartenStimulus

Hotwire Stimulus integration for Marten, built on top of marten-importmap.

Adding a Stimulus controller by hand means pinning it in the importmap and registering it with the application. This shard does both for you. It ships a bundled stimulus-loading.js that resolves controllers from the importmap at runtime, plus a generator that scaffolds a controller file and keeps the importmap configuration up to date.

Installation

Add the shard to your shard.yml:

dependencies:
  marten_stimulus:
    github: treagod/marten-stimulus

marten-importmap comes in as a transitive dependency. You do not need to declare it separately.

Run shards install, then add the requires:

# src/project.cr
require "marten_stimulus"
# src/cli.cr
require "marten/cli"
require "marten_stimulus/cli"  # also loads marten_importmap/cli

Both apps must be registered explicitly, in this order:

config.installed_apps = [
  MartenImportmap::App,
  MartenStimulus::App,
]

Getting started

If importmap is not initialized yet, run:

marten importmap init

This creates config/initializers/importmap.cr, config/initializers/importmap_pins.cr, and src/assets/application.js. See marten-importmap for the details.

Note: marten importmap init looks for a literal require "marten_importmap", so a project that only requires marten_stimulus gets a redundant require line added. Crystal's require is idempotent, so the line does no harm. You can delete it.

Pin Stimulus:

marten importmap pin @hotwired/stimulus

Boot it in src/assets/application.js and let the loader pick up your controllers:

import { Application } from "@hotwired/stimulus"
import { eagerLoadControllersFrom } from "stimulus-loading"

const Stimulus = Application.start()
eagerLoadControllersFrom("controllers", Stimulus)

Finally, add pin_all_from to config/initializers/importmap.cr so the controllers directory ends up in the importmap:

Marten.configure do |config|
  config.importmap.draw do
    pin "application", "application.js"
    pin_all_from(
      "src/assets/controllers",
      under: "controllers"
    )
  end
end

You do not have to pin stimulus-loading yourself. MartenStimulus::App pins it during setup, and Marten's asset pipeline serves it from the shard's bundled assets.

Generating controllers

marten stimulus generate controller hello

The command creates src/assets/controllers/hello_controller.js:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    console.log("HelloController connected", this.element)
  }
}

It also checks that pin_all_from "src/assets/controllers", under: "controllers" is present in config/initializers/importmap.cr and inserts it into the draw block if it is missing.

The generator validates before it writes anything:

  • Names that resolve outside src/assets/controllers are rejected.
  • A missing config/initializers/importmap.cr aborts the command with a hint to run marten importmap init.
  • An existing controller file is skipped instead of overwritten.

Naming

Identifiers follow the Stimulus conventions. The _controller suffix is dropped. Underscores become dashes and directories become double dashes.

FileIdentifierUsage
controllers/hello_controller.jshellodata-controller="hello"
controllers/my_form_controller.jsmy-formdata-controller="my-form"
controllers/admin/user_controller.jsadmin--userdata-controller="admin--user"

Only files ending in _controller.js are treated as controllers. Shared helpers can live under src/assets/controllers/ too. They still land in the importmap and remain importable, but the loader will not register them.

Loading strategies

Eager loading

eagerLoadControllersFrom(under, application) imports and registers every matching controller on page load:

import { eagerLoadControllersFrom } from "stimulus-loading"
eagerLoadControllersFrom("controllers", Stimulus)

Keep the default preload: true on pin_all_from here, since the controllers are needed immediately anyway.

The function returns a promise. A failed import rejects it with the controller identifier and the module path in the error message, so attach a .catch() if you want to report those failures.

Lazy loading

lazyLoadControllersFrom(under, application, element = document) waits until an identifier actually shows up in a data-controller attribute before importing and registering it:

import { lazyLoadControllersFrom } from "stimulus-loading"
lazyLoadControllersFrom("controllers", Stimulus)

To defer the downloads as well, turn off preloading for the controller pins:

pin_all_from(
  "src/assets/controllers",
  under: "controllers",
  preload: false
)

Without preload: false, execution and registration are still lazy, but the browser may already have downloaded every controller through module preload links.

Failed imports are logged to the console with the identifier and the module path. They do not interrupt the rest of the page.

The optional third argument narrows the initial scan to a subtree:

lazyLoadControllersFrom("controllers", Stimulus, element)

The mutation observer still watches the whole document afterwards, so controllers added to the page later are picked up regardless of the scan root.

How it works

MartenStimulus::App pins stimulus-loading during app setup. It points at a stimulus-loading.js file bundled in the shard's assets/ directory, which Marten's asset pipeline discovers on its own.

Both loaders read the importmap JSON from the page at runtime. Eager loading walks the importmap and imports every entry below the given prefix that looks like a controller. Lazy loading resolves identifiers found in data-controller attributes back to module paths. In both cases the module's default export is registered with Stimulus.

Credits

stimulus-loading.js is adapted from hotwired/stimulus-rails, MIT licensed.

License

MIT. See LICENSE.