forked from herberthamaral/jqfundamentals-pt-BR
-
Notifications
You must be signed in to change notification settings - Fork 1
/
custom-events.xml
563 lines (471 loc) · 21.9 KB
/
custom-events.xml
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
<?xml version="1.0" encoding="UTF-8"?>
<chapter version="5.0" xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:m="http://www.w3.org/1998/Math/MathML"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:db="http://docbook.org/ns/docbook">
<title>Eventos Customizados</title>
<section>
<title>Introdução à Eventos Customizados</title>
<para>Nós todos estamos familiares com eventos básicos - click, mouseover,
focus, blur, submit, etc. - estes que nós podemos usar para interações
entre o usuário e o navegador. Eventos customizados abrem um mundo
completamente novo de programação orientada a eventos. Neste capítulo,
nós iremos utilizar o sistema de eventos customizados do jQuery para fazer
uma aplicação de busca simples no Twitter.</para>
<para>Pode ser difícil à primeira instância entender por que você iria
querer utilizar eventos customizados, quando os eventos já prontos parecem
satisfazer bem suas necessidades. Acontece que eventos customizados oferecem
um jeito completamente novo de pensar sobre JavaScript orientado a eventos.
Ao invés de focar no elemento que dispara uma ação, eventos customizados
colocam o holoforte no elemento em ação. Isso traz muitos benefícios,
incluindo:</para>
<itemizedlist>
<listitem>
<para>Comportamentos do elemento alvo podem ser facilmente disparados
por elementos diferentes utilizando o mesmo código.</para>
</listitem>
<listitem>
<para>Comportamentos podem ser disparados através de elementos-alvo
múltiplos e similares de uma vez.</para>
</listitem>
<listitem>
<para>Comportamentos podem ser mais facilmente associados com o elemento
alvo no código, tornando o código mais fácil de ler e manter.</para>
</listitem>
</itemizedlist>
<para>Por que você deveria se importar? Um exemplo é provavelmente a melhor
forma de explicar. Suponha que você tenha uma lâmpada num quarto na casa. A
lâmpada está ligada e ela é controlada por dois switches de três vias e um
clapper:</para>
<programlisting><div class="room" id="kitchen">
<div class="lightbulb on"></div>
<div class="switch"></div>
<div class="switch"></div>
<div class="clapper"></div>
</div></programlisting>
<para>O estado da lâmpada irá mudar disparando o clapper ou mesmo os
switches. Os switches e o clapper não se importam em qual estado a lâmpada
está; eles somente querem mudar o estado.</para>
<para>Sem eventos customizados, você talvez poderia escrever algo assim: </para>
<programlisting>$('.switch, .clapper').click(function() {
var $light = $(this).parent().find('.lightbulb');
if ($light.hasClass('on')) {
$light.removeClass('on').addClass('off');
} else {
$light.removeClass('off').addClass('on');
}
});</programlisting>
<para>Com eventos customizados, seu código pode ficar parecido com este: </para>
<programlisting>$('.lightbulb').bind('changeState', function(e) {
var $light = $(this);
if ($light.hasClass('on')) {
$light.removeClass('on').addClass('off');
} else {
$light.removeClass('off').addClass('on');
}
});
$('.switch, .clapper').click(function() {
$(this).parent().find('.lightbulb').trigger('changeState');
});</programlisting>
<para>Este último pedaço de código não é lá essas coisas, mas algo importante
aconteceu: nós movemos o comportamento da lâmpada para a lâmpada, e ficamos
longe dos switches e do clapper. </para>
<para>Vamos tornar nosso exemplo um pouco mais interessante. Nós vamos
adicionar outro quarto à nossa casa, junto com o switch mestre, como
mostrado a seguir: </para>
<programlisting><div class="room" id="kitchen">
<div class="lightbulb on"></div>
<div class="switch"></div>
<div class="switch"></div>
<div class="clapper"></div>
</div>
<div class="room" id="bedroom">
<div class="lightbulb on"></div>
<div class="switch"></div>
<div class="switch"></div>
<div class="clapper"></div>
</div>
<div id="master_switch"></div></programlisting>
<para>Se há várias luzes na casa, nós queremos que o switch mestre desligue
todas as luzes; de outra forma, nós queremos todas as luzes acesas. Para
fazer isso, nós iremos adicionar dois ou mais eventos customizados às
lâmpadas: <code>turnOn</code> e <code>turnOff</code>. Nós iremos fazer uso
deles no evento customizado <code>changeState</code>, e usar alguma lógica
para decidir qual que o switch mestre vai disparar:</para>
<programlisting>$('.lightbulb')
.bind('changeState', function(e) {
var $light = $(this);
if ($light.hasClass('on')) {
$light.trigger('turnOff');
} else {
$light.trigger('turnOn');
}
})
.bind('turnOn', function(e) {
$(this).removeClass('off').addClass('on');
})
.bind('turnOff', function(e) {
$(this).removeClass('off').addClass('on');
});
$('.switch, .clapper').click(function() {
$(this).parent().find('.lightbulb').trigger('changeState');
});
$('#master_switch').click(function() {
if ($('.lightbulb.on').length) {
$('.lightbulb').trigger('turnOff');
} else {
$('.lightbulb').trigger('turnOn');
}
});</programlisting>
<para>Note como o comportamento do switch mestre está vinculado ao switch
mestre; O comportamento de uma lâmpada pertence às lâmpadas.</para>
<note>
<para>Se você está acostumado com programação orientada a objetos,
talvez seja útil pensar em evetos como se fossem métodos de objetos.
Grosseiramente falando, o objeto que o método pertence é criado pelo
seletor do jQuery. Vinculando o evento customizado changeState para
todos os elementos <code>$(‘.light’)</code> é similar a ter uma classe
chamada <code>Light</code> com um método <code>changeState</code>, e
então instanciamos novos objetos <code>Light</code> para cada elemento
com o nome de classe light.</para>
</note>
<sidebar>
<title>Recapitulando: $.fn.bind e $.fn.trigger</title>
<para>No mundo dos eventos customizados, há dois métodos importantes do
jQuery: <code>$.fn.bind</code> e <code>$.fn.trigger</code>. No capítulo
Eventos, nós vimos como usar estes métodos para trabalhar com eventos
do usuário; neste capítulo, é importante relemebrar duas coisas:</para>
<itemizedlist>
<listitem>
<para>O método <code>$.fn.bind</code> recebe o tipo do evento e
uma função manipuladora do mesmo como argumentos. Opcionalmente, ele
pode também receber dados relacionados ao eventos como seu segundo
argumento, passando a função manipuladora do evento para o terceiro
argumento. Qualquer dado que é passado será disponível para a função
manipuladora do evento na propriedade <code>data</code> do objeto do
evento. A função manipuladora do evento sempre recebe o objeto do
evento como seu primeiro argumento.</para>
</listitem>
<listitem>
<para>O método <code>$.fn.trigger</code> recebe um tipo de evento como
argumento. Opcionalmente, ele também pode receber um array de valores.
Estes valores serão passados para a função manipuladora de eventos como
argumentos depois do objeto de evento.</para>
</listitem>
</itemizedlist>
<para>Aqui há um exemplo de uso do <code>$.fn.bind</code> e
<code>$.fn.trigger</code> que usa dados customizados em ambos os casos:</para>
<programlisting>$(document).bind('myCustomEvent', { foo : 'bar' }, function(e, arg1, arg2) {
console.log(e.data.foo); // 'bar'
console.log(arg1); // 'bim'
console.log(arg2); // 'baz'
});
$(document).trigger('myCustomEvent', [ 'bim', 'baz' ]);</programlisting>
</sidebar>
<section>
<title>Uma aplicação de amostra</title>
<para>Para demonstrar o poder dos eventos customizados, nós vamos criar
uma ferramenta simples para buscar no Twitter. A ferramenta irá oferecer
várias formas de um usuário fazer a busca por um termo: digitando numa
caixa de texto, múltiplos termos de busca na URL e por requisitar os
termos do trending topics. </para>
<para>Os resultados para cada termo serão mostrados em um container de
resultados; Estes containers poderão ser expandidos, contraidos, atualizados,
e removidos, individualmente ou todos de uma vez. </para>
<para>Quando terminarmos, a applicação ficará dessa forma: </para>
<figure>
<title>Nossa aplicação concluída</title>
<screenshot>
<mediaobject>
<imageobject>
<imagedata fileref="http://gyazo.com/70415e9fffab1c47953f5264ecf722fe.png"></imagedata>
</imageobject>
</mediaobject>
</screenshot>
</figure>
<section>
<title>O Setup</title>
<para>Nós iniciaremos com HTML básico: </para>
<programlisting><h1>Twitter Search</h1>
<input type="button" id="get_trends"
value="Load Trending Terms" />
<form>
<input type="text" class="input_text"
id="search_term" />
<input type="submit" class="input_submit"
value="Add Search Term" />
</form>
<div id="twitter">
<div class="template results">
<h2>Search Results for
<span class="search_term"></span></h2>
</div>
</div></programlisting>
<para>Isso nos dá um container (#twitter) para nosso widget, um template
para nossos resultados (escondido via CSS), e um formulário simples,
onde usuários podem entrar com um termo de busca. (Para o bem da
simplicidade, nós iremos assumir que a aplicação é somente em JavaScript
e que nossos usuários sempre terão CSS.)</para>
<para>Há dois tipos de objetos que nós iremos atuar sobre: os containers
de resultados e o container do Twitter.</para>
<para>Os containers de resultados são o coração da aplicação. Nós
criaremos um plugin que preparará cada container de resultados assim
que eles forem adicionados ao container do Twitter. Entre outras coisas,
ele vinculará os eventos customizados para cada container e adicionará
os botões de ação no topo a direita de cada container. Cada container de
resultado terá os seguintes eventos customizados: </para>
<variablelist>
<varlistentry>
<term>refresh</term>
<listitem>
<para>Coloca o container no estado “atualizando”, e dispara a
requisição para obter os dados para o termo de busca.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>populate</term>
<listitem>
<para>Recebe os dados retornados em JSON e usa-os para para
alimentar o container.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>remove</term>
<listitem>
<para>Remove o container da página depois que o usuário efetua
a requisição para faze-lo. A verificação pode ser ignorada
se true for passado como segundo argumento do manipulador de
eventos. O evento remove também remove o termo associado com o
container de resultados do objeto global contendo o termo de
busca.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>collapse</term>
<listitem>
<para>Adiciona a classe collapsed ao container, que esconderá
os resultados via CSS. Ele também irá mudar o botão “Collapse”
do container em um botão “Expand”.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>expand</term>
<listitem>
<para>Remove a classe collapsed do container. Ele também irá
mudar o botão “Expand” em um botão “Collapse”.</para>
</listitem>
</varlistentry>
</variablelist>
<para>O plugin é responsável por adicionar os botões de ação ao container.
Ele vinvula cada evento de clique à cada item da lista de ações e usa
a classe do item de lista para determinar qual evento customizado será
disparado no container de resultados correspondente. </para>
<programlisting>$.fn.twitterResult = function(settings) {
return $(this).each(function() {
var $results = $(this),
$actions = $.fn.twitterResult.actions =
$.fn.twitterResult.actions ||
$.fn.twitterResult.createActions(),
$a = $actions.clone().prependTo($results),
term = settings.term;
$results.find('span.search_term').text(term);
$.each(
['refresh', 'populate', 'remove', 'collapse', 'expand'],
function(i, ev) {
$results.bind(
ev,
{ term : term },
$.fn.twitterResult.events[ev]
);
}
);
// usa a classe de cada ação para determinar qual
// evento ele irá disparar no painel de resultados.
$a.find('li').click(function() {
// passa o li que foi clicado para a função
// para que o mesmo possa manipulado se necessário
$results.trigger($(this).attr('class'), [ $(this) ]);
});
});
};
$.fn.twitterResult.createActions = function() {
return $('<ul class="actions" />').append(
'<li class="refresh">Refresh</li>' +
'<li class="remove">Remove</li>' +
'<li class="collapse">Collapse</li>'
);
};
$.fn.twitterResult.events = {
refresh : function(e) {
// indica que os resultados estão sendo atualizados
var $this = $(this).addClass('refreshing');
$this.find('p.tweet').remove();
$results.append('<p class="loading">Loading ...</p>');
// obtém os dados do Twitter via jsonp
$.getJSON(
'http://search.twitter.com/search.json?q=' +
escape(e.data.term) + '&rpp=5&callback=?',
function(json) {
$this.trigger('populate', [ json ]);
}
);
},
populate : function(e, json) {
var results = json.results;
var $this = $(this);
$this.find('p.loading').remove();
$.each(results, function(i,result) {
var tweet = '<p class="tweet">' +
'<a href="http://twitter.com/' +
result.from_user +
'">' +
result.from_user +
'</a>: ' +
result.text +
' <span class="date">' +
result.created_at +
'</span>' +
'</p>';
$this.append(tweet);
});
// indica que a atualização dos
// resultados está terminada
$this.removeClass('refreshing');
},
remove : function(e, force) {
if (
!force &&
!confirm('Remove panel for term ' + e.data.term + '?')
) {
return;
}
$(this).remove();
// indica que nós não temos mais
// um painel para o termo
search_terms[e.data.term] = 0;
},
collapse : function(e) {
$(this).find('li.collapse').removeClass('collapse')
.addClass('expand').text('Expand');
$(this).addClass('collapsed');
},
expand : function(e) {
$(this).find('li.expand').removeClass('expand')
.addClass('collapse').text('Collapse');
$(this).removeClass('collapsed');
}
};</programlisting>
<para>O container do Twitter terá somente dois eventos:
</para>
<variablelist>
<varlistentry>
<term>getResults</term>
<listitem>
<para>Recebe um termo de busca e checa se há um container de
resultados para o termo; Se não, adiciona um container de
resultados utilizando o template de resultados, configura o
container utilizando o plugin <code>$.fn.twitterResult</code>
discutido acima, e então dispara o evento <code>refresh</code>
no container de resultados para carregar de fato os resultados.
Finalmente, ele armazena o termo de busca para que a aplicação
não obtenha novamente o termo.</para>
</listitem>
</varlistentry>
<varlistentry>
<term>getTrends</term>
<listitem>
<para>Consulta o Twitter para obter os top 10 trending topics,
depois itera sobre os resultados e dispara o evento
<code>getResults</code> para cada um deles, adicionando assim
um container de resultados para cada um dos termos. </para>
</listitem>
</varlistentry>
</variablelist>
<para>As vinculações do container do Twitter vão ficar parecidas com isso:</para>
<programlisting>$('#twitter')
.bind('getResults', function(e, term) {
// certifica que nós já não temos um box para este termo
if (!search_terms[term]) {
var $this = $(this);
var $template = $this.find('div.template');
// faz uma cópia da div do template
// e insere-a como primeiro box de resultados
$results = $template.clone().
removeClass('template').
insertBefore($this.find('div:first')).
twitterResult({
'term' : term
});
// carrega o conteúdo utilizando o evento customizado
// "refresh" que nós vinculamos ao container de resultados
$results.trigger('refresh');
search_terms[term] = 1;
}
})
.bind('getTrends', function(e) {
var $this = $(this);
$.getJSON('http://search.twitter.com/trends.json?callback=?', function(json) {
var trends = json.trends;
$.each(trends, function(i, trend) {
$this.trigger('getResults', [ trend.name ]);
});
});
});</programlisting>
<para>Até agora, nós escrevemos muito código que não faz quase nada,
mas ok. Ao especificar todos os comportamentos que nós queremos que
nossos objetos de base tenham, nós criamos um framework sólido para
construir rapidamente a interface. </para>
<para>Vamos começar ligando nossa caixa de texto e o botão “Load Trending
Terms”. Para a caixa de texto, nós iremos capturar o termo que foi
digitado e iremos passa-lo para o evento <code>getResults</code> do
container do Twitter. O evento <code>getTrends</code> será disparado
ao clicarmos no botão “Load Trending Terms”: </para>
<programlisting>$('form').submit(function(e) {
e.preventDefault();
var term = $('#search_term').val();
$('#twitter').trigger('getResults', [ term ]);
});
$('#get_trends').click(function() {
$('#twitter').trigger('getTrends');
});</programlisting>
<para>Nós podemos remover, contrair, expandir e atualizar todos os
resultados se adicionarmos alguns botões com IDs apropriados, assim
como mostrado abaixo. Perceba como nós estamos passando o valor true
para o manipulador de evento como seu segundo argumento, dizendo ao
mesmo que que nós não queremos verificar a remoção de containers
individuais. </para>
<programlisting>$.each(['refresh', 'expand', 'collapse'], function(i, ev) {
$('#' + ev).click(function(e) { $('#twitter div.results').trigger(ev); });
});
$('#remove').click(function(e) {
if (confirm('Remove all results?')) {
$('#twitter div.results').trigger('remove', [ true ]);
}
});</programlisting>
</section>
<section>
<title>Conclusão</title>
<para>Eventos customizados oferecem uma nova forma de pensar sobre seu
código: eles colocam ênfase no comportamento, não no elemento que o
dispara. Se você gastar um tempo no início para estabelecer as partes
da sua aplicação e os comportamentos que essas partes necessitam ter,
os eventos customizados podem fornecer um método poderoso para você
fazer com que estas partes "conversem", seja uma de cada vez ou uma
conversa em massa. Uma vez que os comportamentos de uma parte foram
descritos, torna-se trivial disparar estes comportamentos de qualquer
parte, permitindo uma criação rápida da lógica da aplicação e que
experimentos com opções da interface sejam triviais. Por último,
eventos customizados podem melhorar a clareza do código e facilitar
sua manutenção, pois eles deixam claro o relacionamento entre um
elemento e seus comportamentos. </para>
<para>Você pode ver a aplicação completa em
<filename>demos/custom-events.html</filename> e
<filename>demos/js/custom-events.js</filename> no código de
amostra.</para>
</section>
</section>
</section>
</chapter>