-
Notifications
You must be signed in to change notification settings - Fork 30
/
14-animations.md.erb
361 lines (261 loc) · 17.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
358
359
360
361
---
title: Animaciones
slug: animations
date: 0014/01/01
number: 14
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/8377615133/
photoAuthor: Mike Lewinski
contents: Veremos lo que pasa entre bastidores cuando Meteor intercambia dos elementos del DOM.|Aprenderemos a animar la reordenación de posts.|Aprenderemos a animar la inserción y borrado de posts|Aprenderemos a animar la transición entre dos páginas.
paragraphs: 58
---
Aunque contamos un sistema de votación en tiempo real, no tenemos una gran experiencia de usuario viendo la forma en la que los posts se mueven en la página principal. Usaremos animaciones para suavizar este problema.
### Introduciendo a los `_uihooks`
Los `_uihooks` son una característica de Blaze relativamente nueva y poco documentada. Como su propio nombre indica, nos da acceso a acciones que podemos ejecutar cuando se insertan, eliminan o animan elementos.
La lista completa de acciones es esta:
- `insertElement`: se llama cuando se inserta un elemento.
- `moveElement`: se llama cuando un elemento cambia su posición.
- `removeElement`: se llama cuando se elimina un elemento.
Una vez definidas, estas acciones *reemplazarán* el comportamiento que Meteor tiene por defecto. En otras palabras, en vez de insertar, mover o eliminar elementos, Meteor usará el comportamiento que hayamos definido – ¡y será cosa nuestra que ese comportamiento sea correcto!
### Meteor y el DOM
Antes de poder empezar con la parte divertida (hacer que se muevan las cosas), tenemos que entender cómo interactúa Meteor con el DOM (Document Object Model - la colección de elementos HTML que componen el contenido de una página).
Lo más importante que hay que tener en cuenta es que los elementos del DOM realmente no se pueden "mover". Sólo se pueden añadir y eliminar (esto es una limitación del propio DOM, no de Meteor). Así que para crear la ilusión de que los elementos A y B se intercambian, Meteor tendrá que eliminar B e insertar una nueva copia (B') antes del elemento A.
Esto hace de la animación algo complicado ya que, no podemos simplemente mover B a una nueva posición, porque B habrá desaparecido tan pronto como Meteor redibuje de nuevo la página (que, como sabemos, sucede instantáneamente gracias a la reactividad). Pero no te preocupes, encontraremos la manera de hacerlo.
### El corredor ruso
Pero, primero, una historia.
En 1980, en pleno apogeo de la guerra fría, los Juegos Olímpicos se celebraban en Moscú, y los soviéticos estaban decididos a ganar la carrera de 100 metros a cualquier precio. Así que un grupo de brillantes científicos soviéticos equiparon a uno de sus atletas con un teletransportador, y en cuanto sonó el disparo de salida, el corredor fue trasladado de inmediato a la línea de meta.
Afortunadamente, los jueces de la carrera se dieron cuenta de la infracción inmediatamente, y el atleta no tuvo más remedio que teletransportarse de nuevo a su casilla de salida, antes de permitirle participar de nuevo corriendo como los demás.
Mis fuentes históricas no son muy fiables, por lo que debes tomar esa historia como cogida con pinzas. Pero trataremos de mantener en mente la analogía del "corredor soviético con teletransportador" a medida que avancemos en este capítulo.
### Analizando el problema detenidamente
Cuando Meteor recibe una actualización y modifica reactivamente el DOM, nuestro post se teletransporta inmediatamente a su posición final, al igual que el corredor soviético. Pero como en los Juegos Olímpicos, en nuestra aplicación, podemos tener alrededor cosas que no se teletransportan. Así que tendremos que teletransportarlo a la "casilla de salida" y hacerlo "correr" (en otras palabras, animarlo) de nuevo hasta la línea de meta.
Para intercambiar los elementos A y B (situados en las posiciones p1 y p2, respectivamente), tendremos que seguir los siguientes pasos:
1. Borrar B
2. Crear B' antes de A en el DOM
3. Teletransportar B' a p2
4. Teletransportar A a p1
5. Amimar A hasta p2
6. Animar B' hasta p1
El siguiente diagrama explica estos pasos con más detalle:
<%= diagram "animation_diagram", "Intercambiando dos posts", "pull-center" %>
De nuevo, en los pasos 3 y 4 no estamos *animando* A y B' hasta sus posiciones sino que las "teletransportamos" allí al instante. Dado que el cambio es instantáneo, parecerá que B no se ha borrado, pero ya tenemos posicionados correctamente los elementos para que puedan ser animados hasta su nueva posición.
Afortunadamente, Meteor se ocupa de los pasos 1 y 2 y re-implementarlos será una tarea fácil. En los pasos 5 y 6, lo único que hacemos es mover los elementos al lugar adecuado. Así que, sólo tenemos que preocuparnos de los pasos 3 y 4, enviar los elementos al punto de arranque de la animación.
### Posicionamiento CSS
Para animar los posts que se están reordenando por la página, vamos a tener que meternos en territorio CSS. Sería recomendable una rápida revisión del posicionamiento con CSS.
Los elementos de una página utilizan posicionamiento **estático** por defecto. Los elementos posicionados de forma estática están fijos y sus coordenadas no se pueden cambiar o animar.
Por otra parte, el posicionamiento **relativo**, implica que el elemento está fijado a la página, pero se puede mover con relación a su posición original.
El posicionamiento **absoluto** va un paso más allá y permite dar coordenadas x/y a un elemento en relación al **documento** o **al primer elemento "padre" posicionado de forma absoluta o relativa**.
Nosotros vamos a usar posicionamiento relativo en nuestras animaciones. Ya disponemos del CSS necesario en `client/stylesheets/style.css`, pero si necesitas añadirlo, este es el código para la hoja de estilo:
~~~css
.post{
position:relative;
}
.post.animate{
transition:all 300ms 0ms ease-in;
}
~~~
<%= caption "client/stylesheets/style.css" %>
Ten en cuenta que sólo animamos los posts con la clase CSS `.animate`. De esta forma, podemos añadir y quitar esa clase para controlar cuándo deben producirse o no las animaciones.
Esto facilita muchos los pasos 5 y 6: todo lo que necesitamos hacer es configurar la parte `top` a `0px` (su valor predeterminado) y nuestros posts se deslizarán de nuevo a su posición "normal".
Esto significa que nuestro único problema es averiguar desde dónde animar los posts (pasos 3 y 4) con respecto a su nueva posición. En otras palabras, en qué posición hay que ponerlo. Pero, esto no es tan difícil: el desplazamiento correcto (offset) es la posición anterior restada a la nueva.
### implementando los _uihooks`
Ahora que entendemos los diferentes factores que entran en juego en la animación de una lista de elementos, estamos listos para empezar a aplicar la animación. Lo primero que necesitaremos para envolver nuestra lista de posts en un nuevo contenedor `.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" %>
Antes de continuar, vamos a revisar cuál es el comportamiento actual *sin* animaciones:
<%= gifscreenshot "14-1", "La lista de posts no-animada." %>
Vamos a por los `_uihooks`. Dentro del callback `onRendered` de la plantilla, seleccionamos el div `.wrapper` , y definimos la acción `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" %>
Cada vez que cambie la posición de un elemento, *en vez de* obtener el comportamiento predeterminado de Blaze, Meteor llamará a la función `moveElement`. Y, dado que la función está vacía, *no va a pasar nada*.
Adelante, probemos: abre la vista de los “Mejores“ posts y vota unos cuantos: el orden no cambiará hasta que no fuerces un re-render (ya sea volviendo a cargar la página o moviéndote entre distintas rutas).
<%= gifscreenshot "14-2", "Un callback moveElement vacío: no ocurre nada" %>
Hemos comprobado que los `_uihooks` funcionan. ¡Ahora vamos a hacer que animen los posts!
### Animando los posts a través del reordenamiento
La acción `moveElement` toma dos argumentos: `node` y `next`.
- `node` es el elemento que se está moviendo a una nueva posición en el DOM.
- `next` es el elemento que hay justo *después* de la nueva posición a la que estamos moviendo `node`.
Sabiendo esto, podemos definir el proceso de animación (si necesitas refrescar la memoria, no dudes en volver al ejemplo del "Corredor Ruso"). Cuando detectamos un nuevo cambio en la posición de un elemento, tendremos que hacer lo siguiente:
1. Insertar `node` antes de `next` (en otras palabras, establecer el comportamieento por defecto, como si no hubiéramos definido la acción `moveElement`).
2. Mover `node` a su posición original.
3. Moveremos todos los elementos que hay entre `node` y `next` para hacer sitio a `node`.
4. Animaremos todos los elementos hasta su posición original.
Para hacer todo esto usaremos la magia de [jQuery](http://jquery.com), de lejos, la mejor librería de manipulación del DOM que existe. jQuery está fuera del alcance de este libro, pero vamos a ver rápidamente los métodos que vamos a usar:
- Con [`$()`](http://api.jquery.com/jQuery/) convertimos cualquier elemento del DOM en un objeto jQuery.
- [`offset()`](http://api.jquery.com/offset/) recupera la posición de un elemento en relación *al documento*, y devuelve un objeto que contiene las propiedades `top` y `left`.
- Con [`outerHeight()`](http://api.jquery.com/outerHeight/) obtenemos la altura “exterior” (incluyendo el padding y, opcionalmente, el margin) de un elemento.
- Con [`nextUntil(selector)`](http://api.jquery.com/nextUntil/) obtenemos todos los elementos que hay después del elemento seleccionado con el `selector`, excepto éste último.
- Con [`insertBefore(selector)`](http://api.jquery.com/insertBefore/) insertamos un elemento antes del que seleccionamos con el `selector`.
- Con [`removeClass(class)`](http://api.jquery.com/removeClass/) eliminamos la clase CSS `class`, si está presente en el elemento.
- Con [`css(propertyName, propertyValue)`](http://api.jquery.com/css/) establecemos el valor `propertyValue` para la propiedad `propertyName`.
- Con [`height()`](http://api.jquery.com/height/) obtenemos la altura de un elemento.
- Con [`addClass(class)`](http://api.jquery.com/addClass/) añadimos la clase `class` a un elemento.
```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" %>
<%= highlight "4~35" %>
Algunas notas:
- Calculamos la altura de `$node` para saber cuánto debemos mover los elementos `$inBetween`. Y usamos `outerHeight(true)` para incluir margen y padding en el cálculo.
- No sabemos si `next` va antes o después de `node` así que comprobamos las dos configuraciones cuando definimos `$inBetween`.
- Para cambiar los elementos de “teletransportados” a “animados”, simplemente añadimos o quitamos la clase `animate` (la animación definida en el código CSS de la aplicación).
- Dado que usamos posicionamiento relativo, siempre podemos poner a 0 la propiedad `top` del elemento para devolverlo a la posición dónde se supone que tiene que ir.
<% note do %>
### Forzando el redibujado
Te estarás preguntando para qué es la línea `$node.offset()`. ¿Para qué obtenemos la posición de `$node` si no vamos a hacer nada con ella?
Míralo así: si le dices a un robot muy inteligente que se mueva al norte 5 kilómetros, y luego al sur otros 5, probablemente sabrá deducir que va a terminar en el mismo sitio, y que puede ahorrar energía y hacer bien el trabajo sin moverse.
Así que si quieres que el robot ande 10 kilómetros, le diremos que mida sus coordenadas a los 5 kilómetros, antes de que de la vuelta.
El navegador funciona de una manera similar: si le damos las instrucciones `css('top', oldTop - newTop)` y `css('top', 0)` a la vez, las nuevas coordenadas reemplazarán las viejas y no pasará nada. Si queremos ver la animación, debemos forzar al navegador a redibujar el elemento después de moverlo la primera vez.
Una forma sencilla de hacerlo es pedirle al navegador el `offset` del elemento.
<% end %>
Vamos a probar de nuevo. Volvamos a la vista “Best” y votemos unos posts: ¡Deberías verlos moviéndose suavemente hacia arriba y hacia abajo como en un ballet!
<%= gifscreenshot "14-3", "Animated reordering" %>
<%= commit "14-1", "Added post reordering animation." %>
### Aparecer y desaparecer
Ahora que ya tenemos resuelta la reordenación más complicada, animar las inserciones y eliminaciones va ser muy sencillo.
Primero, haremos aparecer nuevos posts (esta vez, por simplicidad, usaremos animaciones JavaScript):
```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~8" %>
Para ver el resultado, podemos probar a insertar un post vía consola:
```js
Meteor.call('postInsert', {url: 'http://apple.com', title: 'Testing Animations'})
```
<%= gifscreenshot "14-4", "Fading in new posts" %>
Y ahora, haremos desaparecer los posts eliminados:
```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 "11~15" %>
De nuevo, para ver el efecto, prueba a eliminar algún post desde la consola (`Posts.remove('algunPostId')`).
<%= gifscreenshot "14-5", "Fading out deleted posts" %>
<%= commit "14-2", "Fade items in when they are drawn." %>
### Transiciones entre páginas
Hemos creado animaciones para elementos *dentro* de una página. Pero, ¿qué pasa si queremos animar las transiciones entre páginas?
Las transiciones entre páginas son trabajo del Iron Router. Haces click en un enlace y se reemplaza el contenido del ayudante `{{> yield}}` en `layout.html`
Ocurre que, como cuando reemplazamos el comportamiento de Blaze para la lista de posts, ¡podemos hacer lo mismo para el elemento `{{> yield}}` y añadirle un efecto de transición entre rutas!
Si queremos animar la entrada y salida entre dos páginas, debemos asegurarnos de que se muestran una por encima de la otra. Lo hacemos usando la propiedad `position:absolute` en el contenedor `.page` que envuelve a todas las plantillas de páginas.
Piensa que no queremos que las páginas estén posicionadas de forma absoluta, porque de esta forma, se solaparían con la cabecera de la app. Así que establecemos la propiedad `position:relative` en el div `#main` que las contiene, de forma que el `position:absolute` de `.page` tome su origen desde `#main`.
Para ahorrar tiempo, hemos añadido el código necesario a `style.css`:
```css
//...
#main{
position: relative;
}
.page{
position: absolute;
top: 0px;
width: 100%;
}
//...
```
<%= caption "client/stylesheets/style.css" %>
Es el momento de añadir el código para las transiciones entre páginas. Nos debe resultar familiar, puesto que es exactamente el mismo que para las inserciones y eliminaciones de 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." %>
Hemos visto unos pocos patrones para animar elementos en nuestra aplicación Meteor. Aunque no es una lista exhaustiva, con suerte, nos aportará una base sobre la que construir transiciones más elaboradas.