Implementing wizards with StimulusReflex

27 Apr 2020

Since a little over a year ago, an interesting new category of development techniques emerged in the web technology space. I’m talking about Phoenix LiveView inspired libraries, which provide rich real-time interactivity without (much) client side code.

The basic idea is to keep a websocket connection open for every visitor, send events down the wire where you can react to it on the server side. Afterwards, a full “new” page gets rendered, the result goes back to the client and will then be merged with the current page via a pretty nifty library called morphdom. That might sound complicated, but it really isn’t. From a developer point of view, you won’t even notice how it works. It just does. And it’s plenty fast too.

In the Ruby on Rails world, there is StimulusReflex that does this, which integrates seemless with Stimulus and ActionCable. After some recent efforts to make it compatible with AnyCable, it has become a viable way to build features without worring too much about scaling issues. After building a live search with it in a past project, I recently needed to implement a step wizard to partially fill out a form. Building this kind of feature with stateless HTTP and a state machine gem like Wicked can be daunting to implement. You can build it with frontend stuff like React or Vue too, but you have to take additional care if you want to make it “browser reload safe”.

But as I learned, this is straightforward to create with StimulusReflex. Even the persistence part is quite easy. The current recommendation is to just piggyback on the Rails cache system for anything that needs to be stored (which normally is plenty fast, if you use Redis).

To illustrate, here is a basic Rails form, divided with a @step variable and wired up to talk with a Stimulus controller.

<%= form_with(model: @post, class: "wizard", data: { controller: "wizard" }) do |f| %>
  <% if @step == :title %>
    <%= f.text_field :title, placeholder: "Title", data: { target: "wizard.title" } %>

    <button data-action="click->wizard#next">Next</button>
  <% end %>

  <% if @step == :body %>
    <%= f.text_area :body, placeholder: "Body", data: { target: "wizard.body" } %>

    <button data-action="click->wizard#previous">Previous</button>
    <button data-action="click->wizard#next">Next</button>
  <% end %>

  <% if @step == :submit %>

    <div class="wizard-preview">
      <h2><%= @post.title %></h2>
      <p><%= @post.body %></p>
    </div>

    <%= f.hidden_field :title %>
    <%= f.hidden_field :body %>

    <button data-action="click->wizard#previous">Previous</button>
    <%= f.button "Save" %>
  <% end %>
<% end %>

The controller doesn’t do a lot, it sets the usual initial values:

class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

  def new
    @post ||= Post.persistent(session.id)
    @step ||= Rails.cache.fetch("step:#{session.id}") { :title }
  end

  def create
    @post = Post.create(post_params)
    Post.clear_cache(session.id)

    redirect_to @post
  end

  def show
    @post = Post.find(params[:id])
  end

  protected

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

The only unsual thing above is Post.persistent. Its basically a Post.new, but prefilled with the values from the cache. So the model looks like this:

class Post < ApplicationRecord
  def self.persistent(id, values = {})
    Rails.cache.write("title:#{id}", values["title"]) if values["title"]
    Rails.cache.write("body:#{id}", values["body"]) if values["body"]

    new(
      title: Rails.cache.read("title:#{id}"),
      body: Rails.cache.read("body:#{id}")
    )
  end

  def self.clear_cache(id)
    Rails.cache.delete("step:#{id}")
    Rails.cache.delete("title:#{id}")
    Rails.cache.delete("body:#{id}")
  end
end

Before we get to the Reflex itself, here is the Stimulus controller that sends everything down the wire:

import ApplicationController from "./application_controller";

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

  next(event) {
    event.preventDefault();
    this.stimulate("WizardReflex#next", this.postValues());
  }

  previous(event) {
    event.preventDefault();
    this.stimulate("WizardReflex#previous", this.postValues());
  }

  postValues() {
    return {
      title: this.hasTitleTarget && this.titleTarget.value,
      body: this.hasBodyTarget && this.bodyTarget.value,
    };
  }
}

And finally, the Reflex itself:

class WizardReflex < ApplicationReflex
  STEPS = [:title, :body, :submit]

  def next(values)
    @post = Post.persistent(session.id, values)
    @step = STEPS[STEPS.find_index(read_step) + 1]

    write_step @step
  end

  def previous(values)
    @post = Post.persistent(session.id, values)
    @step = STEPS[STEPS.find_index(read_step) - 1]

    write_step @step
  end

  protected

  def read_step
    Rails.cache.read("step:#{session.id}")
  end

  def write_step(step)
    Rails.cache.write("step:#{session.id}", step)
  end
end

Yep, nothing more. Just a tiny pseudo state machine that sets the @step instance variable needed by the template above. You can have as many steps as you like, the only “complexity” will be in the template of the form itself.

I’ve uploaded an example repository on Github if you want to play around with this technique. Just follow the setup instructions in the README. Don’t skip the bundle exec rails dev:cache part, otherwise it want work in development mode for obvious reasons.


Layout refresh »