Rails Wizards Pt. 1
Jon Sully
7 Minutes
What's a wizard and Why are We Here?
-
Part of the
‘Rails Wizards’
Series:
- • Apr 2021: Rails Wizards Pt. 1 (This page)
- • Apr 2021: Rails Wizards Pt. 2
- • Apr 2021: Rails Wizards Pt. 3
- • Apr 2021: Rails Wizards Pt. 4
- • Apr 2021: Rails Wizards Pt. 5
- • Apr 2021: Rails Wizards Pt. 6
- • Apr 2021: Rails Wizards Pt. 7
- • Apr 2021: Rails Wizards Pt. 8
- • Apr 2021: Rails Wizards Pt. 9
What’s a Wizard and Why are We Here?
Welcome, friends! Let’s jump right in 😀
A wizard (or as I sometimes refer to them, ‘a multi-step form’) is a user workflow pattern where we push users through a sequence of screens to fill out a large amount of data one chunk at a time. It’s the sort of thing where you click “Sign Up” on a site / app and they ask for your first + last name, then you click “Continue” and reach a new screen where they now ask for your address, then you click “continue” again and it asks for more data on the next screen etc. It’s also the entire basis of how TurboTax works 😆. Wizards exist because filling out fifty input fields on a single webpage is no fun and users (myself included) generally don’t prefer it.
This is of particular relevance to me with Agent Pronto as our initial workflow on the public site is indeed a wizard — in this case we’re walking the user through a (rather-beautiful) form to figure out what kind of property they’re looking to buy or sell and how we can help them. First they pick their location then what kind of home they’re looking for, etc. Each step gets its own unique screen that makes the process feel simpler for our users, and we all greatly prefer it.
One of the key notes around both our particular process at AP and this overall series is that we’re going to be talking about wizards that ultimately save down to one model, where each step of the wizard is made to fill out some subset of the attributes on that model. These approaches can be extended and adapted for multi-model wizards, but I’m going to leave that as an exercise for the reader 😉.
Wizard History
Just touching on some background here, there are two major camps I’ve seen developers tend toward when designing wizard workflows: front-end wizards and back-end wizards.
When it comes to front-end wizards, the high level summary is that the multi-page form workflow is all Javascript based. As a user enters fields and hits ‘continue’, no request to the server is actually being made, Javascript is just batching all the values up in memory to be sent as one large POST
once the user has completed the workflow. This can have great benefits from the user-experience standpoint as the user isn’t left waiting for a server to respond with a fully new page just to get to the next step of a wizard, but it can come with its own complexities too — notably having to duplicate validation logic. When you have a front-end-only form batching up data it needs to validate each field along the way, but you also need to make sure your back-end validates all the fields in the final POST
anyway. Beyond that, a failure on the final POST
can make for some tricky Javascript to figure out which step to take the user back to.
On the other side, server-side wizards are built around persisting the partial-object values on the server. The idea here is that a user can fill out some portion of the form and reasonably expect that the server has retained that data if they refresh the page, leave for a while, or don’t complete the wizard in its entirety. In a pure server-side wizard, each step sends the form data back to the server and the server responds with a new page containing the next form step — each step is indeed a full page load. On the plus side, all of the data and model validations and workflow logic can (typically) be written exclusively in the application code for the server / back-end. Very DRY!
There’s a third category of hybrid wizards that use Javascript to some extent on the front end but persist data to the back-end behind the scenes on a per-step basis. Some might call this ‘fat client’ or otherwise (I’m pretty sure TurboTax lives here), but we’re going to ignore those today. They can get pretty complex and I don’t know if I believe there’s a lot of value to be gained over a pure server-side approach. At very least, they may be an unfriendly approach to a small team.
Jon’s Goal
At this point, given that my team is very small and I believe there is a strong path forward with it, this series is going to focus exclusively on the pure server-side wizard. I have high hopes for both the user experience and developer experience working with the wizard — especially with Hotwire having recently been released. Here’s a few things I definitely want to ‘get right’:
- I want the resulting code to be DRY. This is primarily to (hopefully) avoid bugs in the future as I inevitably forget how our wizards work and I need to add a new step somewhere in the middle 😅 (maintainability cost)
- I don’t want to break the RESTful interface of the main model controller; I want a user to be able to walk through the form in the browser and a JSON client submit a
POST
to the same place - I want the model validations to operate on a per-step basis while in the wizard but universally otherwise
- This is mostly so that users can get immediate feedback for incorrect values mid-Wizard but at the same time, API users can get correct feedback on all fields when submitting a JSON
POST
- This is mostly so that users can get immediate feedback for incorrect values mid-Wizard but at the same time, API users can get correct feedback on all fields when submitting a JSON
- I’d like to avoid having a separate Rails controller for each step of the wizard — this is also in the realm of long-term maintenance and trying to minimize code duplication
- Ideally, I want my model to be opaque to the fact that it can be incrementally built with a wizard as much as possible
- I know that when I use this model outside of my wizard controllers, perhaps in a background job or etc., I want to be able to call
ruby>Model.last
orruby>Model.find_by(foo: :bar)
just the same as any other model and not have to worry about partial or incomplete records showing up in the results
- I know that when I use this model outside of my wizard controllers, perhaps in a background job or etc., I want to be able to call
- I want this workflow to prioritize the user experience and graceful user flow, carefully managing these questions:
- Can the user stop the wizard now and resume again in the future? (Not losing user-submitted data)
- Can the user work through two difference instances of the wizard at the same time in two different browser tabs?
- Can the user jump from one step to another relatively easily? (skipping linear steps)
These all feel like important bits of functionality for me. I’m not sure if we can check all of the boxes, but hopefully we get close!
Here’s how this series is laid out:
- Part 1: What’s a Wizard and Why are We Here?
- You’re reading it! Hopefully now you have a good sense of why this series exists and some of my hopes / goals for what’s to come
- Part 2: Choose Your Data Persistence
- I want to take a step before really getting into code to chat through how (and where) data-persistence might happen with our wizards, including the pros and cons of any given route. You’ll choose which works best for you!
- Part 3: Choose Your URL Strategy
- Similarly, we’ll want to choose our routing / URL strategy ahead of time to help drive some of the wizard functionality before we start writing the code for it
- Part 4: Model Validations
- One of the keys to a good wizard is executing the right validations at the right time(s); let’s see if we can do that
- Part 5: Controller(s?)
- The last piece of the puzzle in getting our initial wizard(s) up and running correctly 🎉
- Part 6: Add Hotwire / Turbo!
- This is the part where we take a solidly built wizard and make it a super fluid UX akin to a javascript-only app ✨
- Part 7: Other Modifications / Options
- Here we’ll talk through a number of other modifications and/or considerations for how we might customize the wizard(s) to better fit our particular needs
- Part 8: Did We Do the Thing?
- A review of what we ended up with in context of the goals above
- Part 9: See it in Action!
- I’ve built each variant and combination of tools we cover in this series and they’re available on the public web for you to try out! Go ahead, play around!
Let’s walk this out → Part 2