lundi 26 mai 2008
Imbriquer les templates ERB "à la :render chez rails"
Par lucas, lundi 26 mai 2008 à 22:08 :: General
Parfois on a envie de pouvoir profiter du templating ERB fourni dans Ruby pour se faire des bouts de codes, ou pour toutes autres raisons, toutefois, pour ceux que ça intéresse, je les laisse évaluer un template.run dans un autre template ERB pour voir le résultat. Pour s'en sortir, il va falloir utiliser les bindings Ruby. Il y a peut-être une autre façon de faire avec les obscurs "_erbout", mais il y a peu de documentation là dessus. Aussi, devant le défi et le travail sur les bindings, je me suis laissé tenté.
A noter que pour certains j'aurai l'impression de réinventer la roue, je ne suis pas allé vérifier dans la touffe de code de Rails s'ils utilisaient la même technique.
Tout d'abord, et car j'ai la flemme de faire un bel article avec toutes les étapes qui m'ont conduit à cette solution, il n'y aura qu'un gros bloc de code avec la classe et l'exemple (certains auront l'habitude). La classe est nommée Renderer et je n'ai défini que des méthodes de classe. On pourrait faire autrement avec des instances d'une classe, mais bon, ça ne me semble pas le point essentiel.
Le point essentiel est le binding dans Ruby. Un binding, c'est une référence au contexte d'éxecution d'un bout de code. Ainsi, passer un binding permet d'avoir accès à un contexte d'éxécution autre que le langage seul le permet. C'est ce qu'on passe à ERB pour qu'il sache à quoi correspond quel nom de variable, même si le fichier est évalué dans un objet différent.
Le but étant de passer dans un Hash le nom des variables de l'intérieur du template, et de les convertir en ce qui est pointé par la clé, on sent déjà venir le eval. Pour faire simple je veux pouvoir utiliser un sous-template dans mon template en faisant
<%= render(:template_1,{'x' => 42}) %>
Je vais donc itérer sur les élements du hash, la clé me servira de variable à déclarer, et la valeur sera justement la valeur pointée. Je dois en outre éviter de déclarer une variable qui écraserait, soit une méthode, soit une variable de mon algorithme de transformation lui même (ce serait la catastrophe). Pour rendre cette probabilité plus faible, j'ai donc utilisé des underscores, vu que je serai le seul à utiliser mon système de templates, je sais la convention qui est ne pas définir une variable "__trucmuche__" dans les paramètres.
L'élément binding me permet de pouvoir itérer à l'intérieur du hash, et de faire mes eval dans le bloc passé à each et en faisant survivre hors du bloc les valeurs créées par eval.
Après ces mises en garde, je dois ajouter que j'aurai pu faire des tests et générer une exception, mais pour laisser l'importance à l'algorithme, je ne l'ai pas fait. De même, j'ai effectué un appel à eval par paire clé/valeur, mais on aurait pu faire un seul appel en créant une chaîne plus longue (et limitant un peu plus les effets de bords des écrasements de variables)
Pour finir, l'évaluation d'un template ERB se fera avec le binding passé à eval. C'est à dire qu'en cas de valeur manquante, l'interpréteur ruby cherchera une méthode et lévera une erreur. Je définis donc method_missing afin d'éviter le carnage si d'aventure on veut pouvoir rendre optionnel une des variables du template.
Bon, j'espère ne pas vous avoir assommé. Voici le code, comme d'hab questionnez si vous le voulez. En tout cas je n'ai pas cherché à savoir ce que donnerait un template qui sortirai du code à son tour interprétable par ERB, pour générer un template de template de template de ... (je doute que ça marche "out of the box")
class Renderer
@@partials = {}
def self.add_partial(name,str="")
@@partials[name] = str
end
def self.render(__name__,__params__=nil)
__b__ = binding
__params__.each_key do |k|
__str__ = %{#{k} = __params__['#{k}']}
puts __str__ if $DEBUG
eval __str__ , __b__
end
ERB.new(@@partials[__name__]).result __b__
end
def self.method_missing(m,*args)
puts "missing params or method: #{m}, nil-ed out" if $DEBUG
nil
end
end
#simple example
t1 = %q{
str1 : <%= x %>
}
#calling another partial in a partial
t2 = %q{
str2:
<% values.each do |value| %>
<%= render(:first,{'x' => value})%>
<% end %>
}
#now test missing functions
t3 = %q{
<%= str.class %>
<% unless params.nil? %>
there are some params
<% end %>
}
Renderer.add_partial(:first , t1)
Renderer.add_partial(:second, t2)
Renderer.add_partial(:third , t3)
puts Renderer.render(:first , {'x'=>0})
puts Renderer.render(:second, {'values'=>(0 .. 10)})
puts Renderer.render(:third , {'str'=>Array.new})
puts Renderer.render(:third , {'str'=> "ok", 'params'=>Array.new})