Hotwire: Turbo, Stimulus and the Future of JavaScript in Rails

@timrossinfo

The history of Javascript in Rails

Early Rails: RJS

An early goal of Rails was to allow you to get access to the client-side power of JavaScript while avoiding actually having to write JavaScript.

 

Rails provided the link_to_remote and remote_form_for helpers, which automated the process of making an AJAX call and replacing the contents of an element on the page without a full page refresh.

 

RJS added a series of Ruby commands for simple client-side effects that emitted JavaScript to be sent to the browser for evalulation.

Rails 3.0: UJS

“Unobtrusive JavaScript” became a term for separating JavaScript from the markup.

 

UJS replaced the link_to_remote and remote_form_for helpers with a remote: true attribute.

 

It also provided confirmation dialogs, non-GET links and Rails-specific AJAX events that you could hook into.

Rails 3.1: CoffeeScript and Sprockets

jQuery is fully supported as a replacement for Prototype.

 

CoffeeScript was introduced as the default JavaScript language. CoffeeScript used a more Ruby-like syntax and had features that Javascript didn’t have yet, such as Classes.

 

The asset pipeline, or Sprockets gem, was meant to be the Rails way of packaging files and serving them to the browser.

Rails 4.0: Turbolinks

With Turbolinks, the default behaviour of all links was to go to the server, strip out the body part of the page response, and replace the body in the DOM with the new HTML.

 

The goal of Turbolinks was to get the performance of a single page app while retaining the basic structure of a server-side application.

 

Didn’t always play nicely with other Javascript libraries.

Rails 5.1: Webpacker

Introduced as the new way of packaging JavaScript assets from Rails.

 

Allows for easy installation of many popular JavaScript libraries.

 

Powerful and flexible, but complex configuration.

Hotwire

https://hotwired.dev

Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire.

Turbo - A set of techniques for speeding up page changes and form submissions, dividing complex pages into components, and stream partial page updates over web sockets.

Stimulus - A simple Javascript framework for connecting JavaScript objects to DOM elements on the page.

Strada - Not released yet, but will be responsible for building native experiences on mobile devices.

Hotwire is an umbrella terms for the following frameworks:

Turbo

Turbo Drive - Accelerates links and form submissions by negating the need for full page reloads.

Turbo Frames - Decompose pages into independent contexts, which scope navigation and can be lazily loaded. 

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

Turbo consists of four main parts:

Turbo Native - Provides tooling to wrap a Turbo-enabled web app in a native iOS or Android shell.

Turbo Drive

Intercepts all clicks on <a href> links to the same domain.


When you click on a link, Turbo Drive prevents the browser from following it, changes the browser’s URL using the History API, requests the new page using fetch, and then renders the HTML response.

 

Similar to classic Turbolinks.

Turbo Frames

All interaction within a frame, like clicking links or submitting forms, happens within that frame, keeping the rest of the page from changing or reloading.


To wrap an independent segment in its own navigation context, enclose it in a <turbo-frame> tag.


Use the src attribute to defer loading contents of a frame until the page is loaded.

 

Similar to classic iframes, except part of the same DOM.

<body>
  <div id="navigation">Links targeting the entire page</div>

  <turbo-frame id="message_1">
    <h1>My message title</h1>
    <p>My message content</p>
    <a href="/messages/1/edit">Edit this message</a>
  </turbo-frame>

  <turbo-frame id="comments">
    <div id="comment_1">One comment</div>
    <div id="comment_2">Two comments</div>

    <form action="/messages/comments">...</form>
  </turbo-frame>
</body>
<body>
  <h1>Inbox</h1>

  <div id="emails">
    ...
  </div>

  <turbo-frame id="set_aside_tray" src="/emails/set_aside">
    <img src="/icons/spinner.gif">
  </turbo-frame>

  <turbo-frame id="reply_later_tray" src="/emails/reply_later">
    <img src="/icons/spinner.gif">
  </turbo-frame>
</body>

Turbo Streams

Delivers page changes as fragments of HTML wrapped in self-executing <turbo-stream> elements.


Change any part of the page in response to updates sent over a WebSocket connection.


Seven basic actions: append, prepend, replace, update, remove, before, and after.


Stream updates to the page from an HTTP response (e.g. after a form submission), or a WebSocket connection (using the Broadcastable concern).

class MessagesController < ApplicationController
  def create
    message = Message.create!(
      params.require(:message).permit(:content)
    )

    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.append(
          :messages, partial: "messages/message",
          locals: { message: message }
        )
      end

      format.html { redirect_to messages_url }
    end
  end
end
Content-Type: text/vnd.turbo-stream.html; charset=utf-8

<turbo-stream action="append" target="messages">
  <template>
    <div id="message_1">
      The content of the message.
    </div>
  </template>
</turbo-stream>

Turbo Native

Native navigation UI, while managing a single web view instance.

Migrating from Turbolinks:

Stimulus

The purpose of Stimulus is to automatically connect DOM elements to JavaScript objects. Those objects are called controllers.

 

Use data attributes on HTML elements to connect a controller, declare targets and link events to actions.

 

A Stimulus application’s state lives as attributes in the DOM; controllers themselves are largely stateless.

Controllers

A controller is the basic organisational unit of a Stimulus application.

 

Every controller is associated with an HTML element.

Aside from controllers, the three other major Stimulus concepts are:

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

 

targets, which locate elements of significance within a controller

 

values, which read, write, and observe data attributes on the controller’s element

<div data-controller="hello">
  <input data-hello-target="name" type="text">
  <button data-action="click->hello#greet">Greet</button>
</div>
// src/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "name" ]

  greet() {
    const element = this.nameTarget
    const name = element.value
    console.log(`Hello, ${name}!`)
  }
}

Demo - Jammerly

Javascript in Rails 7

1. A default path with Hotwire and import maps

2. An alternate path using a thin integration with one of the popular JavaScript bundlers

3. A strict API path with a separate repository for the front-end

Rails 7 will have 3 choices for Javascript:

A browser tool that lets you map a logical name to a downloaded module directly in the browser without needing to do further bundling on the server.

With HTTP2, you no longer pay a large penalty for sending many small files instead of one big file.

ES6 is now supported by all popular browsers, removing the need for transpiling.

What are Import Maps?

With this approach you’ll ship many small JavaScript files instead of one big JavaScript file. Thanks to HTTP2 that no longer carries a material performance penalty during the initial transport, and in fact offers substantial benefits over the long run due to better caching dynamics. Whereas before any change to any JavaScript file included in your big bundle would invalidate the cache for the the whole bundle, now only the cache for that single file is invalidated.

From the importmap-rails readme:

# config/importmap.rb
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"
<script type="importmap" data-turbo-track="reload">
  {
    "imports": {
      "react": "https://ga.jspm.io/npm:react@17.0.2/index.js"
    }
  }
</script>
import "react";

Rails 7 will have full support for traditional JS bundling via the jsbundling-rails gem - provides the basic setup required for esbuild, rollup.js, or Webpack

Traditional JS bundling

References

Hotwire.dev:

Rails 7 will have three great answers to JavaScript in 2021+:

Modern web apps without JavaScript bundling or transpiling:

Rails 7 and JavaScript:

Modern JavaScript in Rails 7 without Webpack:

Migrating From Turbolinks To Turbo:

Hotwire: Turbo, Stimulus and the Future of JavaScript in Rails

By timrossinfo

Hotwire: Turbo, Stimulus and the Future of JavaScript in Rails

The goal of this talk is to look back at the role of Javascript in Rails and to look ahead at the choices for integrating JavaScript in Rails 7 and beyond.

  • 295