AngularJS est un framework MVC développé par Google. Il est de plus en plus utilisé et fournit les outils et les concepts pour construire une application “single page”. Nous en avons déjà parlé sur le blog (ici, ici et ici). Dart est un langage créé au sein de Google qui veut le pousser comme le remplaçant du JavaScript. Il n’est donc pas étonnant que les deux technologies se rencontrent pour former AngularDart, framework web développé par la même équipe que AngularJS.
AngularDart a été repensé pour résoudre les différents problèmes de son grand frère. En utilisant, entre autres choses, le paradigme orienté objet, le système d’annotations et le typage des variables offert par le langage Dart.
Limitations
L’article a pour cible les développeurs AngularJS souhaitant découvrir la version Dart du framework. Nous allons nous attacher à présenter AngularDart en le comparant à AngularJS. Nous vous proposons de voir à travers divers exemples les principales différences entre les deux frameworks.
Attention cet article s’appuie sur la version 0.10 d’AngularDart. Ce n’est pas une version stable et l’API a tendance à subir des changements majeurs (nous l’avons désagréablement vécu).
Le databinding et les expressions
Sur ce point précis, les deux frameworks sont quasiment identiques. Vous pouvez toujours utiliser les “double curly braces {{ … }}” ou la directive ng-bind ainsi que le classique ng-model.
Exemple JS et Dart
{{ 1 + 2 }} <h2>Hello {{name}}!</h2> Name: <input type="text" ng-model="name">
Bootstrapper une application
Les deux frameworks se basent sur l’attribut ‘ng-app’ d’un élément HTML. L’application est alors bindée sur cet élément. En Dart, chaque application démarre via une méthode main(). L’application AngularDart nécessite donc de bootstrapper manuellement l’application dans ce main. En AngularJS, ceci est fait de façon automatique au chargement du framework.
Exemple JS
Fichier html
<div ng-app> </div>
Exemple Dart
Fichier html
<div ng-app> </div>
Fichier main.dart
import 'package:angular/angular.dart'; main() { applicationFactory().run(); }
Modules
Les modules sont des containers contenant les parties d’une application.
Exemple JS
angular.module('parentModule', [‘childModule1’, ‘childModule2’, ...]);
Exemple Dart
class ParentModule extends Module { ParentModule() { install(new ChildModule1()); install(new ChildModule2()); ... } } main() { applicationFactory().addModule(new ParentModule()).run(); }
Nous voyons ici que le module AngularDart est une implémentation de la classe ‘Module’. Le module principal est fourni au bootstrap de l’application.
Les controllers
Exemple JS (AngularJS >= 1.2.x)
Fichier html
<div ng-controller="RoomCtrl as room"> <div class="button" ng-click="room.openDoor()"> Open </div> </div>
Fichier javascript
var app = angular.module("app", []); app.controller("RoomCtrl", function() { this.openDoor = function(){ console.log("creak"); };span });
Exemple Dart
Fichier html
<div room-ctrl ><div class="button" ng-click="roomCtrl.openDoor()"> Open </div></div>
Fichier dart
@Controller(selector: '[room-ctrl]', publishAs: 'roomCtrl') class RoomController { openDoor() => print(“creak”); } class AppModule extends Module { AppModule() { type(RoomController); } } main() { ngBootstrap(module : new AppModule()); }
Nous pouvons constater que le principe “controller as” introduit dans AngularJS 1.2 est repris dans la version Dart.
Le controller Dart est une classe à part entière décrite grâce à l’annotation @Controller. Le selector permet de binder le controller avec la partie HTML tandis que le publishAs indique le nom qui sera utilisé dans le DOM.
Les directives
Les directives sont au cœur d’Angular et sont notamment un des passages difficiles d’AngularJS (cf article http://blog.xebia.fr/2013/11/04/initiation-aux-directives-angularjs/)
Dans AngularDart, les directives ont été scindées en 2 parties : les directives et les components.
On ne crée plus de DOM avec les directives dans AngularDart, elles sont désormais dédiées à la décoration de balises existantes. C’est à dire qu’elles ne feront que modifier/ajouter un comportement sur une balise donnée. De plus, elle ne sont plus capables de créer un nouveau scope. Comme elles n’ont qu’un rôle de « composition » sur la balise, on peut cependant attacher autant de directive que l’on souhaite sur un même élément.
L’exemple ci-dessous est une directive qui ajoute la classe “match” quand le contenu de l’input correspond à l’expression régulière.
<input type="text" vs-match="^\d\d$">
Exemple JS
directive("vsMatch", function(){ return { restrict: 'A', scope: {pattern: '@vsMatch'}, link: function(scope, element){ var exp = new RegExp(scope.pattern); element.on("keyup", function(){ exp.test(element.val()) ? element.addClass('match') : element.removeClass('match'); }); } }; });
Exemple Dart
@Decorator(selector: '[vs-match]') class Match implements NgAttachAware{ @NgAttr("vs-match") String pattern; Element el; Match(this.el); attach(){ final exp = new RegExp(pattern); el.onKeyUp.listen((_) => exp.hasMatch(el.value) ? el.classes.add("match") : el.classes.remove("match")); } }
Une des volontés derrière AngularDart tout comme derrière AngularJS 2.0 (avec qui il partagent quelques autres points communs), est de se rapprocher des standards et notamment, des WebComponent. Pour faire court, un WebComponent est un widget composé d’un template (du HTML), d’un style (du CSS) et d’un comportement (du Dart) et se trouvera sous la forme d’une balise HTML grâce au ShadowDOM. C’est justement cette technologie que Google souhaite utiliser pour permettre de construire des interfaces dans AngularDart et ce, en se reposant sur la librairie Polymer. Polymer, fait office de polyfills pour supporter tous les standards cités ci-avant non supportés par les browsers.
L’exemple ci-dessous est un composant très simple qui affiche ou cache son contenu.
<toggle button="Toggle"> <p>Inside</p> </toggle>
Exemple JS
directive("toggle", function(){ return { restrict: 'E', replace: true, transclude: true, scope: {button: '@'}, template: "<div><button ng-click='toggle()'>{{button}}</button><div ng-transclude ng-if='showContent'/></div>", controller: function($scope){ $scope.showContent = false; $scope.toggle = function(){ $scope.showContent = !$scope.showContent ; }; } } })
Exemple Dart
@Component( selector: "toggle", publishAs: 'tgl', template: "<button ng-click='tgl.toggle()'>{{tgl.button}}</button><content ng-if='tgl.showContent'/>", cssUrl: 'toggle.css' ) class Toggle { @NgAttr("button") String button; bool showContent = false; toggle() => showContent = !showContent; }
La déclaration d’une directive se fait par l’annotation @Decorator, celle de component par @Component.
Il existe 3 possibilités différentes de manipuler les données au sein d’une directive ou d’un composant :
- @NgAttr : permet de passer une string (équivalent au @ du AngularJS) ;
- @NgOneWay : évalue la valeur d’attribut et passe le résultat au component/directive de manière unidirectionnelle ;
- @NgTwoWay : la même chose mais de manière bidirectionnelle (équivalent au = du AngularJS).
Les filtres
Pour décrire un filtre, AngularDart se base sur une annotation @Formatter.
Exemple JS
app.filter("truncate", function () { return function (text) { return text.length > 12 ? text.substring(0, 12) + '...' : text; }; });
Exemple Dart
@Formatter(name:"truncate") class TruncateFilter { String call(String url) { if (url.length > 12) return url.substring(0, 12) + "..."; return url; } }
Un filtre (ou formatter) en AngularDart est une simple classe annotée avec @Formatter qui définit une méthode call() prenant au moins un paramètre en entrée (modèle à formater) et retournant une valeur formatée. Les autres paramètres servent au paramétrage du formatter.
L’utilisation des formatter côté HTML est identique dans JS et Dart :
<td>{{log.url | truncate}}</td>
Quant aux formatters fournis par le framework, nous noterons peu de différences.
Il y a quand même quelques subtilités liées aux structures des langages. Par exemple, dans AngularJS le filter dans son utilisation basique (http://code.angularjs.org/1.2.16/docs/api/ng/filter/filter) permet de filtrer une collection d’objet par n’importe quel champ.
Search: <input ng-model="searchText"> <table id="searchTextResults"> <tr><th>Name</th><th>Phone</th></tr> <tr ng-repeat="friend in friends | filter:searchText"> <td>{{friend.name}}</td> <td>{{friend.phone}}</td> </tr> </table>
Pour arriver au même résultat avec AngularDart d’après la documentation (https://docs.angulardart.org/#angular-formatter.Filter) il faut utiliser une syntaxe quelque peu différente :
Dart:
... <tr ng-repeat="friend in friends | filter:{$:searchText}">..</tr> ...
Pourtant après quelques tests, nous ne sommes pas arrivé à le faire marcher…
Les services
L’équipe d’Angular préconise de garder les controllers les plus léger possible et de mettre un maximum de logique métier dans des services.
Exemple JS
app.factory('dateService’, function () { return { today: function() { return new Date(); }; }; }); // Using this service app.controller(“myCtrl”, function(dateService) { console.log(“Today is ” + dateService.today()); });
Exemple Dart
import 'package:angular/angular.dart'; @Injectable() class DateService { Date today() => new Date(); } // Using this service // - do not forget to declare this service as type in your module @Controller(selector: '[my-ctrl]', publishAs: 'myCtrl') class MyController { MyController(DateService dateService) { print(dateService.today().toString()); } }
Nous remarquons ici l’utilisation de l’annotation @Injectable() pour déclarer un service, ce qui rend plus clair le code et le framework plus robuste.
Communiquer avec votre serveur
Exemple JS
app.controller(“myCtrl”, function($http) { $http.get('/logs').success(function (data) { $scope.logs = data; }); });
Exemple Dart
@Controller(selector: '[my-ctrl]', publishAs: 'myCtrl') class MyController { var logs; MyController(Http http) { http.get("/logs").then((HttpResponse httpResponse) { logs = httpResponse.data; }); } }
Le service Http AngularDart comme le $http d’AngularJS permet de communiquer avec un serveur. Cependant le service Http AngularDart retourne un Future, au lieu d’une promesse JS. Le Future Dart, comme la Promesse JS, représente un moyen d’obtenir une valeur dans le futur.
L’injection de dépendance
Ici, nous noterons une différence majeure avec AngularJS. L’injection de dépendances dans AngularDart est faite par type et non par nom. Nous pouvons alors voir des similitudes avec le framework Spring avec son annotation @Autowire.
Exemple JS
function GreetingController($http) { // injection by name // use $http service }
Exemple Dart
@Controller(...) class GreetingController { GreetingController(Http httpService) { // injection by type ! // use httpService } }
Il faut ajouter un élément important concernant l’injection de dépendance. Dans AngularDart, tous les types que vous créez doivent être déclarés dans le module de l’application, sans quoi, ces types ne seront pas disponibles pour l’injection. Ceci est valable pour toute les composantes de l’application, Service, Controller ou encore Formatter.
Dans l’exemple ci-dessus, il faut donc déclarer le controller dans le module de l’application:
class MyModule extends Module { MyModule() { type(GreetingController); } }
Le routeur
Exemple JS
app.config(['$routeProvider', function ($routeProvider) { $routeProvider. when('/', {templateUrl: 'views/log-list.html', controller: LogCtrl}). when('/logs/:logId', {templateUrl: 'views/log-details.html', controller: LogDetailCtrl }) }]);
Exemple Dart
import 'package:angular/angular.dart'; routeInitializer(Router router, RouteViewFactory views) { views.configure({ '/': ngRoute(path: '/', view: 'view-list.html', defaultRoute : true), 'detail': ngRoute(path: '/detail/:detailId', view: 'detail.html') }); } class MyModule extends Module { MyModule() { value(RouteInitializerFn, routeInitializer); // You have to disable PushState system to work in dartium factory(NgRoutingUsePushState, (_) => new NgRoutingUsePushState.value(false)); } }
La méthode routeInitializer respecte le prototype défini par AngularDart via RouteInitializerFn. Cette méthode définit l’ensemble des routes de l’application.
Pour utiliser le routeur dans AngularDart, vous devez l’initialiser dans le module.
Le routeur d’AngularDart est bien plus puissant que celui fourni de base dans AngularJS et s’inspire de UI-Router (https://github.com/angular-ui/ui-router). Il offre par exemple les routes imbriquées.
Exemple:
'recipe': ngRoute( path: '/recipe/:recipeId', mount: { 'view': ngRoute( path: '/view', view: 'view/viewRecipe.html') } // ... })
On peut ensuite utiliser ces routes:
…#/recipe/6/view
…#/recipe/6/edit
Le scope et les Zones
Dans AngularDart comme dans AngularJS 2.0 il y a désormais le concept des Zones. Une Zone est comparable à un ThreadLocal en Java, c’est-à-dire qu’elle permet de partager un état commun entre différentes callbacks. En pratique, les Zones vous permettront de vous débarrasser de $scope.$apply(), car toutes les modifications interviennent dans la même zone.
Exemple JS
controller("CountCtrl", function($scope){ var c = this; this.count = 1; setInterval(function(){ $scope.$apply(function(){ c.count ++; }); }, 1000); });
Exemple Dart
@Controller(selector: "[count-ctrl]", publishAs: 'ctrl') class CountCtrl { num count = 0; CountCtrl(){ new Timer.periodic(new Duration(seconds: 1), (_) => count++); } }
Comme vous pouvez le constater, dans AngularJS vous êtes obligé d’encapsuler la modification de la variable dans $scope.$apply(…), afin de déclencher le databinding. Ceci devient inutile en AngularDart.
Les limitations d’AngularDart par rapport à AngularJS
Dart ne fontionnant que sous Dartium, il n’est pas envisageable de déployer une application Dart en production. Toutefois, il est possible d’exporter une application Dart en Javascript afin qu’elle soit compatible avec tous les navigateurs. Il y aussi des imports type shim pour les anciens navigateurs ne supportant pas encore le shadow dom.
Conclusion
À l’heure actuelle, Dart et le framework AngularDart sont encore limités au navigateur Dartium. Seul ce dernier embarque la machine virtuelle permettant d’exécuter du Dart. Il est peu probable de voir débarquer cette VM dans les autres navigateurs rapidement (en dehors de Chrome), ce qui oblige à convertir les applications en JS.
AngularDart est un framework encore en version bêta. Son API bouge énormément ce qui peut avoir des impacts conséquents sur une application (cf le changelog de la version 0.10). La documentation est quant à elle peu fournie et pas tout le temps à jour (à cause des changements d’API réguliers).
Toutefois, malgré ces aspects négatifs, ce framework a des forces non-négligeables. Le langage Dart apporte une certaine robustesse par rapport au JS notamment grâce au typage optionnel (injection par type et non par nom).
Le système d’annotations est beaucoup plus Java friendly que la syntaxe JS, ce qui pourrait attirer des développeurs Java.
Enfin, Dart et AngularDart sont des projets portés par Google, ce qui a un impact de poids. Google pourrait par ailleurs vouloir unifier ses systèmes Android et ChromeOS autour de Dart afin de s’offrir une porte de sortie dans son conflit avec Oracle autour de Java.
Références
http://victorsavkin.com/post/72452331552/angulardart-for-angularjs-developers-introduction-to
https://angulardart.org/tutorial/
https://docs.angulardart.org/