#346 Wizard Forms with Wicked
- Download:
- source codeProject Files in Zip (92.6 KB)
- mp4Full Size H.264 Video (34.1 MB)
- m4vSmaller H.264 Video (14.9 MB)
- webmFull Size VP8 Video (16.8 MB)
- ogvFull Size Theora Video (31.8 MB)
A continuación se muestra un formulario de registro que contiene un gran número de campos. Los formularios de este tipo pueden intimidar bastante a los usuarios potenciales, algunos de los cuales abandonarán el proceso.
Tenemos la posibilidad, cuando nos encontramos con un formulario tan complejo como este, de dividirlo en múltiples pasos, que es el tipo de formulario conocido como asistente o wizard. La forma más fácil es utilizando JavaScript; de forma que lo podemos hacer todo en el cliente y no tenemos que hacer cambios en nuestra aplicación Rails. Pero no es esta la mejor opción en todos los casos, tal vez queramos que los datos sean persistentes y debamos almacenar los datos de cada paso en la base de datos, o tal vez queramos que el formulario sea dinámico de forma que los pasos vayan cambiando según la lógica de la aplicación. También puede ser que queramos validar algunos de los campos en cada paso.
Wicked
Para gestionar este tipo de asistentes en una aplicación Rails podemos considerar la gema Wicked, por Richard Schneeman. Dedicaremos este episodio a esta gema que añade a un controlador Rails el comportamiento necesario para convertirlo en un formulario de múltiples pasos. El primer paso es recortar nuestro formulario en el menor número de campos como sea posible -una regla empírica es requerir sólo la información del usuario imprescindible para acceder al registro en base de datos-. En el caso de un formulario de alta podríamos reducir los campos a aquellos que tienen que ver con la autenticación, es decir, el nombre de usuario y la clave. Todo lo demás puede ir a diferentes pasos en nuestro asistente, por lo que vamos a reducir el formulario sólo a estos campos.
<h1>Sign Up</h1> <%= form_for @user do |f| %> <% if @user.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :email %><br /> <%= f.text_field :email %> </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation %> </div> <div class="actions"> <%= f.submit "Sign Up" %> </div> <% end %>
Sólo veremos estos campos si recargamos el formulario. Este será el primer paso de nuestro asistente.
En este momento sería buena idea plantearse si realmente necesitamos un asistente. Podríamos tener una página separada de edición de perfil donde el usuario puede rellenar estos campos, pero asumiremos que el mejor enfoque es el de tener un asistente y continuaremos instalando la gema Wicked en el Gemfile
, y luego ejecutaremos bundle
para su instalación.
gem 'wicked'
Lo siguiente será generar un nuevo controlador llamado user_steps
, con el que tendremos un controlador dedicado exclusivamente a los pasos del asistente. Podemos darle a este controlador el nombre que queramos, que a Wicked le dará igual.
$ rails g controller user_steps
Por supuesto tendremos que añadir un recurso llamado user_steps
a nuestro fichero de rutas.
Signup::Application.routes.draw do resources :users resources :user_steps root to: 'users#index' end
Tenemos que incluir el módulo Wicked::Wizard
en este nuevo controlador, lo que nos proporcionará un método llamado steps
que podemos utilizar para definir los pasos de nuestro formulario tras la creación del usuario. Añadiremos dos pasos al formulario, uno para la información personal y otro relacionado con las redes sociales a las que está suscrito el usuario. Se espera que el controlador responda a la acción show
, acción que debería mostrar un paso dado del asistente. Tenemos acceso al método render_wizard
que buscará una plantilla de vista distinta para cada paso dle asistente.
class UserStepsController < ApplicationController include Wicked::Wizard steps :personal, :social def show render_wizard end end
Crearemos estas vistas en el directorio /app/views/user_steps
. Los ficheros se llamarán personal.html.erb
y social.html.erb
. Por ahora simplemente añadiremos las palabras “personal” y “social” a estos archivos para que podamos distinguir un paso de otro. Ya deberíamos poder acceder a cada paso en las URLs /user_steps/<nombre_del_paso>
.
El funcionamiento de render_wizard
es el siguiente. Muestra la plantilla cuyo nombre se pasa en la URL, si visitamos la acción index
de UserStepsController
nos redirigirá al primer paso del proceso, en este caso el paso personal
. Tenemos que conseguir que la acción /users/new
redirija al primer paso del asistente, en este caso el paso llamado personal
. Tenemos que hacer que la acción /users/new
nos redirija a la acción create
de UsersController
.
def create @user = User.new(params[:user]) if @user.save session[:user_id] = @user.id redirect_to users_path, notice: "Thank you for signing up." else render :new end end
La acción guarda el nuevo usuario y luego les inicia la sesión guardando su id
en una variable de sesión. Cuando esto ocurra queremos redirigir el usuario al primer paso del formulario.
def create @user = User.new(params[:user]) if @user.save session[:user_id] = @user.id redirect_to user_steps_path else render :new end end
Esto funciona. Cuando nos registremos ahora seremos redirigidos al paso «personal» del asistente. Ahora tenemos que añadir un formulario en esta página que al ser enviado devolverá una redirección al segundo paso. Si queremos un formulario para editar los detalles en cada paso de render_wizard
tendremos que obtener el usuario correspondiente en la acción show
de UserStepsController
. Ya tenemos un método current_user
en nuestra aplicación (o un método equivalente si estamos usando alguna solución de autenticación estándar). Si no es así, podemos pasar el id
del usuario cuando se haga la redirección a UserStepsController
.
def show @user = current_user render_wizard end
Ya podemos sustituir el texto de prueba de la plantilla personal con un formulario que tenga los campos relevantes:
<%= form_for @user, url: wizard_path do |f| %> <h2>Tell us a little about yourself</h2> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :date_of_birth %><br /> <%= f.date_select :date_of_birth, start_year: 1900, end_year: Date.today.year %> </div> <div class="field"> <%= f.label :bio %><br /> <%= f.text_area :bio, rows: 5 %> </div> <div class="actions"> <%= f.submit "Continue" %> </div> <% end %>
Hemos tenido que pasar una url
a este formulario. Por lo general el formulario atacará a UsersControllers
pero queremos que vaya a UserStepsController
. Podemos utilizar el método helper wizard_path
que hará un POST del formulario a la acción correcta. El formulario básicamente contiene los primeros tres campos que eliminamos del formulario original, así como un botón de envío. Al recargar la página veremos el nuevo formulario.
Al hacer clic en “Continue” se activará la acción update
, que todavía no hemos creado. Será similar a la acción show
pero actualizará algunos de los atributos del usuario actual basándose en los valores recibidos desde el formulario.
def update @user = current_user @user.attributes = params[:user] render_wizard @user end
Nótese que aquí pasamos el usuario actual a render_wizard
. Cuando pasamos un recurso de esta manera Wicked intentará invocar save
sobre él, y si tiene éxito irá al siguiente paso, y si se produce algún error volveremos a ver el mismo paso.
Ahora podemos añadir el formulario a la plantilla social
.
<%= form_for current_user, url: wizard_path do |f| %> <h2>Where can we find you?</h2> <div class="field"> <%= f.label :twitter_username %><br /> <%= f.text_field :twitter_username %> </div> <div class="field"> <%= f.label :github_username %><br /> <%= f.text_field :github_username %> </div> <div class="field"> <%= f.label :website %><br /> <%= f.text_field :website %> </div> <div class="actions"> <%= f.submit "Continue" %> </div> <% end %>
Al enviar este formulario se volverá a activar la acción update
pero como esta vez ya no hay más pasos la redirección se realizará a la raíz del sitio. Si queremos cambiar este comportamiento podemos redefinir el método redirect_to_finish_wizard
en el controlador. Nosotros seguiremos haciendo esa redirección pero mostraremos un mensaje dándole las gracias al usuario por registrarse.
private def redirect_to_finish_wizard redirect_to root_url, notice: "Thanks for signing up." end
Saltar pasos
Lo siguiente que haremos será añadir un enlace junto al botón “Continue” que permite que el usuario se salte un paso. Si vemos en la sección de referencia del README veremos los métodos de Wicked, uno de los cuales es next_wizard_path
que devuelve la URL del siguiente paso. Podemos utilizar este método para nuestro enlace “skip”.
<div class="actions"> <%= f.submit "Continue" %> or <%= link_to "skip this step", next_wizard_path %> </div>
Haremos lo mismo con el siguiente paso. Ahora tenemos un enlace para saltarnos los pasos del formulario, de forma que el usuario sabe que los campos de ese paso son opcionales y se pueden saltar.
Validaciones
A continuación veremos las validaciones. Supongamos que queremos validar el formato del nombre de usuario en Twitter en el paso “Social” del asistente. Ahora mismo no estamos mostrando mensajes de error de validación por lo que pondremos código para hacerlo en cada paso. Si pusiéramos esta aplicación en producción probablemente moveríamos este código a un método helper para evitar la duplicidad, pero dejaremos que se nos pase por alto en este caso.</p>
<% if @user.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>
Ya podemos añadir la validación a nuestro modelo User
. Usaremos validates_format_of
, y es importante que escojamos la opción allow_blank
para que sea realmente un parámetro opcional (de lo contrario se activaría la validación cuando se guarden los detalles del usuario en otros pasos).
class User < ActiveRecord::Base attr_accessible :bio, :date_of_birth, :email, :github_username, :name, :password, :password_confirmation, :twitter_username, :website has_secure_password validates_format_of :twitter_username, :without => /\W/, :allow_blank => true end
Pero ¿y si queremos estar seguros de que se introduce un nombre de usuario de Twitter? Aquí es donde las cosas se complican, porque el registro del usuario se guarda cuando se completa el primer paso. Es fácil validar la presencia del nombre del usuario y su clave en el primer paso pero si intentamos validar campos de los pasos posteriores el nuevo registro User
que se está creando no será válido y por tanto no se guardará. Para superar esto podemos hacer que la validación sea condicional de forma que el campo de nombre de usuario de Twitter sólo se valide en el paso “Social”.
validates_presence_of :twitter_username, if: :on_social_step?
Ahora tenemos que escribir el método on_social_step?
que comprobará en que paso estamos y hará que la validación sea condicional dependiendo del valor que devuelva. Pero no vamos a escribir este método aquí, si necesitamos hacer algo parecido merece la pena ver la sección dedicada a la validación parcial de objetos de Active Record en el wiki de Wicked, que explica exactamente esta situación. Una solución alternativa se describen en el episodio 217 que explica cómo construir desde cero un formulario de múltiples pasos.
Eliminación de duplicidades
Nuestro formulario asistente ya está casi listo pero todavía tenemos muchas cosas duplicadas en los diferentes pasos. Aparte del enabezado y lo nombres de los campos los dos pasos son casi idénticos, lo que hace éste el caso ideal para el uso de un layout parcial. Podemos mover el encabezamiento a la parte superior de la página, fuera del formulario, y luego mover los mensajes de error y el botón de enviar a un nuevo parcial llamado form
. Sin embargo en lugar de mostrar el parcial como un parcial lo mostraremos como un layout y le pasaremos el constructor de formulario.
<h2>Tell us a little about yourself</h2> <%= render :layout => 'form' do |f| %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :date_of_birth %><br /> <%= f.date_select :date_of_birth, start_year: 1900, end_year: Date.today.year %> </div> <div class="field"> <%= f.label :bio %><br /> <%= f.text_area :bio, rows: 5 %> </div> <% end %>
El nuevo parcial del formulario tendrá el siguiente aspecto. Nótese que invocamos yield
entre los mensajes de error y el botón de forma que aparezcan aquí los mensajes de error.
<%= form_for @user, url: wizard_path do |f| %> <% if @user.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <%= yield f %> <div class="actions"> <%= f.submit "Continue" %> or <%= link_to "skip this step", next_wizard_path %> </div> <% end %>
Ahora ya podemos hacer lo mismo con la plantilla “Social” para ordenarla. Nuestro formulario seguirá funcionando igual que antes pero habremos eliminado la duplicidad que había en el código de los diferentes pasos.