• Stars
    star
    292
  • Rank 142,152 (Top 3 %)
  • Language
    HTML
  • License
    MIT License
  • Created about 2 years ago
  • Updated 8 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Commands to help you build robust reactive applications with Rails & Hotwire.

Welcome to TurboBoost Commands πŸ‘‹

Lines of Code GEM Version GEM Downloads Ruby Style NPM Version NPM Downloads NPM Bundle Size JavaScript Style Tests Discord Community Sponsors
Ruby.Social Follow Twitter Follow

TurboBoost Commands enhance the reactive programming model for Rails/Hotwire applications.

Table of Contents

Why TurboBoost Commands?

Commands help you build robust reactive applications with Rails & Hotwire. They allow you to declaratively specify server methods that will execute whenever client side events are triggered by users.

TurboBoost Commands work with Hotwire's Turbo Frames. They also work independent of frames.

Commands let you sprinkle ✨ in reactive functionality and skip the ceremony of the typical REST semantics imposed by Rails conventions and Turbo Frames i.e. boilerplate (routes, controllers, actions, etc...).

Commands are great for features adjacent to traditional RESTful resources. Things like making selections, toggling switches, adding filters, etc... Basically for any feature where you've been tempted to create a non-RESTful action in a controller.

Commands improve the developer experience (DX) of creating modern reactive applications. They share the same mental model as React and other client side frameworks. Namely,

  1. Trigger an event
  2. Change state
  3. (Re)render to reflect the new state
  4. repeat...

The primary distinction being that state is wholly managed by the server.

Commands are executed via a Rails before_action which means that reactivity runs over HTTP. Web sockets are NOT used for the reactive critical path! πŸŽ‰ This also means that standard Rails mechanics drive their behavior.

Commands can be tested in isolation as well as with standard Rails controller, integration, and system tests.

Sponsors

Proudly sponsored by

Dependencies

Setup

Complete the steps below, or use this RailsByte:

rails app:template LOCATION='https://railsbytes.com/script/xkjsbB'
  1. Add TurboBoost Commands dependencies

    # Gemfile
    gem "turbo-rails", ">= 1.1", "< 2"
    +gem "turbo_boost-commands", "~> VERSION"
    # package.json
    "dependencies": {
      "@hotwired/turbo-rails": ">=7.2",
    +  "@turbo-boost/commands": "^VERSION"

    Be sure to install the same version of the Ruby and JavaScript libraries.

  2. Import TurboBoost Commands in your JavaScript app

    # app/javascript/application.js
    import '@hotwired/turbo-rails'
    +import '@turbo-boost/commands'
  3. Add TurboBoost to your Rails app

    # app/views/layouts/application.html.erb
    <html>
      <head>
    +  <%= turbo_boost.meta_tag %>
      </head>
      <body>
      </body>
    </html>

Usage

This example illustrates how to use TurboBoost Commands to manage upvotes on a Post.

  1. Trigger an event - register an element to listen for client side events that trigger server side commands

    <!-- app/views/posts/show.html.erb -->
    <%= turbo_frame_tag dom_id(@post) do %>
      <a href="#" data-turbo-command="PostCommand#upvote">Upvote</a>
      Upvote Count: <%= @post.votes %>
    <% end %>
  2. Change state - create a server side command that modifies state

    # app/commands/post_command.rb
    class PostCommand < TurboBoost::Commands::Command
      def upvote
        Post.find(controller.params[:id]).increment! :votes
      end
    end
  3. (Re)render to reflect the new state - normal Rails / Turbo Frame behavior runs and (re)renders the frame

Event Delegates

TurboBoost Commands use event delegation to capture client side events that invoke server side commands.

Here is the list of default event delegates (DOM event name + CSS selectors) that TurboBoost Commands monitors.

  • change - input[data-turbo-command],select[data-turbo-command],textarea[data-turbo-command]
  • submit - form[data-turbo-command]
  • click - [data-turbo-command]

Note that the list of event delegates is ordinal. Matches are identified by scanning the list of delegates top to bottom (first match wins).

It's possible to override the default event delegates. Just note that registered events are required to bubble up through the DOM tree.

IMPORTANT: New entries and overrides are prepended to the list of delegates and will match before defaults.

// restrict `click` monitoring to <a> and <button> elements
TurboBoost.Commands.registerEventDelegate('click', [
  'a[data-turbo-command]',
  'button[data-command]'
])
// append selectors to the `change` event
const delegate = TurboBoost.Commands.eventDelegates.find(
  e => e.name === 'change'
)
const selectors = [...delegate.selectors, '.example[data-turbo-command]']
TurboBoost.Commands.registerEventDelegate('change', selectors)

You can also register custom events and elements. Here's an example that sets up monitoring for the sl-change event on the sl-switch element from the Shoelace web component library.

TurboBoost.Commands.registerEventDelegate('sl-change', [
  'sl-switch[data-turbo-command]'
])

Lifecycle Events

TurboBoost Commands support the following lifecycle events.

  • turbo-boost:command:start - fires before the command is sent to the server
  • turbo-boost:command:finish - fires after the server has executed the command and responded
  • turbo-boost:command:error - fires if an unexpected error occurs

Targeting Frames

TurboBoost Commands target the closest <turbo-frame> element by default, but you can also explicitly target other frames just like you normally would with Turbo Frames.

  1. Look for data-turbo-frame on the command element

    <input type="checkbox"
      data-turbo-command="ExampleCommand#work"
      data-turbo-frame="some-frame-id">
  2. Find the closest <turbo-frame> to the command element

    <turbo-frame id="example-frame">
      <input type="checkbox" data-turbo-command="ExampleCommand#work">
    </turbo-frame>

Working with Forms

TurboBoost Commands work great with Rails forms. Just specify the data-turbo-command attribute on the form.

# app/views/posts/post.html.erb
<%= turbo_frame_tag dom_id(@post) do %>
  <%= form_with model: @post, data: { turbo_command: "ExampleCommand#work" } do |form| %>
    ...
  <% end %>
<% end %>

<%= turbo_frame_tag dom_id(@post) do %>
  <%= form_for @post, remote: true, data: { turbo_command: "ExampleCommand#work" } do |form| %>
    ...
  <% end %>
<% end %>

<%= form_with model: @post,
  data: { turbo_frame: dom_id(@post), turbo_command: "ExampleCommand#work" } do |form| %>
  ...
<% end %>

Server Side Commands

The client side DOM attribute data-turbo-command indicates what Ruby class and method to invoke. The attribute value is specified with RDoc notation. i.e. ClassName#method_name

Here's an example.

<a data-turbo-command="DemoCommand#example">

Server side commands can live anywhere in your app; however, we recommend you keep them in the app/commands directory.

 |- app
 |  |- ...
+|  |- commands
 |  |- controllers
 |  |- helpers
 |  |- ...

Commands are simple Ruby classes that inherit from TurboBoost::Commands::Command. They expose the following instance methods and properties.

# * controller ...................... The Rails controller processing the HTTP request
# * convert_to_instance_variables ... Converts a Hash to instance variables
# * css_id_selector ................. Returns a CSS selector for an element `id` i.e. prefixes with `#`
# * dom_id .......................... The Rails dom_id helper
# * dom_id_selector ................. Returns a CSS selector for a dom_id
# * element ......................... A struct that represents the DOM element that triggered the command
# * morph ........................... Appends a Turbo Stream to morph a DOM element
# * params .......................... Commands specific params (frame_id, element, etc.)
# * render .......................... Renders Rails templates, partials, etc. (doesn't halt controller request handling)
# * renderer ........................ An ActionController::Renderer
# * state ........................... An object that stores ephemeral `state`
# * transfer_instance_variables ..... Transfers all instance variables to another object
# * turbo_stream .................... A Turbo Stream TagBuilder
# * turbo_streams ................... A list of Turbo Streams to append to the response (also aliased as streams)

They also have access to the following class methods:

# * prevent_controller_action ... Prevents the rails controller/action from running (i.e. the command handles the response entirely)

Here's an example command.

# app/commands/demo_command.rb
class DemoCommand < TurboBoost::Commands::Command
  # The command method `perform` is invoked by an ActionController `before_action`.
  def perform
    # - execute business logic
    # - update state
    # - append additional Turbo Streams
  end
end

Appending Turbo Streams

It's possible to append additional Turbo Streams to the response from within a command. Appended streams are added to the response body after the Rails controller action has completed and rendered the view template.

# app/commands/demo_command.rb
class DemoCommand < TurboBoost::Commands::Command
  def example
    # logic...
    turbo_streams << turbo_stream.append("dom_id", "CONTENT")
    turbo_streams << turbo_stream.prepend("dom_id", "CONTENT")
    turbo_streams << turbo_stream.replace("dom_id", "CONTENT")
    turbo_streams << turbo_stream.update("dom_id", "CONTENT")
    turbo_streams << turbo_stream.remove("dom_id")
    turbo_streams << turbo_stream.before("dom_id", "CONTENT")
    turbo_streams << turbo_stream.after("dom_id", "CONTENT")
    turbo_streams << turbo_stream.invoke("console.log", args: ["Whoa! 🀯"])
  end
end

This proves especially powerful when paired with TurboBoost Streams.

πŸ“˜ NOTE: turbo_stream.invoke is a TurboBoost Streams feature.

Setting Instance Variables

It can be useful to set instance variables on the Rails controller from within a command.

Here's an example that shows how to do this.

<!-- app/views/posts/index.html.erb -->
<%= turbo_frame_tag dom_id(@posts) do %>
  <%= check_box_tag :all, :all, @all, data: { turbo_command: "PostsCommand#toggle_all" } %>
  View All

  <% @posts.each do |post| %>
    ...
  <% end %>
<% end %>
# app/commands/posts_command.rb
class PostsCommand < TurboBoost::Commands::Command
  def toggle_all
    posts = element.checked ? Post.all : Post.unread
    controller.instance_variable_set(:@all, element.checked)
    controller.instance_variable_set(:@posts, posts)
  end
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts ||= Post.unread
  end
end

Prevent Controller Action

Sometimes you may want to prevent normal response handling.

For example, consider the need for a related but separate form that updates a subset of user attributes. We'd like to avoid creating a non RESTful route but aren't thrilled at the prospect of adding REST boilerplate for a new route, controller, action, etc...

In that scenario we can reuse an existing route and prevent normal response handling with a command.

Here's how to do it.

<!-- app/views/users/show.html.erb -->
<%= turbo_frame_tag "user-alt" do %>
  <%= form_with model: @user, data: { turbo_command: "UserCommand#example" } do |form| %>
    ...
  <% end %>
<% end %>

The form above will send a PATCH request to users#update, but we'll prevent normal request handling in the command to prevent running users#update in the controller.

# app/commands/user_command.html.erb
class UserCommand < TurboBoost::Commands::Command
  def example
    # business logic, save record, etc...
    controller.render html: "<turbo-frame id='user-alt'>We prevented the normal response!</turbo-frame>".html_safe
  end
end

Remember that commands are invoked by a controller before action filter. That means controller rendering from inside a command halts the standard request cycle.

Broadcasting Turbo Streams

You can also broadcast Turbo Streams to subscribed users from a command.

# app/commands/demo_command.rb
class DemoCommand < TurboBoost::Commands::Command
  def example
    # logic...
    Turbo::StreamsChannel
      .broadcast_invoke_later_to "some-subscription", "console.log", args: ["Whoa! 🀯"]
  end
end

Learn more about Turbo Stream broadcasting by reading through the hotwired/turbo-rails source code.

πŸ“˜ NOTE: broadcast_invoke_later_to is a TurboBoost Streams feature.

Community

Come join the party with over 2200+ like-minded friendly Rails/Hotwire enthusiasts on our Discord server.

Developing

This project supports a fully Dockerized development experience.

  1. Simply run the following commands to get started.

    git clone -o github https://github.com/hopsoft/turbo_boost-streams.git
    cd turbo_boost-streams
    docker compose up -d # start the envionment (will take a few minutes on 1st run)
    docker exec -it turbo_boost-streams-web rake # run the test suite
    open http://localhost:3000 # open the `test/dummy` app in a browser

    And, if you're using the containers gem (WIP).

    containers up # start the envionment (will take a few minutes on 1st run)
    containers rake # run the test suite
    open http://localhost:3000 # open the `test/dummy` app in a browser
  2. Edit files using your preferred tools on the host machine.

  3. That's it!

Notable Files

Deploying

This project supports Dockerized deployment via the same configurtation used for development, and... it actually runs the test/dummy application in "production". 🀯

The test/dummy app serves the following purposes.

  • Test app for the Rails engine
  • Documentation and marketing site with interactive demos

You can see it in action here. How's that for innovative simplicity?

Notable Files

How to Deploy

fly deploy

Releasing

  1. Run yarn and bundle to pick up the latest
  2. Bump version number at lib/turbo_boost-streams/version.rb. Pre-release versions use .preN
  3. Run rake build and yarn build
  4. Run bin/standardize
  5. Commit and push changes to GitHub
  6. Run rake release
  7. Run yarn publish --no-git-tag-version --access public
  8. Yarn will prompt you for the new version. Pre-release versions use -preN
  9. Commit and push changes to GitHub
  10. Create a new release on GitHub (here) and generate the changelog for the stable release for it

About TurboBoost

TurboBoost is a suite of libraries that enhance Rails, Hotwire, and Turbo... making them even more powerful and boosing your productivity. Be sure to check out all of the various the libraries.

License

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

More Repositories

1

docker-graphite-statsd

Docker image for Graphite & Statsd
Python
869
star
2

rails_standards

A developer's guide of practices to follow when building Rails applications.
354
star
3

universalid

Fast, recursive, optimized, URL-Safe serialization for any Ruby object
Ruby
350
star
4

turbo_boost-streams

Take full control of the DOM with Turbo Streams
HTML
268
star
5

model_probe

ActiveRecord schema visualization and model organization made easy
Ruby
166
star
6

relay

140
star
7

sr_mini

A single file Rails app that will have you running a StimulusReflex and CableReady demo in just 2 steps.
Ruby
139
star
8

turbo_boost-elements

Pre-built easy to use reactive TurboBoost behaviors for Rails/Hotwire apps.
JavaScript
100
star
9

stimulus_reflex_expo

StimulusReflex demos
Ruby
90
star
10

debounced

Debounced versions of standard DOM events
JavaScript
80
star
11

composite_cache_store

A composite cache store comprised of layered ActiveSupport::Cache::Store instances
Ruby
75
star
12

goldmine

Extract a wealth of information from lists
Ruby
75
star
13

stimulus_controllers

[WIP] Stimulus controller library common enough to span multiple projects
JavaScript
58
star
14

chatter

Build a twitter clone in 10 mins with Rails, CableReady, and StimulusReflex
Ruby
53
star
15

tag_columns

Fast & simple Rails ActiveRecord model tagging using PostgreSQL's Array datatype
Ruby
52
star
16

stimulus_reflex_todomvc

An implementation of TodoMVC using Ruby on Rails, StimulusJS, and StimulusReflex
Ruby
49
star
17

bg

Non-blocking ActiveRecord method invocation
Ruby
48
star
18

pipe_envy

Elixir style pipe operator for Ruby
Ruby
46
star
19

hero

Business process modeling for the Rubyist
Ruby
39
star
20

polysearch

Simplified polymorphic full text + similarity search based on postgres
Ruby
38
star
21

coast

RESTful behavior for Rails controllers
Ruby
28
star
22

field_mapper

Data mapping & transformation
Ruby
25
star
23

pry-test

A small test framework that supports debugging test failures & errors when they happen
Ruby
25
star
24

stimulus_toolbox

Stimulus Toolbox is a catalog of Stimulus projects tracked by popularity and other metrics to help you find the libraries you need to build better applications.
Ruby
22
star
25

credentials_demo

Demo of environment aware Rails encrypted credentials with environment variable override
Ruby
21
star
26

active_storage_svg_sanitizer

Sanitize ActiveStorage SVG uploads
Ruby
20
star
27

stimulus_todomvc

[WIP] An implementation of TodoMVC using Ruby on Rails and StimulusJS
Ruby
14
star
28

perm

Simple authorization/permission management in Ruby
Ruby
14
star
29

coin

An absurdly simple DRb based in-memory cache
Ruby
13
star
30

grumpy_old_man

Asserts for RSpec
Ruby
13
star
31

render_later

Improve the user perceived performance of your Rails app
Ruby
13
star
32

trix_embed

Take control over what external links and embedded media is permitted in the Trix editor via copy/paste
JavaScript
11
star
33

docker-ruby-rbx

Trusted Docker Image for Rubinius Ruby
Shell
11
star
34

ellington

A different take on flow based programming.
Ruby
10
star
35

dotfiles

Lua
9
star
36

todomvc

TodoMVC with Rails 7, StimulusReflex, & Import Maps
Ruby
9
star
37

state_jacket

A simple & intuitive state machine
Ruby
9
star
38

turbo_boost-devtools

Devtools for the Hotwire/Turbo ecosystem (TurboBoost, CableReady, StimulusReflex, etc.)
JavaScript
7
star
39

containers

Ruby
7
star
40

roleup

Simple role management
Ruby
6
star
41

legion

Parallel processing made easy
Ruby
6
star
42

kvn

KVN (Key/Value Notation) converter & parser
Ruby
4
star
43

apex-doc

Oracle Application Express (APEX) Quick Reference
4
star
44

bighorn

A standardized interface for event tracking
JavaScript
4
star
45

docker-images

Shell
4
star
46

private-docker-registry

A project to help you build a private docker registry image.
Shell
4
star
47

todo

WIP: CableReady + StimulusReflex + Web Components todo app demo
Ruby
4
star
48

foreign_key_migrations

Plugin that supports adding and removing foreign key constraints in your migrations.
Ruby
3
star
49

github_search

Ruby
3
star
50

docker-nodejs

Docker image for NodeJS
Shell
2
star
51

footing

An ActiveSupport style utility library that employs delegation instead of monkey patching
Ruby
2
star
52

coin_rack

A REST API for Coin
Ruby
2
star
53

fig

The smart way to manage config settings for Rails and Ruby applications using YAML configuration files.
Ruby
2
star
54

self_renderer

Rails model & object rendering outside the context of web requests
Ruby
1
star
55

docker-ruby-mri

Trusted Docker Image for MRI Ruby
Shell
1
star
56

model_definition_tester

Powerful schema and validations testing.
Ruby
1
star
57

string_extensions

Extensions and monkey patches for String.
Ruby
1
star
58

looker

Hash based enumerated types (ENUMS)
Ruby
1
star
59

cable_ready_todomvc

Rails implementation of TodoMVC using the CableReady GEM instead of a heavy SPA framework
Ruby
1
star
60

safe_migrations

Rails plugin that gracefully handles errors in data migrations and prevents you from getting stuck in between.
Ruby
1
star
61

hopsoft.github.io

Web pages for Hopsoft on Github.
JavaScript
1
star
62

docker

Docker images for development and other environments
Dockerfile
1
star
63

active_record_addons

A small library of helper methods for ActiveRecord
Ruby
1
star
64

hustle

Offload CPU heavy computing to separate processes with Ruby blocks.
Ruby
1
star
65

raml_doc

Generate API documentation from RAML files
HTML
1
star
66

stimulus_reflex_client

WIP: Will eventually replace the JavaScript in the StimulusReflex project
JavaScript
1
star
67

han

Hypermedia API Navigation spec
1
star
68

docker-postgres

Trusted Docker Image for PostgreSQL
Shell
1
star