/extension/myblog/design/myblog/stylesheets/white.css
/extension/myblog/design/myblog/stylesheets/black.css

Select your style :

A la une // Les blogs sur le développement Web, l'oenologie, Montpellier, etc...

Faire de l'édition frontale AJAX avec eZ Publish & Mootools (Partie 2)

Ce billet décrit pas à pas le développement de l'édition frontale dont le fonctionnement est visible sur la vidéo du précédent billet (principe du cliquer / éditer). Le framework JavaScript utilisé est Mootools, dont le fonctionnement est tout à fait similaire à ses principaux concurrents, à savoir : jQuery, Prototype & Script.aculo.us, YUI, Dojo, etc.

Pour ceux qui n'ont jamais utilisés un framework JavaScript, j'en profite pour introduire les objectifs et les fonctionnalités de base que l'on peut trouver dans tous ces frameworks, sans pour autant vous aider à faire un choix... Les critères de choix d'un framework dépendent de bien d'autres critères que leur simple capacités individuelles, comme par exemple :

  • Eviter la multiplication des frameworks sur un même site (ce que l'on constate malheureusement un peu partout)
  • Rester dans la logique de dépendance d'un framework et de son environnement de développement (YUI pour eZ Publish, Dojo pour Zend, Prototype pour Symfony, etc.)
  • Maîtriser un framework correctement, plutôt que 3 frameworks passablement

Introduction aux FrameWorks JavaScript

Un framework JavaScript est écrit en JavaScript, et n'est donc pas plus puissant que le langage lui même. Il s'agit seulement d'un ensemble de librairies et d'extensions du modèle objet qui facilitent l'écriture du code, notamment pour :

  • Manipuler le DOM (atteindre, ajouter, modifier ou supprimer des balises et des attributs dans la page)
  • Manipuler les évènements (évènements de la souris, du clavier)
  • Automatiser des effets complexes (effets de fondus, de Drag & Drop, etc.)
  • Faciliter les appels et traitements AJAX
  • Faciliter la POO, en proposant une syntaxe plus familière (classes, héritage)
  • Masquer certaines incompatibilités de navigateurs

La popularisation de ces frameworks a permis de démocratiser un modèle objet de manipulation du DOM, ainsi qu'une syntaxe grammaticale de sélection de noeuds déclinée du CSS3 Selector (dont voici une liste d'exemple en forme de benchmark pro-Mootools) bien plus lisible et compréhensible que l'API DOM & XPATH du W3C, ce qui génère d'ailleurs des initiatives de portage hors JavaScript comme par exemple : http://code.google.com/p/phpquery/

Spécification de la classe Mootools ajaxwebin

Cette classe a pour vocation de déclarer et d'exécuter le comportement de type "cliquer / éditer" sur un ensemble d'éléments similaires dans la page, comme par exemple :

  • Cas 1 : Remplacer sur clic toutes les balises A contenant une classe CSS 'cuvee_invert', par un champs de saisie
  • Cas 2 : Remplacer sur clic toutes les balises A, positionnées sous une balise TD et contenant une classe CSS commençant par 'stock_', par une liste de déroulante de 1 à 10

Prérequis dans le xHTML :

Les xHTML propulsés doivent être préparés pour faciliter l'identification des éléments éditables, et des correspondances entre les objets et les attributs cibles. Le motif xHTML est la suivante :

<HTMLElement id="rootID_ContentObjectID" class="MatchName" />
 

Par exemple concernant le cas 1 :

<a class="cuvee_invert" id="cuveename_2224"></a>
 

Par exemple concernant le cas 2 :

<td class="td80 stock_3" id="stock_2254"><a href="#">3</a></td>
 

Voici la liste des paramètres et leurs rôles respectifs :

  • ajaxmodule (obligatoire) : Racine du module eZ Publish : ajaxmodule/attributeID/objectID/value
  • parentMatch (obligatoire) : Syntaxe CSS3 (transmise en l'état à Mootools dans un sélecteur) qui permet d'atteindre tous les éléments impactés par l'édition frontale
  • targetMatch (optionnel) : Balise optionnelle à atteindre, comme enfant des éléments résultat du parentMatch (permet par exemple d'atteindre les balises A, enfants des balises TD)
  • styleMatch (optionnel) : Permet de définir un style CSS pour faciliter l'identification des éléments éditables dans la page (bordures, couleurs)
  • rootID (obligatoire) : <HTMLElement id="rootID_ContentObjectID" class="MatchName" />
  • attributeID (obligatoire) : Permet de transmettre le paramètre dans l'URL ajaxmodule/attributeID/objectID/value
  • caption (obligatoire) : Permet d'affecter le masque de saisie personnalisé sur clic, détaillé dans le prochain billet

Instanciation des objets de la classe Mootools ajaxwebin

A noter : les objets instanciés dans la propriété 'caption' seront décrits dans le prochain billet

window.addEvent('domready', function() {
 
    var myajaxwebin = new Array();
    
    // Ajout de la gestion des stocks
    myajaxwebin.push( new ajaxwebin({  
     ajaxmodule: '/ajaxwebin/action',
     parentMatch: 'td[class*="stock_"]',
     targetMatch: 'a',
     styleMatch: {border: '1px dotted #ff0000', background: '#ffffdd'},
     rootID: 'stock_',
     attributeID: 'stock',
     caption: (
        new ajaxcaptionsSelect({  
            'minValue': 0,
            'maxValue': 12,
            'setStyles': {margin: '0 -10px', border: '1px dotted #000', background: '#ffffdd'}
        }))
    }));
    
    // Ajout de la gestion des title des cuvées
    myajaxwebin.push( new ajaxwebin({  
     ajaxmodule: '/ajaxwebin/action',
     parentMatch: 'a[class="cuvee_invert"]',
     styleMatch: {border: '1px dotted #ff0000', background: '#ffffdd'},
     rootID: 'cuveename_',
     attributeID: 'title',
     caption: (
        new ajaxcaptionsInput({  
            'setStyles': {border: '1px dotted #000', background: '#ffffdd'}
        }))
    }));
 
});
 
 

Code commenté de la classe Mootools ajaxwebin

Il est difficile d'expliquer et de décrire l'ensemble du code. Les commentaires devraient être suffisants quant à la compréhension des mécanismes. Cependant voici quelques éléments clés de compréhension :

  • Voir la documentation des classes sur Mootools, et notamment l'utilisation du setOptions
  • Voir la documentation sur bind qui est essentielle à la construction des mécanismes qui mixent la construction dynamique du DOM et l'ajout dynamique d'évènements. Cette méthode permet de modifier la portée du this, pour définir quelle est la valeur du this dans l'exécution des évènements (clics ou autres)
var ajaxwebin = new Class({
    
    Implements: [Options],
 
    options: {
       ajaxmodule: null,
       parentMatch: null,
       targetMatch: null,
       styleMatch: null,
       rootID: null,
       attributeID:null,
       caption:null,
       currentselected:null,                
       currentID:null,
       targetElement:null
    },
    
    initialize: function(options) {
       this.setOptions(options);
       this.process();
    },
    
    // Appel AJAX au module eZ Publish, et Mise à jour de l'élément en fonction de la valeur retournée
    updateAJAX: function(value) {
      var object_id = this.options.currentID;
      
      // Définition de l'URL du module eZ Publish
      var url = this.options.ajaxmodule + '/' + this.options.attributeID + '/' + object_id + '/' + value;
      
      new Request({
       method: 'get',
       url: url,
       
       onSuccess: function(responseText, responseXML) {    
        this.options.targetElement.set('html', responseText);
       }.bind(this)
      }).send();
    },
    
    // Supprime le caption généré
    disposeCaption: function() {
       this.options.targetElement.setStyle('display', '');
       this.options.currentselected = null;
       this.options.caption.getElement().dispose();
    },
    
    //Remplace la zone cible par le caption attendu
    setCaption: function(element) {
       var currentselected = this.options.currentselected;
       
       this.options.currentID = element.getProperty('id').replace(this.options.rootID, '');
       this.options.attributeID = this.options.attributeID;
  
 if (currentselected != element) 
 {
 
   //Vérfie si une balise cible enfant a été définie, sinon exploite la balise courante
   this.options.targetElement = (this.options.targetMatch) ? element.getElement(this.options.targetMatch) : element;
   
   // On restore l'élément précent, si 'change' ou 'blur' non provoqué 
   if (currentselected) {
      var restore_selected = (this.options.targetMatch) ? currentselected.getElement(this.options.targetMatch) : currentselected;
    restore_selected.setStyle('display', '');
   }
   
   //On rend invisible l'élement cible, pour pouvoir le remplacer  
   this.options.targetElement.setStyle('display', 'none');
   
   //On affecte le caption xHTML généré avec la valeur de la page
   this.options.caption.setValue( this.options.targetElement.get('text') );
   
   //On supprime les évènements, par sécurité
   this.options.caption.getElement().removeEvents();
   
   //On affecte les évènements génériques à tous les captions xHTML
   this.options.caption.getElement().addEvents({
   'change': function() { //Sur changement, on update les données 
    var value = this.options.caption.getValue();
    this.updateAJAX(value);
    
    this.disposeCaption()
    
   }.bind(this),
   'blur': function() { //Sur perte du focus 
      this.disposeCaption()
   }.bind(this)
  });
   
   //On injecte le caption après l'élément cible
   this.options.caption.getElement().inject(this.options.targetElement, 'after');
   
   this.options.currentselected = element;
 
 }
 
    },
    
    process: function() {
      //Recherche tous les éléments selon le motif parentMatch, et boucle sur les résultats
      $$(this.options.parentMatch).each(function(element, index) {
   
     var elementID = element.getProperty('id');
     //Teste si les éléments possèdents des ID
     if (elementID && (elementID.indexOf(this.options.rootID)) == 0) {
     
     //Neutralise les liens <a>, pour faciliter l'insertion des captions
     if (element.getProperty('href')) element.setProperty('href', 'javascript:void(0)');;
     
     element.getElements('a').each(function(element_a) {
     element_a.setProperty('href', 'javascript:void(0)');
     });
     
     //Ajoute l'évènement de clic sur tous les éléments matché
     element.addEvents({
   'click': function() {
    this.setCaption(element);
    
   }.bind(this)
  });
     element.setStyles(this.options.styleMatch);
   }
   
      }.bind(this));
 
    }
});
 
 

Que boire avec ce billet ?

Domaine du Paternel - Blanc Sec 2006

Région : Provence
Appellation : Cassis
Domaine : Domaine du Paternel
Couleur :
 
Stock : 0
Notation :
Prix : 14 €
Commentaire(s) : 2 Commentaire(s)

Acheté sur le conseil d'un caviste, ce vin est une très bonne révélation. Très équilibré, entre un gras opulent, et une fraîcheur d'agrume et de fleurs blanche, ce vin séduira tous les amateurs de vins ensoleillés. A déguster avec généreux plat méditerranéen.

Publié par : http://truffo.fr, le 27 Septembre 2009 08:52 pm

Grande déception

Comme d'habitude très bon article, cependant comme d'habitude quelque remarque :

Un des point qui me dérange dans Mootools est le fait que l'on veille changer un langage à protoype en langage à objet, et en perdre l'intérêt majeur à savoir l'héritage dynamique. Cela me rappelle un peu les bibliothèques qui font des classes en C ...


La dépendance de Zend Framework à Dojo n'est que relative, du fait du couplage lache de ce (super) Framework, il existe le même composant que pour Dojo pour JQuery au sein mêm du Framework, et faire un composant similaire pour Mootools ne prend pas énormement de temps.

Comme le Zend Framework est un (super) framework, il dispose aussi de son composant pour manipuler le DOM via des CSS, il répond au doux nom de : Zend_Dom_Query.

Publié par : gandbox, le 27 Septembre 2009 09:16 pm

Classes contre prototype

Merci pour la précision concernant Zend. J'évoque surtout le couplage culturel et communautaire, qui généralement crée des couples forts dans l'habitude des développeurs (comme PHP & MySql). Si ce couple existe (ce qui n'est peut être pas le cas avec Zend & Dojo), il reste intéressant de rester dans la mouvance afin de profiter des bases de connaissances & des retours d'expériences autour du couplage.

Concernant le modèle objet contre prototype, il ne s'agit que d'un problème d'adoption de syntaxe, puisqu'au final le langage ne se transforme pas et reste sur du prototype. On peut donc étendre dynamiquement la définition de la classe et mixer les 2 principes. Quant à l'intérêt d'abstraire la notion de classe, tous les Frameworks JavaScript ont franchis le cap (pas seulement mootools), et il faut bien avouer que ça a considérablement booster le développement de composants clés en main... peut être que les langages à prototype (que je préfère, si si !) sont trop éloignés des concepts scolaires et déroutent les communautés de développeurs.

Publié par : msevere, le 28 Septembre 2009 08:02 am

eZJscore

Salut,

Article très intéressant cependant est ce que tu ne penses pas que ce serait plus simple en utilisant ezjscore ?

Matthieu

Publié par : gandbox, le 29 Septembre 2009 06:40 am

Utiliser eZJScore

Bonne question,

Par défaut c'était relativement rapide à faire avec Mootools (que je connais bien) et un simple module eZ. Cependant ça mérite une comparaison, à tester prochainement Smiling

Publié par : masev, le 30 Septembre 2009 11:33 am

Edition en front d'un bloc XML

Est ce que tu as implémenté la possibilité d'éditer un bloc XML ?

Publié par : gandbox, le 30 Septembre 2009 06:17 pm

possibilité d'éditer un bloc XML ?

Nope, mais ca serait faisable, par contre il faudrait modifier quelques éléments de code :
- remplacer les appels AJAX GET en POST
- ajouter un customcaption JavaScript qui charge son xHTML en AJAX (vue spécifique pour le datatype bloc XML)

Je note pour une future évolution,