#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)
Ce qui suit est un formulaire d'inscription avec un grand nombre de champs. Des formulaires comme celui-ci peuvent être relativement intimidant pour un bon nombre d'utilisateurs et peuvent les effrayer.
Une option face à un formulaire aussi complexe est d'en faire un formulaire à plusieurs étapes, aussi connu sous le nom de wizard. La façon la plus simple est d'utiliser JavaScript; de cette manière, on peut tout garder côté client et nous n'avons pas besoin de faire de changements dans notre application Rails. Ceci n'est pas toujours la meilleure solution, cela dit. Nous pourrions vouloir que les données soient plus persistentes et être capable de les stocker en base de données à chaque étape, ou nous pourrions vouloir que le formulaire soit dynamique afin que les étapes changent en fonction de l'application Rails. Nous voudrons probablement ajouter des validations à certains champs à chaque étape également.
Introduction à Wicked
Si vous voulez gérer un wizard dans une application Rails, nous devrions considérer l'utilisation de la gemme Wicked de Richard Schneeman. Elle ajoute des comportements aux contrôleurs Rails pour le transformer en un formulaire à plusieurs étapes et nous allons voir comment ça fonctionne dans cet épisode. La première étape est d'alléger notre formulaire d'autant de champs que possible - une des bonnes pratiques est de ne requérir uniquement des informations nécessaires à l'utilisateur pour accéder à l'enregistrement plus tard. Dans le cas d'une inscription, on peut réduire le formulaire aux champs relatifs à l'identification, soit le nom d'utilisateur et le mot de passe. Tout le reste peut être disposé dans des étapes séparées dans notre wizard donc nous allons réduire notre formulaire à ces champs.
<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 %>
Quand on recharge le formulaire, nous ne verrons plus que ces champs. Ceci est la première étape de notre wizard.
À ce stade, c'est une bonne idée de se demander si on a vraiment besoin d'un wizard. Nous pourrions simplement avoir une page pour modifier son profil et avoir ces champs dedans mais nous allons assumer que le wizard est la meilleure approche ici et poursuivre donc nous allons ajouter la gem Wicked au Gemfile et exécuter bundle
pour l'installer.
gem 'wicked'
Ensuite, nous allons générer un nouveau contrôleur appelé user_steps
. Ça nous donnera un contrôleur dédié à la gestion des étapes du wizard. On peut appeler ce contrôleur comme il nous plait puisque Wicked s'en fiche.
$ rails g controller user_steps
Nous aurons besoin d'ajouter une nouvelle ressource user_steps
à notre fichier de routes pour ce contrôleur.
Signup::Application.routes.draw do resources :users resources :user_steps root to: 'users#index' end
Maintenant, dans notre contrôleur, nous avons besoin d'inclure le module Wicked::Wizard
. Ceci nous donne une méthode steps
que nous pouvons utiliser pour définir les étapes dans notre wizard après que l'utilisateur ait été créé. Nous ajouterons deux étapes à notre formulaires, une pour les informations personnelles et une autre relatives aux réseaux sociaux dont l'utilisateur est membre. On s'attend à ce que ce contrôleur répondre à l'action show
et cette action devrait rendre une étape donnée du wizard. Nous avons accès à la méthode render_wizard
qui va chercher une vue pour chaque étape.
class UserStepsController < ApplicationController include Wicked::Wizard steps :personal, :social def show render_wizard end end
Nous aurons besoin de créer ces vues dans /app/views/user_steps
. Nous créerons des fichiers appelés personal.html.erb
et social.html.erb
. Pour le moment nous ajouterons simplement les mots "personal" et "social" à ces fichiers pour que nous puissions distinguer les étapes. Nous devrions maintenant avoir accès à toutes les étapes depuis /user_steps/<set_name>
.
Voilà comment render_wizard
fonctionne. Elle rend une vue dont le nom est transmis via l'URL. Si on visite l'action index
du contrôleur UserStepsController
, nous serons rediriger vers la première étape du processus, dans notre cas l'étape personal
. Nous avons besoin que l'action /users/new
nous redirige vers la première étape quand nous soumettons ce formulaire. Soumettre le formulaire déclenche l'action
create
du contrôleur 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
L'action sauvegarde le nouvel utilisateur puis le connecte en enregistrant son id
en session. Quand ceci se produit, nous voulons rediriger l'utilisateur à la première étape du wizard plutôt que la page vers laquelle il est actuellement renvoyé.
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
Quand l'utilisateur s'inscrit, il est redirigé vers l'étape "personal" de notre nouveau wizard. Maintenant, nous avons besoin d'ajouter un formulaire à cette page que nous redirigerons vers la seconde page de notre wizard quand il est soumis. Si nous voulons un formulaire pour modifier les détails de l'utilisateur sur chaque étape de render_wizard
, nous aurons besoin de récupérer les informations de l'utilisateur courant dans l'action show
de
UserStepsController
. Nous avons déjà une méthode current_user
dans notre application et si vous avez une sorte de solution d'identification dans votre application, vous aurez généralement besoin d'une méthode pour récupérer l'utilisateur courant. Si vous n'en avez pas, vous pouvez transmettre l'id
de l'utilisateur quand vous redirigez vers UserStepsController
.
def show @user = current_user render_wizard end
Nous pouvons maintenant remplacer le texte dans la vue personal
avec un formulaire contenant des champs pertinents.
<%= 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 %>
Nous avons eut besoin de passer une url
à ce formulaire. Normalement, il sera soumis à UsersController
mais nous voulons qu'il aille vers UserStepsController
. Nous pouvons utiliser le helper wizard_path
qui va POST
le formulaire à la bonne action. Le formulaire contient les trois premiers champs que nous avons précédemment enlevé du formulaire d'origine et un bouton de soumission. Quand on recharge la page, on peut
voir ce nouveau formulaire.
Cliquer sur "Continue" déclenchera l'action update
mais nous ne l'avons pas encore créée. Elle sera similaire à l'action show
mais elle mettra à jour quelques attributs de l'utilisateur courant basés sur les valeurs du formulaire.
def update @user = current_user @user.attributes = params[:user] render_wizard @user end
Notez que nous passons l'utilisateur courant à render_wizard
. Quand on donne une ressource de cette manière, il essaiera d'appeler la méthode save
sur celle-ci et si elle passe, il passera à la prochaine étape. Si l'enregistrement de la ressource échoue, nous verrons l'étape courante à nouveau.
Nous pouvons ajouter le formulaire à la vue social
maintenant.
<%= 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 %>
Soumettre ce formulaire déclenchera l'action update
à nouveau mais comme il n'y a pas d'étapes supplémentaires, elle nous redirigera à la racine de l'application. Si nous voulons changer ce comportement, nous pouvons surcharger la méthode redirect_to_finish_wizard
dans le contrôleur. Nous redirigerons toujours vers l'URL racine mais nous afficherons également un message pour remercier l'utilisateur de s'être inscrit.
private def redirect_to_finish_wizard redirect_to root_url, notice: "Thanks for signing up." end
Passer des étapes
La prochaine chose que nous allons faire est d'ajouter un lien au bouton "Continue" qui permet à l'utilisateur de passer une étape. Si on regarde la section 2 de "Quick Reference" du README, nous trouverons une liste de méthode que Wicked fournit et parmi celles-ci il y a next_wizard_path
qui retourne une URL vers la prochaine étape. Nous pouvons l'utiliser pour faire notre lien "skip".
<div class="actions"> <%= f.submit "Continue" %> or <%= link_to "skip this step", next_wizard_path %> </div>
Nous allons faire la même chose avec l'autre étape. Maintenant, nous avons un lien "skip this step" pour chaque étape de notre wizard afin que l'utilisateur sache que les champs de ces étapes sont facultatifs et peuvent être zappés.
Validations
Nous allons maintenant voir les validations. Disons que nous voulons valider le format du nom d'utilisateur Twitter dans l'étape "Social". Nous n'affichons actuellement aucune erreur de validations donc nous allons ajouter du code à chaque étape pour se faire. Si cette application était prévue pour de la production, nous aurions sûrement déplacé ce code dans un helper pour réduire la duplication de code mais ici nous allons laisser passer ça.
<% 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 %>
Maintenant, nous pouvons ajouter la validation à notre modèle User
. Nous utiliserons validates_format_of
et il est important de définir son option allow_blank
à true
pour la rendre optionnelle afin que la validation n'échoue pas quand l'utilisateur est enregistré à une différente étape.
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
Toutefois, qu'en est-il si nous voulons être sûr qu'un nom d'utilisateur Twitter a été renseigné ? C'est là où les choses peuvent devenir un peu compliquées puisque l'utilisateur est d'abord enregistré à la première étape. C'est très facile de valider la présence d'un nom d'utilisateur et d'un mot de passe à la première étape mais si nous essayons de valider les champs depuis une autre étape le nouvel enregistrement User
sera invalide et ne sera pas sauvegardé. Pour
contourner ça, nous pouvons faire une validation conditionnelle afin que le nom d'utilisateur Twitter soit validé uniquement à l'étape "Social".
validates_presence_of :twitter_username, if: :on_social_step?
Maintenant, il nous faut écrire la méthode on_social_step?
qui va vérifier à quelle étape nous sommes et faire la validation conditionnelle en fonction de sa valeur de retour. Toutefois, nous n'allons pas écrire la méthode ici. Si vous avez besoin de faire quelque chose de ce genre, il vaut mieux jeter un oeil à la rubrique "Partial Validation" de la page Active Record Objects du
wiki de Wicked qui explique exactement cette situation. Pour une solution alternative, jetez un oeil à l'épisode 217 qui montre comment construire un formulaire à étapes multiples from scratch.
Supprimer les duplicats
Notre formulaire wizard est presque complet mais il y a beaucoup de duplication dans différentes étapes du code. En dehors de l'entête et des noms de champ, les deux étapes sont presque identiques ce qui en fait un cas parfait pour un layout partiel. Nous pouvons déplacer l'entête en haut de page, en dehors du formulaire et déplacer les messages d'erreurs et le bouton de validation dans un nouveau partial form
. Plutôt que de rendre le partial en tant que partial, nous allons
le rendre comme un layout et lui transmettre le form builder.
<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 %>
Le nouveau partial de formulaire ressemble donc à ça. Notez que nous appelons yield
entre les messages d'erreurs et le bouton afin que les champs de formulaire soit rendus à cet endroit.
<%= 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 %>