Rozdział 11. Tworzenie własnych zdarzeń

> Dodaj do ulubionych

Zdarzenia własne w jQuery — wprowadzenie

Do tej pory poznaliśmy podstawowe zdarzenia — np. click, mouseover, focus, blur, submit — które możemy wykorzystać podczas interakcji użytkownika z przeglądarką. Zdarzenia własne zaś otwierają przed nami całe spektrum nowych możliwości, jakie oferuje programowanie zdarzeniowe. W tym rozdziale wykorzystamy zdarzenia własne jQuery do utworzenia prostej przeszukiwarki Twittera.

Na początku możesz zastanawiać się, jaki sens ma tworzenie własnych zdarzeń, podczas gdy wbudowane zdarzenia w pełni spełniają swoje zadanie. Własne zdarzenia pozwalają spojrzeć na mechanizm zdarzeń JavaScriptu w całkiem nowym świetle. Zdarzenia własne skupiają się nie na elemencie, który wywołuje daną akcję, lecz na elemencie, który jest jej poddawany. Niesie to ze sobą wiele korzyści, między innymi:

  • Możliwość łatwego wywołania zachowań elementu docelowego za pomocą innych elementów, przy użyciu tego samego kodu.
  • Możliwość jednoczesnego wyzwalania zachowań wielu podobnych elementów docelowych.
  • Zachowania są wyraźniej powiązane z docelowymi elementami, co ułatwia czytanie kodu oraz jego utrzymanie.

Dlaczego warto znać te techniki? Najlepiej pokaże to przykład. Przypuśćmy, że w domu w jednym z pokoi masz żarówkę. Żarówka jest w tej chwili włączona; sterują nią dwa trójobwodowe przełączniki oraz klaskacz:

<div class="room" id="kitchen">
   <div class="lightbulb on"></div>
   <div class="switch"></div>
    <div class="switch"></div>
    <div class="clapper"></div>
</div>

Wyzwolenie klaskacza lub któregoś z przełączników zmieni stan żarówki. Dla przełączników i klaskacza stan żarówki nie ma znaczenia; elementy te chcą po prostu zmienić jej stan.

Tak natomiast mógłby wyglądać twój kod bez zdarzeń własnych:

$('.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');
    }
});

Jeśli użyłbyś zdarzeń własnych, twój kod mógłby wyglądać następująco:

$('.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');
}); 

Ostatnia część kodu być może nie wygląda zbyt fascynujaco, jednak zawiera istotny szczegół: przenieśliśmy zachowanie żarówki z przełączników i klaskacza do żarówki.

Urozmaićmy nieco nasz przykład. Dodamy teraz do naszego domu jeszcze jeden pokój wraz z głównym przełącznikiem, co widać w poniższym kodzie:

<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>

Jeśli w domu będą włączone światła, zadaniem głównego przełącznika ma być ich wyłączenie; w przeciwnym wypadku ma je natomiast włączyć. W tym celu dodamy do żarówek jeszcze dwa zdarzenia własne: turnOn i turnOff. Wykorzystamy je w zdarzeniu własnym changeState oraz dodamy instrukcje warunkowe, dzięki którym główny przełącznik będzie wiedział, które zdarzenie wyzwolić:

$('.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');
    }
});

Zwróć uwagę, że zachowanie głównego przełącznika jest podpięte do głównego przełącznika, a zachowanie żarówki należy do żarówek.

Powtórka: funkcje $.fn.bind i $.fn.trigger

W świecie zdarzeń własnych istnieją dwie ważne metody jQuery: $.fn.bind i $.fn.trigger. Ich zastosowanie w zdarzeniach użytkownika omówione zostało w rozdziale Zdarzenia. Jeśli natomiast chodzi o zagadnienia omawiane obecnie, należy zapamiętać dwie rzeczy:

  • Metoda $.fn.bind przyjmuje jako argumenty typ zdarzenia oraz funkcję obsługi zdarzeń. Opcjonalnie metoda ta może również jako drugi argument przyjąć związane ze zdarzeniem dane — wówczas funkcja obsługi zdarzeń przyjmowana jest jako argument trzeci. Przekazane dane są dostępne dla funkcji obsługi zdarzeń we własności data należącej do obiektu zdarzenia. Funkcja obsługi zdarzeń zawsze przyjmuje obiekt zdarzenia jako pierwszy argument.
  • Metoda $.fn.trigger przyjmuje jako argument typ zdarzenia. Opcjonalnie może także przyjąć tablicę wypełnioną wartościami. Wartości te zostaną przekazane funkcji obsługi zdarzeń jako argumenty w następnej kolejności po zdarzeniu obiektu.

Oto przykład wykorzystania metod $.fn.bind i $.fn.trigger, które w obu przypadkach korzystają ze zdarzeń własnych:

$(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' ]);

Przykładowa aplikacja

Aby pokazać potencjał zdarzeń własnych, utworzymy proste narzędzie do przeszukiwania Twittera. Użytkownik będzie miał możliwość wyszukiwania na kilka sposobów: poprzez wpisanie szukanego hasła w polu tekstowym, dodawanie wielu haseł do adres URL i wysyłanie zapytań o najpopularniejsze tematy poruszane na Twitterze.

Wyniki dla każdego wyszukiwania będą pojawiać się w kontenerach wyników; kontenery będzie można rozwijać, zwijać, odświeżać, oraz usuwać — pojedynczo lub wszystkie na raz.

Kiedy skończymy, nasza wyszukiwarka będzie wyglądać tak:

Zrzut przykładowej aplikacji

Przygotowanie

Zaczniemy od podstawowego kodu HTML:

<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>

W ten sposób utworzymy kontener (#twitter) dla naszego widżetu, szablon dla naszych kontenerów wyników (ukrytych za pomocą CSS) oraz prosty formularz, w który użytkownik wpisuje szukane wyrażenie (dla uproszczenia zakładamy, że nasza aplikacja bazuje tylko na JavaScripcie, a CSS będzie prawidłowo funkcjonował u naszych użytkowników).

Będziemy wykonywać operacje na dwóch typach obiektów: na kontenerach wyników oraz na kontenerze Twittera.

Kontenery wyników stanowią serce aplikacji. Utworzymy wtyczkę przygotowującą kontenery wyników po dodaniu ich do kontenera Twittera. Między innymi powiąże ona zdarzenia własne z każdym kontenerem oraz doda w prawym górnym rogu każdego z nich przyciski akcji. Każdy kontener wyników będzie miał następujące zdarzenia własne:

refresh
Kontener przechodzi w stan „odświeżania” oraz wysłane zostaje żądanie o pobranie danych dla szukanego hasła.
populate
Odbiera zwrócone dane w formacie JSON i wstawia je do kontenera.
remove
Usuwa kontener ze strony po zatwierdzeniu takiej operacji przez użytkownika. Weryfikację można ominąć przez przekazanie do procedury obsługi zdarzeń wartości true jako drugiego argumentu. Zdarzenie remove usuwa również hasło powiązane z kontenerem wyników, który znajduje się w globalnym obiekcie zawierającym szukane hasła.
collapse
Dodaje do kontenera klasę oznaczającą zwinięcie, w wyniku czego wyniki zostaną ukryte za pomocą CSS. Nastąpi także zamiana przycisku „Collapse” na „Expand”.
expand
Usuwa z kontenera klasę oznaczającą zwinięcie. Przycisk „Expand” zostaje zamieniony na „Collapse”.

Zadaniem wtyczki jest również dodawanie do kontenera przycisków akcji. Wtyczka wiąże zdarzenie click z elementem listy każdej akcji i sprawdza klasę elementu do określania, które zdarzenie własne ma zostać wyzwolone w odpowiednim kontenerze wyników.

$.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]
                );
            }
        );
 
        // korzysta z klasy każdej akcji, by sprawdzić
        // które zdarzenie zostanie wyzwolone na panelu wyników
        $a.find('li').click(function() {
            // przekazuje kliknięty element li do funkcji
            // aby w razie potrzeby można nim było później manipulować
            $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) {
           // zaznacza, że wyniki są odświeżane
        var $this = $(this).addClass('refreshing');
 
        $this.find('p.tweet').remove();
        $results.append('<p class="loading">Loading ...</p>');
 
        // pobiera dane Twittera za pomocą techniki JSNOP
        $.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);
        });
 
        // zaznacza, że odświeżanie wyników
        // dobiegło końca
        $this.removeClass('refreshing');
    },
 
    remove : function(e, force) {
        if (
            !force &&
            !confirm('Remove panel for term ' + e.data.term + '?')
        ) {
            return;
        }
        $(this).remove();
 
        // zaznacza, że nie ma już
        // panelu dla danego hasła
        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');
    }
};

Sam kontener Twittera będzie korzystał tylko z dwóch zdarzeń własnych:

getResults
Otrzymuje szukane hasło i sprawdza, czy istnieje już dla niego kontener wyników. Jeśli nie, kontener zostanie dodany i ustawiony przy użyciu szablonu wyników i za pomocą omówionej wyżej wtyczki $.fn.twitterResult. Następnie zostanie na nim wyzwolone zdarzenie refresh w celu załadowania wyników. Na koniec w kontenerze zostanie zachowane szukane hasło, aby aplikacja wiedziała, że nie należy go ponownie ładować.
getTrends
Wysyła do Twittera zapytanie o 10 najpopularniejszych tematów, następnie przegląda je za pomocą pętli i poprzez wyzwolenie zdarzenia getResults dodaje kontener wyników dla każdego tematu.

Oto jak wygląda wiązanie kontenerów Twittera:

$('#twitter')
    .bind('getResults', function(e, term) {
        // upewniamy się, że nie mamy jeszcze pola dla tego hasła
        if (!search_terms[term]) {
            var $this = $(this);
            var $template = $this.find('div.template');
 
            // tworzymy kopię szablonu div
            // i wstawiamy go jako pierwsze pole wyników
            $results = $template.clone().
                removeClass('template').
                insertBefore($this.find('div:first')).
                twitterResult({
                    'term' : term
                });
	 
            // ładuje treść za pomocą zdarzenia własnego
            // refresh, które powiązaliśmy z kontenerem wyników
            $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 ]);
                });
            });
    });

Do tej pory napisaliśmy sporo kodu, który tak naprawdę nic nie robi — nie powinniśmy się jednak przejmować. Dzięki określeniu zachowań naszych podstawowych obiektów stworzyliśmy solidną ramę projektową, która posłuży nam do szybkiego rozbudowania interfejsu.

Zacznijmy od utworzenia punktów zaczepienia dla naszych danych wejściowych w formie tekstu i przycisku „Load Trending Terms”. W przypadku danych z pola tekstowego przechwycimy wpisane hasło i przekażemy je podczas wyzwalania zdarzenia getResults w kontenerze Twittera. Natomiast kliknięcie przycisku „Załaduj najpopularniejsze tematy” wyzwoli zdarzenie getTrends kontenera Twittera:

$('form').submit(function(e) {
    e.preventDefault();
    var term = $('#search_term').val();
    $('#twitter').trigger('getResults', [ term ]);
});
 
$('#get_trends').click(function() {
    $('#twitter').trigger('getTrends');
});

Dzięki dodaniu kilku przycisków z odpowiednimi identyfikatorami, możemy usuwać, zwijać, rozwijać i odświeżać wszystkie kontenery wyników, co widać na poniższym przykładzie. Zwróć uwagę, że w przypadku przycisku usuwania, do procedury obsługi zdarzeń przekazujemy wartość true jako drugi argument — procedura nie będzie więc weryfikować usunięcia pojedynczych kontenerów.

$.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 ]);
    }
});

Podsumowanie

Korzystanie ze zdarzeń własnych dają nam nową perspektywę myślenia o kodzie. W przypadku tych zdarzeń nacisk kładziony jest na docelowym elemencie danego zachowania, a nie na tym, który je wyzwala. Jeśli na początku pracy poświęcisz trochę czasu na wydzielenie poszczególnych części aplikacji oraz ich żądanych zachowań, zdarzenia własne mogą okazać się wszechstronnym narzędziem do „komunikacji” z tymi częściami — z jedną lub z kilkoma na raz. Po opisaniu zachowań danej części, zachowania te mogą być bardzo łatwo wyzwolone w dowolnym miejscu aplikacji, co pozwala na szybkie tworzenie opcji interfejsu i eksperymentowanie z nimi. Ponadto, dzięki czytelnym relacjom pomiędzy elementami i ich zachowaniami, zdarzenia własne ułatwiają czytanie i konserwację kodu.

Autor: Rebecca Murphey

Źródło: http://github.com/rmurphey/jqfundamentals

Tłumaczenie: Joanna Liana

Treść tej strony jest dostępna na zasadach licencji CC BY-SA 3.0 US