Tutoriel
Le micro-templating JavaScript de John Resig à la loupe
par Loïc le Mercredi 05/11/2008 à 00:06, dans JavaScript, Tutoriel
Le templating JavaScript est une technique récente dont l’objectif est l’application du principe de templates dans une page web en les gérant via le JavaScript.
Cette technique trouve tout particulièrement un grand intérêt dans le cadre de communication asynchrones (XML ou JSON) avec la création de toute une partie du DOM à la réception de la réponse.
Le principe est relativement simple : on prépare une structure HTML contenant des mots-clef aux endroits où devront se trouver les données récupérées de façon asynchrones. A la réponse des données en question, le système de templating JavaScript se charge de récupérer le contenu de ce HTML sous forme d’une chaîne de texte et de remplacer les mots-clef par les données adéquates, puis de réinjecter ce nouveau contenu à l’endroit voulu dans le DOM de la page.
Les solutions de templating JavaScript sont encore rares et il est encore plus difficile d’en trouver des efficaces donnant une grande latitude lors de la création d’un template. En juillet dernier, John Resig a publié un script de templating sur son blog offrant la possibilité d’utiliser n’importe quelle fonctionnalité JavaScript dans le template. Etant donné que son code est assez obscur, je me propose de le décortiquer avec vous pour en comprendre le fonctionnement.
Pour décortiquer ce script, nous allons utiliser un exemple. Supposons que nous récupérions en JSON asynchrone (ce sera réalisé avec jQuery) un bloc de données caractérisant un groupe de personnes. Ce groupe est caractérisé par un nom et chaque personne est caractérisée par un ID, une image, un nom et un prénom.
Notre template aura la forme suivante :
<script type="text/html" id="groupTemplate">
<h3><%= name %></h3>
<ul id="users-list">
<% for (i = 0; i < users.length; i++) { %>
<li id="<%= users[i].ID %>">
<img src="<%= users[i].photo %>"/>
<span class="firstname"><%= users[i].firstname %></span>
<span class="name"><%= users[i].name %></span>
</li>
<% } %>
</ul>
</script>
Le template est encapsulé dans un script de type “text/html”. C’est un type MIME inexistant et nous aurions pu mettre n’importe quel type, par exemple “template/html”. L’essentiel est de mettre un type que le navigateur ne comprendra pas et donc ne cherchera pas à traiter.
Notre code JavaScript sera le suivant :
$(document).ready(function() {
$.getJSON("http://webdev-fr.com/examples/templating-js/json.php", null, function(data) {
var mygroup = document.getElementById("my-group");
mygroup.innerHTML = tmpl("groupTemplate", data);
});
});
Le fonctionnement est le suivant : au chargement de la page, on fait un appel asynchrone pour récupérer du code JSON. Ce code est passé au à la méthode javascript de templating à laquelle on passe également l’ID de notre template. Le résultat du templating est inséré dans un DIV ayant pour ID “my-group”.
La structure renvoyée par l’appel JSON est la suivante :
{
name:"Mon super groupe",
users:[
{ID:"u0",firstname:"firstname1",name:"name1",photo:"http://webdev-fr.com/examples/templating-js/img/photo.gif"},
{ID:"u1",firstname:"firstname2",name:"name2",photo:"http://webdev-fr.com/examples/templating-js/img/photo.gif"},
{ID:"u2",firstname:"firstname3",name:"name3",photo:"http://webdev-fr.com/examples/templating-js/img/photo.gif"}
]
}
Bien, ça semble marcher correctement, décortiquons un peu ce qui se passe derrière la fonction tmpl() de John Resig, qui se trouve dans le fichier templating.js…
Le code de John Resig est le suivant :
// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
(function(){
var cache = {};
this.tmpl = function tmpl(str, data){
// Figure out if we're getting a template, or if we need to
// load the template - and be sure to cache the result.
var fn = !/\W/.test(str) ?
cache[str] = cache[str] ||
tmpl(document.getElementById(str).innerHTML) :
// Generate a reusable function that will serve as a template
// generator (and which will be cached).
new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');");
// Provide some basic currying to the user
return data ? fn( data ) : fn;
};
})();
Vu qu’il n’est pas forcément évident de comprendre le fonctionnement de ce code, je me suis permis de le récrire dans un second fichier, templating2.js :
(function(){
var cache = {};
this.tmpl = function tmpl(str, data) {
if (!/\W/.test(str)) {
if (cache[str]) {
var fn = cache[str];
} else {
var fn = tmpl(document.getElementById(str).innerHTML);
}
} else {
var functionCode = "";
functionCode += "var p = [],";
functionCode += "print = function() {";
functionCode += " p.push.apply(p, arguments);";
functionCode += "};";
functionCode += "with(obj) {";
functionCode += " p.push('";
var jsCode = str;
jsCode = jsCode.replace(/[\r\t\n]/g, " ");
jsCode = jsCode.split("<%");
jsCode = jsCode.join("\t");
jsCode = jsCode.replace(/((^|%>)[^\t]*)'/g, "$1\r");
jsCode = jsCode.replace(/\t=(.*?)%>/g, "',$1,'");
jsCode = jsCode.split("\t");
jsCode = jsCode.join("');");
jsCode = jsCode.split("%>");
jsCode = jsCode.join("p.push('");
jsCode = jsCode.split("\r");
jsCode = jsCode.join("\\'");
functionCode += jsCode + "');";
functionCode += "}";
functionCode += "return p.join('');";
var fn = new Function("obj", functionCode);
}
if (data) {
return fn(data);
} else {
return fn;
}
};
})();
Je n’ai pas changé grand chose en fait, j’ai surtout supprimé l’utilisation d’opérateurs ternaires pour les remplacer par des if classiques et j’ai modifié la grosse suite de regexp afin de rendre le pas à pas plus facile à expliquer.
Ce nouveau script fonctionne parfaitement, on peut le tester sur notre précédent exemple en ramplaçant le script original par le nouveau.
Comme vous avez pu le constater plus haut, la méthode tmpl() prend deux paramètres :
- le premier paramètre peut être l’ID d’un template ou bien directement une chaîne de caractères correspondant au template au question. Passer un ID a plus d’intérêt car cela permet de mettre en cache la fonction de transformation correspondant à ce template. Néanmoins, si le template n’est utilisé qu’une seule fois ou bien s’il est dynamique, il peut être intéressant de le passer directement à la fonction.
- le second paramètre est optionnel. C’est l’objet qui correspond aux données qui vont être templatées. Si cet objet est passé en paramètre alors tmpl() retourne un code HTML (qu’on injecte dans notre conteneur résultat via sa propriété innerHTML). Si ce paramètre est absent, la méthode tmpl() retourne la fonction de transformation correspondant à notre template, cette fonction peut ensuite être utilisée en lui passant un objet. Ca peut avoir de l’intérêt si on compte passer plusieurs objets de données à la suite dans un même template : on récupère la fonction de transformation et on l’applique sur les objets en question dans une boucle.
La regexp /\W/.test(str) a pour rôle de tester si le premier paramètre passé à la fonction est un ID ou bien un template sous forme de ligne de texte. Pour rappel, \W correspond à n’importe quel caractère non-alphanumérique ou underscode. Autrement dit, tout sauf “a-z”,”A-Z” et “_”.
En conséquent, le test peut se lire de la façon suivante :
if (!/\W/.test(str)) {
// str ne possède pas de caractère non-alphanumérique, c'est donc un ID
} else {
// str possède un caractère non-alphanumérique, c'est donc un template sous forme de
// chaîne de caractères
}
Si le premier paramètre est un ID (on est alors dans le premier cas du test), alors la méthode est rappelée de façon récursive en lui passant, cette fois-ci, le contenu de la balise dont l’ID correspond au paramètre, autrement dit le contenu de notre template. Le résultat de ce second appel est placé en cache, ce qui permet par la suite de ne pas effectuer le second appel récursif mais de retourner directement le contenu mis en cache.
Le second appel récursif va retourner une fonction de transformation, c’est la raison pour laquelle l’objet de données n’est pas passé en paramètre sur ce second appel.
A la fin de la fonction tmpl(), un test est effectué pour savoir si le deuxième argument a été fourni ou non. Si c’est le cas, on applique la fonction de transformation sur ce second argument, sinon on retourne simplement la fonction de transformation.
if (data) {
return fn(data);
} else {
return fn;
}
En résumé, nous avons 4 cas d’appel pour notre fonction tmpl() :
- le premier argument est un ID, le second argument est fourni
- => on effectue un appel récursif en lui passant le innerHTML correspondant à l’ID, on obtient ainsi une fonction de transformation qu’on applique au second paramètre afin d’obtenir un code HTML qu’on injecte dans notre conteneur résultat.
- le premier argument est un template sous forme de chaîne de caractères, le second argument est fourni
- => pas d’appel récursif effectué, on obtient directement une fonction de transformation à partir de notre template en paramètre et on applique cette fonction au second arguement pour récupérer le code HTML qu’on injecte dans le conteneur résultat.
- le premier argument est un ID, pas de second argument
- => on effectue un appel récursif en lui passant le innerHTML correspondant à notre ID, on obtient une fonction de transformation qu’on retourne
- le premier argument est un template sous forme de chaîne de caractères, pas de second argument
- => on n’effectue pas d’appel récursif, on génère une fonction de transformation à partir de notre template et on retourne cette fonction (c’est ce cas qui se produit dans le cas d’un appel récursif).
Bien, maintenant qu’on sait comment fonctionne l’algorithme, il est temps de nous pencher sur la fonction de transformation et de sa batterie de regexp…
Le plus simple est de nous baser sur notre cas concret en utilisant notre template. L’objectif de la fonction de transformation est de retourner une fonction de la forme suivante :
function fn(data) {
var str = '';
str += '<h3>' + data.name + '</h3>';
str += '<ul id="users-list'>;
for (i = 0; i < data.users.length; i++) {
str += '<li id="' + data.users[i].ID + '">';
str += '<img src="' + data.users[i].photo + '"/>';
str += '<span class="firstname">' + data.users[i].firstname + '</span>';
str += '<span class="name">' + data.users[i].name + '</span>';
str += '</li>';
}
str += '</ul>';
}
Pour arriver à ce résultat, le système de templating utilise quelques subtilités :
- la création d’une fonction via le constructeur Fonction() pour avoir un code dynamique (qui dépend du template). Ce constructeur prend N paramètres : les premiers paramètres correspondent aux paramètres de la fonction, le dernier paramètre correspond au code de la fonction. Par exemple :
var displayAlert = new Function("msg", "alert(msg); return false;"); displayAllert();Le code précédent est équivalent à :
var displayAlert = function(msg) { alert(msg); return false; } displayAlert(); - l’utilisation du mot clef with. Par exemple le code suivant :
with(data) { field1 = "val1"; field2 = "val2"; }est équivalent au code suivant :
data.field1 = "val1"; data.field2 = "val2";
Ce mécanisme a l’avantage de nous éviter de devoir utiliser le même nom d’objet dans notre fonction de transformation et dans notre template.
- l’utilisation intelligente des regexp afin de découper le template en fonction des balises spécifiques au template (<% … %>). Nous les décortiquerons plus loin.
Plus exactement, la fonction de transformation visée aura le code suivant :
function fn(obj) {
var p = [],
print = function() {
p.push.apply(p, arguments);
};
with(obj) {
p.push('<h3>',
name,
'</h3><ul id="users-list">');
for (i = 0; i < users.length; i++) {
p.push('<li id="',
users[i].ID,
'"><img src="',
users[i].photo,
'"/><span class="firstname">',
users[i].firstname,
'</span><span class="name">'
users[i].name,
'</span></li>');
}
p.push('</ul>');
return p.join('');
}
}
Etudions les regexp appliquées une à une, c’est la partie vraiment indigeste du code…
Avant la première regexp, notre variable jsCode a la valeur de str, autrement dit :
<h3><%= name %></h3>
<ul id="users-list">
<% for (i = 0; i < users.length; i++) { %>
<li id="<%= users[i].ID %>">
<img src="<%= users[i].photo %>"/>
<span class="firstname"><%= users[i].firstname %></span>
<span class="name"><%= users[i].name %></span>
</li>
<% } %>
</ul>
La première manipulation est la suivante : jsCode = jsCode.replace(/[\r\t\n]/g, ” “);, autrement dit elle remplace tous les retours chariot (\r), les tabulations (\t) et les retours à la ligne (\n) par des espaces.
Notre variable jsCode aura donc la valeur suivante (je supprime les espaces superflus) :
<h3><%=name%></h3><ul id="users-list"><%for(i=0;i<users.length;i++){%><li id="
<%=users[i].ID%>"><img src="<%=users[i].photo%>"/><span class="firstname">
<%=users[i].firstname%></span><span class="name"><%=users[i].name%></span></li><%}
%></ul>
La seconde regexp fait une découpe sur le séparateur “<%” (le séparateur n’est pas conservé) afin de récupérer le tableau suivant :
jsCode = [
'<h3>',
'=name%></h3><ul id="users-list">',
'for(i=0;i<users.length;i++){%><li id="',
'=users[i].ID%>"><img src="',
'=users[i].photo%>"/><span class="firstname">',
'=users[i].firstname%></span><span class="name">',
'=users[i].name%></span></li>',
'}%></ul>'
]
La commande suivante est l’inverse du split : un join pour réassembler le tableau en une chaîne de caractère en insérant des tabulation entre ses éléments.
jsCode = '<h3> =name%></h3><ul id="users-list"> for(i=0;i<users.length;i++){%><li id="
=users[i].ID%>"><img src=" =users[i].photo%>"/><span class="firstname">
=users[i].firstname%></span><span class="name"> =users[i].name%></span></li>
}%></ul>'
La regexp qui suit est un poil plus compliquée : jsCode = jsCode.replace(/((^|%>)[^\t]*)’/g, “$1\r”);. Disons simplement qu’elle va rajouter des \r là où cela s’avère nécessaire.
La regexp qui suit (jsCode = jsCode.replace(/\t=(.*?)%>/g, "',$1,'"); va remplacer les =champs%> par simplement "', champs, '".
Ensuite on effectue de nouveau un split de notre chaîne, cette fois-ci sur les \t :
jsCode = [
"<h3>', name ,'</h3><ul id="users-list">",
"for(i=0;i<users.length;i++){%><li id="',users[i].ID,'"><img src="',users[i].photo,'"/>
<span class="firstname">',users[i].firstname,'</span><span class="name">',users[i].name,'</span></li>",
"}%></ul>"
]
Le join("');") suivant va permettre de fermer les p.push() pour la fonction.
"<h3>', name ,'</h3><ul id="users-list">"');" for (i = 0; i < users.length; i++) { %><li id="', users[i].ID ,'">
<img src="', users[i].photo ,'"/><span class="firstname">', users[i].firstname ,'</span>
<span class="name">', users[i].name ,'</span></li>"');\" } %></ul>"
Un nouveau split sur "%>" :
jsCode = [
"<h3>', name ,'</h3><ul id="users-list">"');" for (i = 0; i < users.length; i++) { ",
"<li id="',users[i].ID,'"><img src="',users[i].photo,'"/><span class="firstname">',users[i].firstname,'</span>
<span class="name">',users[i].name,'</span></li>"');"}",
"</ul>"
]
On enchaîne avec un nouveau join(), cette fois-ci pour insérer les p.push() :
"<h3>',name,'</h3><ul id="users-list">"');" for (i = 0; i < users.length; i++) { "p.push('"
<li id="',users[i].ID,'"><img src="',users[i].photo,'"/><span class="firstname">',users[i].firstname,
'</span><span class="name">',users[i].name,'</span></li>"');"}"p.push('"</ul>"
Oui, se perd facilement dans tous ces split() et ces join() successifs. On tient néanmoins le bon bout, on enchaîne avec un nouveau split(), sur \r cette fois, mais celle-ci n’a aucun effet dans notre cas.
Enfin, on termine par un join() avec "\\'" :
"<h3>',name,'</h3><ul id="users-list">"');"for(i=0;i<users.length;i++){"p.push('"<li id="',users[i].ID,'">
<img src="',users[i].photo,'"/><span class="firstname">',users[i].firstname,'</span><span class="name">', users[i].name ,'</span>
</li>"');"}"p.push('"</ul>"
Pour finir, on insère tout ça dans le début et la fin de notre fonction de transformation, pour obtenir en définitive le code suivant pour la fonction :
"var p = [],print = function() { p.push.apply(p, arguments);};with(obj) { p.push('<h3>', name ,'</h3><ul id="users-list">');
for (i = 0; i < users.length; i++) { p.push('<li id="',users[i].ID,'"><img src="',users[i].photo,'"/>
<span class="firstname">',users[i].firstname,'</span><span class="name">',users[i].name,'</span></li>');}p.push('</ul>');}
return p.join('');"
On obtient bien notre fonction de transformation…
Tutoriel sur la manipulation des dates en JavaScript
par Loïc le Mardi 28/10/2008 à 22:09, dans JavaScript, Tutoriel
Personnellement, j’oublie en permanence la syntaxe pour manipuler des dates et ce quelque soit le langage utilisé.
Si vous êtes dans le même cas que moi, voici un petit tutoriel sur la manipulation des dates en JavaScript.