Mots-clé : ajax

Sencha / ExtJS : comment garder une colonne triée avec une grid ?

Quand on fait une grille de données (datagrid) et qu’on la lie avec un magasin (store) c’est facile.

On peut autoriser à trier par colonnes dans la datagrid.

Seul problème : si le store est un stocké sur le serveur, il fait une seule fois l’appel et ensuite c’est la datagrid qui gère les tris.

Quand on modifie un enregistrement, il est envoyé au serveur, le serveur l’enregistre, et renvoie le résultat de ce qu’il a enregistré. Généralement, il renvoie exactement ce qu’il a reçu. Le seul problème, c’est qu’au retour, la grille ne rafraichit pas l’ordre de tri selon les colonnes qu’on a choisies.

Exemple concret : vous avez une grille avec plein de noms. Vous cliquez sur la colonne « nom », pour la trier par ordre alphabétique. Vous changez le nom « Albert » par « Zoé ». Voici ce qu’il se passe :

  • Le store envoie id=54, nom="Zoé" au serveur ;
  • Le serveur fait la modification en base, et renvoie id=54, nom="Zoé" en retour ;
  • Le store reçoit id=54, nom="Zoé", fait son changement en interne et le transmet à la datagrid ;
  • La datagrid se rafraichit mais ne change pas le tri et laisse "Zoé" à la même place.

La solution : dans le store, lors de l’événement qui signale que le résultat de l’écriture a été intégré (« write« ) il faut forcer l’appel à sort(); qui sera répercuté sur la datagrid automatiquement.

Voici mon code (raccourci à l’extrême sur ma classe de store qui gère les exceptions et plein d’autres choses) :

Ext.define('Ext.data.StoreHandleErrors', {
    extend: 'Ext.data.Store',
    alias: 'data.storehandleerrors',

    constructor: function(config) {
        this.callParent([config]);

        this.on(
            'write',
            function(me, opts) {
                this.sort();
            },
            this
        );
    }
});

ExtJs : dériver un Store « générique » et s’en servir

Ça fait plus d’une journée que je cherche comment faire un Store générique, c’est à dire que j’ai plusieurs Store qui sont tous basés sur le même modèle avec le même type de proxy, etc.

Donc au lieu de faire un copier coller pour chaque Store, j’ai cherché comment en faire un « générique » auquel je pourrai appliquer une configuration « par défaut ».

Voilà le code complet résultat, avec les fonctions qui gère les erreurs possibles renvoyées par Php (session expirée, problème d’écriture en base de données, etc).

Ce qui m’a pris le plus de temps à trouver c’est que pour « surcharger » le constructeur, ça n’est pas la fonction classique « initComponent: function(){ } » mais la fonction de base "constructor: function(config) { }".

Il ne faut, de plus, surtout pas oublier d’appeler le parent, non pas via this.callParent(); mais via this.callParent([config]);.

Ci suit du code, le code de plus d’une journée de travail, en deux parties (j’espère qu’il sauvera du temps à des personnes, ou qu’il les mettra sur la bonne voie !) :

  • première partie = la surcharge
  • seconde partie = exemple d’utilisation de cette surcharge

Première partie : code de la classe

Ext.define('Ext.data.StoreHandleErrors', {
    extend: 'Ext.data.Store',
    alias: 'data.storehandleerrors',

    constructor: function(config) {
        /* (!!) Réécriture par dessus certaines propriétés
         *      du proxy : si jamais elles existent déjà,
         *      elles vont être réécrites.
         */
        config.autoLoad= true;
        config.autoSync= true;
        config.proxy.type= 'ajax';
        config.proxy.reader= {
            type: 'json',
            successProperty: 'success',
            root: 'data',
            messageProperty: 'message'
        };  
        config.proxy.writer= {
            type: 'json',
            writeAllFields: true,
            root: 'data'
        };  
        config.proxy.listeners= {

            exception: function(proxy, response, operation) {

                var error=operation.getError(),
                    title='Erreur du serveur';

                if (error instanceof Array) {
                  error=error.join("
"); } switch(response.status) { case 200: if (response.responseText!='') { var b = Ext.JSON.decode(response.responseText); if (b.title) { title=b.title; } if (b.success==false) { if (b.timeout==true) { windowLoginPanel.show(); } } } break; case -1: var error= 'Le serveur met trop de temps à répondre'+ '

'+ 'On ne peut rien faire, essayez '+ 'd\'actualiser la page.'; break; case 500: var error= 'Le serveur a une erreur interne.'+ '

'+ 'On ne peut rien faire, essayez '+ 'd\'actualiser la page.'; break; default: var error= 'Erreur renvoyée par le serveur non gérée.
'+ 'Détails :
'+ response.statusText+ '

'+ 'On ne peut rien faire, essayez '+ 'd\'actualiser la page.'; break; } Ext.MessageBox.show({ title: title, msg: error, icon: Ext.MessageBox.ERROR, buttons: Ext.Msg.OK }); } }; this.callParent([config]); this.on( 'write', function(proxy, operation) { if ( (operation.action == 'create') || (operation.action == 'update') ) { var m = this.getById( parseInt( operation.resultSet.records[0].internalId ) ); } else if (operation.action == 'destroy') { var m = this.getAt(0); } if (m) { this.panelGridEtEdit.gsGrid.getSelectionModel().select(m); } else { this.panelGridEtEdit.gsGrid.getSelectionModel().deselectAll(); } Ext.example.msg( Ext.String.capitalize(operation.action), operation.resultSet.message ); }, this ); } });

Seconde partie : utilisation de la classe

    var storeAdresses = Ext.create('Ext.data.StoreHandleErrors', {
        model: 'Intranet.Adresse',
        proxy: {
            api: {
                read: '/json/intranet/liste/adresses/',
                create:  '/json/intranet/item/adresse/?mode=create',
                update:  '/json/intranet/item/adresse/?mode=update',
                destroy: '/json/intranet/item/adresse/?mode=destroy'
            }   
        }   
    }); 

ExtJS : exemple d’un développement

Voici une petite capture d’image d’un Intranet que j’ai développé.

Temps de développement : trois jours à temps plein.

Tout fonctionne parfaitement, en AJAX, synchronisation, etc.

Tout ça pour dire que ExtJS est vraiment une librairie formidable dans ce type de cadre :

Image d'exemple de développement d'un intranet en ExtJS

ExtJs et variables locales – fonctions anonymes

J’ai eu à faire face à un problème.
Malgré le fait que j’aie l’impression que la variable soit accessible parce que c’est un accès à l’intérieur de la classe, ça ne fonctionnait pas et l’erreur ressortait systématiquement :
this.writerForm: variable unknown

J’ai mis en gras ci-dessous le code qui pose problème :

Ext.define('Writer.Panel', {
    extend: 'Ext.panel.Panel',

    initComponent: function(){
        this.writerForm = new Writer.Form({
            listeners: {
                create: function(form, data){
                    this.store.insert(0, data);
                }   
            }   
        }); 
        this.writerGrid = new Writer.Grid({
            store: this.store,
            listeners: {
                selectionchange: function(selModel, selected) {
                    this.writerForm.setActiveRecord(selected[0] || null);
                }   
            }   
        }); 
        Ext.apply(this, {
            layout:'border',
            items:[{
                region:'north',
                layout:'fit',
                items: [this.writerGrid]
            },{ 
                region:'center',
                layout:'fit',
                items: [this.writerForm]
            }]  
        }); 
        this.callParent();
    }   
});

La solution est simple : il semblerait que ça soit un hack ExtJs, mais ça fonctionne, et j’espère que je vais vous éviter de perdre autant de temps que moi. Il faut ajouter « scope: this » au code.
J’ai mis en gras ci-dessous le code qui a résolu problème :

Ext.define('Writer.Panel', {
    extend: 'Ext.panel.Panel',

    initComponent: function(){
        this.writerForm = new Writer.Form({
            listeners: {
                create: function(form, data){
                    this.store.insert(0, data);
                }   
            }   
        }); 
        this.writerGrid = new Writer.Grid({
            store: this.store,
            listeners: {
                /* super important pour les fonctions anonymes */
                scope: this,
                selectionchange: function(selModel, selected) {
                    this.writerForm.setActiveRecord(selected[0] || null);
                }   
            }   
        }); 
        Ext.apply(this, {
            layout:'border',
            items:[{
                region:'north',
                layout:'fit',
                items: [this.writerGrid]
            },{ 
                region:'center',
                layout:'fit',
                items: [this.writerForm]
            }]  
        }); 
        this.callParent();
    }   
});

ExtJS 4 et le MVC (modèle-vue-contrôleur) – howto

Ci suivent quelques notes que j’ai prises après toute une journée passée à créer en entier l’exemple qui fonctionne.
Vous pouvez télécharger, puis décompresser l’exemple en entier en cliquant ici.
Pour l’installer et le tester c’est très simple : le répertoire s’appele « siteinternet » et c’est là qu’est le site internet, il vous suffira d’afficher la page « http://localhost/intranet/gs.php ». Oui j’ai développé cet exemple dans un sous-répertoire « intranet » car ExtJS est destiné, pour ma part à faire uniquement de l’administration.

Voici ce qui fonctionne : la vision d’une liste :

Vision d'une liste avec ExtJS 4.x (juste la vision)

Et le mode édition :

Vision d'une liste avec ExtJS 4.x (mode édition d'une ligne)

Le plus intéressant c’est le modèle MVC.
Je vous laisse lire toute la documentation ici, ici et .

Maintenant je vous fais un résumé de ce que j’ai retenu :

L’architecture

Elle doit absolument suivre ce schéma :

`-- app
    |-- controller
    |-- data
    |-- model
    |-- store
    `-- view

———————
controller =
Ce qui assemble data + model + store + view En général les contrôleurs créent les vues et attendent des évenements pour agir en conséquence.

model =
Là où on définit les colonnes des ensembles de données. C’est là aussi où on définit en général où aller chercher les données par défaut (« proxy »).

store =
Là où on définit sur quels modèles on se base, et où on surcharge là où aller chercher les données (= redéfinition de « proxy »).

Règles de syntaxe à respecter :

store = noms au pluriel :
app/store/Stations.js
model = noms au singulier :
app/model/Station.js

Notation :

  • au début, de type « bosse de chameau »,
  • au milieu tout en minuscules,
  • à la fin, de type « bosse de chameau » :

MonApplication.model.UserList
MonApp.view.user.List
MonApp.view.user.User
Panda.view.NewStation
Panda.view.SongControls

Arbre complet des fichiers de l’exemple ici :

gs
|-- app
|   |-- controller
|   |   `-- Users.js
|   |-- data
|   |   |-- users.json
|   |   `-- usersUpdate.json
|   |-- model
|   |   `-- User.js
|   |-- store
|   |   `-- Users.js
|   `-- view
|       `-- user
|           |-- Edit.js
|           `-- List.js
`-- app.js

A partir de là, à vous de jouer.

Au début, ce n’est pas simple, mais au final, c’est extrêmement évolutif et on gagne énormément de temps. Un peu comme d’apprendre vim :).

Hint et tips jQuery

Voici quelques astuces toutes bêtes mais qui vont vous faire gagner beaucoup de temps si vous démarrez avec jQuery :

  1. Dans d’autres librairies JavaScript (prototype pour ne pas la nommer), le symbole $(« Xx ») va chercher l’objet de votre document (DOM) en fonction de son Id. Donc, ici $(« Xx« ) ira chercher l’objet déclaré avec la propriété Id= »Xx ».
    Avec jQuery, ce n’est pas la même chose : si on veut chercher par Id, il faut ajouter le dièse (#). Donc le code équivalent précédent, mais pour jQuery, serait : $(« #Xx« ).
    J’espère que cette astuce vous fera déjà gagner quelques minutes / heures.
  2. Il y a deux possibilités de mettre du code au chargement de la page :
    1. Lorsque votre document a fini d’être chargé, mais pas forcément le reste : c’est le code suivant :
      $(document).ready(function() {
        // code lorsque le DOM a tout juste fini
        // (la page n'est pas encore visible !)
      });
    2. Lorsque la page au complet, avec toutes les images et tout ce que vous avez déclaré dans la page, a fini d’être chargée : c’est le code suivant :

      $(window).bind('load', function() {
        // code lorsque toute la page a fini
        // d'être chargée
      });

    L’intérêt de ça me direz-vous ? Voilà ce que vous pouvez faire :

    • Créer une page XHTML entièrement valide, mais qui ne cache rien de manière à ce que n’importe quel navigateur, y compris un navigateur destiné à lire en mode texte (lynx, par exemple), afin que des personnes handicapées puissent tout lire sans problème ;
    • Créer un évènement qui, dès le chargement de la page, alors qu’elle n’est pas affichée, cache la plupart des éléments afin d’afficher un message pour patienter ;
    • Créer un évènement qui, une fois que toutes les images et la totale sont chargées, affiche le menu et ce qu’il y a derrière.

    Vous pouvez éventuellement penser que c’est un retour en arrière, à savoir qu’il y a quelques années, on n’affichait la page que lorsque tout était chargé, et puis on s’est dit : « le monde va vite, très vite, il faut afficher des choses dès que l’on peut ». Donc maintenant la plupart des navigateurs, dès qu’ils ont le minimum de choses à afficher, le mettent visuellement devant l’Internaute. Du point de vue élégance, je trouve ça laid et si vous voulez faire quelque chose de moderne, affichez uniquement un texte en fondu qui affiche « patientez, chargement », et vous verrez que le fait d’afficher la page en entier uniquement lorsqu’elle est téléchargée est vraiment sympa et démarque des autres sites. Vous pouvez aussi ne cacher que certains menus qui ne sont pas vraiment présentables tant que les images qui vont avec ne sont pas affichées, et ne pas cacher le reste. Bon bref c’est quelque chose de vraiment utile, de pouvoir gérer ces deux événements qui sont déclenchés à des moments cruciaux.

Javascript : une page xhtml 1.0 valide

Vous voulez inclure directement du code dans votre page xhtml 1.0 et pourtant le validateur râle tout le temps et vous dit que votre page n’est pas valide ?
Voici l’astuce : il faut mettre les tags spécifiant qu’il y a des données « qu’il ne faut pas interpréter », c’est à dire que même s’il y a des caractères interdits (signe supérieur, inférieur, point-virgule, etc) ils seront ignorés. Ces fameux tags sont : <![CDATA[ et ]]>
Bien évidemment, comme ils sont dans une section JavaScript, il faut mettre les commentaires juste avant, commentaires qui sont autorisés :
//. Ce qui donne au final :
<script type="text/javascript">
// <![CDATA[
...
le script proprement dit
...
// ]]>
</script>