Rails Wizards Pt. 8

Series Breakdown
  1. Part 1: What’s a Wizard and Why are We Here?
  2. Part 2: Choose Your Data Persistence
  3. Part 3: Choose Your URL Strategy
  4. Part 4: Model Validations
  5. Part 5: Routing and Controllers
  6. Part 6: Add Hotwire / Turbo!
  7. Part 7: Other Modifications / Options
  8. Part 8: Did We Do the Thing?
  9. Part 9: See it in Action!

Did We Do The Thing?

I wanted to take a moment to circle back on “Jon’s Goal” from Part 1 and really see if we’ve accomplished some of my desires for how a wizard in Rails ought to work. I think we got some things right, some things close, and some things we probably just forgot about 🙂. Let’s see what we had:

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)

Luckily I think we did a good job here. There are real maintainability gains from only have one top-level (mostly-stock) controller and one sub-level controller that coordinates all the steps throughout the wizard.

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

We got this one down! The top-level controller definitely still implements a standard RESTful interface and non-HTML users would be none-the-wiser that there’s a wizard at all!

I want the model validations to operate on a per-step basis while in the wizard but universally otherwise

Pretty happy on this one — our required_for_step? method in our Model that allows the validations to be applied conditionally (but cleanly) is pretty slick.

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 Model.last or 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

Giving this one a… sort of! When going with the cache-persistence route this functionality is automatic. When going with a database-persistence workflow, we need to add a default_scope as discussed in Part 7 but it is totally doable.

Well, did we?

I’m going to say yes, overall. If it was a no, I would’ve wasted a lot of time on writing this series, but I do genuinely think we created a pretty great framework here. As I mentioned in Part 1, there’s simply no “one-size-fits-all” / canonical answer for “how to Rails Wizard”, but there are a few different workflow patterns you can choose from to (hopefully) meet your needs. Put differently, there’s no singular answer but there are, perhaps, “rails” one can follow toward a solid framework 😏. I wrote this whole series just to make that joke.

Well, let’s finish it out with some working examples! Check ‘em out → Part 9: See it in Action!

Header Photo / Banner by: @kellysikkema

Join the Conversation

Latest Blog Posts