Développer une single-page-application avec EmberJS et Rails

Publié le 18 décembre 2013 par Alexandre Salaun | front

Cet article est publié sous licence CC BY-NC-SA

Dans un article précédent nous avons découvert EmberJS et son fonctionnement de base. Nous allons, cette fois, voir comment créer une single-page-application avec l’aide de RubyOnRails.

Avant de commencer le développement de notre application, prenons quelques secondes pour parler de deux outils très utiles lorsque vous utilisez EmberJS.

Le premier est Ember Data. C’est une librairie externe qui, dans une application utilisant EmberJS, permet de gérer la persistance des données en mappant celles-ci aux modèles par exemple. Cette libraire apporte également des adapters afin de permettre la connexion avec une API. Attention toutefois, Ember Data est encore en version bêta, elle est cependant déjà intégrée à la gem ember-rails et les contributeurs sont très actifs.

Le second outil que j’utilise, et qui est très appréciable, est un langage de templating qui ressemble fortement à Slim , il s’agit d’Emblemjs. C’est une alternative plutôt sympathique à Handlebarjs et cela permet d’avoir des templates plus clairs et lisibles.

Nous allons maintenant pouvoir nous lancer dans le développement de notre single-page-application avec EmberJS et RubyOnRails.

Création et initialisation de l’application Rails

Afin de pouvoir afficher les données et permettre à l’utilisateur de les manipuler, il faut commencer par créer l’API qui nous permettra de faire le lien entre la base de données et EmberJS. L’application développée ici, pour le test, aura pour but de lister des évènements sportifs (course à pied dans notre cas). Au fur et à mesure des articles nous pourrons ajouter différentes fonctionnalités.

Après avoir créé votre application via la commande rails new my_app_name et créer la base de données (lancer la commande rake db:create après avoir renseigné les identifiants de connexion à la base dans le fichier config/database.yml).

Commençons par créer un controller et une vue pour la homepage de notre application :

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
end
/ app/views/home/index.html.haml
%h1 Liste des courses
%p liste à venir...

Il faut également définir la route permettant d’arriver sur cette page :

# config/routes.rb
EmberjsSinglePageApp::Application.routes.draw do
  root "home#index"
end

Si vous lancer la commande rails s et que vous vous rendez à l’url localhost:3000 dans un navigateur vous pouvez constater que c’est bien notre page qui est affichée.

Maintenant, nous allons créer le premier modèle et ajouter les routes nécessaires :

rails g model race name:string description:text city:string county:string country:string start_at:datetime

Cette commande a généré le modèle et la migration nécessaire, il suffit de lancer rake db:migrate pour créer la table dans la base de données. Dans le fichier routes.rb, nous ajoutons donc notre nouveau modèle. Nous utilisons un namespace pour ne pas avoir de soucis avec les urls par la suite :

EmberjsSinglePageApp::Application.routes.draw do
  namespace :api do
    resources :races
  end

  ...
end

EmberJS s’attend à recevoir des données JSON à un format bien précis, nous allons donc utiliser la gem active_model_serializers pour formatter ces données :

gem 'active_model_serializers', github: 'rails-api/active_model_serializers'

Ensuite, le serializer de notre modèle correspond à cela :

class RaceSerializer < ActiveModel::Serializer
  attributes :id,
             :name,
             :description,
             :city,
             :county,
             :country,
             :start_at
end

Il faut également ajouter le controller (dans un sous-répertoire api) contenant les actions qui permettront de manipuler nos courses. Il nous renverra des données en JSON.

# app/controllers/api/races_controller.rb
class Api::RacesController < ApplicationController
  respond_to :json

  def index
    @races = Race.all
    respond_with @races
  end

  def show
    @race = Race.find params[:id]
    respond_with @race
  end

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

    respond_with @race, location: api_race_path(@race)
  end

  def update
    @race = Race.find params[:id]
    @race.update_attributes race_parameters

    respond_with @race, location: api_race_path(@race)
  end

  def destroy
    @race = Race.find params[:id]
    @race.destroy

    respond_with @race
  end

  private

  def race_parameters
    params.require(:race).permit(:name, :description, :city, :county, :country, :start_at)
  end
end

Si vous souhaitez vérifier le bon fonctionnement, vous pouvez vous rendre à l’url localhost:3000/api/races.json et constater que c’est un tableau vide qui vous est pour le moment retourné.

Nous avons donc maintenant la possibilité de récupérer en JSON toutes les courses ou bien une seule et d’en créer, modifier ou supprimer une. Cependant, il n’y a, pour le moment, aucune interface graphique permettant d’appeler ces actions du controller.

Intégrer EmberJS dans votre application Rails

Installation et configuration d’EmberJS

Il faut commencer par ajouter les gems nécessaires à votre Gemfile puis lancer la commande bundle install:

gem 'ember-rails'
gem 'ember-source'
gem 'emblem-rails'

La commande rails g ember:bootstrap --javascript-engine coffee permet de finaliser l’installation d’EmberJS et de générer les fichiers nécessaires.

Nous précisons dans le fichier app/assets/javascripts/store.js.coffee l’adapter que nous allons utiliser :

DS.RESTAdapter.reopen
  namespace: "api"

EmberjsSinglePageApp.Store = DS.Store.extend
  adapter: DS.RESTAdapter.create()

On spécifie le namespace afin d’appeler les bonnes URLs depuis Ember.

Notre premier modèle EmberJS

Nous créons, dans les répertoires spécifiques à EmberJS, le modèle et le fichier de routes pour l’index de notre modèle Rails Race :

# app/assets/javascripts/models/race.js.coffee
EmberjsSinglePageApp.Race = DS.Model.extend
  name: DS.attr('string')
  description: DS.attr('string')
  city: DS.attr('string')
  county: DS.attr('string')
  country: DS.attr('string')
  start_at: DS.attr('date')

# app/assets/javascripts/routes/races/index.js.coffee
EmberjsSinglePageApp.RacesRoute = Ember.Route.extend
  model: ->
    @store.find('race')

Enfin dans le fichier de routes global d’Ember il faut spécifier, comme pour Rails, que l’on veut créer des routes pour un modèle donné :

EmberjsSinglePageApp.Router.map ()->
  @resource('races', -> {})

Attention, si vous utilisez uniquement @resource('races') sans passer de fonction comme deuxième argument, la route ne sera pas créée et EmberJS utilisera donc ResourceRoute, ResourceController et la template resource (ce sont des valeurs par défaut). Si vous souhaitez utilisez des valeurs différentes ou même spécifier le template à utiliser vous devez donc passer une fonction en paramètre à @resource (même une fonction vide comme dans l’exemple ci-dessus).

Il ne manque plus que le premier template (app/assets/javascripts/templates/races/index.emblem) et la vue associée (app/assets/javascripts/views/races/index.js.coffee) à créer :

/ Template emblemjs
h2 Listes des courses
ul
  each model
    li= name
# View associated to index action
EmberjsSinglePageApp.RacesIndexView = Ember.View.extend
  templateName: "races/index"

Comme vous pouvez le constater, dans un souci de clarté, j’ai créer des sous répertoires pour les vues et les templates (et également pour les controllers que nous allons voir par la suite).

En vous rendant à l’url localhost:3000/#/races votre observez votre template.

Je vous conseille d’utiliser le débugger EmberJS de Chrome, il s’avère être vraiment pratique en cas de soucis.

L’action new

Maintenant que l’index fonctionne, le but est de pouvoir créer, voir et modifier des objets correspondant à notre modèle Race. Pour cela nous allons commencer par créer une route correspondant à l’action new ainsi que la vue et le template associés. Il sera également nécessaire de créer un controller pour gérer les actions de cette vue. Dans le router global d’EmberJS, on ajoute donc la route new dans le contexte de notre modèle Race :

# app/assets/javascripts/router.js.coffee
EmberjsSinglePageApp.Router.map ()->
  @resource('races', ()->
    @route('new')
  )

On obtient donc une route localhost:3000/#/races/new qui pour le moment ne correspond à rien. Il faut dans un premier temps créer le router spécifique à cette action. Ce dernier ne contient rien de bien complexe, on définit le modèle et on initialise le controller :

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

  setupController: (controller) ->
    controller.set 'model'

La vue est semblable à celle existante pour l’index :

EmberjsSinglePageApp.RacesNewView = Ember.View.extend
  templateName: "races/new"

Elles sont utiles car les fichiers de templates sont dans un sous-répertoire, si ce n’était pas le cas alors ces fichiers de vue seraient inutiles car les valeurs par défaut seraient correctes.

Enfin, il nous faut créer un template pour cette action new :

h2 Créer une nouvelle course

form submit="createRace"
  label Nom
  input name="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
  input name="start_at"
  br
  button action="createRace" Créer

On retrouve donc nos inputs pour les attributs du modèle ainsi que le formulaire et un bouton. Ces deux derniers ont comme action associée createRace qui va devoir être définie dans le controller :

# app/assets/javascripts/controllers/races/new.js.coffee
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')

Cette action récupère donc les valeurs des champs du formulaire et construit un objet Race avec ces attributs. Ensuite, on sauve cet objet via la ligne race.get("store").commit(). Une requête vers l’API va être effectuée à ce moment là pour insérer l’objet dans la base de données. La dernière action @transitionToRoute('races') est une redirection vers la page d’index. Il n’y a pour le moment aucune validation sur notre modèle Rails donc aucun problème à ce niveau.

Vous pouvez vous rendre à l’URL localhost:3000/#/races/new et créer votre première course, vous la voyez ensuite apparaître dans la liste sur l’index.

L’action show

Après avoir pu lister et créer des objets, nous allons passer à l’action show. Encore une fois, on commence par ajouter la route :

EmberjsSinglePageApp.Router.map ()->
  @resource('races', ()->
    @route('new')
    @resource('race', { path: ':race_id' })
  )

La route localhost:3000/#/races/:race_id est maintenant créée. Il suffira de lui passer en paramètre un ID existant.

Le router spécifique à cette action va, dans le cas présent, récupérer l’objet et initialiser le controller :

EmberjsSinglePageApp.RaceRoute = Ember.Route.extend
  model: (params) ->
    @store.find 'race', params.race_id

  setupController: (controller, race) ->
    controller.set 'model', race

La vue reste dans la lignée des précédentes et ne sert qu’à spécifier le fichier de template. Ce dernier va afficher les attributs de l’objet choisi :

h2= name

p= description

p Lieu : #{city} - #{county} (#{country})

Le controller est vide dans le cas présent puisqu’aucune action n’est nécessaire, le fichier ne contient donc que la ligne EmberjsSinglePageApp.RaceController = Ember.ObjectController.extend.

L’action show est donc à présent utilisable en accèdant à l’url d’un objet donné, par exemple localhost:3000/#/races/1.

L’action edit

Maintenant que nous pouvons lister, créer et voir un objet Race, nous allons le modifier. La ligne nécessaire est ajoutée au router :

EmberjsSinglePageApp.Router.map ()->
  @resource('races', ()->
    @route('new')
    @resource('race', { path: ':race_id' })
    @route('edit', { path: '/:race_id/edit' })
  )

Comme pour l’action show, le router spécifique à l’action va récupérer l’objet correspondant à l’ID donnée et initialiser le controller. Ce dernier, comme pour l’action new, va permettre de définir une action afin de modifier l’objet :

EmberjsSinglePageApp.RacesEditController = Ember.ObjectController.extend
  actions:
    updateRace: ->
      race   = @get('model')
      params = $("form").serializeArray()

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

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

Les actions de base étant créées, il serait plus pratique d’avoir des liens entre ces pages pour avoir une vraie navigation. Avec EmberJS, il est très simple de générer des liens, il n’y a pas besoin de récupérer l’évènement click sur un lien pour changer d’URL et donc de vue comme c’est le cas dans Backbone. En effet, linkTo permet, dans un template Emblemjs, de créer des liens entre les pages. Commençons par ajouter des liens sur l’index :

h2 Listes des courses
ul
  each model
    li
      = linkTo "race" this | #{name}
  else
    li Il n'y a aucune courses pour le moment.

= linkTo "races.new" | Créer une course

Vous remarquerez que les routes sont à passer avec une syntaxe spécifique (c’était déjà le cas avec la méthode transitionToRoute dans les controllers). Pour le lien vers l’action new, on spécifie simplement races.new mais pour l’action show il faut passer en paramètre l’objet : linkTo "race" this | #{name}. Attention, il n’y a pas de séparateur entre la route et l’objet. La gestion des liens est donc très simple une fois que l’on a compris la syntaxe des routes. Je vous laisse la possibilité d’ajouter des liens où vous le souhaitez sur les autres pages.

Nous allons nous attardez sur le template correspondant à l’action show et y ajouter un bouton pour supprimer un objet, ce qui constituera la dernière étape de cet article.

L’action destroy

Après avoir créer et modifier des objets, il faut aussi pouvoir en supprimer. Commençons par ajouter le bouton dans le template avec une action que nous définirons ensuite :

h2= name

p= description

p Lieu : #{city} - #{county} (#{country})

button{action "delete"} Supprimer la course
br
= linkTo "races.edit" this | Modifier la course
br
= linkTo "races" | Retour à la liste des courses

Dans le controller, nous créons l’action delete correspondante :

EmberjsSinglePageApp.RaceController = Ember.ObjectController.extend
  actions:
    delete: ->
      race = @store.find('race', @get('model.id'))
      race.deleteRecord()
      race.save()

      @transitionToRoute('races')

Il existe une méthode deleteRecord() qui permet de supprimer l’objet de la collection mais cela ne le supprime pas en base, il faut utiliser save() pour faire un appel à l’API et définitivement supprimer l’objet.

Conclusion

Voilà, vous avez tout en main pour gérer un modèle dans EmberJS. Vous pouvez retrouver le code correspondant à l’application de l’article sur notre page Github. Dans les prochains articles, nous repartirons de cette base pour aller plus loin dans le fonctionnement d’EmberJS.

L’équipe Synbioz.

Libres d’être ensemble.