Live previews with Rails and Stimulus 2

Who wouldn’t want a live preview for writing their great content? If you happen to be running Rails with Hotwire, it’s surprisingly easy with a small Stimulus controller.

The core idea of our preview is that it’s rendered entirely on the backend. This is useful since we can reuse the logic both for presentation as well as actual changes. Whatever happens with the content can come from a single Ruby method. We’ll start with a small controller and a preview method:

class PreviewController < ApplicationController
  def preview
    preview = "#{request.raw_post}
    render plain: preview

I depend on request.raw_post to get the whole POST body sent to the backend. We also render it as plain text, avoiding any HTML safe escaping.

Now we have to open a form for the model we want to preview. Let’s say it’s a Tweet model:

<%= form_with(model: tweet) do |form| %>
    <% if tweet.errors.any? %>

<%= pluralize(tweet.errors.count, "error") %> prohibited this tweet from being saved:

    <% tweet.errors.each do |error| %>
  • <%= error.full_message %>
  • <% end %>
<% end %> class="field"> <%= form.label :body %> <%= form.text_area :body, placeholder: "Type a tweet", data: { "composer-target": "tweet", "action": "input->composer#preview" } %>
class="actions"> <%= form.submit %>
<% end %>

As you can see, I adjusted the default template with a little bit Stimulus 2 data attributes. I wrapped the whole form in a div with data-controller set to composer, which will be the Stimulus controller’s name. I added two data attributes to the text area: data-composer-target identifying the text area and data-action specifying composer#preview action on any input. Then at the bottom, we just have a

with data-composer-target set to output. This target will be used for inserting the rendered HTML from the backend.

For the view to work, we’ll need to write a composer_controller.js that will react to input changes taking data from the text area and present them to the output div.

But before we do, we have to make sure we have both Rails UJS and Stimulus set up as we’ll use UJS Rails.ajax call. The application.js in app/javascript/packs needs to look similar to this:

import Rails from "@rails/ujs"
import "@hotwired/turbo-rails"
import * as ActiveStorage from "@rails/activestorage"
import "channels"


// Stimulus.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("./controllers", true, /.js$/)

The final piece is the controller itself:

import { Controller } from "stimulus"

import Rails from "@rails/ujs"

export default class extends Controller {
  static targets = [ "tweet", "output" ]

  connect() {

  preview() {
    var content = this.tweetTarget.value;
    var preview = this.outputTarget;

      type: "post",
      url: "/preview",
      contentType: "text/plain",
      data: content,
      success: function(data) {
         preview.innerHTML = data

In the beginning, we define the tweet and output targets. We call preview on connect() to make sure the initial preview will be rendered and then define the primary preview method.

The preview() method then takes the content from the tweet target and sends it using the Rails.ajax call as text/plain. Once we receive a preview response back, we directly inject it into the rest of the page by setting the innerHTML attribute on the output target.

And that’s it. If you didn’t modify the initial method, you’d get the whole text previewed in bold.

Any comments? Write me a direct message at


I am writing an introductory book on web application deployment. Networking, processes, systemd, backups, and all your usual suspects.

Open →

Leave a Reply

Your email address will not be published. Required fields are marked *