vendredi 21 février 2014

Drupal + AngularJS : l'ultime solution de theming pour Drupal ?

Customiser le code HTML créé par Drupal et ses modules est sans doute l'un des points les plus difficiles à comprendre. Les solutions proposées pour tenter de faciliter cette tâche sont nombreuses : Starter thèmes, modules type Panels ou  Display Suite, modules d'intégration de bibliothèques JavaScript, la liste est infinie. On passe beaucoup de temps dans la création d'un site sous Drupal à tenter de livrer un code HTML qui conviendra à l'intégrateur, qui de toute façon s'arrachera les cheveux à un moment ou un autre.

AngularJS est un framework Javascript qui adapte et étend le HTML grâce à des directives qu'on y inclut. Mes premiers pas avec AngularJS m'ont donné à penser qu'il pourrait bien être complémentaire de Drupal. Parlons de ces premiers pas justement, la communauté AngularJS propose un tutoriel pour débuter qui permet de créer une application Phonecat qui affiche des informations techniques à propos de téléphones portables.

Je vous propose donc de modifier cette application pour qu'elle récupère ses informations depuis Drupal via des web services. Drupal servirait alors uniquement de backoffice à l'application AngularJS, chose que ce CMS fait très bien.

Fonctionnement


L'idée est donc la suivante :


A partir de Drupal je vais créer un nouveau type de contenu classiquement qui générera des nodes. La liste de ces nodes sera créée via le module Views. Ensuite, la vue et les nodes seront exposés via des webservices REST grâce au module Services.

Côté client, j'utiliserai $resource d'AngularJS pour les accès à ces services REST, et les données pourront être exploitées par les contrôleurs et présentées par les templates  de l'application.



Installation de Drupal et de Phonecat AngularJS


J'ai décidé de positionner l'application AngularJS à la racine de mon répertoire et d'avoir un sous-dossier Drupal dans ce répertoire. Voici l'arborescence que j'ai obtenue après installation :


Créer un virtualhost ng-drupal

J'ai commencé par créer un nouveau virtual host dans Apache qui mène à mon répertoire Drupal/Angular. Voici mon ng-drupal.conf :

<VirtualHost *:80> 
  DocumentRoot /home/desktop/s/ng-drupal 
  ServerName ng-drupal.tld 
  <Directory /home/desktop/s/ng-drupal> 
    AllowOverride all 
  </Directory> 
</VirtualHost>

Cloner le tutorial Phonecat / Angular

Je suis parti du tutorial officiel proposé sur le site officiel AngularJS. Si vous ne connaissez pas ce framework, je vous encourage à le lire dans son ensemble.

Placez-vous à la racine du répertoire précédemment créé et exécutez les deux commandes suivantes. La première permet de copier le code source du tutoriel et la seconde de se placer à la dernière étape.
git clone https://github.com/angular/angular-phonecat.git .
git checkout -f step-12

A l'adresse suivante, j'ai maintenant le tutoriel qui s'affiche :
http://ng-drupal.tld/app/


Installation de Drupal

J'ai ensuite installé Drupal 7 dans le sous-répertoire /drupal du dossier principal. Si vous ne connaissez pas ce CMS, vous pouvez utiliser mon livre librement téléchargeable chez Framabook.

Pour accéder à l'installateur, j'ai lancé cette url : http://ng-drupal.tld/drupal


Après quelques minutes, Drupal était installé :)


Installation de la Feature angular-phonecat

Pour vous éviter de longues configurations et saisies, j'ai créé une « Feature » (module) qui contient les éléments Drupal qui permettent de créer les webservices pour l'application Phonecat. Elle contient :

  • un type de contenu phone avec tous les champs des téléphones du tutorial
  • du contenu qui utilise le type de contenu Phone
  • une vue qui liste tous les téléphones du site
  • un service qui permettent d'exporter sous forme de JSON la vue ou un élément du contenu

J'ai donc installé cette Feature dans le répertoire drupal/sites/default/modules (à créer si il n'existe pas).
Vous pouvez télécharger cette feature ici.


Installation des dépendances

Ce module a des dépendances manquantes, je les ai installées
  • Ctools : boîte à outils utilisée par un grand nombre de modules Drupal
  • Features : permet de créer des fonctionnalités sous forme de module
  • Field_group : utilisé pour regrouper les nombreux champs du contenu phone
  • Node_export_features : permet d'inclure du contenu dans une feature
  • Universally Unique ID : module permettant d'attribuer un id unique à chaque contenu
  • Services : permet à Drupal de créer des webservices
  • Views : permet d'extraire différents types de données de Drupal et de les présenter
  • Libraries : permet aux modules Drupal d'utiliser des bibliothèques externes
  • REST Server : permet de créer un serveur REST
  • Services views : permet à Services d'exposer le résultat des vues
Pour plus de rapidité, j'ai utilisé la commande Drush suivante :
drush dl ctools features views services field_group node_export uuid libraries services_views

J'ai ensuite pu activer les modules et ma feature. J'ai dû m'y reprendre à deux fois car mes contenus ont été importés avant mon type de contenu.

L'installation coté Drupal est terminée !

Les contenus dans Drupal


Vous avez maintenant la possibilité d'ajouter de nouveaux téléphones graphiquement avec Drupal :
Menu Content – Lien Add content – Phone

Voici l'interface que j'ai utilisée pour ajouter les quatre téléphones de la Feature que j'ai créée.


Pour ceux qui connaissent mal Drupal, sachez que vous pouvez créer ce type de formulaire graphiquement dans Drupal en choisissant les champs qui les composent.

Vous pouvez modifier le type de contenu Phone (Menu Structure – Content types, puis lien manage fields de Phone) :



La page de liste

J'ai commencé par créer la page de liste des téléphones.



La vue dans Drupal

Cette liste utilise 4 champs pour chaque téléphone :
  • Le titre
  • La description
  • Le nid (pour créer un lien vers le détail d'un téléphone)
  • L'image
J'ai donc créé une vue Phone-list dans Drupal (menu Structure – Views, puis lien Edit de la vue Phone-list)


Notez que j'ai utilisé le redimensionnement en 100x100 pour l'image, mais j'aurais pu en créer une autre facilement.

Le webservice dans Drupal

Ensuite, pour exporter cette vue en JSON, j'ai utilisé le module services (menu Structure – Services, puis lien Edit Resources du service phones).

J'ai indiqué que le chemin à utiliser pour accéder au service sera phones (onglet Edit) et que ca sera un serveur REST. J'ai autorisé le webservice à accéder à la lecture des nodes pour le détail des téléphones et aux vues pour accéder à celle que je viens de créer : Phone-list (onglet Ressources).

Voici le chemin complet à utiliser pour accéder à une vue via un webservice :
http://example.com/<endpoint path>/views/<view name>

Ce qui m'a donné :
http://ng-drupal.tld/drupal/phones/views/phone_list.json


Récupération dans Angularjs de phone_list.json

Au terme de ce tutoriel, j'aurai donc deux services : un basé sur Views qui me permettra de récupérer la liste des téléphones, un autre sur Nodes qui me permettra de récupérer un seul téléphone.

J'ai donc commencé par créer le premier service, dans le fichier services.js de l'application Phonecat :

phonecatServices.factory('Phones', ['$resource',
  function($resource){
    return $resource('http://ng-drupal.tld/drupal/phones/views/phone_list.json', {}, {
      query: {method:'GET', isArray:true}
    });
  }]);

J'ai utilisé ce service avec le controller PhoneListCtrl dans le fichier controllers.js :

phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phones',
  function($scope, Phones) {
    $scope.phones = Phones.query();
    $scope.orderProp = 'age';
  }]);

Grâce à Batarang, je vois que le JSON est bien chargé.


Affichage dans le template

Il ne reste donc qu'à charger les bons éléments dans le template  partials/phone-list.html

Titre et description :

Les champs s'appellent maintenant node_title, nid et description :
<a href="#/phones/{{phone.nid}}">{{phone.node_title}}</a>
<p>{{phone.description}}</p>

Image :

Pour le champ image, j'injecte directement le html provenant du JSON grâce à la directive ng-bind-html. Pour qu'elle fonctionne, il me faut ajouter la dépendance ngSanitize à mon module phonecatService dans services.js :
var phonecatServices = angular.module('phonecatServices', ['ngResource', 'ngSanitize']);

Je dois également ajouter le script angular-sanitize.js dans index.html :
<script src="lib/angular/angular-sanitize.js"></script>
puis, je complète mon fichier phone-list.html pour l'affichage des images
<a href="#/phones/{{phone.nid}}" class="thumb" ng-bind-html="phone.images"></a>

Filtres

Tout est bien chargé, il ne me reste plus qu'à faire fonctionner les filtres en changeant les noms des champs.

Dans partials/pḧone-list.html :
<option value="node_title">Alphabetical</option> 
<option value="nid">Newest</option>

Dans controller.js , indiquer la valeur par défaut (nid) :
$scope.orderProp = 'node_title';

La liste des téléphones fonctionne maintenant ! Finalement, je n'ai eu à changer que les noms de champs !

Page détail

Création du service dans Drupal

Rien à faire ou presque pour cette page détail coté Drupal puisque le module Service fournit par défaut un moyen de récupérer le JSON d'un node. Il m'a suffit pour cela de cocher le service node/retrieve pour que mon webservice fonctionne avec l'adresse :
http://ng-drupal.tld/drupal/phones/node/1.json


Modification du service dans AngularJS

Coté AngularJS, il faut modifier l'url de la ressource Phone :
phonecatServices.factory('Phone', ['$resource',
  function($resource){
    return $resource('http://ng-drupal.tld/drupal/phones/node/:phoneId.json', {}, {
      query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
    });
  }]);

L'argument n'est donc plus le nom du téléphone, mais le nid drupal du node.


Toutes les données sont chargées, il faut maintenant les afficher !

Modification du phone-detail.html avec le json de Drupal

J'ai eu un travail assez long pour récupérer les valeurs intéressantes dans le JSON généré par Drupal avec l'application Angular. Il a fallu gérer quatre types de données  :

Type texte simple :

Par exemple, le champ RAM est affiché comme cela à l'origine :
{{phone.storage.ram}}

Il devient alors :
{{phone.field_ram.und[0].value}}

« und » est bien connu des drupaliens et veut dire undefined, c'est à dire que la langue n'est pas définie pour ce champ. 0 est l'indice du champ en cas de valeur multiple

Type texte multiple :

Il est possible d'avoir un champ qui contient un nombre non défini de valeurs sous Drupal. C'est par exemple le cas du champ dimensions.
J'ai donc dû boucler sur le tableau phone.field_dimensions.und pour récupérer et afficher chaque valeur :
<dd ng-repeat="dim in phone.field_dimensions.und">{{dim.value}}</dd>

Type booléen :

A l'étape n°9 du tutoriel Angular, un filtre checkmark est ajouté. Il évalue si la valeur est à true ou false alors que Drupal retourne 0 ou 1. J'ai donc dû changer la règle du filtre :
return input == 1 ? '\u2713' : '\u2718';

Et afficher le résultat ainsi :
{{phone.field_infrared.und[0].value | checkmark}}

Les images :

J'ai rencontré un petit souci pour afficher les images car les données du json ne fournissaient qu'un lien vers une url Drupal (public://).

Conclusion


Après la rédaction de ce tutoriel, je n'ai pas de réponse pour savoir si la cohabitation entre Drupal et AngularJS est possible. J'ai trouvé des solutions à tous les problèmes qui se sont posés à moi et j'ai pu entrevoir le potentiel d'AngularJS . Cela m'a donné l'envie d'aller plus loin :
  • Comment fonctionnerait une authentification ?
  • Peut-être aurait-il été encore plus facile et versatile de créer un petit module Drupal utilisant la fonction drupal_json_encode (lien?)
  • L'initiative développée pour Drupal 8 (https://groups.drupal.org/wscci) facilitera-t-elle ce type de développement ?
N'hésitez pas à partager vos réflexions sur le sujet. Si vous avez un projet à développer et que vous aimeriez utiliser Drupal et AngularJS, n'hésitez pas à me contacter !

Téléchargez le dossier app de l'application modifiée.

5 commentaires:

  1. Très intéressant.

    J'y avais déjà songé mais n'avais jamais pris le temps de tester cette solution.
    Elle semble viable mais certainement un peu lourde.

    Pour la partie back de mes applis Angular je mise en général sur le framework Silex. Bien que très performant ce framework n'apporte pas du tout le confort de saisie d'un CMS tel que Drupal.

    Je testerai cette solution pour ma prochaine appli mobile.
    Merci bcp pour cet article

    RépondreSupprimer
  2. Bonjour et merci pour cet article ! Comment as tu résolu le problème d'URL des médias ?
    Question performance, quelle est la perte approximativement ?
    Merci

    RépondreSupprimer
    Réponses
    1. Bonjour,
      Pour la récupération d'URL, il y a deux cas :
      => Pour la liste, le JSON est créé par une vue, donc là, la balise img est déjà formatée avec l'url complète
      => Pour le détail, on a le nom du fichier, il faut donc reconstruire l'url qui est toujours la même sous Drupal :
      img ng-src="{{drupalBaseUrl}}/sites/default/files/{{img.filename}}" ng-click="setImage('{{drupalBaseUrl}}/sites/default/files/' + img.filename)"

      Pour ce qui est des performances, tout dépend de là ou on se place, si on part d'AngularJS, forcément on doit perdre un peu, le chargement des json est de 260ms en local chez moi sans aucun cache coté Drupal.
      Si on se place coté Drupal, on part de quelque chose de très lent lorsqu'aucun cache n'est activé, donc, on y gagne forcément.

      Supprimer
  3. Bonjour,
    Est-il possible de mettre un nouveau lien pour http://www.atelierdrupal.net/angular_phonecat-7.x-0.10.tar
    Il est down :(

    Merci

    RépondreSupprimer