Real-time previews with Rails and StimulusReflex
Tags: rails, ruby, stimulus, stimulusreflex
Published: 2022-02-04 00:10:51

Today we are going to build a live, server-rendered, liquid-enabled markdown previewer with Ruby on Rails and StimulusReflex. It’ll be pretty neat.

Allowing users to preview their content before saving it is a common need in web applications — posts, products, emails. Any user-created content that gets turned into HTML can benefit from a preview function to help users check their work before they save it.

Our StimulusReflex-powered previewer will parse user-generated markdown on the server, insert dynamic content with liquid, and update the DOM in ~100ms, fast enough that the preview feels instant.

When our work is done, the end product will look like this:

![A screen recording of a user typing into a form on a web page. As they type, what they type is displayed in a box beside the form input, with markdown formatting applied.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ntqlvqrm3dlz1wnsysb6.gif)

If you’d like to try out the previewer in a production environment, you can [try it out on Heroku](https://post-previewer-md.herokuapp.com/). The demo application is hosted on Heroku’s free tier so expect a delay on the first page load!

Before diving in, this tutorial assumes a basic level of Rails knowledge. If you have never written Rails before, this tutorial may not be for you. You do not need any prior experience with Stimulus or StimulusReflex to follow along — in fact, this tutorial will be most useful to you if you are new to StimulusReflex and curious about how it can help you build great experiences faster.

Let’s get started!

## Application setup
We are working from a base Rails 7 application with TailwindCSS and StimulusReflex installed. To follow along with this tutorial, start by cloning [the starter repo](https://github.com/DavidColby/stimulus_reflex_previews) so we can skip past the install steps and get to build the application. All of the code changes we will make in this tutorial can be found in [this pull request](https://github.com/DavidColby/stimulus_reflex_previews/pull/1).

Our application will allow users to create and edit `Posts`. As they modify a post, the preview display beside the post will update. Before we can preview posts, we’ll need a `Post` resource to work with, so let’s begin by scaffolding that resource up.

From your terminal:

```bash
rails g scaffold Post title:string body:text
rails db:migrate
```

After you scaffold the resource, start up the rails application and build CSS and JavaScript with `bin/dev`. Head to [http://localhost:3000/posts](http://localhost:3000/posts) and make sure that creating and editing posts works before moving on.

As a reminder, our end goal is a live, updated-as-you-type preview displayed beside the post form. We’ll construct the UI first. Create a new `preview` partial from your terminal:

```bash
touch app/views/posts/_preview.html.erb
```

Fill the new partial in with:

```erb
<div class="bg-gray-200 p-4 shadow-sm rounded-sm">
  <article class="prose">
    <h2 class="text-2xl font-bold"><%= post.title %></h2>
    <div>
      <%= post.body %>
    </div>
  </article>
</div>
```

Update `app/views/posts/new.html.erb` to render the `preview` partial beside the post form, like this:

```erb
<div class="mx-auto md:w-2/3 w-full">
  <div class="flex space-x-12">
    <div class="w-1/2">
      <%= render "form", post: @post %>
    </div>
    <div class="w-1/2">
      <%= render "preview", post: @post %>
    </div>
  </div>
</div>
```

Now update `app/views/posts/edit.html.erb` with the same content:

```erb
<div class="mx-auto md:w-2/3 w-full">
  <div class="flex space-x-12">
    <div class="w-1/2">
      <%= render "form", post: @post %>
    </div>
    <div class="w-1/2">
      <%= render "preview", post: @post %>
    </div>
  </div>
</div>
```

Here, we could have created a new partial with the content we added to `new` and `edit`, but it is not strictly necessary. Feel free to move this content to a partial if you prefer.

Now we have preview content displayed beside the post form, but the preview content is static — visit [posts/new](http://localhost:3000/posts/new) and see that the "preview" is just an empty gray box. Not very useful yet.

In the next section, we will create a StimulusReflex-enabled Stimulus controller to update the preview content as the user types, making the gray preview box a little more useful.

## Adding a Stimulus controller
Our application will eventually rely on a server-side reflex to update the content of the preview partial that we created in the last section. However, before we do that, let’s build a purely client-side implementation of the preview function to ease into things.

From your terminal, use the `stimulus_reflex` generator:

```bash
rails g stimulus_reflex post
rails stimulus:manifest:update
```

After this generator runs, you will see that the generator created both a client-side Stimulus controller and a server-side reflex: `app/javascript/controllers/post_controller.js` and `app/reflexes/post_reflex.rb`.

On the client-side, StimulusReflex enhances vanilla Stimulus controllers. StimulusReflex-powered Stimulus controllers have all of the same functionality of a regular Stimulus controller, with extra functionality layered on top.

To demonstrate this, we are going to start by using the new Stimulus controller, `post_controller.js` to update the content of the post preview as the user types. We will not have any markdown formatting or liquid substitution, but the page will react to user input and we will get to see a few Stimulus concepts in action.

Head to `app/javascript/controllers/post_controller.js` and update it:

```javascript
import ApplicationController from './application_controller'

export default class extends ApplicationController {
  static targets = [ "title", "body", "titlePreview", "bodyPreview" ]

  preview() {
    this.titlePreviewTarget.innerText = this.titleTarget.value
    this.bodyPreviewTarget.innerText = this.bodyTarget.value
  }
}
```

Here, we inherit from `ApplicationController`, the base StimulusReflex controller.

In the controller, we define [targets](https://stimulus.hotwired.dev/reference/targets) that we will use to obtain an easy reference to the elements that we care about. We use these `targets` in the `preview` function. Each time `preview` is called, the `titlePreview` and `bodyPreview` elements are updated with the current value of `titleTarget` and `bodyTarget`, respectively.

For the most part, this is regular JavaScript. If we wanted to, we could remove the `targets` and instead select each element by an id, with something like `document.getElementById('body-target')`. Stimulus targets give us a more convenient method to access any number of elements by a name, but nothing magical is happening.

To use this new `preview` function, we need to connect the new `PostController` to the DOM and add `target` elements and `actions` to the preview and form partials.

In `new.html.erb`:

```erb
<div class="mx-auto md:w-2/3 w-full">
  <div class="flex space-x-12" data-controller="post">
    <div class="w-1/2">
      <%= render "form", post: @post %>
    </div>
    <div id="post-preview" class="w-1/2">
      <%= render "preview", post: @post %>
    </div>
  </div>
</div>
```

Here we added `data-controller="post"` to the div wrapping the `form` and `preview` partials.

To use a Stimulus controller, you must connect the controller to a DOM element with the `data-controller` attribute. Stimulus [scopes controllers](https://stimulus.hotwired.dev/reference/controllers#scopes) based on the DOM hierarchy — the element with the `data-controller` attribute and all of the children of that element will be within that controller’s scope. `targets` and `actions` for a controller must be within the scope to function.

This scoping mechanism allows developers to have multiple independent instances of the same controller present on the page at once, when needed.

Update the `form` partial next:

```erb
<%= form_with(model: post, class: "contents") do |form| %>
  <% if post.errors.any? %>
    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
        <% post.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :title %>
    <%= form.text_field(
      :title,
      class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full",
      data: {
        post_target: "title",
        action: "input->post#preview"
      } 
    ) %>
  </div>

  <div class="my-5">
    <%= form.label :body %>
    <%= form.text_area(
      :body,
      rows: 4,
      class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full",
      data: {
        post_target: "body",
        action: "input->post#preview"
      } 
    ) %>
  </div>

  <div class="inline">
    <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
<% end %>
```

The important updates here are on the two form inputs. On each, we added a `data-post-target` and a `data-action` attribute. The `post-target` attribute defines a target for the `PostController`, and the `action` attribute tells Stimulus to fire the `PostController's` preview function when an `input` event occurs on that input.

With this change in place, each time a user types a character in the `title` or `body` inputs, the `PostController` will run the `preview` function to update the `titlePreview` and `bodyPreview` elements.

This won’t work just yet though. Before it will, we need to set the `titlePreview` and `bodyPreview` targets. Head to `app/views/posts/_preview.html.erb` and update it:

```erb
<div class="bg-gray-200 p-4 shadow-sm rounded-sm">
  <article class="prose">
    <h2 class="text-2xl font-bold" data-post-target="titlePreview"><%= post.title %></h2>
    <div data-post-target="bodyPreview">
      <%= post.body %>
    </div>
  </article>
</div>
```

Notice the `data-post-target` attributes on the header and the body tags.

With this change in place, refresh the new post page, start typing and see that the content of the preview updates as you type.

![A screen recording of a user typing into a form on a web page. Everything they type is copied into a box beside the form.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ux0eqvy22b8f7kk5sr8r.gif)

This is a great start, but we aren’t really “previewing” anything because the Stimulus controller is not parsing markdown content or substituting liquid tags.

To add useful previews, we will use the server-side `PostReflex` that we generated at the beginning of this section.

## Server-side previews with StimulusReflex
Stimulus controllers do their work on the client-side, adding and removing elements, updating classes or attributes. Even when we are using StimulusReflex, this is still true — Stimulus controllers stay on the client-side. To add server-side functionality, we need to move to the `PostReflex`, at `app/reflexes/post_reflex.rb`.

This [reflex class](https://docs.stimulusreflex.com/rtfm/reflex-classes) will be responsible for transforming the content of the preview body from markdown to HTML, and for substituting any liquid tags present in the content. Once the content is transformed, StimulusReflex will send the transformed content back to the client, where it will be inserted seamlessly into the DOM, all in ~100ms.

Before adding markdown and liquid parsing, let’s move the client-side preview updates to the server-side reflex. Update `PostReflex` like this:

```ruby
class PostReflex < ApplicationReflex  
  def preview
    morph '#preview-title', post_params[:title]
    morph '#preview-body', post_params[:body]
  end

  private

  def post_params
    params.require(:post).permit(:title, :body)
  end
end
```

In this reflex, we are using two [selector morphs](https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs) to update the preview content, identifying the element to update with an `id`. Selector morphs allow us to run reflex actions without a full pass through a controller action with ActionDispatch, and are perfect for features that only update a small piece of the page, like our preview functionality.

`post_params` is identical to attribute whitelisting that you’ll find in a standard Rails controller — each time this reflex is called, the data from the post form will be serialized and sent to the server, giving us a handy way to reference the current content of the form.

In StimulusReflex, there are two ways to [call a server-side reflex](https://docs.stimulusreflex.com/rtfm/reflexes).

The first option is through a `reflex` data attribute on a DOM element.  [This approach](https://docs.stimulusreflex.com/rtfm/reflexes#declaring-a-reflex-in-html-with-data-attributes) completely bypasses the reflex’s related Stimulus controller and directly invokes the server-side reflex. This option works well when you have a simple reflex that does not need custom options to function.

Our use case requires us to use the second method for calling reflexes: using [this.stimulate](https://docs.stimulusreflex.com/rtfm/reflexes#calling-a-reflex-in-a-stimulus-controller) in a Stimulus controller. `stimulate` is [extremely flexible](https://docs.stimulusreflex.com/rtfm/reflexes#stimulate-is-extremely-flexible) and allows us to override the `element` passed to the server-side reflex, define options, and run any JavaScript we like before sending the reflex to the server.

To call a reflex with `stimulate`, head back to `app/javascript/controllers/post_controller.js` and update it like this:

```javascript
import ApplicationController from './application_controller'

export default class extends ApplicationController {
  connect() {
    super.connect()
  }

  preview() { 
    this.stimulate("Post#preview", { serializeForm: true })
  }
}
```

Here, we removed the `targets` from the controller and `preview` now just calls `this.stimulate`.

The `serializeForm` [option](https://docs.stimulusreflex.com/v/v3.5/rtfm/working-with-forms#stimulusreflex-form-processing) ensures the content of the closest `form` element in the DOM is passed to the reflex on the server. The data in the form is accessible on the server via `params`, as we saw in the `PostReflex` earlier in this section.

Now we have a reflex defined on the server, and a Stimulus controller ready to call that reflex. To make this work, we need to update the DOM to match the structure expected by the reflex.

Recall in the `preview` method in `PostReflex`, we have two `morphs` that rely on ids to target the right element in the DOM:

```ruby
def preview
  morph '#preview-title', post_params[:title]
  morph '#preview-body', post_params[:body]
end
```

For the reflex to work, we need an element with a `preview-title` id and another with a `preview-body` id. Update the preview partial like this:

```erb
<div class="bg-gray-200 p-4 shadow-sm rounded-sm">
  <article class="prose">
    <h2 id="preview-title" class="text-2xl font-bold"><%= post.title %></h2>
    <div id="preview-body">
      <%= post.body %>
    </div>
  </article>
</div>
```

Now both the title element and the body element have ids that match the expectations of the reflex, so when the reflex runs, those elements will be updated.

Move over to the `form` partial to connect the `post` Stimulus controller to the form:

```erb
<%= form_with(model: post, class: "contents", data: { controller: "post" }) do |form| %>
  <% if post.errors.any? %>
    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
        <% post.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :title %>
    <%= form.text_field(
      :title,
      class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full",
      data: {
        action: "input->post#preview"
      } 
    ) %>
  </div>

  <div class="my-5">
    <%= form.label :body %>
    <%= form.text_area(
      :body,
      rows: 4,
      class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full",
      data: {
        action: "input->post#preview"
      } 
    ) %>
  </div>

  <div class="inline">
    <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
<% end %>
```

Here, notice the `form` element now has a `data-controller="post"` attribute, scoping the Stimulus controller to the form. We also updated the form inputs to remove the unnecessary `data-post-target` attributes. 

Because we are serializing the entire content of the form, we no longer need a direct reference to the individual input elements in the Stimulus controller, so we do not need the target attributes.

One last step to move the preview functionality to the server. Because we connected the `PostController` to the form, we need to remove the controller from the wrapper div in `app/view/posts/new.html.erb`:

```erb
<div class="mx-auto md:w-2/3 w-full">
  <div class="flex space-x-12">
    <div class="w-1/2">
      <%= render "form", post: @post %>
    </div>
    <div class="w-1/2">
      <%= render "preview", post: @post %>
    </div>
  </div>
</div>
```

Previously, we needed a direct reference in the Stimulus controller to the preview elements, so we needed both the form and preview partials to be within the scope of the `PostController`.

Because the preview elements are now updated in the server-side reflex and referenced by id, the preview partial no longer needs to be in the `PostController’s` scope.

With this last change in place, head back to [http://localhost:3000/posts/new](http://localhost:3000/posts/new), start typing and see that the preview content updates each time the form changes. To confirm that StimulusReflex is doing the work on the server, watch the Rails server logs as you type. With each key press, the server logs will output information about the reflex:

![A screen recording of a user typing into a form on a web page with a terminal window display server logs below the browser. As the user types, the characters they type are copied into a preview box and the server logs indicate that the StimulusReflex post#preview action has run.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5jlupr8203av9bx9t518.gif)

Now we are “previewing” content as the user types, but that preview still is not doing anything useful. We just added a round trip to the server, but the server is not doing anything useful with the content yet.

In the next section, we will make the server-side reflex more valuable by adding markdown and liquid tag parsing when the preview content is updated.

## Add markdown and liquid parsing
We will use [redcarpet](https://github.com/vmg/redcarpet) to parse markdown. To do so, We need to add redcarpet to our application.

From your terminal:

```bash
bundle add redcarpet
```

Restart your Rails application and head to `app/helpers/posts_helper.rb` and add a method to parse markdown content:

```ruby
module PostsHelper
  def to_markdown(content)
    return '' if content.blank?

    markdown = Redcarpet::Markdown.new(
      Redcarpet::Render::HTML,
      fenced_code_blocks: true,
      autolink: true
    )
    markdown.render(content).html_safe
  end
end
```

This helper takes a string, initializes a new instance of `Redcarpet::Markdown` as described in the [documentation](https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use), and then renders the content to HTML.

The `to_markdown` helper will be used in two places in our application: When the `preview` partial is loaded during a normal request (like when visiting the post edit page) and when parsing content on the fly in the `PostReflex`.

Update the `preview` partial first, at `app/views/posts/_preview.html.erb`:

```erb
<div class="bg-gray-200 p-4 shadow-sm rounded-sm">
  <article class="prose">
    <h2 class="text-2xl font-bold" id="preview-title">
      <%= post.title %>
    </h2>
    <div id="preview-body">
      <%= to_markdown(post.body) %>
    </div>
  </article>
</div>
```

When a user visits the `new` or `edit` pages, markdown content in the body will be parsed and rendered as HTML.

Update the `PostReflex` to use the new `to_markdown` helper:

```ruby
def preview
  morph '#preview-title', post_params[:title]
  morph '#preview-body', ApplicationController.helpers.to_markdown(post_params[:body])
end
```

Head to `new` or `edit` now, type some markdown content in the `body` input and see that as you type, the markdown is transformed into HTML instantly.

![A screen recording of a user typing into a form on a web page. As they type, what they type is displayed in a box beside the form input, with markdown formatting applied.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kg5bo00yutbnlgzb1uxg.gif)

At this point, we have server-powered markdown parsing — nice work!

Our next step is adding [liquid](https://github.com/Shopify/liquid) support, allowing users to transform [liquid content](https://shopify.github.io/liquid/basics/introduction/) into real data and see that content processed in real time.

From your terminal:

```bash
bundle add liquid
```

Back to the `PostsHelper`:

```ruby
module PostsHelper
  def to_markdown(content)
    return '' if content.blank?

    markdown = Redcarpet::Markdown.new(
      Redcarpet::Render::HTML,
      fenced_code_blocks: true,
      autolink: true
    )
    markdown.render(liquified(content)).html_safe
  end

  def liquified(content)
    Liquid::Template
      .parse(content)
      .render('company_name' => 'Hotwiring Rails')
  end
end
```

`liquified` takes a string of `content`, scans it for matching liquid tags and objects, and translates them.

Our example implementation mocks up a simple `company_name` tag — in a real application we could use liquid tags to insert data about the object we are working on.

Restart your Rails application again and then go back to the `new` or `edit `posts page, enter in a body with some markdown content and the `{{ company_name }}` tag and see that the tag is replaced as you type:

![A screen recording of a user typing into a form on a web page. As they type, what they type is displayed in a box beside the form input, with markdown formatting applied.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufiw01zlrs7pddetpbi8.gif)

## Debouncing requests
Right now, every keystroke triggers a round trip to the server. Many of these requests are unnecessary because the user will have typed another character before the request completes. A simple way to reduce the load on the server is to debounce `preview` function, waiting for the user to pause before triggering the server-side reflex.

To debounce the `preview` function, we will use lodash’s `debounce`. From your terminal:

```
yarn add lodash
```

And then update the `Post` Stimulus controller:

```javascript
import ApplicationController from './application_controller'
import debounce from "lodash/debounce"

export default class extends ApplicationController {
  connect() {
    super.connect()
    this.preview = debounce(this.preview.bind(this), 50)
  }

  preview() { 
    this.stimulate("Post#preview", { serializeForm: true })
  }
}
```

Now, `preview` will wait 50ms before firing. Feel free to play around with the wait period to find the time that feels right to you.

And with that change, you have reached the end of this tutorial, great work today!

## Wrapping up
Today we built a fully functional, StimulusReflex-powered markdown and liquid tag parser. Our application processes content on the server and returns it to the client without page turns, saving records in the database, or dealing with the overhead of a Rails controller action.

While building this application, we learned a bit about how to use Stimulus and StimulusReflex in Rails application and applied some basic techniques of both to create a real-time experience while staying close to core Rails principles.

Before using something like what we built today in production, a couple of things to think about:

### Why not just parse things on the client?
Throughout this tutorial, you may have wondered “Why don’t we just parse the markdown [on the client](https://github.com/markedjs/marked)?”

Liquid processing was introduced to give us a reason to parse content like this on the server. We are here to learn about Stimulus and StimulusReflex, so I fit the requirements to that goal.

If your application just needs markdown parsing without the extra complications of liquid, a client-side parser is a completely reasonable (and more performant) choice!

### Do you need real-time previews?
The live preview we built is useful for learning and showing off what StimulusReflex can do, but it may not be the most desirable user experience in a real application. The live preview experience works fine for some use cases, particularly when the content is only a few paragraphs in length.

As the content gets longer, a better approach is moving the “preview” content into a separate tab, hidden by default. As the user types, update the content in the hidden tab as usual, but keep the content hidden until the user requests it. That experience is likely to scale a bit better, while not being any more technically complex than the experience we built.

### Further reading
The best resources for learning about Stimulus are the official [handbook](https://stimulus.hotwired.dev/handbook/introduction) and [reference](https://stimulus.hotwired.dev/reference/controllers). The official documentation does not cover more advanced use cases and best practices. For that, [BetterStimulus](https://www.betterstimulus.com/) is a great starting point.

To learn more about StimulusReflex, [the documentation](https://docs.stimulusreflex.com/) is exceptional, and is the best place to start your journey. The [StimulusReflex discord](https://discord.gg/stimulus-reflex) is also a wonderful resource full of kind and helpful folks.

That’s all for today. As always, thank you for reading!

PS: If you enjoyed this post, you might enjoy my monthly newsletter on the latest in modern Rails development, [Hotwiring Rails](https://www.getrevue.co/profile/hotwiringrails).