Avec AngularJS, on utilise des expressions pour le binding, et avec de nombreuses directives. Elles ressemblent à des expressions JavaScript, mais n'en sont pas. Elles ne sont pas directement évaluées par l'interpréteur JavaScript, mais parsées et exécutées par AngularJS. Du coup on ne peut utiliser qu'un sous-ensemble des opérateurs et des mots-clefs de JavaScript, et elles divergent aussi de JavaScript sur quelques aspects.
Garder les expressions simples
La règle importante, c'est de ne pas mettre de code dans les vues. Le code mis dans les vues ne peut pas faire l'objet de tests unitaires, et est difficilement gérable.
Donc dans les expressions, il faut se limiter à du binding sur des propriétés des objets du scope, à des appels de fonctions du scope, à des comparaisons simples quand il s'agit d'expressions booléennes, à des choses basiques de ce genre, et ne jamais y mettre du code non trivial qui mérite d'être testé. Il vaut mieux remplacer les expressions complexes par des appels à des fonctions qu'un contrôleur publie dans son scope.
Les mots-clefs et opérateurs disponibles
La liste des mots-clefs utilisables est particulièrement courte, puisqu'ils sont au nombre de quatre, et leur rôle est le même qu'en JavaScript :
- true
- false
- undefined
- null
- + - * / %
- ( )
- == != < > <= >=
- !
- && ||
- === !== en version 1.1.2
- = (affectation d'une variable)
Du coup les opérateurs && et || vont comme en JavaScript court-circuiter l'évaluation dès que c'est possible, et renvoyer la valeur du dernier opérande évalué, donc potentiellement tout autre chose qu'une valeur booléenne.
Les opérateurs de comparaison stricte, c'est-à-dire sans conversion automatique de type, ont été ajouté en version 1.1.2, mais ils ne sont pas disponibles dans la branche 1.0.
L'opérateur d'affectation permet d'affecter une valeur à une propriété du scope. En règle générale, ce n'est pas une bonne idée de publier des choses dans le scope depuis une expression de la vue. Mais il y a quand même quelques cas où c'est tolérable, par exemple pour effacer la valeur d'un champ de filtrage en cliquant sur un lien, une petite croix à côté du champ. On peut préférer faire ça avec un ng-click="search = ''", sans avoir envie de créer une fonction dans le scope juste pour ça.
Il y a un opérateur classique qui manque à la liste, c'est l'opérateur conditionnel ternaire ... ? ... : ... qui n'est effectivement pas disponible dans le langage d'expressions d'AngularJS. Il y a eu des discussions sur le fait de le rajouter, ça n'a pas été fait du moins pour l'instant, l'idée étant que cet opérateur qui s'apparente à un if pourrait inciter les développeurs à mettre trop de logique dans les expressions. On peut contourner cette absence en utilisant une des deux constructions suivantes :
condition && valeurTrue || valeurFalse
{true: valeurTrue, false: valeurFalse}[condition]
Mais ce n'est pas forcément recommandé, le réflexe devrait être plutôt de créer une fonction dans le scope que d'utiliser une de ces deux syntaxes alambiquées.
Propriétés et méthodes
Tous les noms d'identifiants utilisés dans des expressions AngularJS sont recherchés comme des propriétés du scope. C'est la première chose qu'on utilise dans les bindings, je ne vous apprends rien. On peut utiliser le point ou les crochets pour accéder à une propriété d'une objet, ou les crochets uniquement pour accéder à un élément d'un tableau par son index, exactement comme en JavaScript.
La grosse différence, c'est que si dans une expression on essaie d'accéder à une propriété d'un objet nul ou non défini, ça ne provoque aucune erreur. Pareil pour un élément d'un tableau nul ou non défini. Et il est indispensable que ça fonctionne comme ça : une vue ne plante pas si des données ne sont pas encore chargées, simplement ces données apparaîtront lorsqu'elles seront disponibles.
La contrepartie du fait que ça ne provoque aucune erreur, c'est que si l'on a réellement fait une erreur, par exemple une faute de frappe, on ne verra rien. Raison de plus de ne pas mettre de code dans les expressions des vues, car le debug n'y est pas pratique.
On peut bien sûr accéder à n'importe quelle propriété d'un objet JavaScript, par exemple à la propriété length d'un tableau, ou appeler n'importe quelle méthode.
Filtres
Là évidemment c'est quelque chose qui n'existe pas dans des expression JavaScript. Les filtres sont propres à AngularJS. Si le symbole utilisé pour les filtres est la barre verticale, ce n'est pas sans rapport avec les “pipes” d'Unix : un filtre prend des données en entrée, et fournit des données en sortie. Il peut aussi recevoir des paramètres, indiqués à la suite du nom du filtre, séparées par le caractère ':'. Et on peut chaîner plusieurs filtres.
data | filterName:param1:param2 | otherFilter
Création d'objets et tableaux
Dans une expression AngularJS, on peut créer un objet ou un tableau comme on le ferait en JavaScript, avec {...} ou [...].
Là encore c'est à utiliser avec parcimonie, mais ça peut quand même être très utile pour alimenter les paramètres d'une fonction (ou d'un filtre) qui attend un objet ou un tableau.
list | paginate:{index: currentPage, size: pageSize}
Priorités
Les priorités des opérateurs ne sont pas déroutantes puisque ce sont les mêmes qu'en JavaScript. On peut juste se demander à quel niveau de priorité se classe le filtre.Et bien voici un tableau des opérateurs triés par priorité, des plus prioritaires aux moins prioritaires, réalisé d'après le code du framework :
Opérateurs | Description |
---|---|
. [...] (...) | propriété d'un objet, élément d'un tableau, appel de méthode |
(...) [...] {...} | parenthèses pour grouper des opérations, création d'un tableau ou d'un objet |
! - + | opérateurs + et - unaires (ex : -5) |
* / % | |
+ - | opérateurs + et - binaires (ex : 8 - 5) |
< > <= >= | |
== != === !== | en version 1.1.2 pour === et !== |
&& | |
|| | |
= | affectation d'une variable |
| | filtres |
; | séparateur d'instructions |
On voit donc que le filtre est quasiment ce qu'il y a de moins prioritaire, encore moins que l'opérateur d'affectation (mais je ne suis pas sûr que mélanger filtres et affectations soit une grande idée).
Quasiment, parce que le point-virgule séparant deux instructions est encore moins prioritaire que le filtre. Mais franchement je ne vois pas quel bon usage peut-être fait d'un séparateur d'instructions dans une expression AngularJS. D'ailleurs un commentaire dans le code laisse entendre qu'il pourrait être supprimé.
Et s'il manque quelque chose ?
Pour ajouter une fonctionnalité utilisable dans les expressions, vous avez deux possibilités :
- publier une fonction dans un scope
- créer un filtre
Si c'est pour un usage bien localisé, il vaut mieux publier une fonction dans le scope d'un contrôleur, et vous pourrez l'utiliser seulement dans ce scope-là.
Si le besoin est général, faites un filtre, ce n'est pas vraiment plus compliqué à créer qu'une simple fonction JavaScript, et il sera disponible dans toutes les expressions de toutes les vues.
Compiler une expression
Le service $parse intégré au framework permet de compiler une expression AngularJS, ça la transforme en une fonction qui peut être exécutée en lui passant le scope. C'est quelque chose qui peut être utile lors de l'écriture de directives.
Mais attention à ne pas réinventer la roue. Quand il est pertinent de créer un scope isolé pour une directive, alors c'est plus simple de définir des propriétés dans ce scope isolé en utilisant les opérateurs préfixes @, = et & pour référencer des attributs de l'élément HTML. Dans un tel cas on n'aura pas besoin de compiler manuellement les attributs contenant des expressions, le framework s'en charge.
Je crois que le paragraphe précédent doit ressembler à du chinois pour ceux qui n'ont jamais écrit de directives. Il faudra que j'écrive quelques articles là-dessus, mais le sujet est vaste, et ça sera pour une autre fois.
En tout cas j'espère que celui-ci aura permis de clarifier les idées sur le langage d'expressions d'AngularJS.
Bravo Thierry, AngularJS est génialissime et ce blog est passionnant !
RépondreSupprimerMerci pour ces précisions !
RépondreSupprimerArticle bien écrit et clair, comme toujours :)