Follow me
RSS feed
My sources
My Viadeo

Vues et layouts avec Haml, Erb et Markaby

Greg | 16 Jun 2009

rubyDans la version 0.6.2 de Capcode, qui devrait être disponible dans quelques jours, j'ai ajouté la possibilité d'utiliser des layouts1. Pour cela j'ai fait un plongeon un peu plus profond dans les entrailles d'Erb, Haml et Markaby. Sans vous assommer avec Capcode, regardons comment implémenter cela facilement dans un projet fictif.

Avant de commencer...

L'idée générale de ce qui va suivre est de faire, quelque soit le moteur de rendu que nous préférons, toujours à peu prêt la même chose. Je dis à peu prêt, car si avec Haml et Erb, il semble logique d'utiliser des fichiers pour la partie vue+layout, ce n'est pas le cas avec Markaby. Quoi qu'il en soit, nous allons créer une base de framework en faisant en sorte que l'utilisation de la méthode de rendu choisie diverge le moins possible entre les trois possibilités.

Partons de ce qui est fait en Rails. Si nous générons un nouveau contrôleur contenant deux actions, nous obtenons, dans app/controllers une classe qui ressemble à ceci :

class MonControllerController < ApplicationController
  def ActionUne
  end

  def ActionDeux
  end
end

Par défaut, le rendu de ActionUne se fait via le fichier app/views/mon_controller/ActionUne.html.erb et celui de ActionDeux se fait via le fichier app/views/mon_controller/ActionDeux.html.erb

Nous allons nous calquer sur une solution similaire. Pour cela nous allons créer une classe Controller dont héritera notre contrôleur. Dans les actions de ce contrôleur (définies comme des méthodes) nous expliciterons la méthode de rendu. Tout ceci nous donnera quelque chose comme cela :

class MonApp < Controller
  def test( arg )
    # ...
    render_XXX( *params )
  end
end

Notre objectif étant de proposer trois méthodes de rendu (Erb, Haml et Markaby), nous allons créer les trois méthodes suivantes :

Haml

Le Haml (XHTML Abstraction Markup Language) est un langage de balisage utilisé pour décrire simplement le XHTML d'une page internet sans utiliser les traditionnelles lignes de code.2

Je ne m'étendrai pas sur la syntaxe Haml et laisse le soin à ceux qui ne l'ont jamais utilisé de parcourir la documentation. La chose essentielle à savoir c'est que l'inclusion de code se fait via le helper yield. Par exemple, si nous avons la vue suivante :

%p "Bonjour le monde!"

et que nous voulons l'inclure dans le layout suivant, nous utiliserons le helper yield à l'emplacement ou nous souhaitons faire l'inclusion de la vue :

%html
  %body
    %h1 "Welcome!"
    = yield

Ainsi, nous obtiendrons (virtuellement) ceci :

%html
  %body
    %h1 "Welcome!"
      %p "Bonjour le monde!"

Qui sera donc transformé en HTML comme ceci :

<html>
  <body>
    <h1>"Welcome!"</h1>
    <p>Bonjour le monde!</p>
  </body>
</html>

La transformation de code Haml en HTML est très simple à faire :

Haml::Engine.new( haml ).to_html( )

Où haml contient le code à transformer. Si dans ce code nous voulons utiliser des variables, nous avons plusieurs solutions. La première consiste à les déclarer en paramètre de la méthode Haml::Engine.to_html. Ainsi, si dans notre code Haml nous voulons utiliser une variable ma_variable nous pouvons utiliser la syntaxe suivante :

Haml::Engine.new( "%p= ma_variable" ).to_html( nil, : ma_variable => "Hello" )
# => "<p>Hello</p>\n"

Cette solution est élégante, mais peu pratique, car nous ne connaissons pas forcément toutes les variables utilisées dans le code Haml. Les plus attentifs auront remarqué le passage de l'objet de type NilClass passé en premier paramètre de Haml::Engine.to_html. Ce premier paramètre est en fait l'objet de contexte dans lequel est évalué le template Haml. Donc nous pouvons également faire cela :

@ma_variable = "Hello"
Haml::Engine.new( "%p= @ma_variable" ).to_html( self )
# => "<p>Hello</p>\n"

Si nous savons enfin que Haml::Engine.to_html peut également prendre un bloc en paramètre, nous imaginons très facilement comment utiliser un layout :

Haml::Engine.new( layout ).to_html(self) { Haml::Engine.new( vue ).render(self) }

Où layout contient le template Haml pour le layout et vue le template de la vue.

Nous pouvons donc très facilement faire l'implémentation dans notre pseudo-framework :

 1 require 'rubygems'
 2 require 'haml'
 3 
 4 module Helper
 5   def render_haml( f, l = nil )
 6     if( !l.nil? and File.exist?( l ) )
 7       Haml::Engine.new( open( l ).read ).to_html(self) { 
 8         Haml::Engine.new( open( f ).read ).render(self) 
 9       }
10     else
11       Haml::Engine.new( open( f ).read ).to_html( self )
12     end
13   end
14 end
15 
16 class Controller
17   include Helper
18 end

Comme vous pouvez le voir, nous passons en paramètre de render_haml le nom du fichier pour la vue et celui du layout.

Pour tester ce code, il suffit de créer ces deux fichiers :

page.haml
%p= @message
layout.haml
%html
  %body
    %h1 "Welcome!"
    = yield

Nous avons également besoin d'une petite classe héritant de la classe Controller :

class Pipo < Controller
  def test_haml( message )
    @message = message
    render_haml( "page.haml", "layout.haml" )
  end
end

puts Pipo.new().test_haml( "OK avec Haml!" )

Erb (ou eRuby)

eRuby est un système de template qui permet d'embarquer du code Ruby dans un document texte.3

Tout utilisateur de Rails est ici en terrain connu puisqu'eRuby est le système de template utilisé par défaut. L'utilisation d'un layout se fait, ici aussi, via un yield. Donc, si nous reprenons le même exemple que celui utilisé avec Haml, avec ce template comme layout :

<html>
  <body>
    <h1>"Welcome!"</h1>
    <%= yield %>
  </body>
</html>

Et ce template comme vue :

<p>Bonjour le monde!</p>

Nous obtiendrons le code suivant :

<html>
  <body>
    <h1>"Welcome!"</h1>
    <p>Bonjour le monde!</p>
  </body>
</html>

La transformation d'un template Erb ressemble fortement à ce que nous avons vu avec Haml :

ERB.new( erb ).result()

Si en plus nous utilisons des variables dans le template, nous devons passer le contexte d'exécution à la méthode Erb.result. La grande différence avec Haml vient du fait qu'avec Erb, ce contexte est soit un objet Proc soit un objet Binding. Nous retiendrons la seconde solution :

ma_variable = "Hello"
ERB.new( "<p><%= ma_variable %></p>" ).result( binding )
# => "<p>Hello</p>"

Nous utilisons donc Kernel#binding comme contexte, qui nous renvoie l'objet de type Binding au point d'appel.

Pour mettre en place la gestion des layout, nous allons devoir jouer avec l'objet Binding. En effet, rien dans Erb ne permet d'utiliser des blocs. Nous allons donc devoir le faire évaluer via l'objet Binding. Si vous regardez la documentation de Kernel#binding, vous verrez comment faire évaluer une variable via l'objet Binding :

def getBinding(param)
  return binding
end
b = getBinding("hello")
eval("param", b)   #=> "hello"

Nous allons utiliser la même méthode pour un bloc :

def getBinding
  binding
end

b = getBinding { "hello" }
eval( "yield", b )
# => "hello"

Nous savons donc maintenant comment utiliser un layout avec Erb :

def getBinding
  binding
end

ERB.new( layout ).result( getBinding { 
  ERB.new( vue ).result(binding) 
} )

Nous pouvons donc faire l'implémentation de la méthode de rendu pour Erb dans notre pseudo-framework :

 1 require 'erb'
 2 
 3 module Helper
 4   def getBinding
 5     binding
 6   end
 7   
 8   def render_erb( f, l = nil )
 9     if( !f.nil? and File.exist?( l ) )
10       ERB.new(open(l).read).result( getBinding { 
11         ERB.new(open(f).read).result(binding) 
12       } )
13     else
14       ERB.new(open(f).read).result(binding)
15     end
16   end
17 end
18 
19 class Controller
20   include Helper
21 end

Nous pouvons maintenant créer le fichier pour la vue :

page.rhtml
<p><%= @message %></p>
Le fichier du layout : layout.rhtml
<html>
  <body>
    <h1>Welcome!</h1>
    <%= yield %>
  </body>
</html>

Puis faire un test :

class Pipo < Controller
  def test_erb( message )
    @message = message
    render_erb( "page.rhtml", "layout.rhtml" )
  end
end

puts Pipo.new().test_erb( "OK avec Erb!" )

Markaby

Markaby est un petit bout de code permettant d'écrire des pages HTML en pur Ruby.4

La grosse différence avec Haml et Erb, c'est qu'avec Markaby, nous allons écrire nos vues et layouts en Ruby. Ainsi, notre layout aura la tête suivante :

def layout
  html do
    body do
      h1 "Welcome!"
      yield
    end
  end
end

Et la vue aura la forme suivante :

p "Bonjour le monde!"

Ce qui devra permettre d'obtenir le code suivant :

<html>
  <body>
    <h1>"Welcome!"</h1>
    <p>Bonjour le monde!</p>
  </body>
</html>

Le passage de Markaby à HTML se fait simplement en passant le bloc de code Markaby au constructeur de l'objet Markaby::Builder puis en utilisant la méthode Markaby::Builder.to_s

Markaby::Builder.new {
  html do
    body do
      p "Hello"
    end
  end
}.to_s
# => "<html><body><p>Hello</p></body></html>"

Pour l'utilisation de variables dans le code Markaby, nous pouvons utiliser les mêmes méthodes que celles proposées par Haml. A savoir, soit le passage de variables déclarées sous forme de Hash en paramètre du constructeur :

Markaby::Builder.new( {:ma_variable => "Hello"} ) { p ma_variable }.to_s
# => "<p>Hello</p>"

Soit en précisant le contexte d'exécution :

@ma_variable = "Hello"
Markaby::Builder.new( {}, self ) { p @ma_variable }.to_s
# => "<p>Hello</p>"

Le code Markaby n'étant rien d'autre que du code Ruby, l'utilisation de layout est donc très simple :

Markaby::Builder.new({}, self) { self.send( layout ) { self.send( vue ) } }.to_s

L'implémentation dans notre pseudo-framework devient donc aisée :

 1 require 'rubygems'
 2 require 'markaby'
 3 
 4 module Views
 5  end
 6 
 7 class Markaby::Builder
 8   include Views
 9 end
10 
11 module Helper
12   def render_markaby( f, l = nil )
13     Markaby::Builder.new({}, self) { 
14       if self.respond_to?(l)
15         self.send(l.to_s) { self.send(f.to_s) }
16       else
17         self.send(f.to_s) 
18       end
19     }.to_s
20   end
21 end
22 
23 class Controller
24   include Views
25   include Helper
26 end

Comme vous pouvez le voir, nous incluons un nouveau module Views. C'est en effet dans celui-ci que nous définirons le code de nos vues et layouts. L'ajout des lignes 6 à 8 permet de s'assurer que les méthodes définies dans le module Views sont visibles dans le bloc passé au constructeur de Markaby::Builder. Enfin, nous devons également inclure le module Views dans la classe Controller.

Il ne reste plus qu'à tester :

class Pipo < Controller
  def test_markaby( message )
    @message = message
    render_markaby( :page, :layout )
  end
end

module Views
  def page
    p @message
  end
  
  def layout
    html do
      body do
        h1 "Welcome!"
        yield
      end
    end
  end
end

puts Pipo.new().test_markaby( "OK avec Markaby!" )

Oui, mais avec Rails... et patati, et patata!

Rails propose un système amusant permettant d'utiliser plusieurs yield dans un même layout. Pour savoir quoi faire pour chaque yield, il existe le helper content_for qui réagit en fonction du paramètre envoyé par yield. Ainsi avec le layout suivant :

<html>
  <head>
    <%= yield :head %>
  </head>
  <body>
    <%= yield :content %>
  <body>
</html>

Je peux m'amuser à créer les vues suivantes :

index.html.erb
<% content_for :head do %>
  <%= stylesheet_link_tag :style_index %>
  <title>Index page</title>
<% end %>

<% content_for :content do %>
  <h1>Index</h1>
  <p>Ma page d'index...</p>
<% end %>
other.html.erb
<% content_for :head do %>
  <%= stylesheet_link_tag :style_other %>
  <%= javascript_include_tag :defaults %>
  <title>Le titre de ma page</title>
<% end %>

<% content_for :content do %>
  <h1>Bla bla bla...</h1>
  <p>Une autre page...</p>
<% end %>

Vous avez compris le principe...

Et bien entendu, vous voulez pouvoir faire la même chose... Je ne vais pas pousser l'idée à fond et me contenter simplement de permettre de récupérer les paramètres du yield dans la vue.

Dans la version Haml, remplacez la ligne 7 à 9 par ceci :

      Haml::Engine.new( open( l ).read ).to_html(self) { |*args| 
        @__args__ = args
        Haml::Engine.new( open( f ).read ).render(self) 
      }

Dans la version Erb, remplacez les lignes 10 à 12 par :

      ERB.new(open(l).read).result( get_binding { |*args| 
        @__args__ = args
        ERB.new(open(f).read).result(binding) 
      } )

Dans la version Markaby, remplacez les lignes 12 à 18 par :

    Markaby::Builder.new({}, self) { 
      if self.respond_to?(l)
        self.send(l.to_s) { |*args| 
          @__args__ = args
          self.send(f.to_s) 
        }
      else
        self.send(f.to_s) 
      end
    }.to_s

Il suffit alors d'inspecter le contenu de la variable @__args__ dans les vues pour récupérer les valeurs passées au yield.

Si vous souhaitez voir tout cela en application, je vous renvoie au code des systèmes de rendu de Capcode...

1 des schémas...
2 Wikipedia
3 Wikipedia aussi...
4 Pas wikipedia...

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.