Skip to content

Instantly share code, notes, and snippets.

@hopsoft
Last active July 5, 2024 00:23
Show Gist options
  • Save hopsoft/3738a8292eff2bc0cd0165164a557263 to your computer and use it in GitHub Desktop.
Save hopsoft/3738a8292eff2bc0cd0165164a557263 to your computer and use it in GitHub Desktop.
TurboBoost Commands generator example

TurboBoost Commands Generator

The next version of TurboBoost will ship with generators to help you get started with Commands quickly.

  1. 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.

  1. 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

Generated Files

Command

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

Command Test

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

ApplicationCommand

app/commands/application_command.rb

# frozen_string_literal: true

class ApplicationCommand < TurboBoost::Commands::Command
  # Place shared command methods/logic here
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment