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.
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.
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.
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.
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.
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")
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"
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
:
friends
est remplacée par un nouveau tableauPour onlineFriendsCount
:
friendsCount
online
de l’un des objets friends
changeLes 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 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.
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é.computed.and
: ET logique sur les propriétés passées en paramètrescomputed.or
: OU logique sur les propriétés passées en paramètrescomputed.not
: retourne la valeur booléenne inverse pour la propriété passée en paramètrescomputed.any
: retourne la valeur du premier élément évalué à true dans la liste des propriétés passées en paramètrescomputed.filter
: retourne un tableau filtré du tableau passé en premier paramètre. Le filtre est une fonction passée en second paramètrecomputed.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ètrescomputed.uniq
: retourne un tableau à valeurs uniques depuis l’ensemble des tableaux passés en paramètrescomputed.union
: alias de computed.uniqcomputed.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ètrecomputed.min
: retourne la valeur minimale présente dans le tableau passé en paramètrecomputed.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 tricomputed.equal
: vérifie que la propriété passée en premier argument est égale à la valeur passée en deuxième argumentcomputed.empty
: vérifie que la propriété passée en premier argument (chaîne, tableau, fonction) est videcomputed.notEmpty
: vérifie que la propriété passée en premier argument (chaîne, tableau, fonction) n’est pas videcomputed.collect
: retourne les valeurs de l’ensemble des propriétés passées en paramètrescomputed.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ètrecomputed.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ètrecomputed.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ètrecomputed.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ètrecomputed.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ètreLes 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.