https://nts.strzibny.name/rails-stimulus-live-preview/ /home/nts Live previews with Rails and Stimulus 2 23 Mar 2021 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. Based on the feedback to this post (primarily thanks to edwinvlieg from HN), I decided to include a solution using Turbo as well. So the first example uses a regular AJAX with Rails UJS and then we change it to a Turbo Frame. Here's the simple demo: demo Rails UJS + Stimulus 2 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}
<% 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 %>
<%= form.label :body %> <%= form.text_area :body, placeholder: "Type a tweet", data: { "composer-target": "tweet", "action": "input->composer#preview" } %>
<%= 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" Rails.start() ActiveStorage.start() // Stimulus.js import { Application } from "stimulus" import { definitionsFromContext } from "stimulus/webpack-helpers" const application = Application.start() const context = require.context("./controllers", true, /\.js$/) application.load(definitionsFromContext(context)) 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() { this.preview() } preview() { var content = this.tweetTarget.value; var preview = this.outputTarget; Rails.ajax({ 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. If you didn't modify the initial method, you'd get the whole text previewed in bold. Turbo Frame + Stimulus 2 The basic idea of a Turbo Frame is that is will automatically load a remote HTML page or partial from a given URL: <%= 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 %>
<%= form.label :body %> <%= form.text_area :body, placeholder: "Type a tweet", data: { "composer-target": "tweet", "action": "input->composer#preview" } %>
<%= form.submit %>
<% end %> The form is the same except two changes. First, we change the regular div for the output to be the Turbo Frame: And we include the preview URL we'll need as part of the Stimulus value attribute:
The idea is to take this URL (it has to be an absolute URL), append the text from the text area, and update the url attribute of the Turbo Frame. Turbo Frame then reloads the preview automatically. On the backend, the preview method renders the full preview from a body param: ... def preview @preview = "#{params[:body]}" end ... But this time we have to include the frame as well, so I will create a proper preview.html.erb template: <%= raw @preview %> The Stimulus controller then has to take the value from the textarea and append it to the preview URL: import { Controller } from "stimulus" import Rails from "@rails/ujs" export default class extends Controller { static targets = [ "tweet", "output" ] static values = { previewUrl: String } connect() { this.preview() } preview() { let url = new URL(this.previewUrlValue); url.searchParams.append('body', this.tweetTarget.value); this.outputTarget.src = url.toString(); } } Turbo Frame does the the AJAX for us and all is well. And that's it. Two different approaches of how to start doing previews with Rails and Hotwire. There are even more ways how to achieve it, but I deliberately avoided depending on passing a model around. rails stimulus Any comments? Write me a direct message at @strzibnyj. [twitter] - SOON [cover_160] I am writing an introductory book on web application deployment. Networking, processes, systemd, backups, and all your usual suspects. Open -