The next version of TurboBoost will ship with generators to help you get started with Commands quickly.
- View help for the generator
bin/rails g turbo_boost:command -h
Usage:
bin/rails generate turbo_boost:command NAME [options]
Options:
[--skip-namespace] # Skip namespace (affects only isolated engines)
# Default: false
[--skip-collision-check] # Skip collision check
# Default: false
-c, [--comments], [--no-comments], [--skip-comments] # Include helpful comments
# Default: true
-d, [--directory=DIRECTORY] # The path to the TurboBoost Commands directory
# Default: app/commands
-e, [--examples], [--no-examples], [--skip-examples] # Include helpful examples
# Default: true
Runtime options:
-f, [--force] # Overwrite files that already exist
-p, [--pretend], [--no-pretend], [--skip-pretend] # Run but do not make any changes
-q, [--quiet], [--no-quiet], [--skip-quiet] # Suppress status output
-s, [--skip], [--no-skip], [--skip-skip] # Skip files that already exist
Creates a new TurboBoost Command
NOTE: The default settings creates the Command with helpful comments and examples. This behavior can be disabled by passing
--skip-comments
and--skip-examples
.
- Run the generator
bin/rails g turbo_boost:command Demo
generate turbo_boost:application_command # <- Creates ApplicationCommand if it doesn't exist
rails generate turbo_boost:application_command
create app/commands/application_command.rb
create app/commands/demo_command.rb # <- Creates the Command
create test/commands/demo_command_test.rb # <- Creates the Command test
app/commands/demo_command.rb
# frozen_string_literal: true
# TurboBoost Commands are executed via a before_action in the Rails controller lifecycle.
#
# Commands have access to 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)
#
# Commands have access to the following Class methods and properties:
#
# - prevent_controller_action ... Prevents the rails controller/action from running
# i.e. the Command handles the response entirely
class DemoCommand < ApplicationCommand
# ----------------------------------------------------------------------------
# Response Handling
# ----------------------------------------------------------------------------
# Uncomment the line below if you plan to handle the response in the Command itself
# prevent_controller_action
#
# ----------------------------------------------------------------------------
# Filters
# ----------------------------------------------------------------------------
# Commands support before/after/around filters similar to Rails controllers
#
# - before_command
# - after_command
# - around_command
#
# Examples:
#
# before_command { # logic... }
# before_command -> { # logic... }
# before_command :some_method, :another_method
#
# A Command can be aborted/halted by invoking `throw :abort` in a `before_command` filter
#
# Example:
#
# before_command { throw :abort }
#
# ----------------------------------------------------------------------------
# Command Rescue
# ----------------------------------------------------------------------------
# Unhandled errors and aborted commands can be rescue and handled with `rescue_from`
#
# Examples:
#
# # Handle aborted commands
# rescue_from TurboBoost::Commands::AbortError do |error|
# # do something...
# end
#
# # Handle command errors
# rescue_from TurboBoost::Commands::PerformError do |error|
# # do something...
# end
#
# ----------------------------------------------------------------------------
# Command Methods
# ----------------------------------------------------------------------------
# The default Command method name is `perform`
#
# Registering the `perform` Command method in client markup is easy and straightforward
# This example uses a <button> element, but Commands can be registered on any HTML element
#
# Examples:
#
# <!-- clicking the button will invoke the command -->
# <button data-turbo-command="DemoCommand#perform">
# Invoke the perform command
# </button>
#
# - or -
#
# <!-- clicking the button will invoke the command -->
# <button data-turbo-command="DemoCommand">
# Invoke the perform command
# </button>
#
# - or -
#
# <!-- clicking the button will invoke the command -->
# <button data-turbo-command="Demo">
# Invoke the default command
# </button>
def perform
# Suggested operations (pick and choose based on your use case)
# - implement business logic
# - update data stores
# - modify TurboBoost state
# - append TurboStreams to the response
# - broadcast updates
# - etc.
# State examples
state[:state_example] = "TurboBoost state is remembered across command invocations"
state.now[:state_now_example] = "TurboBoost state.now is discarded after the current response completes"
# TurboStream examples
morph id: "dom-id-morph", html: "<div id=\"dom-id-morph\">Use morph to append a TurboBoost invoke morph TurboStream</div>"
streams << turbo_stream.invoke("console.log", args: ["TurboBoost Streams gives you full control over the DOM"])
streams << turbo_stream.append("dom-id-append", "<div>Standard TurboStreams are also supported</div>")
end
# It's also possible to define custom Command methods
#
# Registering a custom Command method in client markup is easy and straightforward
# This example uses a <button> element, but Commands can be registered on any HTML element
#
# Examples:
#
# <!-- clicking the button will invoke the command -->
# <button data-turbo-command="DemoCommand#custom">
# Invoke the custom command
# </button>
#
# - or -
#
# <!-- clicking the button will invoke the command -->
# <button data-turbo-command="Demo#custom">
# Invoke the custom command
# </button>
# def custom
# # logic...
# end
end
test/commands/demo_command_test.rb
# frozen_string_literal: true
require "test_helper"
# Example tests to get you started, update for your use-case(s)
class DemoCommandTest < TurboBoost::Commands::CommandTestCase
tests TurboBoost::Commands::TestController
delegate :turbo_boost, to: :@controller
delegate :command, to: :turbo_boost
test "unperformed" do
get :show
refute turbo_boost.command_performed?
assert_nil command
end
test "performed" do
get :show, command: "DemoCommand#perform"
assert turbo_boost.command_performed?
assert turbo_boost.command_succeeded?
end
test "perform with state change" do
get :show, command: "DemoCommand#perform"
expected = "TurboBoost state is remembered across command invocations"
assert_equal expected, command.state[:state_example]
end
test "perform with state.now change" do
get :show, command: "DemoCommand#perform"
expected = "TurboBoost state.now is discarded after the current response completes"
assert_equal expected, turbo_boost.state.now[:state_now_example]
end
test "perform with invoke morph stream" do
get :show, command: "DemoCommand#perform"
doc = Nokogiri::HTML.fragment(command.streams.to_a[0])
stream = doc.css("turbo-stream")
assert_equal "invoke", stream.attribute("action").value
template = stream.css("template")
data = JSON.parse(template.inner_text)
# Invoke morph TurboStream Payload Example:
# {
# "id"=>"d4e5d9b8-0c40-44f6-900d-e79348725f50",
# "selector"=>"#dom-id-morph",
# "method"=>"morph",
# "args"=>["<div id=\"dom-id-morph\">Use morph to append a TurboBoost invoke morph TurboStream</div>"],
# "delay"=>0
# }
assert_equal "#dom-id-morph", data["selector"]
assert_equal "morph", data["method"]
assert_equal ["<div id=\"dom-id-morph\">Use morph to append a TurboBoost invoke morph TurboStream</div>"], data["args"]
assert_equal 0, data["delay"]
end
test "perform with invoke console.log stream" do
get :show, command: "DemoCommand#perform"
doc = Nokogiri::HTML.fragment(command.streams.to_a[1])
stream = doc.css("turbo-stream")
assert_equal "invoke", stream.attribute("action").value
template = stream.css("template")
data = JSON.parse(template.inner_text)
# Invoke console.log TurboStream Payload Example:
# {
# "id"=>"7b8c9c1b-289b-474d-9d83-c74a7ef481d0",
# "receiver"=>"console",
# "method"=>"log",
# "args"=>["TurboBoost Streams gives you full control over the DOM"],
# "delay"=>0
# }
assert_equal "console", data["receiver"]
assert_equal "log", data["method"]
assert_equal ["TurboBoost Streams gives you full control over the DOM"], data["args"]
assert_equal 0, data["delay"]
end
test "perform with append stream" do
get :show, command: "DemoCommand#perform"
doc = Nokogiri::HTML.fragment(command.streams.to_a[2])
stream = doc.css("turbo-stream")
assert_equal "append", stream.attribute("action").value
assert_equal "dom-id-append", stream.attribute("target").value
template = stream.css("template")
assert_equal "<div>Standard TurboStreams are also supported</div>", template.inner_html
end
end
app/commands/application_command.rb
# frozen_string_literal: true
class ApplicationCommand < TurboBoost::Commands::Command
# Place shared command methods/logic here
end