Utilisation des associations entre les Models dans ExtJs
ExtJs 4 fournie un nouveau modèle de gestion des données basé essentiellement sur les classes Ext.data.Model et Ext.data.Store. Dans cet article, je vais vous montrer comment utiliser la capacité de ExtJs à associer les Model ensemble, de la même façon que l’on définit les relations dans les ORM.
1. Données de départ
Le jeu de données va être très simple : une liste d’auteur et une liste de livre associé.
Dans cet article je vais montrer comment utiliser ExtJs dans un environnement dit « classique ».
Je vais créer une page HTML avec 3 liens : le premier ne chargera que les auteurs, le second que les livres et le dernier les deux associés.
Je me suis basé sur la version 4.0.2a de ExtJs pour cet article.
2. Gestion des auteurs
2.a. Le Model
Les bonnes pratiques d’ExtJs est d’utiliser leur modèle objet. Ext.define() permet de définir de nouvelles classes avec de l’héritage et Ext.create() permet de créer un instance d’une classe.
Le Model dans Extjs permet de décrire un enregistrement (Record). Il doit définir au minimum les champs et leurs types. Dans les bonnes pratiques de Extjs, il est également conseillé d’y définir le proxy, qui pourra toujours être surchargé dans le Store si besoin.
Dans un fichier Author.js je définis mon Model comme suit :
Ext.define('Author', { extend: 'Ext.data.Model', fields: [ {name: 'ID', type: 'string'}, {name: 'NAME', type: 'string'} ], proxy: { type: 'ajax', url: '/data/authors.json', reader: { type: 'json', root: 'AUTHORS', totalProperty: 'totalCount'} } });
2.b. Le Store
Le Store dans ExtJs est un conteneur d’enregistrement (Record). C’est cet objet que l’on manipulera pour charger les données depuis le serveur mais aussi pour accéder aux différents enregistrements.
Dans un fichier Authors.js je définis mon Store comme suit :
Ext.define('Authors', { extend: 'Ext.data.Store', model: 'Author' });
2.c. Les données
Dans le Proxy du Model j’ai spécifié l’adresse d’un fichier plat contenant mes données au format json, en accord avec la configuration du Reader. Voici le contenu de ce fichier :
{ "success": true, "totalCount": 2, "AUTHORS": [ { "ID": 1, "NAME": "Stephen King" }, { "ID": 2, "NAME": "J.K. Rowling" } ] }
2.d. Utilisation du Model et du Store
Je définis dans un autre fichier javascript une fonction qui charge les données du fichier authors.json dans le Store et parcoure ce Store pour afficher son contenu dans une fenêtre.
Le chargement des données étant asynchrone, je place le code parcourant les données dans une fonction callback afin d’être sûr que le code ne s’exécute pas avant que les données soient disponibles.
function showAuthor() { var Authors = Ext.create('Authors'), //instanciation du store Authors html = ''; //chargement des données dans le store Authors.load({callback: function(){ Authors.each(function(Author){ html += '<p>'; html += 'ID: '+Author.get('ID')+'<br/>'; html += 'NAME: '+Author.get('NAME'); html += '</p>'; }); //affichage du résultat Ext.create('Ext.window.Window', { title: 'Authors', height: 400, width: 600, html: html, autoScroll: true }).show(); }}); }
Il ne reste plus qu’a créé une page HTML pour charger les fichiers et appelé la fonction showAuthor().
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Nested</title> <link rel="stylesheet" type="text/css" href="ext-4.0.2a/resources/css/ext-all.css"> <script type="text/javascript" src="ext-4.0.2a/ext-all-debug.js"></script> </head> <body> <p> <a href="#" onclick="showAuthor(); return false;">Voir les Auteurs</a> </p> <script type="text/javascript" src="nested_models/Author.js"></script> <script type="text/javascript" src="nested_models/Authors.js"></script> <script type="text/javascript" src="nested_models/app.js"></script> </body> </html>
En cliquant sur le lien « Voir les Auteurs » vous devriez obtenir le résultat suivant.
3. Gestion des livres
J’utilise le même principe de développement pour les livres.
3.a. Le Model
Ext.define('Book', { extend: 'Ext.data.Model', fields: [ {name: 'ID', type: 'string'}, {name: 'TITLE', type: 'string'} ], proxy: { type: 'ajax', url: '/data/books.json', reader: { type: 'json', root: 'BOOKS', totalProperty: 'totalCount' } } });
3.b. Le Store
Ext.define('Books', { extend: 'Ext.data.Store', model: 'Book' });
3.c. Les données
{ "success": true, "totalCount": 3, "BOOKS": [ { "ID": 1, "TITLE": "It" }, { "ID": 2, "TITLE": "Charlie" }, { "ID": 3, "TITLE": "Dreamcatcher" } ] }
3.d. Utilisation du Model et du Store
La fonction javascript qui s’occuper de charger les données et de les parcourir.
function showBook() { var Books = Ext.create('Books'), //instanciation du store Books html = ''; //chargement des données dans le store Books.load({callback: function(){ //parcours des données Books.each(function(Book){ html += '<p>'; html += 'ID: '+Book.get('ID')+'<br/>'; html += 'TITLE: '+Book.get('TITLE'); html += '</p>'; }); //affichage du résultat Ext.create('Ext.window.Window', { title: 'Books', height: 400, width: 600, html: html, autoScroll: true }).show(); }}); }
Mise à jour du fichier HTML :
<body> <p> <a href="#" onclick="showBook(); return false;">Voir les livres</a> </p> <p> <a href="#" onclick="showAuthor(); return false;">Voir les Auteurs</a> </p> <script type="text/javascript" src="nested_models/Author.js"></script> <script type="text/javascript" src="nested_models/Book.js"></script> <script type="text/javascript" src="nested_models/Authors.js"></script> <script type="text/javascript" src="nested_models/Books.js"></script> <script type="text/javascript" src="nested_models/app.js"></script> </body>
En cliquant sur le lien « Voir les livres » vous devriez obtenir le résultat suivant.
4. Relation entre les Models (Nested data)
ExtJs dans sa version 4 supporte les relations entre les Models et permet de « peupler » des stores à partir d’un seul appel aux données ce qui limite le nombre d’appel au serveur et réduit l’association des données qui du coup n’as plus besoin d’être codé en javascript.
4.a. Les Models
Un peu à l’image de Doctrine2, même si je suis sûr qu’il n’y a aucun lien entre les deux, les relations doivent être définies dans le sens que l’on souhaite les utiliser. Dans mon cas je veux retrouver tous les livres d’un auteur.
Pour cela je dois modifier le Model Author pour lui indiquer la liaison.
Ext.define('Author', { extend: 'Ext.data.Model', fields: [ {name: 'ID', type: 'string'}, {name: 'NAME', type: 'string'} ], hasMany: {model: 'Book', name: 'books'}, proxy: { type: 'ajax', url: '/data/authors.json', reader: { type: 'json', root: 'AUTHORS', totalProperty: 'totalCount' } } });
J’ai ajouté la ligne hasMany : …. où model référence le Model Book et name représentes mon futur accesseurs.
Si j’avais besoin de retrouver l’auteur depuis un livre, je devrai définir une relation belongsTo dans le Model Book.
4.b. Le Store
Je crée un nouveau Store qui va étendre le Store Authors, car j’ai besoin de surcharger le proxy pour charger un autre fichier de donnée json.
Ext.define('NestAuthors', { extend: 'Authors', proxy: { type: 'ajax', url: '/data/nest_authors.json', reader: { type: 'json', root: 'AUTHORS', totalProperty: 'totalCount' } } });
4.c. Les données
Mon nouveau fichier de données ressemble à ça :
{ "success": true, "totalCount": 2, "AUTHORS": [ { "ID": 1, "NAME": "Stephen King", "books": [ { "ID": 1, "TITLE": "It" }, { "ID": 2, "TITLE": "Charlie" }, { "ID": 3, "TITLE": "Dreamcatcher" } ] },{ "ID": 2, "NAME": "J.K. Rowling", "books": [ { "ID": 1, "TITLE": "Harry Potter Vol. 1" }, { "ID": 2, "TITLE": "Harry Potter Vol. 2" }, { "ID": 3, "TITLE": "Harry Potter Vol. 3" }, { "ID": 4, "TITLE": "Harry Potter Vol. 4" } ] } ] }
Pour chaque auteur je définis une liste de livre. Notez que j’utilise « books » que j’ai définis pour nommer la relation dans le Model.
4.d. Utilisation du Model et du Store
L’utilisation ressemble beaucoup à celles faites précédemment.
function showNested() { var NestAuthors = Ext.create('NestAuthors'), html = ''; NestAuthors.load({callback: function(){ NestAuthors.each(function(Author){ html += '<p>'; html += 'ID: '+Author.get('ID')+'<br/>'; html += 'NAME: '+Author.get('NAME')+'<br/>'; html += 'Books: '+'<br/>'; html += '<ul>'; Author.books().each(function(Book){ html += '<li>TITLE: '+Book.get('TITLE')+'</li>'; }); html += '</ul>'; html += '</p>'; }); Ext.create('Ext.window.Window', { title: 'Authors & Books', height: 400, width: 600, html: html, autoScroll: true }).show(); }}); }
Notez que pour chaque Author ExtJs construit un accesseur basé sur le paramètre « name » définis dans la relation. Cet accesseur ne retourne pas le Model Book mais le Store Books peuplé par les données du fichier json.
Mise à jour du fichier HTML :
<body> <p> <a href="#" onclick="showBook(); return false;">Voir les livres</a> </p> <p> <a href="#" onclick="showAuthor(); return false;">Voir les Auteurs</a> </p> <p> <a href="#" onclick="showNested(); return false;">Voir le nested</a> </p> <script type="text/javascript" src="nested_models/Author.js"></script> <script type="text/javascript" src="nested_models/Book.js"></script> <script type="text/javascript" src="nested_models/Authors.js"></script> <script type="text/javascript" src="nested_models/Books.js"></script> <script type="text/javascript" src="nested_models/NestAuthors.js"></script> <script type="text/javascript" src="nested_models/app.js"></script> </body>
En cliquant sur le lien « Voir le nested » vous devriez obtenir le résultat suivant.
Finalement les relations entre les Models sont assez simples à mettre en œuvre avec ExtJs et permettent de gagner en tant de développement. N’hésitez pas à les utiliser même si vous n’en avez pas besoin dans toutes vos pages. ExtJs associe les données si elles sont définies dans les retours json et n’envoie aucune erreur s’il n’y a pas de données imbriquées.
Vous pouvez télécharger les sources de l’exemple ici. Attention la librairie ExtJs n’est pas incluse.
Je vous propose aussi cet article sur l'utilisation des Models, Stores et des relations entre les model dans le context MVC de ExtJs.
Réponse de Ulrich le 9 nov. 2012