Skip to content

Instantly share code, notes, and snippets.

@baphled
Last active August 1, 2019 12:48
Show Gist options
  • Save baphled/3be4c3da65ae19742ff06d0789ca251e to your computer and use it in GitHub Desktop.
Save baphled/3be4c3da65ae19742ff06d0789ca251e to your computer and use it in GitHub Desktop.
# Journeys
All of our services have one common theme, they require a user to complete, or at least start, a journey.
Journeys comprise of steps, a single step can have a page, a form or another step.
A page typically has some copy, some information for the user to consider. Where by a form contains validation and
fields for a user to fill in.
In this case we can learn from past projects and take what has worked with them in the past.
## A Journey
A journey is a path a user has to go through to complete a given task, i.e. Apply for a Fishing License, Waste
Exemption or another service.
As journeys can become quite complex it is crucial that we have a standardised method of providing these services.
A service can have a number of journeys and these journeys can change, not only depending on the users choices but, also
the type of user using the service.
To assist with a users journey they will be taken through a number of steps, each step can affect the journey and
require us to create a sequence of steps that make up the journey.
A step has a single form (allowing a user to provide information) or a page (providing the user with
information). We'll use a naming convention to allow us to focus on implementing the journey and not have to worry
about the moving parts.
In the past mapping steps to forms has worked well, but we've ended up always creating one or more maps which end up
becoming huge, and require comments to keep them organised.
We should be able to nest steps, allowing us to create complex paths in a more structured fashion.
`ruby
Journey.steps do |journey|
new_journey: [
:personal_information,
:contact_details,
review_new_registration: [
:review_details,
:declaration,
cancel_new_journey: if ->(object) { object.cancel_journey == true }
],
:complete_new_journey,
],
existing_journey: [
find_step_current_step: [
:journey_found,
:journey_not_found
]
],
renewal_journey: [
:find_registration,
:email_renewal_information,
:show_renewal_details,
review_new_registration: [
:declaration,
cancel_renewal: if ->(object) { object.change_required == true }
],
:confirmation
]
end
`
By mapping a journey is this way we can easily organise a collection of steps.
There are a number of benefits to this approach: -
* We can easily re-use steps.
* Re-ordering steps is as easy as re-ordering `Journey.steps`.
* Easier to reason about the logic required to initiate a step in the journey.
* Can get an understanding of the services steps, at a glance.
## Defining a Step
A step allows us to map a form, or a page, to a given step in the journey. Essentially being the glue that
binds individual steps into a complete journey.
To define a step we simply create a class with the corresponding name, the following example defines the `new_journey`
step.
`ruby
class NewJourneyStep < Journey::Step
def initialize(journey, user, options = {})
self.journey = journey
self.user = user
self.page = NewJourneyPage.new(journey, user)
end
end
`
The above step declares that the `NewJourneyStep` step is a page. This page can be used to delegate any presentational
data to the appropriate class, allowing us to keep our models clean and purely focus on what the actual page requires.
`ruby
class PersonalInformationStep < Journey::Step
def initialize(journey, user, options = {})
self.journey = journey
self.user = user
self.form = PersonalInformationForm.new(journey, user)
end
def new
respond_to do |format|
format.html { render :new }
end
end
def create
respond_to do |format|
if @form.submit(personal_information_params)
format.html { redirect_to @journey.next_step_path }
else
format.html { render :new }
end
end
end
protected
def personal_information_params
params
.require(:form)
.permit(:first_name, :last_name, :email, address: [:first_line, :second_line, :county, :post_code])
end
end
`
## Implementing a Form
A form can take an object, using the same approach that waste-carriers-renewal takes, we can use a form object to
validate and send payloads to our data models.
The sole purpose of this form object is to validate and submit data to the appropriate model(s).
`ruby
class PersonalInformationForm < Journey::Form
attr_accessor :first_name
attr_accessor :last_name
attr_accessor :email
attr_accessor :address
validate_presence_of :first_name
validate_presence_of :last_name
validate_presence_of :email
def initialize(journey, user)
self.journey = journey
self.user = user
end
def submit(params)
self.address = Address.new(params[:address])
super(params)
end
end
`
## Implementing a Page
A page will typically just provide some information for the user, this can comprise of a decorator or presenter to
assist with handling the dynamic information on the page.
`
class ReviewDetailsPage < Journey::Page
def initialize(journey, user)
self.journey = journey
self.user = user
end
def show
@project = ProjectPresenter.new(journey.project)
respond_to do |format|
format.html { render :review_details }
end
end
end
`
Above we define a page that responds to the `show` action, that displays the details we want a user to review. We
assign `@project` using a presenter specific for a users project.
Finally we render the view.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment