Introducing Live Elements

A bird band singing live
Image by Annie Ruygt

Ruby on Rails contains everything you need:

Rendering HTML templates, updating databases, sending and receiving emails, maintaining live pages via WebSockets, enqueuing jobs for asynchronous work, storing uploads in the cloud, providing solid security protections for common attacks. Rails does it all and so much more.

But I still want more. Phoenix LiveView supports Interactive, Real-Time Apps. No Need to Write JavaScript. Yea, I definitely want that. I want to build applications like the following with no custom JavaScript:

While what I am about to present doesn’t satisfy all the use cases that Phoenix LiveView supports, it does support a huge chunk of them. And does so in a way that both builds on what Rails already offers and does so in a way that will be instantly familiar to Rails developers.

Once this stabilizes we can move on to the next chunk.

What we have to work with

There is really no need to start from scratch. Hotwire already has some awesome building blocks for us to use. First Turbo Streams:

Turbo Streams deliver page changes over WebSocket, SSE or in response to form submissions using just HTML and a set of CRUD-like actions.

And Stimulus:

actions, which connect controller methods to DOM events using data-action attributes

The difference here is that what we want is to associate DOM events to server controller events. That what Live Elements does. Without WebSockets or long polling. Or you needing to write even a single line of JavaScript code.

Demo time!

While it is said that a picture is worth a thousand words, a demo can be priceless.

For the impatient, a live_elements_demo shell script is provided which you can copy and paste into a terminal window running a bash or dash shell and get up and running in seconds.

Let’s start with a modest Rails form, decorated with data attributes:

<div>
   <%= render partial: 'header', locals: {color: "yellow"} %>

   <%= form_with data: {controller: "live-elements"} do |form| %>
     <%= form.button "blue", name: 'color',
       data: {action: {click: demo_click_path}},
       class: "bg-blue-500 hover:bg-blue-700 text-white 
               font-bold py-2 px-4 rounded" %>

     <%= form.button "red", name: 'color',
       data: {action: {click: demo_click_path}},
       class: "bg-red-500 hover:bg-red-700 text-white
               font-bold py-2 px-4 rounded" %>
   <% end %>
</div>

Everything here is standard Ruby on Rails. In fact, it looks like we are using a Stimulus controller, and that’s because we are. Live Elements is a Stimulus controller, just one that looks for actions and routes them to the server via fetch requests rather than to client side JavaScript.

In addition there are two data actions defined. Both associate DOM click events to server side routes. You are free to use any DOM event and any server route. In most cases HTTP POST will be used, but if the element is associated with a form, the method from the form will be used.

Now let’s look at the partial:

  <%= turbo_frame_tag "header",
    class: "block bg-#{color}-400 mb-4" do %>
    <h1 class="font-bold text-4xl">Live button demo</h1>
  <% end %>

  <!-- bg-yellow-400 bg-blue-400 bg-red-400 -->

Not much to see here. The color is the only part that changes. Note the comment at the bottom will get Tailwind to include these classes in the CSS that is sent to the client..

Finally, the controller:

class DemoController < ApplicationController
  def button
  end

  def click
    respond_to do |format|
      format.turbo_stream { 
        render turbo_stream: turbo_stream.replace('header',
          render_to_string(partial: 'header',
            locals: {color: params[:color]}))
      }
    end
  end
end

Again, a standard Rails controller, with two actions. One accesses a param from the request and responds with a turbo stream response. In this case, the part of DOM identified with an id of header will be replaced with the rendering of the partial. Any Turbo Stream action can be used, and you can even respond with multiple actions by rendering a view containing turbo-stream templates.

So far, not a single line of JavaScript was written. I promised that there would be no custom JavaScript. So without further ado, lets install Live Element:

bin/importmap pin @flydotio/stimulus-live-elements@0.1.0
echo 'export { default } from  "@flydotio/stimulus-live-elements"' > \
  app/javascript/controllers/live_elements_controller.js

Hopefully in the future there will be a standard way to install third party Stimulus controllers, but for now this will do.

All that is left is to wire up some routes:

Rails.application.routes.draw do
  root "demo#button"
  post "demo/click"
end

Running bin/dev and navigating to the site will produce this masterpiece.

This demo isn’t very fancy, and to be honest could have been done without TurboStream at all and would have had roughly the same user experience even though it would be sending complete rendering of the page over the wire.

Check out the three demos you saw above - run them locally or deploy to fly.io. Each of these demos were created without any custom JavaScript, just vanilla views, partials, and controllers returning Turbo Stream responses.

You can play with this right now.

Deploy using [Fly.io terminal](https://fly.io/terminal) or see our [Hands-on](https://fly.io/docs/hands-on/) guide that will walk you through the steps.

Try Fly for free

Since this is all vanilla HTTP request/response, things like supporting sessions is possible and will scale with existing web servers with processes/threads. What it doesn’t have is any notion of server push or server side actors, so subscribing to server events would still have to be done via ActionCable.

Hopefully this inspires ideas and use cases. Drag and drop, keyboard events, and more should be possible but would require a change to the library. Try it out. Start a discussion, create an issue or open a pull request.


Technical details

For those that want to know more about how this works under the covers:

  • Using TurboStream With the Fetch API covers how to produce a correct X-CSRF-Token and how to use Turbo.renderStreamMessage.
  • Form parameters are passed using search parameters when sent via HTTP get, and are passed using x-www-form-urlencoded body for all other HTTP methods.
  • MutationObserver is used to detect changes to the DOM, and additions are scanned for data-action attributes and event listeners are attached to such elements.
  • Actions are processed sequentially in the order that they are received, and requestIdleCallback is used to ensure that turbo stream actions are applied before the next action is taken. As Safari does not yet support this API, a setTimeout with a value of 50 is used instead.
  • While the documentation doesn’t mention it, and DHH would disagree, render turbo_stream can be passed an array of turbo stream actions to be applied. The search demo above flashed the screen when the entire output is replaced, but is smooth when individual rows added/removed using individual remove, after, and prepend actions.

Appendix: Comparison with other Frameworks

While Phoenix LiveView is the gold standard here, I’m going to limit this discussion to components that can be used with Rails applications. The general theme is that the others are more mature but don’t build on TurboStreams so are modestly larger in size, may involve “some assembly required” to get started, and introduce a different programming model than one finds in Rails applications.

And the good news is that you can use Live Elements in conjunction with other frameworks. So not only can you get started quicker with Live Elements you can switch when needed to a more capable framework should you end up needing it. For many cases, YAGNI applies.

HTMX

This is the closest conceptual cousin to Live Elements. Instead of data- attributes, you will use hx- attributes, and more of them. That’s because with HTMX you specify things like the target and swap strategies using attributes. Rails helpers encourage the use of data- attributes so there may be some additional minor syntax advantage here.

More importantly, with Live Elements all of this is controlled by the response generated by the server, including an obvious way to respond back with multiple DOM updates.

The end result is a consistent programming model between Turbo Streams typically used with WebSockets and with Live Elements. In many cases you will be able to share partials between the two.

StimulusReflex

This is a much more ambitious and capable library. It requires web sockets. It also is currently built on Webpacker.

With StimulusReflex you create “Reflex actions” instead of standard controller actions, and even (*** gasp ***) write JavaScript controllers. While there appears to be no reason why you can’t mix and match this with Live Elements, if you find yourself needing what StimulusReflex provides it is best to think of Live Elements as training wheels and build a plan to converge over time to just StimulusReflex.

CableReady

This is a prerequisite for StimulusReflex but can be used standalone. Again, it requires web sockets, and currently presumes that you have bought into the whole javascript ecosystem vs import maps.

While StimulusReflex may be seen as a potentially better alternatives to Live Element, CableReady can be seen as an addition to ActionCable that may compliment Live Element.

While CableReady uses a JSON wire format as opposed to TurboStream’s HTML fragment approach, it would be an interesting project to map CableReady’s operations to a HTML syntax and to add HTML custom elements to process these operations too. See update:

Update: Turbo Boost

Marco Roth pointed me to Turbo Boost Streams which already does what the deleted paragraph above describes.

bundle add turbo_boost-streams --version 0.0.8
bin/importmap add @turbo-boost/streams@0.0.8

Add to the top of app/javascript/controllers/application.js:

import  "@turbo-boost/streams"

And then you can add turbo boost actions to your responses:

stream << turbo_stream.invoke("console.log", args: ["Hello World!"])

Sweet!

Phlex

This one probably doesn’t belong on the list as they serve different needs, but I’m including it as it is worth pointing out that from a Phlex perspective Live Elements are just Hash Attributes so the approaches will work well together.

Phlex is also looking into streaming HTML using turbo-stream actions as they were intended to be used. Again, a good match.