Wednesday, October 5, 2022
HomeRuby On RailsConstructing a modular a number of flows wizard in Ruby

Constructing a modular a number of flows wizard in Ruby


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.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments