Wizards are a typical element in quite a lot of purposes. Both for signing up new customers, creating merchandise, buying objects and plenty of extra.
They are often difficult to handle as soon as they get greater and extra advanced. At Getaround, we’ve got a number of wizards which don’t share their structure. A typical structure can’t match each use case with completely different wants, movement, and person expertise.
To record new vehicles on our platform, hosts present a number of items of knowledge on the car, themselves, and their wants. We could ask for extra data or skip some steps. Such constraints result in complexity and problem in dealing with and testing each variation.
After a number of iterations, we ended up with a modular structure that was much less strict than a choice tree and allowed us to design wizards with advanced or easy logic.
On this article, I’ll attempt to information you thru constructing such a modular structure. We’ll use a type object for every step and a Supervisor
to orchestrate all the pieces.
Type object interface
ActiveModel
offers handy modules to create customized type objects and manipulate attributes. We’re going to use the next modules:
Utilizing these modules will even enable us to make use of Rails type helpers as if the manipulated object had been an precise mannequin. Many because of Intrepidd for sharing code that led to this base type.
Our BaseForm
would then seem like this:
require "active_model"
class BaseForm
embrace ActiveModel::Mannequin
embrace ActiveModel::Attributes
embrace ActiveModel::Validations
attribute :automobile
def full?
increase NotImplementedError
finish
def next_step
increase NotImplementedError
finish
def submit(params)
params.every { |key, worth| public_send("#{key}=", worth) }
carry out if legitimate?
finish
personal
def carry out
increase NotImplementedError
finish
finish
Let’s take the time to elucidate this code. First, we’re declaring attribute :automobile
as a result of our type objects shall be initialized with a Automobile
mannequin. This object would be the supply of reality, the one we’ll fill with new knowledge and depend on to find out what’s lacking from it.
full?
would be the methodology referred to as to know if a step has been efficiently accomplished. On this methodology we are able to for instance test if a selected attribute has been stuffed in on our automobile file.
next_step
handles the logic to compute what the following step shall be. A step is aware of what the following one is as a result of it should depend on what was submitted beforehand.
submit
is the strategy to submit our params from the related type. It’ll solely name carry out
, presupposed to be applied on every type object, if all validations handed.
With this public interface, we are able to create as many steps as we would like and they’ll create the movement by themselves utilizing carry out
to avoid wasting knowledge, after which full?
and next_step
to deal with going from one step to a different.
Supervisor
Having steps dealing with themselves is nice, however we nonetheless want some logic to provoke the wizard and decide which step the person is presently on.
Our Supervisor
object will deal with this logic. It can also handle having obtainable steps, and non-available steps. For example, if we’ve got steps A
, B
and C
, with step A
already being submitted. The following step to submit is B
, however I ought to be allowed to entry step A
once more if I wish to right what I submitted. Step C
just isn’t accessible so long as step B
just isn’t full, it shouldn’t even be seen to the person.
Our Supervisor
might then seem like this:
class Supervisor
FIRST_STEP = :nation
STEP_FORMS = {
nation: CountryForm,
insurance_provider: InsuranceProviderForm,
mileage: MileageForm,
}.freeze
def initialize(automobile:)
@automobile = automobile
@instantiated_forms = {}
@possible_steps = compute_possible_steps
finish
def current_step
@possible_steps.discover do |step|
return nil if step.nil?
type = find_or_instantiate_form_for(step)
!type.full?
finish
finish
def form_for(step)
STEP_FORMS.fetch(step)
finish
personal
def compute_possible_steps
steps = []
steps_path(FIRST_STEP, steps)
steps
finish
def steps_path(starting_step, steps_acc)
steps_acc.push(starting_step)
return if starting_step.nil?
type = find_or_instantiate_form_for(starting_step)
steps_path(type.next_step, steps_acc) if type.full?
finish
def find_or_instantiate_form_for(step)
@instantiated_forms.fetch(step) do
form_for(step)
.new(automobile: @automobile)
.faucet @instantiated_forms[step] = type
finish
finish
finish
Each step is asserted with its related type object. At initialization, all attainable steps are computed utilizing the general public full?
, calculating one step after one other with next_step
. The tactic form_for
will enable the controller to govern the precise type object from the supervisor.
As we assist a number of flows, the final step is probably not the final one outlined within the record. We then count on next_step
to return nil
when there’s no step left.
Steps
Within the supervisor, I discussed three steps, nation
, insurance_provider
and mileage
. Let’s construct them and see how with solely 3 steps we are able to have already got a number of flows.
Nation
This step will merely save the chosen nation on the automobile file. Nonetheless, its subsequent step will rely on what nation was chosen.
class CountryForm < BaseForm
ALLOWED_COUNTRIES = %w[CA ES PK JP].freeze
COUNTRY_REQUIRING_INSURANCE_PROVIDER = %w[ES].freeze
attribute :nation, :string
validates_inclusion_of :nation, in: ALLOWED_COUNTRIES
def carry out
automobile.replace!(nation: nation)
finish
def full?
!automobile.nation.nil?
finish
def next_step
if COUNTRY_REQUIRING_INSURANCE_PROVIDER.embrace?(automobile.nation)
:insurance_provider
else
:mileage
finish
finish
finish
First we are able to see how readable the attributes and validations are because of ActiveModel
. nation
is a string attribute and we count on it to be one of many allowed international locations, outlined in a relentless. The carry out
methodology will solely be referred to as if the necessities are met, if legitimate?
returns true
.
full?
solely checks if nation
has been efficiently saved on the automobile file.
Lastly, next_step
depends upon the nation chosen. If an insurance coverage supplier is required within the nation, then we’ll want the person to offer this knowledge. If not, we determine to go on to the following one, mileage
.
In fact we might enhance issues right here, particularly in carry out
. We in all probability don’t wish to use replace!
which raises when it fails, however we nonetheless wish to make sure what’s inside this methodology is correctly executed. For the sake of simplicity I didn’t add such a logic right here, however we are able to simply play with errors
obtainable because of ActiveModel::Validations
.
Insurance coverage supplier
Nothing specific to say about this one, besides that it will likely be displayed provided that the automobile’s nation requires an insurance coverage supplier.
The following step is outlined as mileage
, however from right here we might think about one other department within the choice tree, a number of flows, a number of potentialities.
class InsuranceProviderForm < BaseForm
attribute :insurance_provider, :string
def carry out
automobile.replace!(insurance_provider: insurance_provider)
finish
def full?
!automobile.insurance_provider.nil?
finish
def next_step
:mileage
finish
finish
Mileage
In our instance, mileage
is the final step. As soon as a mileage integer is submitted, validated and saved, there’s no different step to go to. next_step
returns nil
to announce that the wizard is completed.
class MileageForm < BaseForm
attribute :mileage, :integer
validates :mileage, numericality: { greater_than: 0 }
def carry out
automobile.replace!(mileage: mileage)
finish
def full?
!automobile.mileage.nil?
finish
def next_step
nil
finish
finish
Controller and routes
We solely want two routes to assist this wizard: present
and replace
. present
will show the step to the person whereas replace
will deal with the step submission.
# config/routes.rb
assets :car_wizards, solely: %i[show update]
On the controller aspect, we’re presupposed to let all of the logic come from the supervisor and solely deal with rendering, type submission and redirections.
class CarWizardController < ApplicationController
before_action :initialize_variables, solely: %i[show update]
def present
if @step == :present
redirect_to car_wizard_path(@automobile, @step_manager.current_step)
elsif @step_manager.possible_step?(@step)
set_form
render @step
else
render "errors/not_found", standing: :not_found
finish
finish
def replace
if !@step_manager.possible_step?(@step)
return render("errors/not_found", standing: :not_found)
finish
set_form
if @type.submit(params[:car])
redirect_to car_wizard_path(@automobile, @type.next_step)
else
render @step
finish
finish
personal
def initialize_variables
@automobile = current_user.vehicles.incomplete.discover(params[:car_id])
@step = params[:id].to_sym
@step_manager = Supervisor.new(automobile: @automobile)
rescue ActiveRecord::RecordNotFound
redirect_to root_path, error: "Automobile not discovered"
finish
def set_form
@type ||= @step_manager.form_for(@step).new(automobile: @automobile)
finish
finish
View
Lastly, every step has its related view. A step view solely wants a type helper occasion, primarily based on the shape object, to show type fields. At Getaround, we even have an related presenter for every step, which permits us to share data between internet, internet cellular and cellular apps.
simple_form_for @type, as: :automobile, url: car_wizard_path(@automobile, @step), methodology: :put do |f|
= f.choose :mileage, car_mileage_options_for_select, label: t("car_wizard.steps.mileage.attributes.mileage.label")
Execs and cons
With this structure, we are able to construct a fancy wizard, with a number of flows. A person can cease and resume it any time, and it’s attainable to have many flows with many guidelines with out having to write down your entire logic in a single single file, which might be a lot tougher to grasp and preserve.
Every step having its personal logic permits us to check the movement step-by-step, independently. The straightforward public API helps us to check service perfoming logic and attribute validation individually.
It’s simple to combine into our MVC sample with a quite simple controller and primary views. The wizard supervisor itself is just a easy algorithm to compute attainable steps.
Nonetheless, having most logic inside the shape objects forces us to learn every step to grasp how the flows work. If the entire wizards will get too difficult, computing every step might start to take a while, so that is one thing to be careful on the long run.
Additionally, the step completion relies on saving issues on database information. Having informational steps is a problem to deal with as a result of we have to discover different methods to retailer state to point that they’ve been seen, or depend on the state of adjoining steps.
For the simplicity of the article we haven’t present all of the options we’ve got primarily based on the automobile wizard. For example, we’ve got a logic to deal with monitoring on every step robotically. Additionally, the cellular wizard is pushed by the backend, primarily based on a devoted API. With this in thoughts, such an structure permits us to reorder, add or take away any step with out having to deploy a brand new model of our cellular apps. Effectively, to be trustworthy, it’s slightly bit extra difficult than that relying on what the cellular app helps, however you get the thought.
Conclusion
This modular movement is one answer to the wizard drawback. It gained’t go well with each want, however an identical structure has its benefits for those who search to handle a number of flows with advanced choice timber.
Be happy to remark and tell us what you consider this structure and the way we might enhance it.
Cheers.