Skip to content

Latest commit

 

History

History
408 lines (314 loc) · 12.3 KB

README.md

File metadata and controls

408 lines (314 loc) · 12.3 KB

<button> + <turbo-stream>

Harness the power of Turbo Streams to declare click event handlers as a series of HTML mutations.

Combine built-in <button> elements with <turbo-stream> elements to declaratively drive client-side interactions with server-generated HTML.

<button type="button" id="call-to-action"
        data-controller="turbo-stream-button"
        data-action="click->turbo-stream-button#evaluate">
  <span>Click me to insert the template's contents after this button!</span>

  <template data-turbo-stream-button-target="turboStreams">
    <turbo-stream action="after" target="call-to-action">
      <template><p>You clicked the button!</p></template>
    </turbo-stream>
  </template>
</button>

Try it out.

Usage

In your JavaScript code, import and register the turbo-stream-button controller with your Stimulus application:

import "@hotwired/turbo"
import { Application } from "stimulus"
import { TurboStreamButtonController } from "@seanpdoyle/turbo_stream_button"

const application = Application.start()
application.register("turbo-stream-button", TurboStreamButtonController)

In your Rails templates, call the turbo_stream_button_tag helper or render the turbo_stream_button view partial to create the <button> element. The view partial renders:

  • the block content as the <button> element's content
  • other keyword arguments as the <button> element's attributes
  • any content captured by any call to the #turbo_streams method invoked on the block's single argument

When the button is clicked, the turbo-stream-button Stimulus controller invokes the evaluate Action to insert the contents of the <template> element, activating any <turbo-stream> elements nested inside.

Introductory: Hello, world

<%= turbo_stream_button_tag id: "the-button" do |button| %>
  <span>Click me to say "hello"</span>

  <% button.turbo_streams do %>
    <%= turbo_stream.after "the-button", "Hello, world!" %>
  <% end %>
<% end %>

<%# =>  <button type="button" id="the-button"
                data-controller="turbo-stream-button"
                data-action="click->turbo-stream-button#evaluate">
          <span>Click me to say "hello"</span>

          <template data-turbo-stream-target="turboStreams">
            <turbo-stream action="after" target="the-button">
              <template>Hello, world!</template>
            </turbo-stream>
          </template>
        </button> %>

Intermediate: Compose with other Stimulus controller actions

<script type="module">
  import "@hotwired/turbo"
  import { Application, Controller } from "stimulus"
  import { TurboStreamButtonController } from "@seanpdoyle/turbo_stream_button"

  class ClipboardController extends Controller {
    copy({ target: { value } }) {
      navigator.clipboard.writeText(value)
    }
  }

  const application = Application.start()
  application.register("turbo-stream-button", TurboStreamButtonController)
  application.register("clipboard", ClipboardController)
</script>

<div id="flash" role="alert"></div>

<%= turbo_stream_button_tag value: "invitation-code-abc123",
                            data: { controller: "clipboard", action: "click->clipboard#copy" } do |button| %>
  Copy to clipboard

  <% button.turbo_streams do %>
    <turbo-stream action="append" target="flash">
      <template>
        <p>Copied "invitation-code-abc123" to your clipboard!</p>
      </template>
    </turbo-stream>
  <% end %>
<% end %>

<%# =>  <button type="button" value="invitation-code-abc123"
                data-controller="turbo-stream-button clipboard"
                data-action="click->turbo-stream-button#evaluate click->clipboard#copy">
          Copy to clipboard

          <template data-turbo-stream-target="turboStreams">
            <turbo-stream action="append" target="flash">
              <template>
                <p>Copied "invitation-code-abc123" to your clipboard!</p>
              </template>
            </turbo-stream>
          </template>
        </button> %>

Advanced: Nest buttons

<div id="flash" role="alert"></div>

<%= turbo_stream_button_tag do |button| %>
  Append flash message

  <% button.turbo_streams do %>
    <turbo-stream action="append" target="flash">
      <template>
        <div id="a_flash_message" role="status">
          Hello, world!

          <%= turbo_stream_button_tag do |button| %>
            Dismiss

            <% button.turbo_streams do %>
              <%= turbo_stream.remove "a_flash_message" %>
            <% end %>
          <% end %>
        </div>
      </template>
    </turbo-stream>
  <% end %>
<% end %>

<%# =>  <button type="button"
                data-controller="turbo-stream-button"
                data-action="click->turbo-stream-button#evaluate">
          Append flash message

          <template data-turbo-stream-target="turboStreams">
            <turbo-stream action="append" target="flash">
              <template>
                <div id="a_flash_message" role="status">
                  Hello, world!

                  <button type="button"
                          data-controller="turbo-stream-button"
                          data-action="click->turbo-stream-button#evaluate">
                    Dismiss

                    <template data-turbo-stream-target="turboStreams">
                      <turbo-stream action="remove" target="a_flash_message"></turbo-stream>
                    </template>
                  </button>
                </div>
              </template>
            </turbo-stream>
          </template>
        </button> %>

Advanced: Append form controls

<script type="module">
  import "@hotwired/turbo"
  import { Application } from "stimulus"
  import { TurboStreamButtonController } from "@seanpdoyle/turbo_stream_button"
  import { TemplateInstance } from "https://cdn.skypack.dev/@github/template-parts"

  class CloneController extends Controller {
    static targets = [ "template" ]
    static values = { count: Number, counter: String }

    templateTargetConnected(target) {
      const templateInstance = new TemplateInstance(target, {
        [this.counterValue]: this.countValue
      })

      target.content.replaceChildren(templateInstance)

      this.countValue++
    }
  }

  const application = Application.start()
  application.register("turbo-stream-button", TurboStreamButtonController)
  application.register("clone", CloneController)
</script>

<%= form_with scope: :applicant do |form| %>
  <fieldset data-controller="clone" data-clone-counter-value="counter" data-clone-count-value="0">
    <legend>References</legend>

    <ol id="references"></ol>

    <%= form.fields :reference_attributes, index: "{{counter}}" do |reference_form| %>
      <%= turbo_stream_button_tag do |button| %>
        Add reference

        <% button.turbo_streams do %>
          <turbo-stream action="append" target="references">
            <template data-clone-target="template">
              <li>
                <%= reference_form.label :referrer %>
                <%= reference_form.text_field :referrer %>

                <%= reference_form.label :relationship %>
                <%= reference_form.text_field :relationship %>
              </li>
            </template>
          </turbo-stream>
        <% end %>
      <% end %>
    <% end %>
  </fieldset>
<% end %>

<%# =>  <button type="button"
                data-controller="turbo-stream-button"
                data-action="click->turbo-stream-button#evaluate">
          Add reference

          <template data-turbo-stream-button-target="turboStreams">
            <turbo-stream action="append" target="references">
              <template data-clone-target="template">
                <li>
                  <label for="applicant_reference_attributes_{{counter}}_referrer">Referrer</label>
                  <input type="text" name="applicant[reference_attributes][{{counter}}][referrer]" id="applicant_reference_attributes_{{counter}}_referrer">

                  <label for="applicant_reference_attributes_{{counter}}_relationship">Relationship</label>
                  <input type="text" name="applicant[reference_attributes][{{counter}}][relationship]" id="applicant_reference_attributes_{{counter}}_relationship">
                </li>
              </template>
            </turbo-stream>
          </template>
        </button> %>

Helpers

There are two helpers declared by the engine:

turbo_stream_button_tag

The turbo_stream_button_tag helper renders a <button> element ready to evaluate a collection of <turbo-stream> elements:

<%= turbo_stream_button_tag do |button| %>
  Click to append "Hello!"

  <% button.turbo_streams do %>
    <%= turbo_stream.append_all "body" do %>
      Hello!
    <% end %>
  <% end %>
<% end %>

turbo_stream_button

The turbo_stream_button helper returns an attributes-aware HTML tag builder. Render a <button> element with the appropriate [data-controller] and [data-action] attributes by calling #tag:

<%= turbo_stream_button.tag do %>
  Click to append "Hello!"
<% end %>

The return a Hash of attributes containing the appropriate [data-controller] and [data-action] attributes, call #merge, #to_h or splat them into keyword arguments (with **):

<%= form_with model: Post.new do |form| %>
  <%= form.button **turbo_stream_button, type: :submit do %>
    Click to append "Hello!"

    <%= tag.template turbo_stream_button.template.merge(data: { a_controller_target: "template" }) do %>
      <%= turbo_stream.append_all "body" do %>
        Hello!
      <% end %>
    <% end %>
  <% end %>
<% end %>

To render the <template> element nested within the <button>, call #template_tag:

<%= turbo_stream_button.tag do %>
  Click to append "Hello!"

  <% turbo_stream_button.template_tag do %>
    <%= turbo_stream.append_all "body" do %>
      Hello!
    <% end %>
  <% end %>
<% end %>

The return a Hash of attributes containing the appropriate [data-turbo-stream-button-target] attribute, call #merge, #to_h, or splat them into keyword arguments (with **):

<%= turbo_stream_button.tag do %>
  Click to append "Hello!"

  <%= tag.template turbo_stream_button.template.merge(data: { a_controller_target: "template" }) do %>
    <%= turbo_stream.append_all "body" do %>
      Hello!
    <% end %>
  <% end %>
<% end %>

Exploring examples

To poke around with some working examples, start the dummy application locally:

cd test/dummy
bundle exec rails server --port 3000

Then, visit http://localhost:3000/examples.

Run on Repl.it

You can also fork the @seanpdoyle/turbo_stream_button sandbox project on replit.com.

Installation

Add the turbo_stream_button dependency to your application's Gemfile:

gem "turbo_stream_button", github: "seanpdoyle/turbo_stream_button", branch: "main"

And then execute:

$ bundle

Installation through importmap-rails

Once the gem is installed, add the client-side dependency mapping to your project's config/importmap.rb declaration:

# config/importmap.rb

pin "stimulus", to: "stimulus.min.js", preload: true
pin "@seanpdoyle/turbo_stream_button", to: "turbo_stream_button.js"

Installation through npm or yarn

Once the gem is installed, add the client-side dependency to your project via npm or Yarn:

yarn add https://github.com/seanpdoyle/turbo_stream_button.git

Contributing

Please read CONTRIBUTING.md.

License

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