Jak zrobić własny event handler?

Jakiś czas temu zrobiłem swój własny event handler dla wszystkich niecodziennych zdarzeń (czyli innych niż domyślne, DOMowe), a że z równania „dawno nie pisałem” + „data ostatniego postu” wychodzi mi „wstyd” – chciałbym opisać proces tworzenia takiego skryptu.

Na wstępnie chciałbym zaznaczyć, że nie chcę zaprzestawać pisania bloga, ale z uwagi, że mam ograniczony wolny czas, postanowiłem pisać notki w maksimum 45 minut plus ewentualna korekta. Wierzę, że okaże się to solidnym antidotum na brak nowych postów. Tak więc – zaczynamy.

Pierwsze, co należy sobie uzmysłowić przy pisaniu event handlera to sposób jego działania. To jest relatywnie proste. Przyjmijmy, że mamy zdarzenie o nazwie item changed, które wskazuje, że coś tam nam się zmieniło w aplikacji – na przykład właściwość innerHTML jakiegoś elementu DOM. Analogicznie do zdarzeń typu onclick, mouseover i tak dalej – przypisujemy zdarzeniu item changed jakąś funkcję, która się odpali po każdym wystąpieniu… item changed. Z kolei to swoiste wystąpienie zdarzenia definiujemy za pomocą funkcji, takich jak fireEvent, fire, triggerEvent czy jakiejkolwiek, która będzie nam się podobać.

Pseudokod wygląda następująco:

BibliotekaOdEventow.przypiszJakiesZdarzenieFunkci(
nazwaZdarzenia, 
funkcjaKtoraMaSieOdpalicPoWykryciuZdarzenia,
kontekstWJakimUruchomionaBedzieFunkcja);

W innej części aplikacji, jeśli zdarzenie wystąpiło, dajemy o tym znać części odpowiedzialnej za zdarzenia:

BibliotekaOdEventow.wywolajZdarzenie(nazwaZdarzenia);

W ten sposób funkcja, którą przypisaliśmy do zdarzenia o danej nazwie automatycznie uruchomi się po użyciu pseudo-metody wywolajZdarzenie.

System ten działa podobnie do standardowych zdarzeń DOM:

element.addEventListener("click", function() {}, false);

Tyle że w naszym wypadku musimy dać znać, że chcemy wywołać jakieś zdarzenie. W DOM naturalnie robi to za nas przeglądarka i silnik JS. Do dzieła!

Definiujemy najpierw obiekt – bibliotekę, która będzie odpowiedzialna za trzy zadania – dodanie zdarzenia, uruchomienie go i usunięcie.

var EventHandler = {
	_events: {}
};

Trochę pustawo – mamy jedynie jedną składową obiektu, którą jest _events. To w tym podobiekcie, robiącym za tablicę asocjacyjną, będziemy przetrzymywać wszystkie dodane zdarzenia. No właśnie, musimy je jakoś dodawać. Niech służy temu metoda bind:

var EventHandler = {
	_events: {},

	bind: function(name, callback, context) {
		if (arguments.length === 1 && typeof arguments[0] === "object") {
			var eventName = arguments[0].name;
			var callback = arguments[0].callback;
			var context = arguments[0].context;
		}
		
		var eventName = eventName || name;

		if (!eventName || !callback) {
			throw new Error("Name and callback are required!")
		} 
	
		if (!(typeof eventName == "string")) {
			throw new Error("Event name should be a string!");
		} 
		
		if (!(typeof callback == "function")) {
			throw new Error("Event callback should be a function!");
		}
		
		if (!eventName.replace(/\s/, "")) {
			throw new Error("Event name should not be an empty string!");
		}

		if (!this._events[eventName]) {
			this._events[eventName] = [];
		}

		this._events[name].push((!arguments[1]) ? arguments[0] : {name: eventName, callback: callback, context: context});
	},

Dzięki takiej, a nie innej konstrukcji funkcji bind, możemy jej użyć dwojako:

EventHandler.bind("item changed", function() { alert("Item is changed"); }, this);

EventHandler.bind({name: "item changed", callback: function() { alert("Item is changed"); }, context: this});

Co ciekawego robi funkcja bind? Najpierw sprawdza, czy argumentem jest obiekt, jeśli tak to przypisuje jego właściwości do zmiennych. Potem mamy serię sprawdzeń, czy podane parametry funkcji są właściwe. Jeśli nie – plujemy stosownym błędem, wbudowanym w silnik JS. Na końcu dodajemy do tablicy w _events nowy obiekt, reprezentujący zdarzenie.

Czas na metodę fire:

fire: function(name, data) {
	if (name && this._events[name]) {
		var data = data || {};
		
		for (var i = this._events[name].length-1; i > -1; i--) {
			var res = this._events[name][i].callback.call(this._events[name][i].context, data);
	
			if (res === false) {
				return false;
			}
		}
	}
},

Sprawdza ona, czy zdarzenie o podanej nazwie name zostało dodane i uruchamia wszystkie callbacki co do tego zdarzenia w podanym kontekście (context). Opcjonalnie możemy uruchomić fire z drugim parametrem. Wtedy zostanie on przekazany do każdego callbacka. Co ciekawe, jeśli nasza funkcja zwraca false – zatrzymuje uruchamianie pozostałych callbacków. Zdarzenia uruchamiane są bąbelkowo.

Usuwanie jest trywialne:

remove: function(name) {
	if (this._events[name]) {
		delete this._events[name];
	}
}

Jak to działa w praktyce? Na przykład tak:

var Controller = function() {
	this.itemValue = "value";

	this.setValue = function(value) {
		this.itemValue = value;

		EventHandler.fire("item changed", { newValue: value });
	};
};

var View = function() {

	this.alert = function(eventData) {
		alert("Nowa wartość to: " + eventData.newValue);
	};

	EventHandler.bind("item changed", this.alert, this);
};

var c = new Controller;
var v = new View;
c.setValue("new value");

Działa nieźle.

Warto dodać, że podobną funkcjonalność oferuje jQuery – możemy w łatwy sposób podpinać swoje zdarzenia, a „wyzwalać je” (co u nas robi funkcja fire) poprzez trigger.

Komentarze

1

Bedą kolejne części jquery?

2

Teksty o jQuery nie stanowia dla mnie wyzwania – nawet nie wiem, co mialbym napisac. Jesli padna propozycje tematow to sie zastanowie :-)

3

@matiit
Ludzie, co wy byscie zrobili jakby John nie napisal jQuery? ;-)
JS to taki (calkiem) prosty i fajny jezyk…

4

Bardzo fajny art – warto wiedziec jak zrobic takie rzeczy „od kuchni”.
Prototype robi dokladnie to samo uzywajac wrapperow: Event.observe(‚twoj:event’) by ‚zlapac’ event, oraz funkcji .fire(‚twoj:event’) ktora go wywoluje. Bardzo przydatne przy skryptach ktore ladowane sa zbyt pozno (np. po document.ready/dom:loaded)

5

a moze cos o json, ajax w jquery? lub plugin?

Dodaj komentarz

Dozwolone tagi: <blockquote>, <code>, <strong>