Ajouter des validations sur vos modèles Ember.js

Publié le 26 février 2014 par Alexandre Salaun | front

Cet article s’inscrit dans la série d’articles concernant Ember.js. Nous avons précédemment vu les bases de ce framework et également comment créer une single page application avec ce dernier. Ici, nous allons voir de plus près comment faire le lien entre les modèles Ember.js et ceux de votre API Rails. Nous allons aborder les validations des modèles (Rails et Ember.js).

Si vous souhaitez suivre l’avancement au fur et à mesure de l’article, vous pouvez cloner l’application présente sur le dépôt Github en vous plaçant au commit suivant : 53b836b11d. Vous serez donc au même niveau que moi au début de cet article.

Validations côté serveur

Suite à l’article précédent, nous pouvions créer, modifier ou supprimer des objets en base. Cependant, nous n’avions pas de validations dans nos modèles Rails. Nous allons donc en ajouter ici et voir comment cela se répercute dans Ember.js.

Dans un premier temps, nous allons donc ajouter des validations dans notre modèle Rails, comme si nous étions dans une application Rails classique. Nous reprenons donc notre modèle Race :

class Race < ActiveRecord::Base
  validates :name, :start_at, presence: true
end

Il y a donc maintenant, dans l’application Rails, une validation sur le nom et la date de début de nos races. Il faut également adapter le controller associé pour qu’il renvoie bien les erreurs et réponde avec le statut HTTP 422 (qui correspond au fait qu’une erreur se soit produite pendant l’enregistrement) :

class Api::RacesController < ApplicationController
  respond_to :json

  ...

  def create
    @race = Race.new race_parameters
    @race.save

    respond_with :api, @race
  end

Voilà, la partie Rails est à jour pour gérer les validations sur le modèle. Ne reste maintenant plus qu’à traiter ces erreurs avec Ember.js.

Nous allons commencer par l’action new. Dans le controller Ember.js correspondant (app/assets/javascripts/controllers/races/new.js.coffee) nous allons donc légèrement modifier la méthode createRace qui permet de sauver l’objet en base via un appel à l’API. Voici ce que nous utilisions jusqu’ici :

EmberjsSinglePageApp.RacesNewController = Ember.ObjectController.extend
  actions:
    createRace: ->
      params = $("form").serializeArray()
      race   = EmberjsSinglePageApp.Race.createRecord({})

      for param in params
        race.set(param.name, param.value)

      race.get("store").commit()
      @transitionToRoute('races')

Si vous laissez en l’état, votre formulaire sera soumis et la requête à l’API effectuée mais que l’objet soit valide ou non vous serez redirigé vers la liste des races et il sera créé par Ember.js. Attention, l’objet sera stocké par Ember.js mais pas sauvé par Rails (car invalide), si vous rechargez votre page les objets non valides ne seront plus présents. Vous pouvez d’ailleurs constater que si vous ne remplissez pas les champs obligatoires c’est bien une 422 qui vous est renvoyée par l’API.

Maintenant, le but est d’avoir un comportement différent si l’objet est valide ou non. Pour cela, nous allons utiliser la méthode save et ses deux callbacks associés (un pour le succès et l’autre pour l’erreur) :

EmberjsSinglePageApp.RacesNewController = Ember.ObjectController.extend
  actions:
    createRace: ->
      params = $("form").serializeArray()
      race   = EmberjsSinglePageApp.Race.createRecord({})

      for param in params
        race.set(param.name, param.value)

      race.save().then( () =>
        console.log "ok"
        @transitionToRoute('races')
      , (errors) ->
        console.log "errors"
        console.log errors
      )

Dans cet exemple, l’utilisateur sera redirigé vers la liste des races uniquement si l’objet est sauvé, et donc valide. Sinon, il restera sur cette page et on loggue donc les erreurs.

Si vous soumettez maintenant le formulaire sans remplir les champs demandés, vous constatez donc que vous n’êtes pas redirigé et dans la console de votre navigateur vous pouvez observer que l’objet qui est loggué contient bien les erreurs attendues.

Maintenant, l’idéal serait d’afficher les erreurs à l’utilisateur afin qu’il puisse les corriger. Pour cela, nous allons tout d’abord modifier le template :

h2 Créer une nouvelle course

form submit="createRace"
  label Nom
  Ember.TextField valueBinding='name' name='name'
  if errors
    span.error= errors.errors.name
  br
  label Description
  textarea name="description"
  br
  label Ville
  input name="city"
  br
  label Région
  input name="county"
  br
  label Pays
  input name="country"
  br
  label Début
  Ember.TextField valueBinding='start_at' name='start_at'
  if errors
    span.error= errors.errors.start_at
  br
  button action="createRace" Créer

a class="links" click="cancel" Retour à la liste des courses

Le fait d’ajouter Ember.TextField valueBinding='name' name='name' à la place du textField permet d’afficher un champ qui va binder l’attribut name du modèle afin d’être mis à jour.

Dans le controller, il faut mettre à jour les attributs si il y a une erreur de validation :

EmberjsSinglePageApp.RacesNewController = Ember.ObjectController.extend
  content: null
  name: null
  start_at: null
  description: null
  city: null
  county: null
  country: null
  errors: null

  actions:
    createRace: ->
      @race  = EmberjsSinglePageApp.Race.createRecord({})
      params = $("form").serializeArray()

      for param in params
        @race.set(param.name, param.value)

      @race.save().then( () =>
        @transitionToRoute('races')
      , (errors) =>
        @set('errors', errors)
        @race.deleteRecord()
      )

On affecte donc les erreurs au modèle si il y une ou plusieurs erreurs afin de les afficher sur la page.

Nous avons maintenant un formulaire avec des validations qui crée un objet si il est valide (et redirige vers la liste) ou qui affiche les erreurs si il y en a. Il faut aussi penser à remettre le formulaire à zéro si l’on change de page. Pour cela, on modifie légèrement la méthode setupController dans le fichier de routes correspondant :

EmberjsSinglePageApp.RacesNewRoute = Ember.Route.extend
  model: ->
    @store.find 'race'

  setupController: (controller) ->
    controller.startEditing()

Ensuite, dans le controller, on crée la méthode startEditing qui va initialiser le controller :

EmberjsSinglePageApp.RacesNewController = Ember.ObjectController.extend
  content: null
  name: null
  start_at: null
  description: null
  city: null
  county: null
  country: null
  errors: null

  startEditing: ->
    @race   = EmberjsSinglePageApp.Race.createRecord({})
    @set "model", @race

  rollbackModel: ->
    if @race.get("isDirty")
      @set('errors', null)
      for attr in Ember.keys(Ember.meta(EmberjsSinglePageApp.Race.proto()).descs)
        @set(attr, null)

      @race.rollback()
      @race.deleteRecord()

  actions:
    createRace: ->
      params = $("form").serializeArray()

      for param in params
        @race.set(param.name, param.value)

      @race.save().then( () =>
        @transitionToRoute('races')
      , (errors) =>
        @set('errors', errors)
        @race.deleteRecord()
      )

    cancel: ->
      @rollbackModel()
      @transitionToRoute('races')

Lorsque l’on clique sur le lien pour retourner sur la liste des races, on ré-initialise le modèle. Il y a une méthode pour faire cela dans Ember.js mais elle ne fonctionne pas complètement… Les champs ne sont pas remis à zéro. On crée donc une méthode qui va remettre à zéro l’objet (attributs et erreurs). On peut donc soumettre le formulaire, avoir des erreurs, changer de page et y revenir avec le formulaire remis à zéro.

Voilà donc comment ajouter des validations sur vos modèles Rails et afficher les erreurs sur les formulaires. À vous maintenant de transposer tout ceci sur les autres formulaires.

Si vous voulez voir le code correspondant à cette étape, placez vous sur le commit 47d44c0096.

Validations côté Ember.js

Nous avons, ci-dessus, utilisé les validations de Rails pour nos formulaires Ember.js mais il est également possible d’effectuer des validations directement dans Ember.js. Pour cela, il suffit d’ajouter Ember Validations dans votre application et de préciser que votre modèle Ember.js hérite d’Ember Validations :

EmberjsSinglePageApp.Race = DS.Model.extend Ember.Validations.Mixin,
  name:        DS.attr('string')
  description: DS.attr('string')
  ...

Vous avez donc maintenant la possibilité d’ajouter des validations directement dans votre modèle Ember.js comme si vous êtiez dans un modèle Rails. Il existe en effet une grande diversité de validations, on y retrouve tout, ou presque, ce que l’on peut utiliser dans Rails : validations de présence, de format, de type de données…

Dans notre cas nous allons, dans un premier temps, ajouter dans ce modèle les mêmes validations que l’on a préciser dans le modèle Rails correspondant, c’est-à-dire la présence des attributs name et start_at :

EmberjsSinglePageApp.Race = DS.Model.extend Ember.Validations.Mixin,
  name:        DS.attr('string')
  description: DS.attr('string')
  city:        DS.attr('string')
  county:      DS.attr('string')
  country:     DS.attr('string')
  start_at:    DS.attr('string')

  validations:
    name:
      presence: true
    start_at:
      presence: true

Les validations sont maintenant ajoutées. Quelques modifications sont nécessaires dans le controller afin d’effectuer ces validations :

EmberjsSinglePageApp.RacesNewController = Ember.ObjectController.extend
  content: null
  name: null
  start_at: null
  description: null
  city: null
  county: null
  country: null
  errors: null

  startEditing: ->
    @race = EmberjsSinglePageApp.Race.createRecord({})
    @set "model", @race

  rollbackModel: ->
    if @race.get("isDirty")
      @set('errors', null)
      for attr in Ember.keys(Ember.meta(EmberjsSinglePageApp.Race.proto()).descs)
        @set(attr, null)

      @race.rollback()
      @race.deleteRecord()

  actions:
    createRace: ->
      params = $("form").serializeArray()

      for param in params
        @race.set(param.name, param.value)

      @race.validate().then( =>
        @race.save()
        @transitionToRoute('races')
      , () =>
        @set('errors', @race.get("errors"))
      )

    cancel: ->
      @rollbackModel()
      @transitionToRoute('races')

Comme vous pouvez le constater, on appelle maintenant la méthode validate() sur l’objet race. Si ce dernier est valide, on le sauve et on redirige l’utilisateur vers la page listant les races. Si, par contre, il n’est pas valide, on affecte les erreurs obtenues aux erreurs du modèle lié au controller : @set('errors', @race.get("errors")) afin de mettre à jour le template et donc d’afficher les erreurs. L’objet race dispose maintenant d’une propriété errors qui va contenir toutes les erreurs de validations, ceci est ajouté par Ember Validations, ce n’est pas quelque chose qui existe par défaut.

Il y a très peu de différence entre le code permettant de gérer les erreurs côté serveur et celui-ci. La dernière modification concerne le template. En effet, comme l’objet race a maintenant une propriété errors il faut remplacer errors.errors.name par errors.name (idem pour les autres champs) :

h2 Créer une nouvelle course

form submit="createRace"
  label Nom
  Ember.TextField valueBinding='name' name='name'
  if errors
    span.error= errors.name
  br
  label Description
  textarea name="description"
  br
  label Ville
  input name="city"
  br
  label Région
  input name="county"
  br
  label Pays
  input name="country"
  br
  label Début
  Ember.TextField valueBinding='start_at' name='start_at'
  if errors
    span.error= errors.start_at
  br
  button action="createRace" Créer

a class="links" click="cancel" Retour à la liste des courses

On retrouve maintenant, pour l’utilisateur, exactement le même fonctionnement qu’avec les validations côté serveur.

Voyons maintenant quelques exemples de validations disponibles avec Ember Validations. Nous allons valider la présence des champs name et start_at ainsi que le format de ce dernier et l’inclusion de la valeur de l’attribut county dans une liste de valeurs données (tout en permettant qu’il ne soit pas rempli). Voici à quoi ressemble donc le modèle Ember.js de race maintenant :

EmberjsSinglePageApp.Race = DS.Model.extend Ember.Validations.Mixin,
  name:        DS.attr('string')
  description: DS.attr('string')
  city:        DS.attr('string')
  county:      DS.attr('string')
  country:     DS.attr('string')
  start_at:    DS.attr('string')

  validations:
    name:
      presence:
        message: 'doit être rempli'
      length:
        minimum: 3
        messages:
          tooShort: 'minimum 3 caractères'
    county:
      inclusion:
        in: ['Nord Pas-de-Calais', 'Basse-Normandie']
        allowBlank: true
        message: 'doit être soit dans le Nord Pas-de-Calais, soit en Basse-Normandie'
    start_at:
      presence:
        message: 'doit être rempli'
      format:
        with: /(0[1-9]|[12][0-9]|3[01])[-](0[1-9]|1[012])[-](19|20)\d\d/
        allowBlank: false
        message: 'format attendu dd-mm-yyyy (ex. : 12-05-2015)'

Il faut aussi penser à modifier le template pour afficher les erreurs concernant le champ county :

label Région
Ember.TextField valueBinding='county' name='county'
if errors
  span.error= errors.county

Vous pouvez constater que j’ai aussi modifié les messages d’erreurs. Il est donc possible de personnaliser les messages sans trop d’efforts.

Voici le résultat :

Formulaire Ember.js

Conclusion

Nous avons donc vu ici comment ajouter des validations sur vos modèles, que ce soit avec Rails ou directement dans Ember.js. À vous de choisir ce qui vous parait le plus intéressant suivant les cas d’utilisation sachant que l’on peut effectuer les mêmes validations dans les deux cas. Vous pouvez, de toute façon, faire vos validations dans Ember.js et aussi les ajouter dans vos modèles Rails afin de ne pas avoir de données non valides (pour Ember.js) si vous modifier des choses directement en console.

Les validations restent assez simple à mettre en place. Il m’a fallu un peu de temps pour bien comprendre le fonctionnement. C’est assez dommage que la méthode rollback d’ember-data ne fonctionne pas correctement. D’ailleurs, il s’est avéré que ce dernier (qui n’est pas encore “Production Ready” d’après la page Github) évolue rapidement (un peu trop parfois) et contient encore un certain nombre de bugs… Il est cependant possible de ne pas utiliser Ember-data dans une application Ember.js et de préférer faire soit même les appels AJAX dans les modèles.

L’application Ember.js utilisée se complète donc petit à petit. Le point négatif reste sans aucun doute le style (sur lequel je ne me suis pas attardé comme vous pouvez le constater). Si l’un de vous souhaite donc s’amuser avec afin d’avoir une application un peu plus jolie je lui laisse volontiers cette partie.

L’équipe Synbioz.

Libres d’être ensemble.