Strona robocza Nux-a
Ostatnia modyfikacja: 2009-09-06

Wstęp

Na tej stronie opisuję przede wszystkim jak dodawać elementy wg modelu DOM, ale także jak powinno się to robić w wersji skróconej, czyli z użyciem złowieszczego atrybutu innerHTML. Co prawda nie rozważam tutaj jak dodawać/zmieniać poszczególne atrybuty elementów, ale pod koniec strony znajduje się osobna sekcja opisująca jak zmieniać/dodawać atrybuty odpowiedzialne za styl. Na koniec jeszcze parę słów o terminologii na konkretnym przykładzie.

Dodawanie elementów

Dodawanie elementów przy pomocy JS bywa problematyczne, szczególnie gdy strona ma działać pod różnymi przeglądarkami.

Poniżej opisuję czynności, które należy wykonać w przeglądarkach zgrubsza zgodnych z DOM (na pewno działa w: IE6 i wyżej, FF 1.5, Opera 8).

Szukanie elementu nadrzędnego

Zanim pomyślimy o tworzeniu elementu, wypadałoby mieć gdzie go wstawić.

Najłatwiej szukać po id elementu

var element = document.getElementById('someid');

Można też po nazwie taga, ale wtedy dostaje się listę indeksowaną od 0, numer wybiera się na końcu polecenia w nawiasach kwadratowych (tutaj akurat szukamy samego ciała HTML)

var element = document.getElementsByTagName('body')[0];

A tu połączenie obu technik (w elemencie o id=someid wybierz drugi element div)

var element = document.getElementById('someid').getElementsByTagName('div')[1];

Tworzenie elementu

Najpierw stworzymy sobie sam element (póki nigdzie się go nie podczepi, wisi sobie w pustce)

var newEl = document.createElement('span');

Dodawanie standardowych atrybutów

Niektóre lepiej tak...

newEl.title = 'opis';
newEl.className = 'nazwa_klasy';

...a niektóre tak

var newAttr = document.createAttribute('name');
newAttr.nodeValue = 'value'
newEl.setAttributeNode(newAttr);

Dodawanie zdarzeń (ang. event)

Bywa to zdradliwe, ale zwykle wystarczy tak...

newEl.onclick = nazwaFunkcji;

...a tu taki mały trick (przydatny jeśli mamy problem z funkcją, której przekazuje się atrybuty)

newEl.onclick = new Function('alert("anything you want");');

Jest jeszcze na to parę sposobów, ale nie będziemy się przemęczać ;)... A tak serio, to po prostu jeśli nie pisze się skomplikowanych skryptów, to powyższe powinno być najbezpieczniejsze. Mam tu na myśli to, że oba powyższe powinny działać praktycznie wszędzie, a to dlatego, że pochodzą ze specyfikacji JavaScriptu w wersji 1.3 (czyli bardzo starej).

Zwykły tekst

W razie potrzeby dodajemy tekstową zawartość elementu (np. element nowej linii nie potrzebuje jej nigdy)

newEl.appendChild(document.createTextNode('some inner text'));

Podczepianie (wstawianie) elementu

Najprościej dodać na koniec tego nadrzędnego:

element.appendChild(newEl);

trochę gorzej na początek...

element.insertBefore(newEl, element.firstChild);

Problem w tym, że jeśli element jest pusty, to powyższe nie zadziała. W tym wypadku należy właściwie napisać sobie prostą funkcję, która sprawdzi czy element jest pusty i odpowiednio do niego będzie wstawiać elementy.

Czasem możemy chcieć wstawić nowy element w śćiśle określone miejsce. Wówczas można wstawić go obok (przed) element. Podany poniżej kod powinnien zawsze zadziałać jako, że każdy element powinien mieć swojego „rodzica” (ang. parent),

element.parentNode.insertBefore(newEl, element);

Podobny problem jak z wstawianiem na początek może być przy wstawianiu za dany element:

element.parentNode.insertBefore(newEl, element.nextSibling);

Powyższe nie zadziała, jeśli element nie ma przed za sobą rodzeństwa (ang. sibling). Podobnie jak wyżej, jeśli nie wiemy, że dany element ma zawsze za sobą rodzeństwo, to można napisać odpowiednią funkcję, która to sprawdzi i wstawi jak powyżej, bądź na koniec rodzica.

Pułapki z encjami HTML

Najłatwiej chyba złapać się na coś takiego:

newEl.appendChild(document.createTextNode('coś tam'));

No i jest klops – dostajemy encję w formie zwykłego tekstu, a nie (w tym wypadku) twardej spacji. Będzie to szczególnie dotkliwy problem jeśli byłby to kod w jakiejś funkcji a tekst byłby przekazywany przez jej parametr. Jeśli ktoś zna troszkę teorii, to pomyślu o stworzeniu encji i otrzyma taki kod:

newEl.appendChild(document.createTextNode('coś'));
newEl.appendChild(document.createEntityReference('nbsp'));
newEl.appendChild(document.createTextNode('tam'));

Ale tak naprawdę problem w tym, że   nie jest zwykłą encją zdefiniowaną w danym dokumencie, tylko encją podstawową (predefiniowaną), a zamienianą przez procesor HTML/XML przeglądarki na odpowiedni znak Unikodu (ang. Unicode). Najprostsze wyjście, jakie znam, to użycie złowieszczego i generalnie odradzanego innerHTML (o czym w dalszej sekcji). Można też do końca być zgodnym z modelem DOM W3C i zrobić taki trick:

newEl.appendChild(document.createTextNode('coś'));
newEl.appendChild(document.createTextNode('\u00a0'));
newEl.appendChild(document.createTextNode('tam'));

Czy też nieco prościej:

newEl.appendChild(document.createTextNode('coś\u00a0tam'));

00a0 to szenstkowo 160, czyli kod twardej spacji w unikodzie. Konstrukcja typu '\uXXXX' (gdzie XXXX jest szenstkowym numerem znaku w Unikodzie) jest wymieniona już w specyfikacji JavaScript 1.3, więc powinna zawsze działać. Można też tutaj wykorzystać wersję skróconą:

newEl.appendChild(document.createTextNode('coś\xa0tam'));

'\xa0' to ten sam znak co '\u00a0', tylko w wesji skróconej. Tutaj się udało skrócić, bo kod znaku jest mniejszy od 28, czyli mieści się na jednym bajcie, co można rozpoznać po dwóch początkowych zerach.

Metodę tę odkryłem dosyć niedawno i wydaje mi się nieco nienaturalna. Poza tym gruncie rzeczy nie rozwiązuje problemu. Powiedzmy, że zamiast   mam >, <, &guot;, „, ”... Oczywiście można sobie odnaleźć odpowiedni kod tutaj, tutaj lub tutaj (kody znaków wg Unikodu przedstawiane jako U+XXXX). Tylko czy naprawdę to jest konieczne? Niestety chcąc być w zgodzie z W3C DOM, tak. Chociaż wydaje mi się to raczej swoistym błędem przeglądarek niż W3C.

Uproszczone dodawanie elementów

Zwykle jeśli zależy mi na czasie i robię coś ulotnego, bądź do użytku własnego, to grzeszę użyciem atrybutu innerHTML. Jest to sposób uznawany za przestarzały, ale... ale powiem Ci, drogi czytelniku, w tajemnicy, że mam wrażenie, że jest tak naprawdę najszybszym z możliwych sposobów (o ile używany z rozwagą!). Otóż moim zdaniem przeglądarka znacznie szybciej zinterpretuje sobie zwykłe:

newEl.innerHTML = '<span id="cos">jakiś tekst</span><br /> <span>jakiś jeszcze tekst (taka sobie twarda&nbsp;spacja)</span>'
...niż coś takiego:
var subnewEl = document.createElement('span');
var newAttr = document.createAttribute('id');
newAttr.nodeValue = 'cos'
subnewEl.setAttributeNode(newAttr);
subnewEl.appendChild(document.createTextNode('jakiś tekst'));
newEl.appendChild(subnewEl);

var subnewEl = document.createElement('br');
newEl.appendChild(subnewEl);

newEl.appendChild(document.createTextNode(' '));

var subnewEl = document.createElement('span');
subnewEl.appendChild(document.createTextNode('jakiś jeszcze tekst (taka sobie twarda'));
subnewEl.appendChild(document.createEntityReference('nbsp'));
subnewEl.appendChild(document.createTextNode('spacja)'));
newEl.appendChild(subnewEl);

Poza tym, że i tak nie będzie to działać (ze względu na wspomniany problem encji), to ileż tego kodu się porobiło... Moim zdaniem po prostu bardziej naturalne dla przeglądarki internetowej byłoby zinterpretowanie kodu HTML – w końcu jest to coś, do czego została stworzona. No, ale jakby co, to ja nic nie mówiłem. DOOM jest spoko ;).

Niezależnie od sytuacji pokusiłbym się jednak o parę skrótów (rozwiązując przy okazji problem encji). Czyli z powyższego wyszłoby coś takiego:

var subnewEl = document.createElement('span');
subnewEl.id = 'cos';
subnewEl.innerHTML = 'jakiś tekst';

newEl.appendChild(document.createElement('br'));
newEl.appendChild(document.createTextNode(' '));

var subnewEl = document.createElement('span');
subnewEl.innerHTML = 'jakiś jeszcze tekst (taka sobie twarda&nbsp;spacja)';
newEl.appendChild(subnewEl);

I chociaż tak bym pewnie zrobił, to nie wiem, która z dwóch poniższych wersji jest szybsza:

subnewEl.appendChild(document.createTextNode('jakiś tekst'));
subnewEl.innerHTML = 'jakiś tekst';

Wydaje mi się, że akurat w tym wypadku lepiej byłoby skorzystać z nieco mniej przejrzystej konstrukcji, czyli wersji z appendChild, a nie innerHTML. Powód? W pierwszej wersji od razu deklaruję, że wstawiam tekst, w drugiej musi to być sprawdzone przez interpreter.

Uwzględniając powyższe i hack encjowy można by zapisać to tak:

var subnewEl = document.createElement('span');
subnewEl.id = 'cos';
newEl.appendChild(document.createTextNode('jakiś tekst'));

newEl.appendChild(document.createElement('br'));
newEl.appendChild(document.createTextNode(' '));

var subnewEl = document.createElement('span');
newEl.appendChild(document.createTextNode('jakiś jeszcze tekst (taka sobie twarda\xA0spacja)'));
newEl.appendChild(subnewEl);

Czego NIGDY nie robić?

Kiedyś się z czymś podobnym spotkałem, więc ostrzegam – nigdy nie róbcie czegoś takiego(!):

var element = document.getElementsByTagName('body')[0]; // ciało HTML
element.innerHTML += '<a href="extra_linka.htm">fajna stronka</a>';

Owszem pisałem powyżej o potencjalnej szybkości innerHTML, ale zastanówmy się przez chwilę czym jest innerHTML? — Otóż jest to tekst. A co robi tutaj operator '+='? — Bierze starą wartość innerHTML i dołącza do niego nowy łańcuch znaków. No i dobrze, na czym polega problem? — mógłby ktoś zapytać. Odpowiedź jest taka, że po wykonaniu łączenia interpretacji zostanie poddana ponownie cała strona, a nie tylko ten mały fragment, który dodajemy. Tak przynajmniej wynika z moich obserwacji i z właściwości tego atrybutu. Po prostu innerHTML jest zwykłym tekstem, a nie jakimś obiektem, który pod sobą ma te wszystkie tagi i do którego można by dołączyć inny obiekt poprzez zwykłe '+='. Oczywiście mogę się mylić i mogą istnieć przeglądarki, które tak zdefiniowały swój silnik JS, żeby to zoptymalizować. Osobiście jednak szczerze w to wątpię.

Jakie jest rozwiązanie? Wystarczy stworzyć jakiś element nadrzędny i umieścić go w odpowiednim miejscu zgodnie z podanym na początku przepisem. Następnie do tego nowego elementu można wstawić odpowiednią treść poprzez innerHTML.

Dodatek – atrybuty odpowiedzialne za styl

Często konieczne jest w dynamicznych skryptach zmienianie wyglądu jakiegoś elementu. Czasem można poradzić sobie przez użycie ':hover' itp. już w ramach samego CSS, ale tej kwestii tu nie będę rozważał.

Konkretne właściwości

Jeśli chodzi o konkretne właściwości, to przy znajomości CSS jest to proste i wykonuje się tak:

element.style.nazwaWłaściwości = 'wartość';

Bywa to jednak kłopotliwe, bo nie zawsze przekształcenie jest tak schematyczne jak: 'color' na 'color', 'margin-top' na 'marginTop', 'border-top-color' na 'borderTopColor' itd. Niektórych właściwości może nie być dla JS lub mogą być one różne dla różnych przeglądarek. Tak jest też z właściwością 'float', którą próbowałem niegdyś ustawić.

Ze wspomnianych powodów najlepiej użyć po prostu:

element.style.cssText = 'float: right';

Składnia jest identyczna jak dla atrybutu 'style' w HTML-u. Czyli można zrobi np. coś takiego:

element.style.cssText = 'float: right; margin: 20px 10px 8em; padding: 0px;';

O ile mi wiadomo, nie ma ograniczeń, a najpiękniejsze jest to, że działa we wszystkich liczących się przeglądarkach. Patrz: www.quirksmode.org/dom/w3c_css.html

Notatka: Do przetestowania w Operze (choć w nowej prawie na pewno działa w każdej sytuacji, to co z Operą 8?).

Dodatek – terminologia

Przy okazji przeanalizujmy sobie wspomniany wcześniej kod HTML do wstawienia:
<span id="cos">jakiś tekst</span><br /> <span>jakiś jeszcze tekst (taka sobie twarda&nbsp;spacja)</span>

Mamy tutaj 9 węzłów, a dokładniej:

  • 3 elementy ['span', 'br', 'span'],
  • 1 atrybut ['id'],
  • 4 węzły tekstowe ['jakiś tekst', ' ', 'jakiś jeszcze tekst (taka sobie twarda', 'spacja)']
  • 1 encja ['nbsp'].
top