-
Notifications
You must be signed in to change notification settings - Fork 32
/
14-animations.md.erb
357 lines (257 loc) · 18.4 KB
/
14-animations.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
---
title: Animations
slug: animations
date: 0014/01/01
number: 14
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/8377615133/
photoAuthor: Mike Lewinski
contents: Voir ce qui ce passe en coulisses quand Meteor change (swaps) deux éléments du DOM. | Apprenez comment animer la réorganisation des posts. | Apprenez comment animer l'insertion et la suppression des posts. | Apprenez comment animer les transitions entre deux pages.
---
Nous avons maintenant le vote temps-réel, le score et le classement. Cependant, ceci génère du désagrément, une expérience utilisateur erratique due aux mouvements des posts sur la page d'accueil. Nous allons utiliser l'animation pour éviter cela.
### Présentation `_uihooks`
`_uihooks` est une fonctionnalité de Blaze relativement nouvelle et, jusqu'à présent, non documentée. Comme son nom l'indique, elle donne accès à des accroches (hooks) qui peuvent être déclenchées quand les éléments sont insérés, supprimés ou animés.
La liste complète des hooks est la suivante:
- `insertElement`: appelé quand un nouveau élément est inséré.
- `moveElement`: appelé quand un élément change de position.
- `removeElement`: appelé quand un élément est supprimé.
Une fois défini, les hooks *remplaceront* le comportement par défaut de Meteor. En d'autres mots, au lieu d'insérer, déplacer, ou supprimer des éléments, Meteor héritera du comportement que nous aurons indiqué - et il sera à nous, de nous assurer que le comportement fonctionne bien !
### Meteor & Le DOM
Avant que nous puissions attaquer la partie fun (faire bouger les choses), nous avons besoin de comprendre comment Meteor interagit avec le DOM (Document Object Model -- la collection d'éléments HTML qui fait le contenu d'une page).
Le point crucial à garder à l'esprit est que les éléments dans le DOM ne peuvent pas vraiment "bouger"; cependant, ils peuvent être supprimés et créés (il est à noter que c'est une limitation du DOM lui-même, pas de Meteor). Donc pour donner l'illusion que les éléments A et B changent de place, Meteor va en fait supprimer l'élément B et insérer une nouvelle copie (B') devant l'élément A.
Cela rend l'animation un peu compliquée, comme nous ne pouvons juste animer le mouvement de B à sa nouvelle position, parce que B aura disparu aussitôt que Meteor aura renouvelé (re-render) la page (ce qui se produit instantanément grâce à la réactivité). Pas de souci, nous allons trouver une solution.
### The Soviet Runner / Le Coureur Soviétique
Mais avant, une histoire.
1980, au sommet de la Guerre Froide. Les Jeux Olympiques se tenaient à Moscou, et l'URSS était déterminée à gagner le 100 mètres à n'importe quel prix. Alors un groupe de brillants scientifiques soviétiques ont équipés l'un de leurs athlètes avec un téléporteur et à l'instant où le coup de départ était entendu le coureur disparaissait en un éclair et était instantanément ré-inséré dans le bon continuum spatio-temporel, devant la ligne d'arrivée.
Heureusement, les organisateurs remarquèrent immédiatement l'infraction, et l'athlète n'eut pas d'autres choix que de se re-téléporter à la ligne de départ, et de courir comme les autres s'il voulait être autorisé à participer.
Mes sources historiques n'étant pas particulièrement fiables, je vous suggère de prendre cette histoire avec un peu de distance. Mais essayez de garder l'analogie du "coureur soviétique au téléporteur" en tête au fil de notre avancée dans ce chapitre.
### Décomposons
Quand Meteor reçoit une update et modifie le DOM réactivement, notre post sera instantanément téléporté à sa position finale, comme le coureur soviétique. Mais que ce soit aux JO ou dans notre app, nous ne pouvons pas accepter d'avoir des éléments qui se téléportent dans tous les sens. Donc nous allons (re)téléporter l'élément droit dans ses "starting blocks" et le faire "courir" (en d'autres mots, le faire animer) jusqu'à la ligne d'arrivée.
Donc pour échanger (switch) les posts A et B (respectivement positionnés en p1 et p2), nous passerons par les étapes suivantes :
1. Supprimer B
2. Créer B' avant A dans le DOM
3. Téléporter B' en p2
4. Téléporter A en p1
5. Animer A en p2
6. Animer B' en p1
Le diagramme ci-dessous explique les étapes suivantes avec plus de détails:
<%= diagram "animation_diagram", "Swtiching two posts", "pull-center" %>
Répétons, dans l'étape 3 et 4 nous *n'animons* pas A et B' vers leurs positions mais nous les "téléportons" instantanément. Puisque c'est instantané, cela donnera l'illusion que B n'a jamais été supprimé et que les deux éléments seront correctement placés pour être animés vers leurs nouvelles positions.
Par défaut, Meteor prend soin des étapes 1 & 2, et les réimplanter nous-mêmes sera assez simple. Et pour les étapes 5 et 6 tout ce que nous faisons c'est déplacer les éléments vers leurs places. Donc la seule partie dont nous avons à nous soucier est celle des étapes 3 et 4, i.e. envoyer les éléments au point départ de l'animation.
### CSS Positionnement
Pour animer les posts réorganisés de la page, nous allons être amenés à nous aventurer dans les territoires du CSS. Une rapide révision du positionnement CSS serait intéressante.
Les éléments d'une page utilisent le positionnement **static** par défaut. Positionnés statiquement, les éléments sont liés au défilement de la page et leurs coordonnées sur l'écran ne peuvent être changées ou animées.
Le positionnement **relative**, de l'autre côté, signifie également que l'élément est lié au défilement de la page mais peut être positionné de manière *relative à sa position d'origine*.
Le positionnement **absolute** va un peu plus loin et nous laisse paramétrer les coordonnées x/y liées au **document** ou **le premier absolute ou l'élément parent en position relative**.
Nous utiliserons le positionnement relative pour animer nos posts. Nous nous sommes déjà occupés du CSS pour vous, mais si vous voulez le faire par vous-même tout ce dont vous avez besoin de faire c'est d'ajouter ce code à votre feuille de style (stylesheet) :
~~~css
.post {
position: relative;
}
.post.animate {
transition: all 300ms 0ms ease-in;
}
~~~
<%= caption "client/stylesheets/style.css" %>
Notez que nous animons seulement les posts qui ont la classe CSS `.animate`. Cela nous donne la possibilité d'ajouter ou d'enlever la classe pour contrôler quand une animation doit ou ne doit pas se produire.
Cela rends les étapes 5 et 6 plutôt facile : tout ce dont nous avons besoin c'est de réinitialiser `top` en `0px` (valeur par défaut) et nos posts glisseront à leurs positions "normales".
En résumé, notre principal challenge est de trouver **à partir** d'où les animer (étapes 3 et 4) relatif à leurs nouvelles positions. En d'autres mots, combien faut-il pour compenser le glissement. Mais ce n'est pas très difficile non plus : la bonne compensation est simplement la position précédente du post moins sa nouvelle.
### Implémenter `_uihooks`
Maintenant que nous comprenons les différents facteurs en jeu dans l'animation d'une liste d'items, nous sommes prêt à implémenter l'animation. Nous allons avoir besoin en premier d'envelopper (wrap) notre liste de posts dans un nouvel élément conteneur `.wrapper`:
```html
<template name="postsList">
<div class="posts page">
<div class="wrapper">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{else}}
{{#unless ready}}
{{> spinner}}
{{/unless}}
{{/if}}
</div>
</template>
```
<%= caption "client/templates/posts/posts_list.html" %>
<%= highlight "3,7" %>
Avant de continuer, revoyons le comportement de nos posts, *sans* animation:
<%= gifscreenshot "14-1", "The non-animated post list." %>
Faisons entrer `_uihooks`. Nous allons sélectionner le div `.wrapper` dans le callback du template `onRendered` et définir un hook `moveElement`.
```js
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
// do nothing for now
}
}
});
```
<%= caption "client/templates/posts/posts_list.js" %>
<%= highlight "1~7" %>
La fonction `moveElement` que nous venons juste de définir sera appelée chaque fois que la position d'un élément change *à la place* du comportement par défaut de Blaze. Et puisque la fonction est vide, cela signifie que *rien ne se produira*.
Allons-y et essayons d'ouvrir la vue "Best" et upvoter quelques posts : l'ordre ne changera pas tant que nous ne forçons pas un nouveau rendu (rerender) (soit par un reload de la page ou par un changement de routes).
<%= gifscreenshot "14-2", "An empty moveElement callback: nothing happens" %>
Nous avons vérifié que `_uihooks` fonctionne. Maintenant animons-le!
### Animer La Réorganisation Des Posts
Le hook `moveElement` prend deux arguments : `node` et `text`.
- `node` est l'élément en mouvement vers une nouvelle position dans le DOM.
- `next` est l'élément juste *après* la nouvelle position où `node` est déplacé.
Sachant cela, nous pouvons élaborer l'animation suivante (n'hésitez pas à relire l'exemple du "Coureur sovétique" si vous avez besoin de vous rafraîchir la mémoire). Quand le changement d'une position est détecté, nous allons :
1. Insérer `node` avant `next` (en d'autres mots, le comportement par défaut qui se produira si on ne spécifie aucun `moveElement` hook).
2. Remettre `node` à sa position d'origine.
3. Pousser chaque élément entre `node` et `next` pour faire de la place à `node`.
4. Animer tous les éléments de retour à leurs nouvelles positions par défaut.
Nous allons faire tout cela via la magie du [jQuery] (http://jquery.com), de loin la meilleure libraire pour manipuler le DOM. jQuery est en dehors du cadre de ce livre, mais faisons un rapide inventaire des méthodes jQuery que nous allons utiliser :
- [`$()`](http://api.jquery.com/jQuery/): cette méthode englobe n'importe quel DOM élément pour en faire un objet jQuery.
- [`offset()`](http://api.jquery.com/offset/): retire la position courante d'un élément relatif au *document* et retourne un objet contenant les propriétés `top` et `left`.
- [`outerHeight()`](http://api.jquery.com/outerHeight/): prend la hauteur "extérieure" d'un élément (incluant padding et optionnellement margin).
- [`nextUntil(selector)`](http://api.jquery.com/nextUntil/): prend tous les éléments après l'élément cible (target) jusqu'à (mais n'incluant pas) l'élément du `selector`.
- [`insertBefore(selector)`](http://api.jquery.com/insertBefore/): insère un élement avant celui du `selector`.
- [`removeClass(class)`](http://api.jquery.com/removeClass/): retire la `class` CSS, si présent sur l'élément.
- [`addClass(class)`](http://api.jquery.com/addClass/): ajoute la `class` CSS à un élément.
```js
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
var $node = $(node), $next = $(next);
var oldTop = $node.offset().top;
var height = $node.outerHeight(true);
// find all the elements between next and node
var $inBetween = $next.nextUntil(node);
if ($inBetween.length === 0)
$inBetween = $node.nextUntil(next);
// now put node in place
$node.insertBefore(next);
// measure new top
var newTop = $node.offset().top;
// move node *back* to where it was before
$node
.removeClass('animate')
.css('top', oldTop - newTop);
// push every other element down (or up) to put them back
$inBetween
.removeClass('animate')
.css('top', oldTop < newTop ? height : -1 * height)
// force a redraw
$node.offset();
// reset everything to 0, animated
$node.addClass('animate').css('top', 0);
$inBetween.addClass('animate').css('top', 0);
}
}
});
```
<%= caption "client/templates/posts/posts_list.js" %>
Quelques notes:
- On calcule la hauteur de `$node` ainsi nous serons par combien retirer avec `$inBetween`. Nous utilisons `outerHeight(true)` ainsi margin et padding seront compris dans le calcul.
- Nous ne savons pas si `next` vient avant ou après `node` quand on parcours le DOM. Alors on vérifie l'une et l'autre configurations quand on définit `$inBetween`.
- Pour switcher entre "téléporter" et "animer" les éléments, nous allons simplement faire basculer (toggling) les classes CSS animés entre on et off (l'animation actuelle a été définit dans le code CSS de l'app).
- Puisque nous utilisons un positionnement relatif, nous pouvons toujours réinitialiser n'importe quelle propriété `top` d'un élément à 0 pour le ramener là où il est supposé se trouver.
<% note do %>
### Forcer La (Re)composition (Redraw)
Vous vous posez surement des questions à propos de la ligne `$node.offset()`. Pourquoi demandons nous la position de `$node` alors que nous allons rien en faire?
Pensez-y de cette manière: si vous disiez à un droïde parfaitement logique de marcher vers le nord pendant 5 kilomètres et une fois fait, de remarcher vers son point de départ, il déduirait surement que puisque il finira à la même place il ferait tout aussi bien, pour économiser son énergie, de ne pas se déplacer du tout.
Donc, pour nous assurer que notre droïde marche bien durant 10km, nous lui demanderons de mesurer ses coordonnées au 5ème kilomètre avant de revenir.
Le navigateur fonctionne de la même manière: si on donne juste les instructions `css('top', oldTop - newTop)` et `css('top', 0)` simultanément, les nouvelles coordonnées remplacerons simplement les anciennes et il ne se passera rien. Si nous voulons bien voir notre animation, nous devons forcer le navigateur à (re)composer (redraw) l'élément après le changement de la première position.
Et une manière simple de forcer la (re)composition (redraw) est de demander au navigateur de vérifier l' `offset` de l'élément -- il ne peut le savoir tant qu'il n'a pas, à nouveau, composé l'élément.
<% end %>
Donnons lui du mouvement. Retournons à la vue "Best" et upvotons: vous devriez voir nos posts glisser vers le haut et vers le bas avec grâce comme dans un ballet!
<%= gifscreenshot "14-3", "Animated reordering" %>
<%= commit "14-1", "Added post reordering animation." %>
### Les Fondus
Maintenant que nous nous sommes occupé de la complexe séquence de réorganisation, animer les posts entrain d'être inséré ou enlevé sera du gâteau!
En premier, nous allons faire un fondu d'ouverture des nouveaux posts (noter que pour des raisons de simplicité, nous allons passer par de l'animation JavaScript cette fois):
```js
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
}
}
});
```
<%= caption "client/templates/posts/posts_list.js" %>
<%= highlight "3~7" %>
Pour avoir une vision claire du résultat, nous pouvons faire un test pour une nouvelle animation en insérant un post via la console avec:
```js
Meteor.call('postInsert', {url: 'http://apple.com', title: 'Testing Animations'})
```
<%= gifscreenshot "14-4", "Fading in new posts" %>
Puis nous allons faire un fondu de fermeture pour les posts supprimés:
```js
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});
```
<%= caption "client/templates/posts/posts_list.js" %>
<%= highlight "12~16" %>
A nouveau, supprimons un post via la console pour voir l'effet en action (utiliser `Posts.remove('somePostId')`)
<%= gifscreenshot "14-5", "Fading out deleted posts" %>
<%= commit "14-2", "Fade items in when they are drawn." %>
### Transitions Des Pages
Jusqu'à présent nous avons animé des éléments *à l'intérieur* de la page. Mais quand est-il si nous voulons faire une animation de transition *entre* les pages?
La transition des pages est le job de Iron Router. Nous cliquons sur un lien et le contenu du helper `{{> yield}}` dans `layout.html` est automatiquement remplacé.
Il s'avère que comme pour contourner le comportement par défaut de Blaze de notre list de post, nous pouvons faire la même chose pour `{{> yield}}` et ajouter un fondu de transition entre les routes!
Si nous voulons faire un fondu d'ouverture et de fermeture des pages, nous devons nous assurer qu'elles s'affiches l'une au-dessus de l'autre. Nous arrivons à faire cela en utilisant `position:absolute` sur le div contenant `.page` qui englobe (wraps) chaque template de page.
Nous ne voulons pas que nos pages soit positionnées en absolues par rapport à la fenêtre, puisque elles doivent se superposer au header de l'app. Donc nous mettons `position:relative` au div contenant `#main` ainsi le div `.page` `position:absolute` prend son origine de `#main`.
Pour gagner du temps, nous avons déjà ajouté le code CSS nécessaire à `style.css`:
```css
//...
#main{
position: relative;
}
.page{
position: absolute;
top: 0px;
width: 100%;
}
//...
```
<%= caption "client/stylesheets/style.css" %>
Temps à présent d'ajouter le code pour le fondu. Il devrait vous sembler familier puisque c'est exactement le même code que celui que nous avons précédemment utilisé pour l'insertion et la suppression des posts:
```js
Template.layout.onRendered(function() {
this.find('#main')._uihooks = {
insertElement: function(node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});
```
<%= caption "client/templates/application/layout.js" %>
<%= gifscreenshot "14-6", "Transitioning in-between pages with a fade" %>
<%= commit "14-3", "Transition between pages by fading." %>
Nous venons de voir juste quelques modèles pour animer des éléments dans votre Meteor app. L'objectif n'été pas de faire une liste exhaustive mais plutôt de fournir les fondations sur lesquelles vous pouvez bâtir des transitions plus élaborées.