The project is in a healthy, maintained state
Add Stimulus attributes using a nicer Ruby syntax.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

>= 7.0.3.1
 Project Readme

StimulusBuilder

Manually adding Stimulus Attributes to HTML elements was something that I didn't like because:

  • they were hard to notice and track in the view files
  • had to write them manually without any structure
  • didn't look good clubbed with other attributes

So, I decided to write this gem to provide a syntax that translates to the Stimulus Attributes behind the scene.

Usage

The gem takes inspiration from ActionView::Helpers::FormBuilder and tries to follow the syntax set by it.

Connecting controllers

<%= stimulated.div do |component| %>
  <% component.connect(:reference) %>
<% end %>

will output:

<div data-controller="reference"></div>

The #connect method accepts strings as well:

<% component.connect("clipboard") %>

Using strings, you can also specify a namespaced controller:

<%= stimulated.div do |component| %>
  <% component.connect("users/list_item") %>
<% end %>

which will output:

<div data-controller="users--list-item"></div>

If your controller consists of multiple words, you can use snake_case notation via symbols:

<%= stimulated.div do |component| %>
  <% component.connect(:date_picker) %>
<% end %>

or via strings:

<%= stimulated.div do |component| %>
  <% component.connect("date_picker") %>
<% end %>

both will output:

<div data-controller="date-picker"></div>

Connecting multiple controllers

<%= stimulated.div do |component| %>
  <% component.connect(:clipboard) %>
  <% component.connect(:list_item) %>
<% end %>

will output:

<div data-controller="clipboard list-item"></div>

Attaching actions

<%= stimulated.div do |component| %>
  <% gallery = component.connect(:gallery) %>

  <%= stimulated.button do |button| %>
    <% button.on("click") { gallery.next } %>
  <% end %>
<% end %>

will output:

<div data-controller="gallery">
  <button data-action="click->gallery#next"></button>
</div>

The #connect method returns a representation of the controller that's passed to it. Calling methods on this object inside the block passed to #on, and also passing the event will convert it to an action attribute.

Event shorthand

If you want to fallback to the default event, use the #fire method instead of #on:

<%= stimulated.button do |button| %>
  <% button.fire { gallery.next } %>
<% end %>

will generate:

<button data-action="gallery#next"></button>

Global events

The second parameter to #on is where the event should be attached. It can either be :window, or :document:

<%= stimulated.div do |component| %>
  <% gallery = component.connect(:gallery) %>

  <% component.on("resize", :window) { gallery.layout } %>
<% end %>

will output:

<div data-controller="gallery" data-action="resize@window->gallery#layout"></div>

Action options

Action options can be passed via hash parameters to #on:

<%= stimulated.div do |component| %>
  <% gallery = component.connect(:gallery) %>

  <% component.on("scroll", passive: false) { gallery.layout } %>

  <%= stimulated.img do |image| %>
    <% image.on("click", capture: true) { gallery.open }
  <% end %>
<% end %>

will output:

<div data-controller="gallery" data-action="scroll->gallery#layout:!passive">
  <img data-action="click->gallery#open:capture">
</div>

Multiple actions

Calling #on more than once will append actions to the action attribute:

<%= stimulated.div do |component| %>
  <% field = component.connect(:field) %>
  <% search = component.connect(:search) %>

  <%= stimulated.input(type: "text") do |input| %>
    <% input.on("focus") { field.highlight } %>
    <% input.on("input") { search.update } %>
  <% end %>
<% end

will output:

<div data-controller="field search">
  <input type="text" data-action="focus->field#highlight input->search#update">
</div>

Naming conventions

If the method calls on the controller representation object is more than one word, it'll camelCase it:

<%= stimulated.div do |component| %>
  <% profile = component.connect(:profile) %>

  <%= stimulated.button do |input| %>
    <% input.on("click") { profile.show_dialog } %>
  <% end %>
<% end

will output:

<div data-controller="profile">
  <button data-action="click->profile#showDialog">
</div>

Action parameters

<%= stimulated.div do |component| %>
  <% item = component.connect(:item) %>
  <% spinner = component.connect(:spinner) %>

  <%= stimulated.button do |input| %>
    <% input.fire do %>
      <% item.upvote(id: "12345", url: "/votes", active: true) %>
    <% end %>
    <% input.fire { spinner.start } %>
  <% end %>
<% end

will output:

<div data-controller="item spinner">
  <button data-action="item#upvote spinner#start"
    data-item-id-param="12345"
    data-item-url-param="/votes"
    data-item-active-param="true">
  </button>
</div>

Referencing targets

The syntax to an element as a target for a controller is [controller].[target_name] = [element].

For example:

<%= stimulated.div do |component| %>
  <% search = component.connect(:search) %>

  <%= stimulated.input(type: "text") do |input| %>
    <% search.query = input %>
  <% end %>

  <%= stimulated.div do |element| %>
    <% search.error_message = element %>
  <% end %>

  <%= stimulated.div do |element| %>
    <% search.results = element %>
  <% end %>
<% end %>

will output:

<div data-controller="search">
  <input type="text" data-search-target="query">
  <div data-search-target="errorMessage"></div>
  <div data-search-target="results"></div>
</div>

Shared targets

You can add same element as a target for different controllers:

<%= stimulated.form_for(:users, url: "/users") do |form| %>
  <% search = form.connect(:search) %>
  <% checkbox = form.connect(:checkbox) %>

  <%= stimulated.check_box do |input| %>
    <% search.projects = input %>
    <% checkbox.input = input %>
  <% end %>

  <%= stimulated.check_box do |input| %>
    <% search.messages = input %>
    <% checkbox.input = input %>
  <% end %>
<% end %>

will output:

<form action="/users" accept-charset="UTF-8" method="post" data-controller="search checkbox">
  <input type="checkbox" data-search-target="projects" data-checkbox-target="input">
  <input type="checkbox" data-search-target="messages" data-checkbox-target="input">
</form>

Naming conventions

When target names are more than one word, use snake_case for the method names on the [controller]:

<%= stimulated.div do |component| %>
  <% search = component.connect(:search) %>

  <% stimulated.span do |element| %>
    <% search.snake_case = element %>
  <% end %>
<% end %>

Outlets

The #use method present on the element returns an object that is a logical representation of an outlet controller. We can then assign this outlet object to the controller where this outlet needs to be accessible by using the controller's #[]= method.

<div>
  <%= stimulated.div(class: "online-user") do |component| %>
    <% component.connect(:user_status)
  <% end %>
  <%= stimulated.div(class: "online-user") do |component| %>
    <% component.connect(:user_status)
  <% end %>
  <%# ... %>
</div>

<%# ... %>

<%= stimulated.div(id: "chat") do |component| %>
  <% chat = component.connect(:chat) %>
  <% user_status = component.use(:user_status) %>

  <% chat[".online-user"] = user_status %>
<% end %>

The above will add a data-chat-user-status-outlet=".online-user" attribute on the div#chat element.

#use accepts a name of the controller to be used as an outlet, and it has the same naming convention rules as the #connect method. The only difference between the both is that the controller returned by the #use method is simpler than the one returned by the #connect method, i.e. it cannot be used to do anything else like setting up actions, or adding targets, etc.

Values

You can pass values to the controller by passing a hash as the second parameter to #connect. This hash needs to have the :values key present, containing all the values.

<%= stimulated.div do |component| %>
  <%
    component.connect(:loader, {
      values: { url: "/messages" },
    })
  %>
<% end %>

will output:

<div data-controller="loader" data-loader-url-value="/messages"></div>

Classes

You can pass classes (Stimulus classes, not HTML classes) to the controller by passing a hash as the second parameter to #connect. This hash needs to have the :classes key present, containing all the values.

<%= stimulated.form_for(:user, url: "/users") do |form| %>
  <%
    form.connect(:search, {
      classes: { loading: "search--busy" },
    })
  %>
<% end %>

will output:

<form action="/users" data-controller="search" data-search-loading-class="search--busy"></form>

Installation

Add this line to your application's Gemfile:

gem "stimulus_builder"

And then execute:

$ bundle

Or install it yourself as:

$ gem install stimulus_builder

Contributing

Contribution directions go here.

License

The gem is available as open source under the terms of the MIT License.