Stimulus & Rails Remote Forms

By Dan Bridges

Dan Bridges
· 3 min read

In this article I want to demonstrate how to integrate existing Rails unobtrusive javascript (rails-ujs) patterns with the new Stimulus javascript framework by Basecamp. For illustration we’ll be implementing a simple comments page. This page will contain a list of comments and a form at the bottom to add new comments. When a new comment is added we want to append the comment to the list and clear the form.

Typically the way to implement behavior like this in Rails is to create a form with theremote: true option, then after submit render a server generated javascript response (SJR) to append the new comment and reset the message form value. For those unfamiliar, the general approach to SJR in Rails is outlined by DHH here.

The SJR way of updating the page. Simple enough, but not very extendable.

Using SJRs to add sprinkles of interactivity has a low barrier to entry, and is often a great solution. But once the complexity of the action increases, SJRs quickly become bloated with conditional logic. For instance, you might want to perform different updates to the frontend depending on the page that you are creating the comment from, or maybe you have multiple comment areas on a single page that need to be updated. All of a sudden your SJR is not two or three lines, but a mess of complex code handling all of these different scenarios. When things get this complex we need a framework to organize the frontend logic—enter Stimulus.

For those that know a bit of Stimulus, or have read the Stimulus Handbook, the obvious way to do this is to render the textarea and a button, attach a click event action to the submit button, and finally initiate an ajax call from within your Stimulus controller using jQuery or fetchto grab the new comment html to be appended. This seems reasonable, but it also means you lose all of the awesome ujs goodness that makes ajax so easy in Rails. Wouldn’t it be nice to still use a remote form and offload the ajax call to rails-ujs like we normally do?

Fortunately, leveraging rails-ujs together with Stimulus is actually fairly straightforward! The two keys to it are (i) we need to specify the remote form to have html datatype, and (ii) specify a Stimulus action for the ajax:success event. Time for some code:

Our comments page which renders a list of comments and a form to add new comments.

Our comment list is fairly straightforward. We add the Stimulus data-controller attribute to the container, as well as a data-target to the list itself. We use form_with which defaults to remote: true. In our data attributes on the form we specify type: "html" and action: "ajax:success->comment-list#onPostSuccess. This connects the form submission ajax request performed by rails-ujs to our Stimulus controller action.

In our Rails controller we just render the newly created comment, returning html instead of an SJR:

Our Stimulus controller handles updating the DOM and clearing the form:

Now all of our UI logic is handled by Stimulus, while our backend just provides the rendered html. We can easily add multiple comment areas in a single page without adding additional data attributes to identify them, which would be required if the update was handled by an SJR. Additionally, if the comment itself contained interactive javascript, we don’t have to initialize it as we would in an SJR. The comment logic would be self contained in another Stimulus controller, and Stimulus would take care of the initialization as soon as we add it to the DOM.

Using this approach we retain the ease of using remote forms in Rails, while also leveraging Stimulus to better organize our associated javascript code.

Update 10/19/2018

A reader asked how you would handle errors in this setup. You would use the same exact approach with an additional Stimulus action ajax:error->comment-list#onPostError. Your Rails controller would return the desired errors to be rendered as html, and your Stimulus controller would render those errors. A complete gist detailing this is shown below: