Quem nunca fez um wizard form no Sign Up?!

Formulários wizard com Ruby on Rails

Caio Ergos
Red Academy Journal

--

Já tomou um susto com aquele formulário de cadastro de 42 campos daquele site que você tentou criar uma conta, mas desistiu no meio do processo porque simplesmente: haja paciência?

Algumas vezes nossas aplicações precisam guardar vários dados de seus usuários, mas ao mesmo tempo, nós programadores não podemos tornar o processo de preencher um formulário tedioso para o usuário. Afinal, não podemos nos dar ao luxo de perder nosso cliente pelo simples motivo dele não ter paciência para preencher 20 campos a fim de realizar um cadastro.

Cansou só de ver, né? Eu também! Que tal dividirmos o cadastro em partes mais toleráveis e oferecer a sensação de progresso ao usuário?

Podemos separar as partes do nosso cadastro agrupando as informações.

Os três primeiros campos são referentes apenas à identificação do usuário, então podemos ter um passo apenas para eles.

Passo 1

Já mais embaixo, podemos ver que temos campos referentes apenas à identificação do usuário perante às autoridades, ou seja, os dados de seus documentos de identificação.

Passo 2

No terceiro passo de nosso cadastro, podemos agrupar os campos referentes ao endereço do usuário.

Passo 3

Por último, o usuário poderá dizer quais são seus interesses dentro da nossa plataforma.

Passo final

Nesse momento é comum nos vir alguns questionamentos.

  • Devo guardar as informações do passo a passo do formulário no banco de dados para só então de fato “guardá-los”?
  • Caso não optemos por guardar tudo no banco, como irei guardar as informações de cada já que o HTTP não possui estado?

Vamos lá!

O Rails possui um recurso chamado sessão. Graças à sessão, o Rails consegue transformar o HTTP em um protocolo com estado. Mas o que seria essa sessão? A sessão consiste de um hash de valores que é passado em cada request e é único por usuário. Sua sintaxe é bastante simples:

# Guardando o ID de um certo usuário na sessão.
session[:user_id] = @user.id

O Rails faz uso de cookies encriptados. A chave de encriptação é a variável SECRET_KEY_BASE que se encontra no arquivo secrets.yml.

Iremos utilizar a sessões para guardar os estados do form e poder ir e voltar nele livremente.

Já ouviu de Form Objects? Não é um recurso do Rails, mas um padrão de projeto que nos será bastante útil. Você certamente já deve ter sentido aquele code smell quando sua aplicação tinha um certo formulário que simplesmente não se dava bem com as validações do seu model. Um bom exemplo seria: seu usuário precisa ter um nome, apelido, email, senha e um endereço. Porém você quer captar o máximo de usuários possível, então, para se cadastrar, o usuário bastaria colocar email e senha. Acabamos criando alguns problemas por conta disso:

  • O usuário deverá se cadastrar com todos os dados requeridos?
  • Retiramos validações do usuário validações do usuário para cumprir com as regras de negócio?

Se reparar bem, nos dois casos, você estará cumprindo com as regras de negócio por um lado e quebrando-as por outro. Logo, nenhum das soluções é boa. Form Objects são perfeitos para esse tipo de problema. A função de validação e de persitência de certos dados são delegados, agora, para esses objetos. Você poderá entender um pouco de como funcionam esses tipos de objetos nos links abaixo:

Cada passo do nosso Wizard terá uma classe designada para ele. Primeiramente, teremos um classe base do nosso formulário.

# app/forms/users/signup_wizard/base.rb
module Users
module SignupWizard
STEPS = %w[step1 step2 step3].freeze

class Base
include ActiveModel::Model

attr_accessor :user

delegate *User.attribute_names.map { |attr| [attr, "#{attr}="] }.flatten, to: :user
delegate :errors, to: :user

def initialize(user_attributes = {})
@user = User.new(user_attributes)
end

def submit
user.save
end

def steps
STEPS
end

def current_step
raise NotImplementedError
end

def next_step
raise NotImplementedError
end
end
end
end

Essa classe base ficará responsável pelo código compartilhado entre os passos do formulário.

  • Cada passo deverá ter um usuário associado;
  • Cada passo poderá realizar a ação de submit;
  • Cada passo deverá saber em que passo está;
  • Cada passo deverá saber qual o próximo passo.

Incluímos o módulo ActiveModel::Model para que nosso formulário tenha alguns comportamentos de model, tais como validações e delegações de métodos e atributos.

Delegamos todos os atributos de usuário ao formulário a fim de que a interface de acesso à classe não fique diferente daquela que já somos acostumados a utilizar. Estamos utilizando o conceito de Duck Typing.

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

Ou seja, a interface de acesso ao nosso objeto de formulário será muito parecido com o do model de usuário, não causando estranheza.

Veja como as classes referentes a cada passo são bem simples e como também cada uma herdará do passo anterior, mantendo, assim, todas as validações anteriores.

# app/forms/users/signup_wizard/step1.rb
module Users
module SignupWizard
class Step1 < Base
validates_presence_of :username

def current_step
:step1
end

def next_step
:step2
end
end
end
end
# app/forms/users/signup_wizard/step2.rb
module Users
module SignupWizard
class Step2 < Step1
# Apenas checar se o email possui um @.
validates :email, presence: true, format: { with: /\A[^@\s]+@[^@\s]+\z/ }

def current_step
:step2
end

def next_step
:step3
end
end
end
end
# app/forms/users/signup_wizard/step3.rb
module Users
module SignupWizard
class Step3 < Step2
validates :address, presence: true

def current_step
:step3
end

def next_step
nil
end
end
end
end

Repare que cada classe implementa os métodos current_step e next_step. O valor desses métodos serão utilizados tanto no controller quanto nos formulários (views) para identificar em qual passo estamos.

Nossas rotas ficarão assim:

# config/routes.rb
Rails.application.routes.draw do
root "wizards#step1"

resources :users, except: [:new, :create] do
collection do
resource :wizard, path: "signup", only: [:create] do
get :step1
get :step2
get :step3

post :perform_step
end
end
end
end

Temos cada passo e uma ação que irá servir para todos os passos: perform_step. Ela irá decidir qual classe instanciar dependendo do passo no qual estamos a partir dos dados guardados na sessão. Se as validações do passo estiverem OK, iremos redirecionar para o próximo passo. Caso contrário iremos renderizar o mesmo formulário.

Caso não haja outros passos, iremos pegar os dados coletados até então e chamar o método create.

Por padrão, load_wizard que utilizará o nome da ação e os atributos contidos na sessão para instanciar um objeto referente ao passo do formulário.

# app/controllers/wizards_controller.rb
class WizardsController < ApplicationController
before_action :load_wizard, except: :perform_step

def perform_step
store_wizard_params_in_session

@wizard = load_wizard(params[:current_step])

if @wizard.valid?
unless @wizard.next_step
create
return
end

redirect_to action: @wizard.next_step
else
render @wizard.current_step
end
end

def create
if @wizard.submit
clean_wizard_session
redirect_to users_path
else
render @wizard.class.STEPS.first
end
end

def step1; end
def step2; end
def step3; end

private

def clean_wizard_session
session[session_name] = nil
end

def load_wizard(step_name = action_name, attributes = session[session_name])
@wizard =
"Users::SignupWizard::#{step_name.camelize}".constantize.new(attributes)
end

def session_name
:user_sign_up_attributes
end

def store_wizard_params_in_session
if session[session_name].nil?
session[session_name] = user_wizard_params.to_h
else
session[session_name].merge!(user_wizard_params)
end
end

def user_wizard_params
params.require(:"users_signup_wizard_#{params[:current_step]}").permit(:email, :username, :address)
end
end

No final deste artigo se encontra o gist com todo o código utilizado. Tire um tempo para vê-lo com calma e abstrair o fluxo em sua cabeça.

OBS: A sessão possui tamanho máximo de 4Kb, então não recomendo utilizar esse método em formulários que deverão ter imagens ou em que a quantidade de informações inseridas possa superar esse limite. Lembrando que a sessão também é utilizada para fins de autenticação, então na verdade você terá um pouco menos de 4Kb. Mas não se engane: 4Kb ainda é muita informação para um formulário! :)

Aqui está o código fonte utilizado: https://github.com/RedRocket/blogpost-wizard-form-rails

Se você gostou deste artigo, por favor recomende e compartilhe.

--

--