Rails : Validation de formulaire en AJAX step by step !

Posted by Pierre Rigal on février 04, 2007

ajax L’architecture de Rails basée sur le modèle MVC permet de réaliser des validations de formulaires en se basant sur le “model”. Cette validation est très pratique et très bien conçue mais est quelque peu limitée dans sa construction par défaut. Ainsi il est possible de valider l’ensemble d’un formulaire lors de sa soumission, les erreurs seront alors affichées ensemble au dessus du formulaire. Bien qu’il soit possible d’exécuter ce processus en AJAX ce qui “web2ise” un peu l’appli, ce n’est pas encore la panacée…

Effectivement, une grande partie des nouveaux site Web2 proposent une validation “live” et “step by step” de leur formulaire.

Prenons un cas concret, sur une même page proposant la création d’un compte utilisateur, un formulaire bien fait vérifiera l’existence du compte à créer dès que l’utilisateur l’aura saisi (c’est à dire sans attendre la soumission) et fera aussi en sorte de controler la “force” d’un mot de passe avant sa soumission.

Je vous propose de mette en place ce type de formulaire dans votre application Rails sans casser (ni doubler) le mécanisme des validations déjà présent sur votre modèle (validates_presence_of …)

Il existe une foule de systèmes de gestion d’utilisateurs, j’avais notamment écris quelques billets sur le LoginEngine ou encore sur le auth_generator, cette fois je me suis basé sur “act_as_authenticated” mais mon explication se veut centrée sur les formulaires et ne présente donc pas ce plugin.

User model

Voici un modèle basique de classe User héritant de ActiveRecord :

class User < ActiveRecord::Base
  validates_presence_of     :login , :email, :password, :password_confirmation
  validates_confirmation_of :password
  validates_uniqueness_of   :login, :email, :case_sensitive => false
  ...

Grâce aux expressions régulières il est possible d’améliorer sensiblement les validations de base, ainsi on peut forcer le login à commencer par une lettre et ne contenir que des caractères alphanumérique en minuscule. (C’est par exemple recommandé si vous voulez plus tard faire en sorte de proposer un sous-domaine à votre compte utilisateur.)

validates_format_of :login, :with => /^[a-z][a-z0-9]+$/ , :message=>'is invalid (must start with a letter, only small letters and numbers are allowed)'

Il peut être aussi intéressant de valider que le login ne contiennent pas de mots réservés. On peut imaginer ainsi que vous voulez protéger des comptes comme “admin” ou “admin1” ou encore test (et ses déclinaisons test1, test2 …)

validates_format_of :login, :with => /^(?!(test[0-9]*|admin[0-9]*)$).*$/, :message=>'is reserved'

Enfin concernant le mot de passe, il peut être intéressant d’inciter et même de forcer l’utilisateur à utiliser un mot de passe “fort”. L’expression régulière vérifiera que le mot de passe contiennent entre 6 et 255 caractères et remplisse au moins 3 des 4 conditions suivantes :

  1. Le mot de passe doit contenir au moins un caractère alphabétique minuscule. (a)
  2. Le mot de passe doit contenir au moins un caractère alphabétique majuscule. (A)
  3. Le mot de passe doit contenir au moins un caractère numérique. (1)
  4. Le mot de passe doit contenir au moins un caractère spécial. ($)

Voici donc l’expression à utiliser :

 validates_format_of :password, :with => /(?=^.{6,255}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))^.*/, :message => 'is not strong enough (must check 3 of 4 following statements : at least 1 uper case character, at least 1 lower case character, at least 1 numerical character and at least 1 special character)'

Séparation des erreurs

Il est intéressant de pouvoir séparer les erreurs les unes des autres, cela permet ainsi d’afficher les erreurs concernant le login près du login et les erreurs concernant le mot de passe près du mot de passe. J’utilise pour cela la méthode trouvée ici.

Dans votre application helper, placer le code suivant :

def error_for(object, method = nil, options={})
      if method
        err = instance_variable_get("@#{object}").errors.on(method).to_sentence rescue instance_variable_get("@#{object}").errors.on(method)
      else
        err = @errors["#{object}"] rescue nil
      end
      options.merge!(:class=>'fieldWithErrors', :id=>"#{[object,method].compact.join('_')}-error", :style=>(err ? "#{options[:style]}" : "#{options[:style]};display: none;"))
      content_tag("p",err || "", options )
end

Ensuite dans votre view, vous pourrez supprimer le traditionnel <%= errormessagesfor ‘user’ %> et à chaque fois que c’est nécessaire utiliser :

<%= error_for :model, :field %>

Voyons donc a quoi ressemble nos vues correspondant à l’action “signup” de notre controller :

#signup.rhtml
<div id="signup_form">
  <%= render :partial=>"signup_form" %>
</div>

le partial :

#_signup_form.rhtml
<% remote_form_for :user, :url => { :action => 'signup'},:update => "signup_form", :html => { :action => 'signup', :method => 'post'} do |f| -%>
 <ul class="signup">
  <li>login : <%= f.text_field :login %><span id="login_informations"><%= error_for :user, :login %></span></li>
  <li>Pass : <%= f.password_field :password %><span id="password_informations"><%= error_for :user, :password %></span></li>
  <li>Confirm Pass : <%= f.password_field :password_confirmation %><span id="confirmation_informations"><%= error_for :user, :password_confirmation %></span></li>
  <li>Email : <%= f.text_field :email %><%= error_for :user, :email %></li>
 </ul>
 <%= submit_tag 'Sign up' %>
<% end -%>

De cette façon, à la validation du formulaire si des erreurs sont présentes elles seront affichées aux bonnes positions.

Vérification “live” (avant la soumission)

Jusqu’à présent nous avons un formulaire rails classique soumis en AJAX, la seule modification étant la position d’affichage des erreurs. Nous allons maintenant faire en sorte que ces erreurs soient “levées” dès la saisie par l’utilisateur. La première solution qui peut venir à l’idée est d’ajouter un observer au formulaire afin que ce dernier soit soumis à chaque fois que l’utilisateur change de champs.

Cette solution serait fonctionnelle mais à pour inconvénient de faire afficher toutes les erreurs, hors lorsque l’utilisateur saisi son login, nous n’avons pas envie de lui notifier que son mot de passe n’est pas assez fort car il n’y a pas encore touché… Pour faire en sorte que seulement la bonne erreur soit affichée, nous allons devoir les intercepter et les afficher au cas par cas. La solution est donc d’utiliser un observer pour chaque champs que nous voulons vérifier avant la soumission.

Prenons l’exemple du login, le partial deviendra donc :

#_signup_form.rhtml
  ...
  <li>login : <%= f.text_field :login %><span id="login_informations"><%= render :partial=>'login_errors'%></span></li>
  <%= observe_field "user_login",
               <img src='http://www.stoneageblog.com/wp-includes/images/smilies/icon_surprised.gif' alt=':o' class='wp-smiley' /> n => 'blur', # c'est à dire des que le champs perd le focus
               :update => 'login_informations',
               :with=>"'login=' + value",
               :url=>{:action=>'check_account', <img src='http://www.stoneageblog.com/wp-includes/images/smilies/icon_surprised.gif' alt=':o' class='wp-smiley' /> nly_path => false} %>
  ...

et le sous partial :

#_login_errors.rhtml
 <%= error_for :user, :login %>

Il faut donc maintenant écrire la méthode check_account dont le rôle sera de filtrer les erreurs concernant le login et de mettre à jour le sous-partial. Cette méthode devra être placée dans le controlleur gérant vos utilisateurs :

def check_account
  @user = User.new
  @user.login = params[:login]
  @user.valid? 
  login_errors = @user.errors.on('login')
  if login_errors.nil?
     render :text => 'OK, this account is available.'
  else
     render :partial=>'login_errors'
  end
end

Voilà ! Maintenant allez sur votre formulaire, essayer d’entrer une information sur votre login et dès le changement de champs vous saurez si vous avez des erreurs ou non, à vous les formulaire 2.0.

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. Anonyme mer, 14 fév 2007 10:11:12 CET

    Rails : Validation de formulaire en AJAX step by step !…

    Une grande partie des nouveaux site Web2 proposent une validation live et step by step de leur formulaire. Prenons un cas concret, sur une mme page proposant la cration dun compte utilisateur, un formulaire bien fait vrifiera lexistence du compte crer…

  2. khalil mar, 03 avr 2007 09:03:08 CEST

    thank you

  3. yoann jeu, 31 mai 2007 09:02:07 CEST

    Merci pour cet article. J’essaye de mettre en place le même système pour un projet, mais je suis confronté à 2 problèmes:

    • Comment fait on pour utiliser la validation classique du formulaire en parallèle en gardant les messages d’erreurs “localisés” ?

    • Comment traiter la confirmation de mot de passe ? A priori une requête ajax ne sert à rien dans le cas présent, non ?

    Enfin petite remarque sur la forme : il y a un problème dans le 9e encadré de code, les ‘:on’ ont été remplacés par le code d’un smiley…

    merci encore

  4. Pierre Rigal jeu, 31 mai 2007 09:31:11 CEST

    Yoann > A priori, les messages d’erreurs restent localisés (sauf pour la gestion du compte ou j’utilise seulement un “flash”), mais il est possible de récupérer le texte associé automatiquement par rails je pense dans l’objet “login_errors”…

    Pour la confirmation du mot de passe, cela peut très bien être géré en Ajax, il suffit de poser un observer sur le champs de confirmation dont l’action se lance lorsque le champs perd son focus en veillant a bien envoyer le mot de passe + sa confirmation afin de correctement construire l’objet à faire valider…

    NB : Impossible de virer le smiley :(

  5. yoann jeu, 31 mai 2007 13:50:48 CEST

    Oui en fait ca tombe sous le sens, il n’y a pas de raison qu’on ne puisse pas utiliser la même solution pour tous les champs…

    Mais entre nous une requête au serveur pour vérifier la concordance entre 2 champs c’est un peu excessif :) Je débute sur Ror mais j’imagine qu’on peut appeler une méthode de contrôle purement javascript depuis un observer, non ?

    En finissant d’adapter mon formulaire, j’ai été confronté à l’erreur object nil sur les :partial qui me faisait tout planter. J’ai contourné le problème en initialisant l’objet :user dans mon controller, est-ce que cela te semble une bonne idée ? En tout cas cela a l’air de fonctionner :)

    Ps: Pour le smiley , il va falloir songer à passer sous mephisto ;)

  6. Pierre Rigal ven, 01 juin 2007 10:25:38 CEST

    Je suis d’accord, c’est un peu excessif ! C’est tout à fait faisable sans la requête serveur mais tu vas perdre la gestion des erreurs déclarées sur le modèle…

    Concernant l’objet :user, il faut effectivement le créer dans ton controller (dans la méthode “signup” du controller user) !

    Pour mephisto, j’y passerai bien si… j’avais encore du temps à consacrer à un changement de blog. (Design, Urls, Caches, Contenus, Filtres de texte, Flux, autant de choses qui ne sont pas traitées de la même façon sur chaque plateforme et qui prennent des heures à porter…)

  7. cestpasdur jeu, 15 nov 2007 16:09:48 CET

    Il pourrait être interessant de préciser d’ajouter au layout ceci :

    <%= javascript_include_tag :defaults %>
    

    je dis interessant car j’ai cherché quelques minutes pourquoi je n’avais aucun ajax !

Comments (Syntaxe Markdown)