Real-time previews with Rails and StimulusReflex03 Feb 2022
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:
If you’d like to try out the previewer in a production environment, you can try it out on Heroku. 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!
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 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.
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:
bin/dev. Head to 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:
Fill the new partial in with:
app/views/posts/new.html.erb to render the
preview partial beside the post form, like this:
app/views/posts/edit.html.erb with the same content:
Here, we could have created a new partial with the content we added to
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 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
After this generator runs, you will see that the generator created both a client-side Stimulus controller and a server-side reflex:
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.
Here, we inherit from
ApplicationController, the base StimulusReflex controller.
In the controller, we define 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
bodyPreview elements are updated with the current value of
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.
Here we added
data-controller="post" to the div wrapping the
To use a Stimulus controller, you must connect the controller to a DOM element with the
data-controller attribute. Stimulus scopes controllers 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.
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.
form partial next:
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
body inputs, the
PostController will run the
preview function to update the
This won’t work just yet though. Before it will, we need to set the
bodyPreview targets. Head to
app/views/posts/_preview.html.erb and update it:
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.
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
This reflex class 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:
In this reflex, we are using two 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.
The first option is through a
reflex data attribute on a DOM element. This approach 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 in a Stimulus controller.
stimulate is extremely flexible and allows us to override the
To call a reflex with
stimulate, head back to
Here, we removed the
targets from the controller and
preview now just calls
serializeForm option 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:
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:
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.
Now move over to the
form partial to connect the
post Stimulus controller to the form:
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
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
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
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
With this last change in place, head back to 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:
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 to parse markdown. To do so, We need to add redcarpet to our application.
From your terminal:
Restart your Rails application and head to
app/helpers/posts_helper.rb and add a method to parse markdown content:
This helper takes a string, initializes a new instance of
Redcarpet::Markdown as described in the documentation, and then renders the content to HTML.
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
preview partial first, at
When a user visits the
edit pages, markdown content in the body will be parsed and rendered as HTML.
PostReflex to use the new
edit now, type some markdown content in the
body input and see that as you type, the markdown is transformed into HTML instantly.
At this point, we have server-powered markdown parsing — nice work!
From your terminal:
Back to the
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
edit posts page, enter in a body with some markdown content and the `` tag and see that the tags are replaced as you type:
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:
And then update the
Post Stimulus controller:
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!
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?”
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.
The best resources for learning about Stimulus are the official handbook and reference. The official documentation does not cover more advanced use cases and best practices. For that, BetterStimulus is a great starting point.
To learn more about StimulusReflex, the documentation is exceptional, and is the best place to start your journey. The StimulusReflex discord is also a wonderful resource full of kind and helpful folks.
That’s all for today. As always, thank you for reading!