INDEVELOPMENTbeta






Subskrybuj m1chu.eu – another devblog
 
  •  Łukasz: Na stronce http://www.beautifulcode.pl/we bmaster/php/przewodnik-po-zabe zpieczeniach-aplikacji-php/...
  •  m1chu: Gdyby ktoś miał kiedyś problem z nieprawidłową wielkością pobieranego pliku, chociażby w moich, wyżej...
  •  m1chu: Jeżeli chodzi o szybką konwersję z Flash na HTML to szczerze nie wiem. Nigdy nie potrzebowałem żadnej...
  •  m1chu: @Michal: wrzuć linki w jakieś kontenery (listę ul -> li, dl -> dt/dd, czy chociażby w divy). Ustaw ich...
  •  Józek: Wszystko pięknie opisane, ja mam małe pytanko. Od jakiegoś czasu staram się dowiedzieć jak zmienić...
  •  Michal: a co jesli chciałbym umiescic kilka takich linkow obok siebie ?jesli zmienie wartość display na inline...
  •  mano: Co należy zrobić aby podmiane przycisku zastasowac kilka razy na stronie z różnymi grafikami ? Trzeba...

Dołącz do fanów!

Ankieta!

  • Jak oceniasz poziom artykułów? (dokładną opinię umieść w komentarzu)

    View Results

    Loading ... Loading ...




Wzorzec projektowania wtyczek jQuery na bazie metod i funkcji


Wzorzec projektowania wtyczek jQuery na bazie metod i funkcji

Praca z jQuery prędzej, czy później wymusza na nas kompleksowe tworzenie rozwiązań wykonywanych po stronie użytkownika. Metody filtrujące, animacje, czy efekty na galeriach zdjęć łatwiej będzie osiągnąć i rozwijać posługując się, łączącymi się na wtyczki, mechanizmami metod i funkcji, dostępnymi w frameworku.

Metoda, a funkcja?

Podstawowa różnica pomiędzy nimi polega na tym, że operują na innych obiektach. Każda nowa metoda musi być dołączona do prototypu jQuery.fn, a funkcja po prostu do obiektu jQuery.

/* metoda */
jQuery.fn.nazwa = function() {
	[...]
};
/* funkcja */
jQuery.nazwa = function() {
	[...]
};

W praktyce powoduje to, że do metody odwołujemy się operując na jakimś elemencie, identyfikatorze, bądź na klasie znajdującej się na stronie internetowej, a w przypadku funkcji, po prostu ją wywołujemy w celu wykonania jakieś akcji.

/* wywołanie: metoda */
$(element).nazwa();
/* wywołanie: funkcja */
var x = $.nazwa();
alert(x);

Obydwie możemy parametryzować. Argumentem może być zwykła zmienna, dowolnego, obsługiwanego przez JavaScript typu lub obiekt przechowujący zbiór pól o konkretnym przeznaczeniu.

/* metoda z parametrem */
jQuery.fn.nazwa = function(options) {
	[...]
};
/* funkcja z parametrem */
jQuery.nazwa = function(options) {
	[...]
};

Umowne zasady tworzenia pluginów

Twórcy jQuery, poprzez dokumentację, podpowiadają jakimi zasadami należy się kierować, aby poprawnie tworzyć wtyczki. Mianowicie:

  1. Terminologie funkcja, metoda, czy klasa są stosowanie umownie, z racji specyfiki przeznaczenia każdego z tych elementów. Fraza metoda jest w powyższym wypadku synonimem całego pojęcia wtyczki.
  2. Nazwy plików powinny być formowane następująco: jquery.[nazwa_wtyczki].js. Przykładowo: jquery.nazwa.js.
  3. W metodach this jest referencją do aktualnie przetwarzanego obiektu jQuery.
  4. Powinno się dołączać wtyczkę do obiektu nadrzędnego poprzez jQuery, a nie $, co użytkownikom zwiększa pole manewru we wprowadzaniu zmian z użyciem jQuery.noConflict().
  5. W celu uniknięcia problemów z poprawnością kodu po jego wcześniejszej kompresji, wszelkie metody oraz funkcje powinny kończyć się znakiem średnika (;).
  6. Użycie .each pozwala na bezproblemową iteracje po wszystkich elementach na których operujemy w metodzie.
  7. Metoda domyślnie musi zwracać wartość.

.each(function(indeks, element)) – przenosi się po każdym elemencie drzewa DOM, będących częścią obiektu jQuery, za każdym razem wykonując funkcję określoną jako parametr opisywanej metody.

Przykład:

<ol>
	<li>play</li>
	<li>the</li>
	<li>game</li>
</ol>
$('ol').each(function(i, e) {
	// e == this
	// e jest warte użycia w konkretnych zagnieżdżeniach, kiedy to this nie zawsze wskazuje na aktualnie analizowany element pętli
	$(e).html('<strong>' + $(this).text() + '</strong>');
});

Przekazywanie parametrów w celu kontrolowania zachowań wtyczki

Czym byłby plugin bez możliwości prostej parametryzacji określonych zachowań? W celu ułatwienia zrozumienia ich tworzenia postaramy się stworzyć bardzo prosty tester poprawności wpisywanych do pola danych (np. o identyfikatorze #nick). Sprawdzimy w nich, czy wprowadzony tekst będzie odpowiadał odpowiedniemu wzorcowi wyrażenia regularnego i będzie wystarczająco długi. Ponadto określimy do jakiego elementu zwrócić informację na wypadek niespełnienia wcześniej wymienionych kryteriów. Warto więc będzie zdefiniować na stałe wartości tych opcji oraz dać przyszłym jej użytkownikom możliwość nadpisania ich w argumentach wywoływanej metody.

jQuery.extend([ tryb_rekursywny ,] obiekt_rozszerzany, obiekt_1_rozszerzający [, ... obiekt_n_rozszerzający]) – łączenie dwóch lub kilku właściwości obiektów do osobnego.
  • tryb_rekursywny – przyjmuje wartości logiczne (true/false) w celu wykonania rekursywnego łączenia,
  • obiekt_rozszerzany – element nadpisywany (w celu uniknięcia należy pozostawić puste),
  • obiekt_x_rozszerzający – elementy łączone.

Załóżmy więc, że nazwiemy naszą wtyczkę filtrate, a domyślne dane będziemy przetrzymywać w obiekcie defaults.

jQuery.fn.filtrate = function() {
	var defaults = {
		length:		6, // minimalna długość wprowadzanego tekstu
		regexp:		/[0-9a-z]/gi, // kryterium wprowadzanych znaków
		returnto:	'footer' // informacja o błędzie zostanie zwrócona do tego elementu
	};

	alert(defaults.regexp); // przykładowe użycie
};

Za pomocą parametru options przekażemy spersonalizowane dane konfiguracyjne.

jQuery.fn.filtrate = function(options) {
	var defaults = {
			length:		6, // minimalna długość wprowadzanego tekstu
			regexp:		/[0-9a-z]/gi, // kryterium wprowadzanych znaków
			returnto:	'footer' // informacja o błędzie zostanie zwrócona do tego elementu
	}; 

	// rozszerzenie domyślnej konfiguracji
	var options = $.extend(defaults, options);

	alert(options.regexp); // przykładowe użycie
};

Propozycje wywołań metody z użyciem argumentów.

$('#nick').filtrate({length: 3});
// lub
$('#nick').filtrate({
	regexp: /[0-9]/gi,
	length: 3
});

Do ustawień nie ma jednak publicznego dostępu. Możemy co prawda skonfigurować je przy wywoływaniu wtyczki, ale nie bezpośrednio, np. później. W tym celu należy przenieść domyślne wartości do własności składowej metody oraz połączyć wprowadzane i predefiniowane dane bez modyfikacji tych drugich.

jQuery.fn.filtrate = function(options) {
	// rozszerzenie domyślnej konfiguracji bez jej nadpisywania (pierwszy argument pusty)
	var options = $.extend({}, $.fn.filtrate.defaults, options);

	alert(options.regexp); // przykładowe użycie
};

$.fn.filtrate.defaults = {
	length:		6, // minimalna długość wprowadzanego tekstu
	regexp:		/[0-9a-z]/gi, // kryterium wprowadzanych znaków
	returnto:	'footer' // informacja o błędzie zostanie zwrócona do tego elementu
};

Zewnętrzny programista ma teraz szansę wykonania ustawień przed i po wywołaniu wtyczki, a także bez konieczności robienia tego w bloku jQuery(document).ready(function() [...]);.

// nadpisanie długości spoza parametru
$.fn.filtrate.defaults.length = 3;
$('#nick').filtrate({regexp: /[0-9]/gi});

Prywatne funkcje – ukrywamy poszczególne elementy

Nie każda partia kodu powinna być dostępna do bezpośredniego wywołania przez użytkownika wtyczki. Przykładem może być tutaj funkcja użytkowa, stworzona stricte do celów danej metody. Aby tego dokonać, należy opleść całą definicję pluginu w funkcję zamykającą (closure) oraz po za samą metodą użyć standardowej formy tworzenia funkcji w JavaScript. Metodologia tworzenia funkcji w jQuery nie zda tutaj prawidłowego rezultatu. Dostęp będzie nadal publiczny.

(function($) { // start: closure
	$.fn.filtrate = function() {
		$.logs(1); // ostrzeżenie z zawartością '1'

		logs(1); // ostrzeżenie z zawartością '1'
	};		

	function logs($obj) {
    	alert($obj);
  	};

	$.logs = function($obj) {
		alert($obj);
	};
})(jQuery); // end: closure

jQuery(document).ready(function() {
	$('#nick').filtrate();

	$.logs(1); // ostrzeżenie z zawartością '1'
	logs(1); // brak dostępu do tej funkcji z zewnątrz
});

Wywoływanie funkcji oraz innych metod we wtyczce

Zaistnieć może także sytuacja przeciwna do powyższej. Z różnych powodów możemy chcieć skorzystać z publicznie dostępnej metody, zaimplementowanej wewnątrz wtyczki, którą tworzymy lub z funkcji przedstawionych na początku artykułu.

[...]
        <footer>
        	<p>Wpisz swój script nick</p>
            <p>Wpisz swój a href nick</p>
        </footer>
[...]

Mamy przykładowy zestaw paragrafów, z których będziemy chcieli wykluczyć pewny ciąg znaków. Ponieważ będziemy sprawdzać więcej elementów niż jeden, skorzystamy z wcześniej wymienionej metody iteracyjnej .each(), a następnie pobierzemy ich zawartości, usuniemy stosowną frazę (korzystając z osobnej funkcji/metody) i zwrócimy wynik do znacznika je zawierającego.

jQuery.fn.filtrate = function(options) {
	var defaults = {
		erase:	 	/a href/g, // tekst do usunięcia
		usemethod:	true // pomocnicze, użycie metody lub funkcji
	};
	// rozszerzenie domyślnej konfiguracji
	var options = $.extend(defaults, options);

	return this.each(function() {
		var context = $(this).html(); // pobranie zawartości elementu
		switch (options.usemethod) // dodany w celach naukowych ;]
		{
			case false:
				context = $.cleanup(context, options.erase); // wywołanie funkcji
				break;
			default:
				context = $.fn.filtrate.cleanup(context, options.erase); // wywołanie metody
				break;
		}
		$(this).html(context); // zapisanie zmodyfikowanej zawartości elementu
	});
};		

// metoda zaimplementowana we wtyczce
jQuery.fn.filtrate.cleanup = function(context, erase) {
	return context.replace(erase, ''); // usunięcie frazy z ciągu
};

// osobna funkcja
jQuery.cleanup = function(context, erase) {
	return context.replace(erase, ''); // usunięcie frazy z ciągu
};

jQuery(document).ready(function() {
	// tryb wywoływania z metody
	$('footer p').filtrate({erase: /script/g});

	// tryb wywoływania spoza metody
	return $('footer p').each(function() {
		var context = $(this).html();
		context = $.fn.filtrate.cleanup(context, /a href/g);
		context = $.cleanup(context, /script/g);
		$(this).html(context);
	});
});

Osiągamy postawiony cel

Na podstawie powyższych rad możemy osiągnąć postawiony sobie cel – tester poprawności wpisywanych danych do pola typu input. Rozwiązanie będzie zezwalać na testowanie wielu pól naraz.

[...]
    	<header>
			<p><input type="text" id="nick"></p>
			<p><input type="text" id="nick_second"></p>
        </header>
[...]

Wywołanie odbywać się będzie w momencie zwolnienia przycisku klawiatury.

jQuery(document).ready(function() {
	$('input').keyup(function() {
		$('input').filtrate({regexp: /[a-z]/g});
	});
});

W przestrzeni funkcji zawierającej wtyczkę znajdzie się prywatna funkcja logująca akcje oraz dwie publiczne metody implementowane w pluginie. Pierwsza z nich będzie sprawdzać poprawność wpisywanego tekstu pod względem zadanego wzorca, a druga długość wprowadzonego ciągu znaków.

	// metoda testująca poprawność wpisanych znaków
	$.fn.filtrate.preg_match = function(pattern, subject) {
		var subject_splited = subject.split(''); // utworzenie tablicy znaków
		for ( i=0; i < subject_splited.length; ++i ) // iteracja po każdym ze znaków
		{
			// jeżeli którykolwiek znak nie będzie odpowiadał wzorcowi metoda zwraca fałsz
			if ( subject_splited[i].match(new RegExp(pattern)) == null )
			{
				return false;
			}
		}
		return true; // wszystkie znaki są odpowiednie, prawda - jedziemy dalej ;]
	};

	// metoda porównująca długości ciągów
	$.fn.filtrate.strlen_compare = function(string, min_length) {
		var length = string.length;
		if ( length < 1 ) // tekst nie został wpisany
		{
			return false;
		}
		else if ( length < min_length ) // tekst krótszej długości niż wymagana
		{
			return min_length - length;
		}
		return true; // tekst odpowiedniej długości
	};

	// funkcja prywatna, logująca akcje
	function log_alerts(returnto, text)
	{
		// jeżeli nie podano tekstu do wypisania, czyścimy zawartość znacznika przetrzymującego ostrzeżenia i informacje
		if ( text == undefined )
		{
			$(returnto).html('');
			return;
		}

		// dodanie nowego akapitu z zwróconą treścią do podanego znacznika
		$('<p>' + text + '</p>').appendTo(returnto);
	}

Domyślne opcje ustawimy poza wtyczką. W niej samej, dla każdego wskazanego elementu strony, sprawdzimy najpierw warunek długości, a gdy zostanie spełniony, także zawartości.

(function($) { // start: closure
	$.fn.filtrate = function(options) {
		// rozszerzenie domyslnej konfiguracji bez jej nadpisywania (pierwszy argument pusty)
		var options = $.extend({}, $.fn.filtrate.defaults, options);

		var context, strlen_compared_result;

		// czyszczenie znacznika zawierającego informacje o stanie pól
		log_alerts(options.returnto);

		// sprawdzenie każdego elementu
		return this.each(function(i) {
			context = $(this);

			// testowanie długości
			strlen_compared_result = $.fn.filtrate.strlen_compare(context.val(), options.length);
			switch (strlen_compared_result)
			{
				case false: // w wypadku niewpisania tekstu
					// logowanie akcji
					log_alerts(options.returnto, 'Pole <strong>' + context.attr('id') + '</strong> jest puste');
					return true;
					break;
				case true: // w wypadku odpowiedniej długości tekstu
					// puste
					break;
				default: // w wypadku za krótkiego tekstu zwracany jest: tekst + wymagana długość + ilość brakujących znaków
					log_alerts(options.returnto, 'Pole <strong>' + context.attr('id') + '</strong> zawiera za mało znaków (minimum: ' + options.length + ' / brakuje: ' + strlen_compared_result + ')');
					return true;
					break;
			}

			// testowanie typów znaków znajdujących się w tekście
			switch ($.fn.filtrate.preg_match(options.regexp, context.val()))
			{
				case false: // w wypadku pojawienia się niedozwolonych znaków
					log_alerts(options.returnto, 'Pole <strong>' + context.attr('id') + '</strong> zawiera niedozwolone znaki');
					return true;
					break;
				default: // w wypadku poprawnego sformowania tekstu
					log_alerts(options.returnto, 'Pole <strong>' + context.attr('id') + '</strong> jest prawidłowo wypełnione');
					break;
			}
		});
	};

	$.fn.filtrate.defaults = {
		length:	 6, // minimalna dlugosc wprowadzanego tekstu
		regexp:	 /[0-9a-z]/gi, // kryterium wprowadzanych znaków
		returnto:   'footer' // informacja o bledzie zostanie zwrócona do tego elementu
	};

	// pozostałe dwie metody + prywatna funkcja
})(jQuery); // end: closure

Przykład ten powinien przybliżyć kwestię wcześniej poruszone w artykule. Na jego podstawie udostępniłem także testową stronę oraz gotowe pliki do pobrania.


3 komentarzy

Dodaj własny komentarz

Możesz użyć następujących tagów XHTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>