Szczypta dynamiki
Od pewnego czasu pracuję nad nową wersją Canoe. Jednym z ważniejszych punktów na mojej liście rzeczy do zrobienia było napisanie zgrabnego instalatora, obsługiwanego w całości z poziomu przeglądarki. Po zaprogramowaniu mechanizmów tworzących bazę danych przyszedł czas na napisanie interface'u. Stworzenie szablonu z formularzem i wzbogacenie go o odpowiedni arkusz stylów nie było dla mnie niczym nowym. Założenia jakie przyjąłem wymagały jednak również dodania dynamiki do procesu instalacji. Jednym z pytań jakie stawia się użytkownikowi jest pytanie o rodzaj i szczegóły (takie jak login i hasło) dotyczące bazy danych. Problem jednak w tym, że różne rodzaje baz wymagają podania różnych zestawów parametrów. Nie chcąc czynić całego procesu dwustopniowym, postanowiłem włożyć odrobinę wysiłku w przygotowanie JavaScriptu zapewniającego wymaganą funkcjonalność. W tym artykule przedstawiłem proces pisania tego właśnie skryptu, linijka po linijce opisując wykorzystane konstrukcje języka. Moja uwaga skupia się również na analizie błędów typowych dla początkującego programisty JavaScriptu. Jeżeli chcesz zrozumieć podstawy tworzenia dynamicznych stron, ten artykuł jest dla Ciebie.
Zanim przejdziesz dalej, obejrzyj jak wygląda efekt działania skryptu. Gdy już się nim nacieszysz, wróć do artykułu, by dowiedzieć się w jaki sposób to wszystko działa.
Narzędzia
Jako platformę testową wykorzystałem przeglądarkę Firefox w wersji 1.5 z zainstalowaną wtyczką FireBug, stanowiącą nieocenioną pomoc przy śledzeniu błędów w skryptach (i nie tylko, bo równie dobrze ułatwia poprawianie arkuszy stylów).
Przygotowanie dokumentu
Mając już gotowy dokument HTML rozejrzałem się wpierw za metodami
pozwalającymi na dołączenie do niego skryptów. Po przeczytaniu
artykułu wspominającego o
problemach
wynikających z osadzania JavaScriptu bezpośrednio w dokumentach
HTML, zdecydowałem oszczędzić sobie kłopotu i użyć jedynej
słusznej metody umieszczenia skryptu w oddzielnym pliku. W
szablonie strony pomiędzy tagami <head>
dodałem linię:
i utworzyłem nowy plik engine.js, który wkrótce
zapełni się treścią skryptu.
Wiedząc, że jednym z filarów manipulacji treścią dokumentu
jest document.getElementById, oznaczyłem listę
typów baz danych unikatowym identyfikatorem engine:
<option value="mysql" selected="selected">
MySQL
</option>
<option value="sqlite">
SQLite
</option>
</select>
Zdarzenia
Nie czekając ani chwili dłużej zapisałem pierwsze dwie linie skryptu. W pierwszej za pomocą wspomnianej funkcji znajduję element o identyfikatorze engine, w drugiej wyświetlam ostrzeżenie, o treści będącej identyfikatorem obiektu element (czyli po prostu engine):
alert(element.id);
Z nadzieją w sercu wcisnąłem Ctrl+R,
nadaremnie jednak. Firebug powiadomił mnie o
błędzie:

element nie posiada atrybutów
Dlaczego interpreter twierdzi, że obiekt zwrócony przez
getElementById nie posiada żadnych atrybutów?
Zaglądając do
dokumentacji
można dowiedzieć się, że funkcja ta może zwrócić jedną z dwóch
wartości: albo to będzie obiekt opisujący element o zadanym
identyfikatorze, albo null. Z tych dwóch tylko
null nie posiada atrybutów, co oznacza, że w pierwszej
linijce skryptu element przyjmuje właśnie wartość
null. To zaś świadczy o tym, że element o podanym
identyfikatorze w dokumencie nie został znaleziony. Dlaczego tak
jest, skoro umieściliśmy w dokumencie element <select>
o atrybucie id="engine"? Przyczyną jest sposób
działania przeglądarki, która wczytuje i interpretuje
elementy dokumentu jeden po drugim, przez co skrypt zostaje
wykonany jeszcze zanim reszta dokumentu, a w tym i element
select, zostaną wczytane1!
Mało eleganckim rozwiązaniem
(i niezgodnym ze standardem) byłoby przeniesienie elementu
<script> na koniec dokumentu. Istnieje jednak
w zupełności poprawna metoda na osiągnięcie naszego celu, jakim
jest opóźnienie wykonania naszego kodu do momentu wczytania całego
dokumentu. Z pomocą przychodzą nam
zdarzenia
(ang. events). Z każdym ze zdarzeń możemy związać jedną lub
więcej funkcji, które zostaną wywołane przez interpreter JavaScriptu
w momencie wystąpienia danego zdarzenia. Interesującym nas zdarzeniem,
pod który podłączymy swój kod, jest load; występuje
ono zaraz po wczytaniu dokumentu. Do rejestracji zdarzeń służy
metoda addEventListener2.
Ostatnią rzeczą, jaka jest nam potrzebna jest sposób
definiowania funkcji, który jest w istocie bardzo prosty:
{
instrukcje;
}
We wnętrzu funkcji zmienne definiuje się przy pomocy słowa kluczowego var:
Jeżeli chcemy, by funkcja zwracała wartość, oznaczamy to poprzez return. Punktów wyjścia z funkcji (a więc i dyrektyw return) może być kilka. Jeżeli wykonywanie funkcji dotrze do nawiasu końcowego, funkcja zwróci wartość null (ang. nic). Przykładowo, jeżeli chcemy zwrócić sumę dwóch liczb podanych jako argumenty, możemy napisać:
{
return a + b;
}
Uzbrojony w taką wiedzę, byłem w stanie napisać wreszcie działającą wersję skryptu:
{
var element = document.getElementById("engine");
alert(element.id);
}
window.addEventListener("load", init, true);
Szkielet skryptu
Po przełamaniu pierwszych lodów należałoby wreszcie zastanowić się nad logiką całego skryptu. Nie jest ona szczególnie skomplikowana: dla każdej z opcji chcę wyświetlić inny zestaw pól tekstowych do wypełnienia. Dla bazy danych SQLite będzie to pojedyncze pole do wpisania pełnej ścieżki do pliku bazy danych, zaś dla silnika MySQL pola będą już cztery, odpowiadające kolejno za nazwę hosta, nazwę bazy danych, jak i za nazwę i hasło użytkownika. Gdy użytkownik wybierze z listy silnik bazy danych, którego chciałby użyć, wchodzi do gry nasz skrypt i dynamicznie dostosowuje listę pól wyświetlanych na stronie. Całość można ująć następująco:
-
po wczytaniu strony:
- utwórz listę pól dla każdego z silników
- zarejestruj obsługę zdarzenia "zmiana opcji" dla elementu o identyfikatorze "engine"
-
przy zmianie silnika:
- znajdź element, do którego podłączymy listę opcji
- podłącz do niego listę pól odpowiednią dla wybranego silnika
Na podstawie tych punktów przygotowałem zarys kodu. Punkt
1a rozpisany jest w liniach 18-27.
Nazwy sqlite_elements i mysql_elements
wiążę z tablicami, której elementami są akapity zwrócone przez funkcję
create_input_p. Warto zwrócić tutaj uwagę na prostotę
definiowania tablic: wystarczy elementy rozdzielone przecinkiem umieścić
pomiędzy nawiasami kwadratowymi. Definicję tablic umieściłem na poziomie
globalnym skryptu, a nie we wnętrzu funkcji init(), bo przy
ich konstrukcji nie będziemy korzystać z elementów dokumentu, przez co
możemy tę konstrukcję przeprowadzić od razu, nie czekając na wczytanie
całej strony. Punkt 1b realizuje wnętrze funkcji
init() (linie 29-34), znajdując element
select i wyznaczając do obsługi zdarzenia zmiany jego
wartości ("change" - ang. zmiana) funkcję
change() (definicja w liniach 13-16).
Punkty 2a i 2b zaimplementowane
zostaną właśnie we wnętrzu funkcji change().
- /*
- * Stwórz akapit z polami do wypełnienia
- */
- function create_input_p(label_text, input_name)
- {
- /* ... */
- }
- /*
- * Ustaw odpowiedni akapit na podstawie
- * wartości elemetu "select"
- */
- function change()
- {
- /* ... */
- }
- sqlite_elements = [
- create_input_p("Path: ", "db_path"),
- ]
- mysql_elements = [
- create_input_p("Host: ", "db_host"),
- create_input_p("Database name: ", "db_name"),
- create_input_p("Username: ", "db_username"),
- create_input_p("Password: ", "db_password"),
- ]
- function init()
- {
- var select = document.getElementById("engine");
- select.addEventListener("change", change, true);
- }
- window.addEventListener("load", init, true);
Konstrukcja pól tekstowych
Zaimplementujemy teraz funkcję create_input_p, która
zwracać ma akapit zawierający w sobie obiekt pola formularza. Mówiąc
konkretnie, ma generować kod HTML przypominający ten poniżej:
<label for="input_name">label_text</label>
<input type="text" value=""
id="input_name" name="input_name" />
</p>
Nie będziemy jednak operować na znacznikach HTML, tylko na obiektach JavaScriptu. Wartości label_text i input_name będą argumentami naszej funkcji. Pierwszy jest treścią etykiety pola, drugi jego identyfikatorem. Zanim zaczniemy pisać kod, musimy zapoznać się jeszcze z funkcjami, które umożliwią nam stworzenie i połączenie elementów, takich jak akapit, czy pole tekstowe.
Funkcja document.createElement(nazwa) zwraca
obiekt reprezentujący element o podanej nazwie. Stąd linia
div = document.createElement("div") utworzy element
odpowiadający w języku HTML tagowi <div>. Tak
utworzonemu elementowi można nadać atrybuty, służy do tego metoda
setAttribute(atrybut, wartość). Jeżeli
więc na uprzednio utworzonym obiekcie div wywołamy tę
metodę w następujący sposób: div.setAttribute("class",
"content"), to uzyskamy wynik przekładający się na kod:
<div class="content">.
Jako, że cały dokument HTML można zapisać w postaci
drzewa,
w którym każdy element może mieć wiele potomków i dokładnie jednego
rodzica, musi istnieć prosty sposób łączenia elementów ze sobą. Metodą,
służącą do tego celu jest
rodzic.appendChild(obiekt), która
czyni obiekt jednym z potomków rodzica. Przykładowo,
jeżeli fragment struktury dokumentu przedstawia się następująco:
<li>Element pierwszy listy</li>
<li>Element drugi listy</li>
</ul>
to po wykonaniu tego kodu Javascript:
ul = document.getElementById("lista");
ul.appendChild(li);
uzyskamy zmieniony dokument, w którym element ul posiada troje potomków:
<li>Element pierwszy listy</li>
<li>Element drugi listy</li>
<li></li>
</ul>
Tworzenie elementów i nadawanie im atrybutów to podstawy, jednak aby
manipulować treścią potrzebujemy jeszcze sposobu na stworzenie obiektu
tekstowego. Efekt ten uzyskujemy poprzez wywołanie metody
document.createTextNode(tekst), której argument
tekst wypełniamy własną treścią. Obiekt zwrócony przez tę
funkcję możemy podłączyć do innych obiektów zwyczajnie
za pomocą appendChild. W ten sposób bardzo łatwo
uzupełnić poprzedni przykładowy skrypt, nadając nowemu elementowi listy
treść "Element trzeci listy":
li = document.createElement("li");
li.appendChild(li_tekst);
ul = document.getElementById("lista");
ul.appendChild(li);
W tej chwili wiemy już wszystko, co potrzebne nam do stworzenia funkcji
create_input_p. Przypomnijmy sobie efekt jaki chcemy
osiągnąć:
<label for="input_name">label_text</label>
<input type="text" value=""
id="input_name" name="input_name" />
</p>
Potrzebujemy więc utworzyć tekstowy element o treści label_text i element label o atrybucie input_name, które następnie połączymy ze sobą (czyniąc element tekstowy potomkiem elementu label). Dalej stworzymy element input o atrybutach type, value, id i name. Na końcu utworzymy element akapitu p i podłączymy do niego elementy label i input. W efekcie uzyskamy drzewo o postaci:

Kod funkcji create_input_p poniżej:
{
var label_t = document.createTextNode(label_text);
var label = document.createElement("label");
label.setAttribute("for", input_name);
label.appendChild(label_t);
var input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("value", "");
input.setAttribute("id", input_name);
input.setAttribute("name", input_name);
var paragraph = document.createElement("p");
paragraph.appendChild(label);
paragraph.appendChild(input);
return paragraph;
}
Podłączanie pól do formularza
Drugą białą plamą, jaką zostawiliśmy w skrypcie na etapie projektowania
jest funkcja change(). Jej działanie ma ograniczyć się do
odnalezienia wartości elementu select i na jej
podstawie dołączenie odpowiedniego akapitu do treści strony. Dołączać
będziemy do elementu fieldset, któremu w szablonie
nadałem identyfikator dynamic_options:
By rozróżnić wartości elementu select potrzebować
będziemy instrukcji warunkowej. Jej postać jest bardzo prosta:
instrukcje wykonywane gdy warunek był prawdą;
} else {
instrukcje wykonywane gdy warunek nie był prawdą;
}
Wartości prawda i fałsz, idealne do
wykorzystania w instrukcji warunkowej, zwracają m.in. wszystkie
operatory porównujące: > (większe od),
< (mniejsze od), >= (większe lub równe),
< (mniejsze lub równe), == (równe)
i != (różne).
1 != 2 jest zatem prawdą, zaś "napis" == "inny napis"
jest fałszem. Elementy
<select> i <fieldset>
znajdujemy za pomocą getElementById, zaś elementy listy
akapitów dołączamy wykorzystując appendChild, nie ma w tym
nic nowego. Dołączanie moglibyśmy zaprogramować "na sztywno", w taki oto
sposób:
{
var select = document.getElementById("engine");
var fieldset = document.getElementById("dynamic_options");
if (select.value == "mysql") {
fieldset.appendChild(mysql_elements[0]);
fieldset.appendChild(mysql_elements[1]);
fieldset.appendChild(mysql_elements[2]);
fieldset.appendChild(mysql_elements[3]);
} else if (select.value == "sqlite") {
fieldset.appendChild(sqlite_elements[0]);
}
}
Jednak o wiele lepiej wykorzystać pętlę, w kolejnych iteracjach
wykorzystującą kolejne wartości zadanej tablicy. Długość tablicy
pobrać możemy poprzez atrybut length. Iterując więc po
indeksach tablicy od 0 do length-1
w kolejności otrzymujemy dostęp do wszystkich elementów tablicy.
Elementy tablicy indeksuje się przy pomocy nawiasów kwadratowych. Dla
tablicy o nazwie elementy pierwszy element uzyskujemy
pisząc elementy[0], drugi: elementy[1],
trzeci elementy[2], itd. W naszym kodzie skorzystamy z
pętli for, której składnia jest identyczna do tej
znanej z języków takich jak C, czy PHP:
instrukcje wykonywane po sprawdzeniu warunku;
}
Model wykonania pętli jest następujący. Na początku wykonywane są
instrukcje wstępne. Zazwyczaj w tym miejscu umieszcza się deklaracje
zmiennych wykorzystywanych w trakcie wykonywania pętli. Ten krok, w
przeciwności do pozostałych, wykonywany jest jedynie raz. Następnie
sprawdzany jest warunek, na tych samych zasadach, co w instrukcji
warunkowej if. Jeżeli warunek jest prawdziwy, kolejno
wykonywane są wszystkie instrukcje zawarte w treści pętli pomiędzy
nawiasami klamrowymi. Gdy wykonane zostaną wszystkie, sterowanie
przekazywane jest do instrukcji końcowych.
Po tym kroku możemy mówić o wykonaniu jednego przebiegu pętli. Dalej
ponownie sprawdzany jest warunek i jeżeli wciąż jest prawdziwy, to
sterowanie znów skacze do wnętrza pętli, po czym następuje wykonanie
instrukcji końcowych. Te trzy kroki wykonywane są dopóki warunek jest
spełniony. Wykorzystując pętlę możemy już funkcję change()
skrócić do postaci:
{
var select = document.getElementById("engine");
var fieldset = document.getElementById("dynamic_options");
if (select.value == "mysql") {
for (var i=0; i < mysql_elements.length; i++) {
fieldset.appendChild(mysql_elements[i]);
}
} else if (select.value == "sqlite") {
for (var i=0; i < sqlite_elements.length; i++) {
fieldset.appendChild(sqlite_elements[i]);
}
}
}
To, czego warto wystrzegać się podczas programowania, to powtórzenia.
Funkcje są jednym z najłatwiejszych sposobów na uogólnienie
powtarzającego się kodu i zamknięciu go w prostym w użyciu wywołaniu. W
kodzie powyżej powtarzającym się schematem są dopiero co zaimplementowane
pętle. Obie dołączają do elementu fieldset wartości
odpowiednich list. Czynność tę można uogólnić, tworząc funkcję, która
czyni podany element rodzicem elementów zadanej listy. Całość można
zapisać w sześciu prostych liniach:
{
for (var i=0; i < elements.length; i++) {
node.appendChild(elements[i]);
}
}
W ten oto sposób sam kod funkcji change() uległ uproszeniu
i nie zawiera już zbędnych powtórzeń:
{
var select = document.getElementById("engine");
var fieldset = document.getElementById("dynamic_options");
if (select.value == "mysql") {
insert_elements_after(fieldset, mysql_elements);
} else if (select.value == "sqlite") {
insert_elements_after(fieldset, sqlite_elements);
}
}
Usuwanie elementów dokumentu
Łącząc napisany dotychczas kod w jedną całość uzyskaliśmy skrypt,
który co prawda wykonuje się, ale jego działanie nie spełnia naszych
oczekiwań. Bowiem już przy drugim wyborze opcji z listy baz danych
okazuje się, że pola nie są zastępowane, tylko dopisywane do zestawu pól
elementu <fieldset>. W naszym schemacie aplikacji
zapomnieliśmy uwzględnić usuwania starych elementów. By jednak zapełnić
tę lukę, musimy zapoznać się z jeszcze jedną funkcją służącą do
manipulowania drzewem DOM. Jest to removeChild, która
działa odwrotnie do appendChild, tzn. usuwa z listy dzieci
danego węzła podany element. Podobnie, jak dla appendChild
argumentem metody removeChild musi być obiekt usuwanego
elementu, co wymusza na nas uprzednie jego znalezienie. Jako, że nie
wszystkie dzieci elementu fieldset są manipulowane przez
nasz skrypt (zostaje bowiem stały element select), musimy
również w jednoznaczny sposób oznaczyć i następnie zidentyfikować
elementy, na których operujemy. Przyjmijmy, że wszystkim elementom jakie
dodajemy nadamy klasę "dynamic" i na jej podstawie będziemy
elementy usuwać. Do pobrania wartości atrybutu służy metoda
getAttribute. Jedyne, co nam jest potrzebne do szczęścia,
to informacja o tym, w jaki sposób pobrać listę potomków danego węzła.
Nie trzeba
daleko szukać,
by dowiedzieć się, że atrybut ten nazywa się childNodes.
Kod znajdujący i usuwający elementy o klasie "dynamic" mógłby
wyglądać następująco:
var children = fieldset.childNodes;
for (var i=0; i < children.length; i++) {
if (children[i].getAttribute("class") == "dynamic") {
fieldset.removeChild(children[i]);
}
}
Należy również uzupełnić definicję funkcji create_input_p o
linię nadającą akapitowi klasę "dynamic":
Jeżeli wstawimy wcześniejszy fragment kodu do wnętrza funkcji
change() i uruchomimy w przeglądarce, przekonamy się, że
usuwa on tylko niektóre z elementów. Żeby zrozumieć, dlaczego tak się
dzieje, musimy jeszcze raz przypomnieć sobie, co robi
removeChild. Metoda ta usuwa określony węzeł z listy
potomków. W ten sposób indeksy wszystkich następujących elementów
zmniejszają się o jeden, zaś sama długość listy potomków skraca się
również o jeden. W ten sposób omijamy średnio co drugi element, bo
chociaż indeksy elementów się zmieniają, to zmienna i
za każdym razem zwiększana jest o jeden. Problem, ogólnie ujmując,
polega na tym, że modyfikujemy listę, po której iterujemy w pętli. Błąd
ten naprawia się rozbijając całe zadanie na dwie pętle. W pierwszej
tworzymy listę elementów do usunięcia, w drugiej te elementy usuwamy.
Implementacja wygląda następująco:
var children = fieldset.childNodes;
var to_remove = [];
for (var i=0; i < children.length; i++) {
child = children[i];
if (child.getAttribute("class") == "dynamic") {
to_remove.push(child);
}
}
for (var i=0; i < to_remove.length; i++) {
fieldset.removeChild(to_remove[i]);
}
Oczywiście nie wypada tak dużego kawałku kodu wklejać bezpośrednio do
treści funkcji change(). Stworzymy więc funkcję, która
usuwa z listy dzieci podanego węzła wszystkie elementy o zadanej klasie:
{
var children = node.childNodes;
var to_remove = [];
for (var i=0; i < children.length; i++) {
child = children[i];
if (child.getAttribute("class") == class) {
to_remove.push(child);
}
}
for (var i=0; i < to_remove.length; i++) {
node.removeChild(to_remove[i]);
}
}
W definicji zaś funkcji change() wywołujemy tę funkcję w
sposób następujący:
Jak jednak można się domyślić, świat byłby zbyt piękny, gdyby kod zadziałał za pierwszym razem. Zostałem uraczony tajemniczym błędem:

child.getAttribute nie jest funkcją
Chociaż napisane jest, że getAttribute nie jest funkcją,
należy rozumieć to jako informację, że metoda getAttribute
dla obiektu child nie została znaleziona i dlatego
wywołanie jej jest niepoprawne. Jeżeli obejrzymy sobie drzewko DOM
elementu fieldset, na którym manipulujemy, z łatwością
zauważymy, że jego potomkami są nie tylko inne elementy, takie jakie jak
legend, czy p, ale również wartości tekstowe.
Występują one ze względu na spacje oddzielające tagi w źródle strony
HTML.

By otworzyć w Firefoksie okno inspektora DOM, wciśnij
Ctrl+Shift+i.
Obiekty tekstowe nie posiadają atrybutów, a w związku tym również
metody getAttribute. Jeżeli dany obiekt nie posiada
atrybutów, to z pewnością nie ma klasy równej "dynamic".
Dlatego zwyczajnie możemy pominąć wszystkie elementy nie posiadające
metody getAttribute. Istniejąca metoda lub atrybut
w warunku zwraca wartość prawda, jeżeli zaś nie istnieje - fałsz.
Możemy więc warunek rozszerzyć w następujący sposób:
if (child.getAttribute("class") == class) {
to_remove.push(child);
}
}
Powyższe dwa zagnieżdżone warunki można połączyć w jeden za pomocą
operatora sumy logicznej: &&. Warunek zewnętrzny
trafia na lewą stronę operatora, warunek wewnętrzny na prawą:
&& child.getAttribute("class") == class) {
to_remove.push(child);
}
Po tych poprawkach nareszcie możemy sobie pogratulować - skrypt działa
poprawnie i dostosowuje treść formularza zależnie od wybranego silnika
bazy danych. Jedyną zmianą funkcjonalną, jaką można by wprowadzić, to
wymuszenie wywołania funkcji change() zaraz po załadowaniu
strony, co przystosuje treść formularza do początkowej wartości elementu
select. Ostatecznie funkcja init() wyglądać
więc będzie następująco:
{
var select = document.getElementById("engine");
select.addEventListener("change", change, true);
change();
}
Paskudne szczegóły
Niestety, ale na tym cała historia się nie kończy. Chociaż JavaScript jest językiem objętym standardem, to przeglądarki w różnym stopniu wytyczne standardów implementują. Czasami niektóre funkcje nie są dostępne i trzeba posiłkować się środkami zastępczymi. W innym przypadku obiekty zachowują się w odmienny sposób, niż oczekiwany. Dostosowanie skryptu do indywidualnych potrzeb różnych przeglądarek jest sztuką samą w sobie, zupełnie odrębną od pisania samego skryptu. Temat jest na tyle rozległy, że można by o nim napisać książkę, ograniczę się więc w tym miejscu jedynie do przedstawienia drobnych modyfikacji naszego skryptu, które to pozwolą mu działać nie tylko w przeglądarkach z silnikiem Gecko (a więc i używanym przez nas do testów Firefoksie), ale również pod Internet Explorerem i Operą. A dokładnie testować będziemy IE w wersjach 5.50 i 6.0, zaś Operę w wersji 8.52.
Na pierwszy ogień idzie Opera. Po wczytaniu strony widać, że początkowe
pola formularza zostały ustawione prawidłowo. Niestety zmiana opcji nie
przynosi żadnych efektów, formularz ani drgnie. Co gorsza wbudowana w
przeglądarkę konsola JavaScriptu nie wskazuje żadnych błędów. Ale nie
jest jeszcze tak źle, bo wiemy, że funkcja change() działa
dobrze, brak jedynie reakcji na zmianę. Prawdopodobnie więc problemem
jest przechwycenie lub podłączenie się pod obsługę zdarzenia zmiany
wartości elementu select. Z braku innych pomysłów
spróbowałem więc przypisać funkcję do zdarzenia change
starym (nie)dobrym sposobem:
I zadziałało! Szybkie sprawdzenie, czy nie zepsuło to niczego w obsłudze
skryptu pod Firefoksem (na szczęście nie) i możemy przejść do czarnej
owcy przeglądarek, Internet Explorera. Dawkując sobie poziom rozrywki
zaczniemy od wersji nowszej, czyli szóstej. Na początek IE nie podoba
się coś w funkcji remove_children_of_class(). Tutaj słowo
ostrzeżenia: IE ma własne zdanie dotyczące numeracji linii i znaków w
plikach skryptu, należy więc podanymi miejscami wystąpienia błędu
kierować się jedynie orientacyjnie. W tym przypadku punktem spornym
okazuje się nazwa zmiennej class w definicji funkcji.
Wystarczy zamienić wszystkie wystąpienia nazwy class w
nagłówku i treści funkcji na coś innego (np. z hiszpańskiego na
el_class) i IE zostawi nas w spokoju. Oczywiście tylko
dla tego błędu, bo to nie koniec naszych zmagań. Do poprawki bowiem
trafia metoda addEventListener(). W IE nie jest dostępna
metoda o takiej nazwie, jest za to jej odpowiednik
attachEvent. Przyjmuje on nazwy zdarzeń z przedrostkiem
"on", więc zamiast zdarzenia "load" należy
użyć "onload". Metoda jest na tyle automatyczna, że
można spokojnie jej wykonanie powierzyć odpowiedniej funkcji:
{
if (obj.addEventListener) {
obj.addEventListener(event, handler, true);
} else if (obj.attachEvent) {
obj.attachEvent("on"+event, handler);
}
}
Jeszcze inny sposób rejestrowania funkcji obsługi zdarzeń można znaleźć
w tym wpisie blogowym Simona Willisona.
Mając już zdefiniowaną tak sprytną funkcję, możemy zamienić rejestrację
funkcji init() na postać następującą:
Obsługi zdarzenia "change" nie zmieniamy, by nie zepsuć kodu dla Opery. Jak więc widać, zostaliśmy zmuszeni użyć trzech różnych sposobów na funkcjonalnie tą samą czynność. Kod zaczyna wyglądać brzydko, choć jeszcze w granicach akceptowalności. Warto sporne miejsca dobrze skomentować. Co ciekawe, po tych dwóch drobnym zmianach, IE 6 miłosiernie decyduje się zaakceptować nasz skrypt do interpretacji.
Ostatnim krokiem było sprawdzenie działania skryptu pod IE 5.50. Jak
można się domyślić, poprawki były nieuniknione (poczujcie się jak na
studiach). Przeglądarka poinformowała
mnie o błędzie w bardzo przejrzysty sposób, mianowicie wypisując
Wiersz 41, znak 9: Pomyłka literowa. Rzecz prawdopodobnie
dotyczyła funkcji insert_elements_after(). Nie tracąc
nadziei postanowiłem prześledzić sytuację dodając tu i tam najpopularniejszą
instrukcję do debugowania, tzn. alert(). I wreszcie
alert(elements.length) mnie oświeciło: dla tablicy
mysql_elements pokazało 5! Czemu tablica o czterech
elementach ma długość pięć? A może rzeczywiście elementów jest pięć?
Spojrzałem jeszcze raz na definicję tablic i bez przekonania usunąłem
przecinek po ostatnim elemencie. W tych językach, które to umożliwiają,
zawsze zostawiam separator listy po ostatnim elemencie, co ułatwia
dodawanie nowych elementów i zmianę kolejności obecnych. I tym razem
miałem nosa, po tej drobnej zmianie IE 5.50 obsłużyło kod mojego skryptu
jak należy.
Jak widać problem przenośności nie jest taki prosty i oczywisty. Na szczęście wraz ze wzrostem znaczenia standardów sieciowych i coraz większą powszechnością nowoczesnych technologii webowych sytuacja zdaje się poprawiać. Cieszy również zwiększone zainteresowanie kwestiami dostępności i używalności, co naturalnie prowadzi do ujednolicenia wsparcia popularnych technologii w przeglądarkach. Obecny rozwój dynamicznych stron internetowych zawdzięczamy nie tylko potężniejszym przeglądarkom, ale również istnieniu JavaScriptowych bibliotek (takich jak behaviour, czy prototype), które kwestie przenośności kodu pomiędzy przeglądarkami biorą na siebie, pozwalając programiście skupić się wyłącznie na tworzeniu aplikacji. Mam nadzieję, że kiedyś doczekam czasów, gdy będzie można publikować dynamiczną (i tą statyczną również) treść w sieci opierając się wyłącznie na jasnych wytycznych standardów. Czego sobie i czytelnikom życzę.
Literatura
- Wprowadzenie do DOM (po polsku)
- Wprowadzenie do DOM (po angielsku)
- Dokumentacja DOM obsługiwanego w silnikach Gecko
- Samouczek języka JavaScript
- O stylu programowania w języku JavaScript
Ostateczna wersja skryptu
Kod źródłowy, jako część projektu Canoe, objęty jest licencją BSD.
* IE have attachEvent,
* Opera and Gecko addEventListener
*/
function addEvent(obj, event, handler)
{
if (obj.addEventListener) {
obj.addEventListener(event, handler, true);
} else if (obj.attachEvent) {
obj.attachEvent("on"+event, handler);
}
}
function create_input_p(label_text, input_name)
{
var label_t = document.createTextNode(label_text);
var label = document.createElement("label");
label.setAttribute("for", input_name);
label.appendChild(label_t);
var input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("value", "");
input.setAttribute("id", input_name);
input.setAttribute("name", input_name);
var paragraph = document.createElement("p");
paragraph.setAttribute("class", "dynamic");
paragraph.appendChild(label);
paragraph.appendChild(input);
return paragraph;
}
function insert_elements_after(node, elements)
{
for (var i=0; i < elements.length; i++) {
node.appendChild(elements[i]);
}
}
/*
* IE doesn't want to work if variable is called "class"
*/
function remove_children_of_class(node, el_class)
{
var children = node.childNodes;
var to_remove = [];
for (var i=0; i < children.length; i++) {
child = children[i];
if (child.getAttribute
&& child.getAttribute("class") == el_class) {
to_remove.push(child);
}
}
for (var i=0; i < to_remove.length; i++) {
node.removeChild(to_remove[i]);
}
}
function change()
{
var select = document.getElementById("engine");
var fieldset = document.getElementById("dynamic_options");
remove_children_of_class(fieldset, "dynamic");
if (select.value == "mysql") {
insert_elements_after(fieldset, mysql_elements);
} else if (select.value == "sqlite") {
insert_elements_after(fieldset, sqlite_elements);
}
}
sqlite_elements = [
create_input_p("Path: ", "db_path")
]
mysql_elements = [
create_input_p("Host: ", "db_host"),
create_input_p("Database name: ", "db_name"),
create_input_p("Username: ", "db_username"),
create_input_p("Password: ", "db_password")
]
function init()
{
var select = document.getElementById("engine");
/*
* Opera doesn't handle
* select.addEventListener("change", change)
* for some reason
*/
select.onchange = change;
change();
}
addEvent(window, "load", init);
Przypisy
1 Biblioteka obsługi zdarzeń wydana przez Yahoo pozwala podłączać funkcje do obiektów, które jeszcze nie zostały wczytane .
2
Szczegóły dotyczące argumentów funkcji addEventListener
i działania obsługi wyjątków można znaleźć w
dokumentacji DOM,
a w skróconej formie również na
stronach DevMo.
Autor: Michał Kwiatkowski
Data ostatniej modyfikacji: 7 marca 2006
Licencja: Creative Commons Attribution-ShareAlike 2.5
Technorati tags: javascript programming
Skomentuj
ten artykuł.