So far in our journey to create a server-side multi-step form in a Rails application, we have made two major strides. We have built a way to maintain state between steps through the SubscriptionCandidate
, and we have built an interface for receiving input and managing that state. Now we need a way of organizing our steps.
In order to do that, we need to decide the ideal order of our steps. Let’s start by gathering information specific to our watch subscription service, then move onto gathering the user’s personal information. With that in mind, the order of our steps will be as follows
1 | Style Preferences |
2 | Logistics |
3 | Address |
4 | Signup |
5 | Account Type |
6 | Payment |
That seems pretty straight-forward. So let’s add some complexity to it that requires some dynamic adaptation.
Let’s say after doing some research, we find that there are a good amount of people who are interested in our service, but are still skeptical of our ability to cater to their tastes. These potential users don’t want to risk paying a monthly fee for watches they won’t enjoy. To help convert these prospects into users (and eventually into paying customers), we decide to add a fourth type of account they can sign up for. Unlike our other three account types, this one is free. Instead of receiving a watch every month, they will receive an emailed with high-resolution photos and a detailed description of the watch we would have sent them based on their tastes. Hopefully, after a few months of tempting emails, they will decide to upgrade to a paid account.
Now the order of our steps is not so simple. If the user chooses a paid account in the Account Type
step, the next step needs to be the Payment
step. If they choose a free account, we don’t want to scare them off by requiring a credit card from them. So choosing a free account should complete the Subscription creation process completely.
Now that we have defined the order order of our steps, we need to build an object to manage that order. This object should have three main responsibilities:
- Determine what the current step is, and return the model for that step
- Determine what the previous step is
- Determine what the next step is
Because this is supervising the creation process, let’s call it our SubscriptionCreationProcess
. Let’s start by building it’s ability to determine the current step.
# /lib/subscription_creation_process.rb class SubscriptionCreationProcess def initialize(candidate, current_step_name = SubscriptionCreationStep::Style.step_name) @candidate = candidate @current_step = build_step_from_candidate(current_step_name) end attr_reader :candidate, :current_step private def build_step_from_candidate(current_step_name) step_klass = "SubscriptionCreationStep::#{current_step_name.camelize}".constantize step_klass.new(candidate) end end
Here we are initializing the supervisor object with access to the candidate. This will will help the supervisor build the current step, among other things. We are also initializing the object with the name of the current step. It will use this string to initialize the current step’s model. If no step name is given, it will assume the user is on first step in the process (the Style Preferences step).
Next, let’s add the ability to determine what the next step should be.
# /lib/subscription_creation_process.rb class SubscriptionCreationProcess COMPLETION_STEP_NAME = "complete" def initialize(candidate, current_step_name) @candidate = candidate @current_step = build_step_from_candidate(current_step_name) end attr_reader :candidate, :current_step def current_step_name current_step.step_name end def next_step_name next_step_sequence[current_step_name] end private def build_step_from_candidate(current_step_name) step_klass = "SubscriptionCreationStep::#{current_step_name.camelize}".constantize step_klass.new(candidate) end def next_step_sequence { SubscriptionCreationStep::Style.step_name => SubscriptionCreationStep::Logistics.step_name, SubscriptionCreationStep::Logistics.step_name => SubscriptionCreationStep::Address.step_name, SubscriptionCreationStep::Address.step_name => SubscriptionCreationStep::Signup.step_name, SubscriptionCreationStep::Signup.step_name => SubscriptionCreationStep::AccountType.step_name, SubscriptionCreationStep::AccountType.step_name => determine_step_after_account_type, SubscriptionCreationStep::Payment.step_name => COMPLETION_STEP_NAME } end def determine_step_after_account_type if candidate.account_type == Signup::FREE COMPLETION_STEP_NAME else SubscriptionCreationStep::Payment.step_name end end end
Here, we are adding a private hash called next_step_sequence
. Each key in this hash is the name of the current step, and it’s value is the name of the next step. This way, we can use the current step to check what the next step will be. If the next step is meant to be dynamic, instead of a static string, the value can be set to a method that is called. This method can then check the state of the candidate to determine the next step. In our case, it is checking to see if the account chosen by the user is a free account. If it is, the next step is a string that will signal the completion of the process. If it is a paid account, it will return the name of the Payment step.
We can employ a similar pattern to determine what the previous step should be.
# /lib/subscription_creation_process.rb class SubscriptionCreationProcess COMPLETION_STEP_NAME = "complete" def initialize(candidate, current_step_name) @candidate = candidate @current_step = build_step_from_candidate(current_step_name) end attr_reader :candidate, :current_step def current_step_name current_step.step_name end def next_step_name next_step_sequence[current_step_name] end def previous_step_name previous_step_sequence[current_step_name] end private def build_step_from_candidate(current_step_name) step_klass = "SubscriptionCreationStep::#{current_step_name.camelize}".constantize step_klass.new(candidate) end def next_step_sequence { SubscriptionCreationStep::Style.step_name => SubscriptionCreationStep::Logistics.step_name, SubscriptionCreationStep::Logistics.step_name => SubscriptionCreationStep::Address.step_name, SubscriptionCreationStep::Address.step_name => SubscriptionCreationStep::Signup.step_name, SubscriptionCreationStep::Signup.step_name => SubscriptionCreationStep::AccountType.step_name, SubscriptionCreationStep::AccountType.step_name => determine_step_after_account_type, SubscriptionCreationStep::Payment.step_name => COMPLETION_STEP_NAME } end def previous_step_sequence { SubscriptionCreationStep::Payment.step_name => SubscriptionCreationStep::AccountType.step_name SubscriptionCreationStep::AccountType.step_name => SubscriptionCreationStep::Signup.step_name, SubscriptionCreationStep::Signup.step_name => SubscriptionCreationStep::Address.step_name, SubscriptionCreationStep::Address.step_name => SubscriptionCreationStep::Logistics.step_name, SubscriptionCreationStep::Logistics.step_name => SubscriptionCreationStep::Style.step_name, } end def determine_step_after_account_type if candidate.account_type == Signup::FREE COMPLETION_STEP_NAME else SubscriptionCreationStep::Payment.step_name end end end
And with that, we have our steps all in order. The only thing left to do now is build a Rails controller to put it all together. We’ll tackle that in the next, and final, article.