Rails Wizards Pt. 3
8 min read | April 19 2021
- Part 1: What’s a Wizard and Why are We Here?
- Part 2: Choose Your Data Persistence
- Part 3: Choose Your URL Strategy
- Part 4: Model Validations
- Part 5: Routing and Controllers
- Part 6: Add Hotwire / Turbo!
- Part 7: Other Modifications / Options
- Part 8: Did We Do the Thing?
- Part 9: See it in Action!
We’ve got a data persistence strategy! Now we need a URL/routing strategy. We’ve got two main options here and, similarly to the persistence strategy, there are pros and cons either way. Either persistence strategy can work with either routing/URL strategy described below, so feel free to make the choice independent of your persistence model.
The high level concept behind this URL/routing strategy is that the user’s wizard data-persistence ‘space’ (whether a database record or a chunk of cache) will be uniquely identified by some kind of key, and we use that key directly in the routing for the wizard. Let’s say we’ve got a wizard within some kind of user-dashboard that allows the user to fill out the details of their car. We’ll say the first step of the wizard is called the
car_overview step and the user enters general vehicle details there. Make, model, trim, etc. Let’s also say step two is called the
car_histoy step and it’s where they add the mileage and maintenance history, etc. Let’s presume one of our users just began this workflow.
The ID/Key-in-URL strategy might look like this:
# User gets to their dashboard example.com/dashboard # ID/Key-in-URL with Database Persistence: # User fills out first step of Car ⏎ example.com/cars/48/steps/car_overview # User proceeds to second step in wizard ⏎ example.com/cars/48/steps/car_history # or # ID/Key-in-URL with Cache Persistence: # User fills out first step of Car ⏎ example.com/cars/m1rh99x/steps/car_overview # User proceeds to second step in wizard ⏎ example.com/cars/m1rh99x/steps/car_history
The above hypothesizes in the DB-Persistence model that the newly created DB record is routed by ID and has ID #48, so we simply use that ID as the route ‘key’, and that the Cache-Persistence model is using a random cache key as the ‘key’. I call this strategy “ID/Key-in-URL” since the ID or Cache Key is clearly in the URL / route 😛. We’ll get to the pros and cons after I introduce the other strategy.
Boiling this down to a fairly simple difference, the idea in this strategy is that for the user’s wizard data-persistence ‘space’ (whether a database record or a chunk of cache), our key for uniquely identifying that ‘space’ will be stored in the user’s session object rather than the routing/URL. Given the same example, and using a static prefix of
/build_car/ for the sake of namespacing, that would look something like this:
# User gets to their dashboard example.com/dashboard # ID/Key-in-Session with Database Persistence: # User fills out first step of Car ⏎ example.com/build_car/car_overview # User proceeds to second step in wizard ⏎ example.com/build_car/car_history # or # ID/Key-in-Session with Cache Persistence: # User fills out first step of Car ⏎ example.com/build_car/car_overview # User proceeds to second step in wizard ⏎ example.com/build_car/car_history
You may notice that those URLs/paths are the same for both type of data-persistence and you’d be right! The ID/Key-in-Session strategy obscures the underlying data persistence implementation a little better (by default) than the ID/Key-in-URL strategy. More on that in Part 7 😀.
Unsurprisingly, there are pros and cons on either side.
- (Pro) the path/routing tends to adhere to RESTful/resourceful routing more canonically — the object in its entirety would be available at
/cars/48and the additional
/steps/car_historycan be considered partial-views of the same single resource (or sub-resources if you prefer), but overall that resource is clearly and uniquely available at
/cars/48regardless of who’s asking for it
- (Pro) this resourceful style of routing can help us retain some default Rails functionality around resource-based controllers
- (Con) special care may need to be taken to avoid nosy-neighbors in some cases. Someone might open a new wizard instance, see ID 48 in the URL, and attempt to find out what shows up if they adjust it to 49 or 50. Without the correct precautions there, a rogue user could find out data on another user just by changing a URL (more in Part 7)
- (Pro) this style of routing allows users to have multiple wizards open in various tabs and work through them each independently, which always feels like a helpful feature to me
- (Con) Not everyone loves the resource-id-in-url style of URLs/routing, aesthetically speaking. It typically has four parts: the top-level resource (
/houses), the top-level resource ID (
5), the sub-level resource (
/steps), and the sub-level resource ID (
- (Pro) the URL doesn’t ever reveal the underlying object ID (the user never sees it at all)
- (Pro) since the resource identifier doesn’t have to be in the URL, the aesthetic URL path options are much more flexible
- (Con) the users won’t be able to walk through two different wizards in two different tabs since multiple tabs share a session
I should note here as well, neither is “more correct” than the other. As I’ve mentioned, there’s no uniformly perfect answer to wizards. You just need to choose the right options for the specific wizard you’re building. So give it some good thought and walk through how your users might use your wizard in your head then pick one of these strategies.
Back in my own context and problem-at-hand, we believe it’s unlikely for a single (public) user of the Agent Pronto wizard to want to fill out multiple forms at the same time across tabs given what our service is / does. We tend to also be pretty picky about our URL aesthetics, especially for our public-facing site and form. For those reasons, we’re going to go with ID/Key-in-Session.
Okay, time for the fun part, let’s start writing some code! → Part 4: Model Validations