-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy path14-animations.md.erb
273 lines (187 loc) · 14.5 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
---
title: Animazioni
slug: animations
date: 0014/01/01
number: 14
points: 10
photoUrl: http://www.flickr.com/photos/ikewinski/8377615133/
photoAuthor: Mike Lewinski
contents: Vedi cosa succede nei retroscena quando Meteor scambia due elementi del DOM.|Impara come animare il riordinamento dei messaggi.|Impara come animare l'inserimento dei messaggi.
paragraphs: 58
---
A questo punto abbiamo un sistema di votazione, contatore punteggi e valutazione in tempo reale. Questo tuttavia risulta in un'esperienza erratica ed irritante, con messaggi che saltano da una parte all'altra della homepage. Per migliorare questa situazione useremo le animazioni.
### Meteor & il DOM
Prima di iniziare la parte divertente (fare sì che le cose si muovano), dobbiamo capire come Meteor interagisce con il DOM (Document Object Model -- la collezione di elementi HTML che costituiscono i contenuti di una pagina).
È cruciale ricordarsi che gli elementi *non possono essere spostati*. Possono solamente essere eliminati e creati (nota che questa è una limitazione del DOM, non di Meteor). Quindi per dare l'illusione che gli elementi A e B cambino di posto, Meteor cancellerà l'elemento B e inserirà una copia nuova di zecca (B') prima dell'elemento A.
Questo rende l'animazione difficoltosa, siccome non puoi semplicemente animare B per muoverlo in una nuova posizione, perché B sarà scomparso non appena Meteor renderizza la pagina (che come sappiamo accade instantaneamente, grazie alla reactivity). Dovrai invece animare B' mentre si muove dalla vecchia posizione di B verso la sua nuova posizione prima di A.
Per scambiare i messaggi A e B (posizionati rispettivamente nelle posizioni p1 e p2), dobbiamo seguire i seguenti punti:
1. Elimina B
2. Nel DOM, prima di A crea B'
3. Muovi B' verso p2
4. Muovi A verso p1
5. Anima A verso p2
6. Anima B' verso p1
Questi punti sono esposti in dettaglio nel seguente diagramma:
<%= diagram "animation_diagram", "Scambiare due messaggi", "pull-center" %>
Nota che nei punti 3 e 4 non stiamo *animando* A e B' verso le loro posizioni, ma li stiamo "teleportando" istantaneamente. Questo darà l'illusione che B non è mai stato cancellato e posizionerà entrambi gli elementi così che possano essere animati verso la loro nuova posizione.
Fortunatamente Meteor si prende cura dei punti 1 & 2, quindi ci dobbiamo preoccupare solamente dei punti da 3 a 6.
Nei punti 5 e 6 inoltre, stiamo semplicemente spostando gli elementi nelle loro giuste posizioni. Quindi le uniche parti di cui ci dobbiamo veramente preoccupare sono i punti 3 e 4, cioè mandare gli elementi verso il punto iniziale dell'animazione.
### Tempismo giusto
Fino ad ora abbiamo parlato di *come* animare i nostri messaggi, ma non di *dove* animarli.
Per i punti 3 e 4, la risposta sta nel template callback `rendered` all'interno del gestore `post_item.js`, che è scatenato ogni volta che cambia la proprietà di un messaggio (nel nostro caso il punteggio).
I punti 5 e 6 sono un pò più complessi. Pensaci su: se tu dicessi ad un automa di correre verso nord per 5 minuti, dopodichè di correre verso sud per 5 minuti, probabilmente l'automa dedurrà che siccome finirà nello stesso posto, potrebbe risparmiarsi le sue forze e non correre per niente.
Quindi se vuoi assicurarti che il tuo automa corra per tutti e 10 i minuti, devi *aspettare* fino a che non ha corso i primi 5 minuti, e *dopo* dirgli di tornare indietro.
Il browser funziona in una simile maniera: se simultaneamente gli diamo entrambe le istruzioni, le nuove coordinate semplicemente sostituirebbero quelle vecchie e non accadrebbe nulla. In altre parole, il browser ha bisogno di registrare i cambiamenti di posizione come punti separati nel tempo, altrimenti non sarà in grado di animarli.
Meteor non fornisce una callback `justAfterRendered`, ma può imitarla usando `Meteor.defer()`, che semplicemente prende una funzione e pospone la sua esecuzione appena in tempo per registrarsi come un evento diverso.
### Posizionamento con i CSS
Per animare i messaggi che si stanno riordinando per la pagina, dovremo avventurarci nel mondo dei CSS. È quindi d'ordine un breve ripasso sul posizionamento con i CSS.
Gli elementi di una pagina sono predefiniti per avere un posizionamento **statico**. Gli elementi posizionati staticamente si adattano al flusso della pagina e le loro coordinate sullo schermo non possono essere cambiate o animate.
Un posizionamento **relativo** invece significa che l'elemento anche in questo caso si adatta al flusso della pagina, ma può essere posizionato *relativamente* alla sua posizione originale*.
Un posizionamento *assoluto* va un passo più in avanti e ti permette di specificare delle coordinate x/y relative al *documento* oppure **al primo elemento padre posizionato in modo relativo o assoluto**.
Per animare i nostri messaggi useremo un posizionamento relativo.
~~~css
.post{
position:relative;
transition:all 300ms 0ms ease-in;
}
~~~
<%= caption "client/stylesheets/style.css" %>
Questo permette di fare facilmente i punti 5 e 6: dobbiamo semplicemente impostare `top` a `0px` (il suo valore predefinito) così i nostri messaggi scorreranno indietro verso la loro posizione "normale".
Questo vuol dire che la nostra unica sfida è quella di calcolare da *dove* animarli (punti 3 e 4), relativamente alla loro nuova posizione. In altre parole, di quanto compensarli. Ma anche questo non è molto difficile: la giusta compensazione è semplicemente la posizione del messaggio precedente meno quella del nuovo.
<% note do %>
### Position:absolute
Per posizionare i nostri elementi potremmo anche usare `position:absolute` con un padre relativo. Ma un gran svantaggio degli elementi posizionati in questo modo è che sono completamente rimossi dal flusso della pagina, causando il collasso del loro contenitore padre come se fosse vuoto.
A sua volta questo significa che dovremmo impostare l'altezza del contenitore artificialmente con Javascript, invece di lasciare che il browser aggiusti naturalmente gli elementi. Di conseguenza, ogni qual volta sia possibile è meglio rimanere con il posizionamento relativo.
<% end %>
### Richiamo totale
Tuttavia abbiamo ancora un problema. Mentre l'elemento A persiste nel DOM e quindi può "ricordare" la sua posizione precedente, l'elemento B viene reincarnato e riprende vita sotto forma di B', con la memoria cancellata.
Meteor fortunatamente viene alla riscossa dandoci accesso all'oggetto **istanza di template** nella callback `rendered`. La [documentazione di Meteor](http://docs.meteor.com/#template_rendered) illustra:
> Nel body della callback, `this` è un oggetto istanza di template che è unico a questa occorrenza del template ed è persistente tra diversi renderings.
Quello che faremo quindi, è trovare la posizione corrente di un messaggio nella pagina e salvare la posizione nell'oggetto istanza di template. In questa maniera, saremo in grado di sapere da dove animare il messaggio, anche quando viene eliminato e ricreato.
Le istanze di template ci permetteno anche di accedere alla collezione di dati tramite la proprietà `data`. Questo ci tornerà utile per prendere il punteggio di un messaggio.
### Punteggio dei messaggi
Abbiamo parlato del punteggio dei messaggi, ma questo "valore" effettivamente non esiste come una proprietà del messaggio, sicome è semplicemente una conseguenza dell'ordine dei messaggi che sono elencati nella nostra collezione. Dovremo in qualche maniera trovare un modo per far apparire questa proprietà dall'aria se vogliamo essere in grado di animare i messaggi a seconda del loro punteggio.
Nota che siccome il punteggio è una proprietà relativa che dipende da come ordini i messaggi (un messaggio può essere valutato primo mentre si ordina per data, ma terzo quando si ordina per punteggio), non possiamo mettere questa proprietà `rank` nel database stesso.
Idealmente metteremmo la proprietà nelle nostre collezioni `newPosts` e `topPosts`, ma Meteor al momento non offre un meccanismo conveniente per farlo.
Inseriremo invece `rank` all'ultimo momento possibile, nel `postList` template manager:
~~~js
Template.postsList.helpers({
postsWithRank: function() {
this.posts.rewind();
return this.posts.map(function(post, index, cursor) {
post._rank = index;
return post;
});
}
});
~~~
<%= caption "/client/views/posts/posts_list.js" %>
<%= highlight "2~8" %>
Invee di ritornare il cursore `Posts.find({}, {sort: {submitted: -1}, limit: postsHandle.limit()})` come nei nostri `posts` helpers precedenti, `postsWithRank` prende il cursore e aggiunge la proprietà `_rank` per ognuno dei suoi documenti.
E non dimenticarti di aggiornare il template `postsList`:
~~~html
<template name="postsList">
<div class="posts">
{{#each postsWithRank}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{/if}}
</div>
</template>
~~~
<%= caption "/client/views/posts/posts_list.html" %>
<%= highlight "3" %>
<% note do %>
### Sii cortese, riavvolgi
Meteor è uno dei web frameworks più progressisti e all'avanguardia che ci siano. Ma una delle sue funzionalità, la funzione `rewind()`, sembra essere un ritorno ai giorni passati delle video cassette e dei VCRs.
Ogni volta che usi un cursore con `forEach()`, `map()`, oppure `fetch()`, dovrai riavvolgerlo prima di poterlo usare di nuovo.
E in alcuni casi è meglio stare sul sicuro e riavvolgere il cursore in maniera preventiva piuttosto che rischiare un errore.
<% end %>
### Mettere tutto insieme
Ora possiamo mettere tutto insieme usando la template callback `rendered` del manager in `post_item.js` per la logica della nostra animazione:
~~~js
Template.postItem.helpers({
//...
});
Template.postItem.rendered = function(){
// anima questo messaggio dalla posizione precedente a quella nuova
var instance = this;
var rank = instance.data._rank;
var $this = $(this.firstNode);
var postHeight = 80;
var newPosition = rank * postHeight;
// se l'elemento ha una currentPosition (non è il primo rendering)
if (typeof(instance.currentPosition) !== 'undefined') {
var previousPosition = instance.currentPosition;
// calcola la differenza tra vecchia e nuova posizione ed invia lì l'elemento
var delta = previousPosition - newPosition;
$this.css("top", delta + "px");
}
// lascia che venga disegnato nella vecchia posizione, dopodichè...
Meteor.defer(function() {
instance.currentPosition = newPosition;
// porta l'elemento indietro alla sua posizione originale
$this.css("top", "0px");
});
};
Template.postItem.events({
//...
});
~~~
<%= caption "/client/views/posts/post_item.js" %>
<%= highlight "5~27" %>
<%= commit "14-1", "Added post reordering animation." %>
Seguire non dovrebbe essere troppo difficile se fai riferimento al diagramma precedente.
Nota che siccome abbiamo impostato la proprietà `currentPosition` dell'istanza di template nella callback `defer`, questa proprietà non esisterà al primo rendering del frammento di template. Ma non è un problema dato che non siamo interessati ad animare il primo rendering.
Apri il sito e prova a votare. Dovresti vedere i messaggi, che con grazia da ballerine, si spostano gentilmente su e giù!
### Animare nuovi messaggi
Ora i nostri messaggi si riordinano correttamente, ma non abbiamo ancora un'animazione per un "nuovo messaggio". Invece di far spuntare immediatamente i nuovi messaggi, facciamoli apparire gradualmente.
In verità questo è più complesso di quel che sembra. Il problema è che la callback `rendered` di Meteor viene scatenata in due casi separati:
1. Quando un nuovo template viene inserito nel DOM
2. Quando i dati del template vengono cambiati
Solamente il primo caso dovrebbe essere animato, a meno che tu non voglia un'interfaccia utente che si illumina come un albero di natale ogni volta che i tuoi dati cambiano.
Assicuriamoci quindi di animare solamente i messaggi che sono veramente nuovi e non quelli che vengono ri-renderizzati perché i loro dati sono cambiati. Stiamo già testando per la presenza di una variabile d'istanza (che è impostata solamente dopo il primo render), quindi dobbiamo solo tornare indietro alla nostra callback `rendered` e aggiungere un blocco `else`:
~~~js
Template.postItem.helpers({
//...
});
Template.postItem.rendered = function(){
// anima questo messaggio dalla posizione precedente a quella nuova
var instance = this;
var rank = instance.data._rank;
var $this = $(this.firstNode);
var postHeight = 80;
var newPosition = rank * postHeight;
// se l'elemento ha una currentPosition (non è il primo rendering)
if (typeof(instance.currentPosition) !== 'undefined') {
var previousPosition = instance.currentPosition;
// calcola la differenza tra vecchia e nuova posizione ed invia lì l'elemento
var delta = previousPosition - newPosition;
$this.css("top", delta + "px");
} else {
// è il primo evento render in assoluto, quindi nascondi l'elemento
$this.addClass("invisible");
}
// lascia che venga disegnato nella vecchia posizione, dopodichè...
Meteor.defer(function() {
instance.currentPosition = newPosition;
// porta l'elemento indietro alla sua posizione originale
$this.css("top", "0px").removeClass("invisible");
});
};
Template.postItem.events({
//...
});
~~~
<%= caption "/client/views/posts/post_item.js" %>
<%= highlight "8~10" %>
<%= commit "14-2", "Fade items in when they are drawn." %>
Nota che la `removeClass("invisible")` che abbiamo aggiunto nella funzione `defer()` verrà eseguita ad ogni rendering. Ma farà qualcosa solamente se la classe `.invisible` è presente sull'elemento, il che è vero solamente la prima volta che è renderizzato.
<% note do %>
### CSS & JavaScript
Avrai notato che stiamo usando una classe CSS `.invisible` per scatenare l'animazione invece di animare direttamente la proprietà CSS `opacity` come abbiamo fatto per `top`. Questo è perché per `top` avevamo bisogno di animare la proprietà usando un valore specifico che dipende dai dati dell'istanza.
Dall'altro canto qui vogliamo solo mostrare e nascondere un elemento, indipendentemente dai suoi dati. Siccome è una buona idea lasciare il più possibile CSS fuori da Javascript, qui aggiungeremo e toglieremo solamente la classe e specificheremo i dettagli dell'animazione nel nostro stylesheet.
<% end %>
Finalmente dovremmo avere l'animazione che volevamo! Lancia la tua app e provala! Puoi anche divertirti con le classi `.post` e `.post.invisible` per vedere se riesci a trovare altri modi di animare. Suggerimento: [CSS easing functions](http://matthewlein.com/ceaser/) è un buon punto per cominciare!