Ember - les propriétés calculées

Publié le 26 mars 2014 par Nicolas Cavigneaux | front

Cet article est publié sous licence CC BY-NC-SA

Dans cet article à propos d’Ember, nous allons voir comment mettre en place des propriétés calculées (computed properties) au niveau modèles et contrôleurs. Les exemples seront écrits en coffeescript dans un souci de concision.

Les propriétés calculées sont des propriétés que vous pouvez utiliser dans vos vues pour afficher des informations et qui seront mises à jour automatiquement si l’une des valeurs permettant le calcul change.

C’est donc très pratique pour éviter d’avoir à le faire en amont (informations calculées dans le json) ou de mettre en place des helpers pour générer ces affichages calculés.

Un modèle de base

Pour commencer prenons comme exemple un modèle de base qui nous permettra ensuite de l’enrichir avec des propriétés calculées. Nous allons utiliser un modèle mais les propriétés calculées à venir pourraient être implémentées de la même façon dans les contrôleurs.

App.User = Ember.Object.extend
  firstName: null
  lastName: null
  age: null
  email: null
  phone: null
  mobile: null
  friends: []

Les différentes propriétés définies ici ne sont absolument pas nécessaires en l’état. Elle sont ici uniquement dans un souci de clarté. On peut d’ailleurs ne pas les passer à l’instanciation de l’objet ou même en passer d’autres qui ne sont pas listées ici.

Pré-définir ces attributs serait plus intéressant si nous souhaitions avoir des valeurs par défaut.

Nous pourrions donc instancier et utiliser notre objet comme suit :

nico = App.User.create
  firstName: "Nicolas"
  lastName: "Cavigneaux"

nico.get("firstName") # "Nicolas"
nico.get("phone")     # null
nico.get("foo")       # undefined

Nous pouvons maintenant passer à l’implémentation de nos propriétés calculées.

Ajout de propriétés calculés

Les propriétés calculées sont à utiliser si vous souhaitez créer une valeur en fonction d’une ou plusieurs propriétés données. Il s’agit ici de synthétiser d’autres propriétés. Les propriétés calculées ne doivent pas contenir de logique applicative et ne doivent donc pas causer d’effet de bord quand elles sont appelées.

Propriétés calculées simples

Ajoutons maintenant une propriété calculée qui va nous permettre de générer le nom complet de l’utilisateur :

App.User = Ember.Object.extend
  firstName: null
  lastName: null
  age: null
  email: null
  phone: null
  mobile: null
  friends: []

  fullName: (->
    @get("firstName") + " " + @get("lastName")
  ).property("firstName", "lastName")

Utilisons cette nouvelle propriété :

nico = App.User.create
  firstName: "Nicolas"
  lastName: "Cavigneaux"

nico.get("fullName") # "Nicolas Cavigneaux"

L’idée derrière les propriétés calculées est donc d’écouter les changements sur une ou plusieurs propriétés via la méthode property. Si l’une de ces propriétés change, notre méthode sera notifiée et à nouveau exécutée pour mettre à jour sa valeur et qu’elle soit répercutée en cascade dans les templates.

Plutôt pratique d’autant plus que si l’utilisateur venez à modifier son prénom ou son nom, le changement serait reflété directement dans la page aux endroits où la propriété fullName est utilisée.

Il est à noter qu’il n’est pas nécessaire de passer des propriétés à écouter à property. Si vous n’en passez pas, l’appel à property servira simplement à transformer votre méthode en propriété utilisable. En effet, si vous créez une méthode, elle n’est pas considérée automatiquement comme une propriété et n’est donc pas utilisable dans les templates.

Propriétés calculée chaînée

Il est également possible de chaîner les propriétés calculées, c’est à dire qu’il est tout à fait possible d’utiliser une propriété calculée dans une autre propriété calculée. Nous pourrions par exemple vouloir générer une adresse email standardisée sous la forme “prénom nom ". Plutôt que de devoir ré-utiliser indépendamment le prénom et le nom, nous allons faire appel à notre propriété calculée `fullName` :

App.User = Ember.Object.extend
  firstName: null
  lastName: null
  age: null
  email: null
  phone: null
  mobile: null
  friends: []

  fullName: (->
    @get("firstName") + " " + @get("lastName")
  ).property("firstName", "lastName")

  emailWithName: (->
    "#{fullName} <#{email}>"
  ).property("fullName", "email")

Utilisons cette nouvelle propriété :

nico = App.User.create
  firstName: "Nicolas"
  lastName: "Cavigneaux"
  email: "nico@blog.com"

nico.get("emailWithName") # "Nicolas Cavigneaux <nico@blog.com>"

Deux choses sont à remarquer ici. Tout d’abord dans notre méthode emailWithName nous utilisons notre propriété calculée fullName plutôt que de prendre firstName et lastName, on évite donc la duplication. La seconde chose est qu’on écoute les changements (via property) directement sur la propriété calculée plutôt que d’écouter firstName et lastName. C’est en ce sens que les propriétés calculées sont chainables.

Propriétés calculée avec conditions

Vous l’aurez certainement deviné mais précisons le tout de même, il est possible de mettre du code bien plus évolué que celui présenté jusqu’à maintenant dans les propriétés calculées. On va donc pouvoir par exemple mettre des conditions pour retourner une valeur différente en fonction de la situation. N’oubliez pas cependant qu’il est plus que conseillé que votre propriété calculée retourne toujours la même valeur au sein d’un rendu.

Un exemple simple serait d’avoir une méthode qui retourne le numéro de téléphone le plus utile à savoir le téléphone portable ou le fixe ou sinon une chaîne indiquant qu’aucun numéro n’est disponible :

bestPhone: (->
  @get("mobile") or @get("phone") or "non disponible"
).property("phone", "mobile")

Affectation via une propriété calculée

Jusqu’à présent nous avons utilisé les propriétés calculées comme des “getters” mais il est également possible de s’en servir pour affecter des valeurs à des propriétés. La propriété calculée permettra donc de récupérer une information mais aussi d’affecter des valeurs à plusieurs propriétés d’un coup. Prenons l’exemple de la méthode fullName, il serait intéressant de pouvoir lui passer le nom complet puis qu’elle redispatch automatiquement les informations dans les propriétés concernées :

fullName: ( (key, value) ->
  # Setter
  if arguments.length > 1
    [firstName, lastName] = value.split(/\s+/)
    @set "firstName", firstName
    @set "lastName", lastName

  # Getter
  @get("firstName") + " " + @get("lastName")
).property("firstName", "lastName")

Pour permettre à votre propriété calculée de servir à la fois de getter et de setter, il faut faire en sorte que sa signature accepte les deux formes. Le getter passe en premier argument la propriété qui a été modifiée et qui déclenche donc l’appel à la propriété calculée. Le setter quant à lui passe ce même premier argument ainsi que la nouvelle valeur en seconde position.

Nous vérifions donc arguments qui est un tableau contenant la liste des arguments passés lors de l’appel de la fonction. S’il y a plus d’un argument, l’appel a pour but de définir la valeur. On récupère donc la valeur qui est passée, valeur censée contenir prénom et nom, pour la découper en mots et affecter le prénom puis le nom grâce à des appels à @set.

Dans tous les cas nous retournons la valeur calculée pour fullName. L’intérêt de retourner la valeur calculée même dans le cas d’une affection est qu’Ember en profitera pour cacher cette valeur directement et non pas au prochain appel au getter.

Voici donc un exemple d’utilisation :

user = App.User.create
  firstName: "Nicolas"
  lastName: "Cavigneaux"

user.get("fullName") # "Nicolas Cavigneaux"

user.set "fullName", "Yehuda Katz"
user.get "firstName" # "Yehuda"
user.get "lastName"  # "Katz"

Propriété calculée sur un jeu de données

Les propriétés calculées peuvent aussi être utilisées sur les tableaux. Il est en fait très courant de devoir écrire une propriété calculée basée sur les éléments d’un tableau. Vous pouvez donc non seulement vérifier si le contenu d’un tableau change mais aussi écouter les changements sur un attribut donné des éléments de ce tableau.

Pour l’exemple disons que mon objet utilisateur à une liste d’amis, ces amis pouvant être connectés ou déconnectés. Nous voudrions afficher sur notre page le nombre de nos amis mais aussi le nombre de nos amis connectés.

Un propriété virtuelle @each est mise à disposition pour pouvoir travailler sur les collections via les observers. On peut donc sur notre collection friends appliquer .@each pour signifier qu’on veut observer les changements sur les éléments eux même plutôt que de simplement observer la propriété friends. Sans ce .@each, seule une ré-affectation complète de friend déclencherait les propriétés calculées.

friendsCount: (->
  @get("friends").get("length")
).property('friends.@each')

onlineFriendsCount: (->
  @get("friends").filterBy('online', true).get("length")
).property('friends.@each.online')
user = App.User.create
  friends: [
    App.User.create({firstName: "Yehuda", lastName: "Katz", online: true})
    App.User.create({firstName: "Jim", lastName: "Weirich", online: false})
  ]

user.get("friendsCount") # 2
user.get("onlineFriendsCount") # 1

Grâce à ce .@each, nous observons le contenu de notre collection et les observers sont déclenchés pour les situations suivantes :

Pour friendsCount :

  • un élément est ajouté au tableau
  • un élément est supprimé du tableau
  • la propriété friends est remplacée par un nouveau tableau

Pour onlineFriendsCount :

  • les trois comportements décrits pour friendsCount
  • la propriété online de l’un des objets friends change

Les valeurs affichées dans les templates par ces deux propriétés calculées seront mises à jour en temps réel si un amis est ajouté ou retiré, si le tableau friends est complètement redéfini ou si un ami de la liste voit sa propriété online changer de valeur.

Les propriétés calculées pré-définies

Les propriétés calculées sont tellement pratiques et utilisées dans Ember que les plus courantes sont déjà accessibles directement dans le framework. Voici ce qu’Ember nous fourni de base. Pour les exemples je vous invite à consulter la documentation d’Ember qui est très claire à ce sujet. En ce qui me concerne je décrierai simplement l’utilité de chaque méthode.

Alias

  • computed.alias : crée un alias (getter et setter) pour la propriété donnée.
  • computed.oneWay : crée un alias getter uniquement. La méthode set sur la nouvelle propriété changera uniquement la valeur de cette propriété sans affecter la propriété d’origine.
  • computed.readOnly : crée un alias getter uniquement. Il est impossible d’utiliser la méthode set sur cette nouvelle propriété.
  • computed.defaultTo : utilise la valeur de la propriété référencée si aucune valeur n’est pas définie sur la nouvelle propriété.

Booléens

  • computed.and : ET logique sur les propriétés passées en paramètres
  • computed.or : OU logique sur les propriétés passées en paramètres
  • computed.not : retourne la valeur booléenne inverse pour la propriété passée en paramètres
  • computed.any : retourne la valeur du premier élément évalué à true dans la liste des propriétés passées en paramètres

Tableaux

  • computed.filter : retourne un tableau filtré du tableau passé en premier paramètre. Le filtre est une fonction passée en second paramètre
  • computed.filterBy : retourne un tableau filtré du tableau passé en premier paramètre. Le second paramètre est la propriété à vérifier, le troisième étant la valeur souhaitée pour la propriété.

  • computed.map : retourne un tableau de valeurs. Le premier paramètre est le tableau d’entrée, le second la fonction générant les valeurs pour chaque élément du tableau.
  • computed.mapBy : retourne un tableau de valeurs. Le premier paramètre est le tableau d’entrée, le second la propriété a utiliser comme valeur de retour.

  • computed.intersect : retourne un tableau d’intersection entre l’ensemble des tableaux passés en paramètres
  • computed.uniq  : retourne un tableau à valeurs uniques depuis l’ensemble des tableaux passés en paramètres
  • computed.union : alias de computed.uniq
  • computed.setDiff : retourne un tableau des éléments présents dans le premier tableau mais pas dans le deuxième

  • computed.max : retourne la valeur maximale présente dans le tableau passé en paramètre
  • computed.min : retourne la valeur minimale présente dans le tableau passé en paramètre
  • computed.sum : retourne la somme des valeurs présentes dans le tableau passé en paramètre

  • computed.sort : retourne un tableau trié du tableau passé en premier paramètre. Le second paramètre peut être la propriété des éléments du tableau servant au tri ou une fonction de tri

Propriétés

  • computed.equal : vérifie que la propriété passée en premier argument est égale à la valeur passée en deuxième argument
  • computed.empty : vérifie que la propriété passée en premier argument (chaîne, tableau, fonction) est vide
  • computed.notEmpty : vérifie que la propriété passée en premier argument (chaîne, tableau, fonction) n’est pas vide
  • computed.collect : retourne les valeurs de l’ensemble des propriétés passées en paramètres
  • computed.none : retourne true si la valeur de la propriété passée est null ou undefined

  • computed.gt : retourne true si la valeur de la propriété passée en premier paramètre est supérieure à la valeur passée en second paramètre
  • computed.gte : retourne true si la valeur de la propriété passée en premier paramètre est supérieure ou égale à la valeur passée en second paramètre
  • computed.lt : retourne true si la valeur de la propriété passée en premier paramètre est inférieure à la valeur passée en second paramètre
  • computed.lte : retourne true si la valeur de la propriété passée en premier paramètre est inférieure ou égale à la valeur passée en second paramètre
  • computed.match : retourne true si la valeur de la propriété passée en premier paramètre satisfait l’expression rationnelle passée en second paramètre

Conclusion

Les propriétés calculées sont une des bases d’Ember. Extrêmement pratiques, je vous conseille vivement de bien vous en imprégner pour vous éviter des implémentations parfois complexes et ainsi garder votre code concis, lisible et maintenable.


L’équipe Synbioz.

Libres d’être ensemble.