Tworzenie i usuwanie elementów
Potrafimy już poruszać się po elementach, potrafimy je pobrać, przechodzić z jednego elementu na drugi itp. Czas zacząć je tworzyć a potem usuwać. Nie jest to wcale takie trudne. Wystarczy, że skorzystamy z kilku podstawowych metod oferowanych przez JavaScript.
Tworzenie elementu
Aby utworzyć nowy element na stronie korzystamy z metody document.createElement(tagname). Po utworzeniu nowego elementu możemy na nim działać tak jakbyśmy go wcześniej pobrali za pomocą querySelector i innych metod, czyli obowiązują nas te same zasady co wcześniej. Taki element jest totalnie pusty, dlatego przed wstawieniem dodajmy mu kilka rzeczy:
const el = document.createElement("div");
let counter = 0;
el.classList.add("element");
el.style.background = `hsl(${Math.random()*360}, 90%, 70%)`;
el.style.color = "#333";
el.innerText = ++counter;
Wstawianie elementu do HTML
Utworzony w powyższy sposób element jest dostępny dla skryptu, ale nie ma go jeszcze w drzewie dokumentu. Musimy go tam wstawić.
Możemy to zrobić na kilka sposobów.
Dwie klasyczne metody to parentElement.appendChild(newElement) oraz parentElement.insertBefore(newElement, element).
Ta pierwsza służy do wstawiania nowego elementu na koniec wybranego elementu:
let counter = 0;
const el = document.createElement("div");
el.classList.add("element");
el.style.background = `hsl(${Math.random()*360}, 90%, 70%)`;
el.style.color = "#333";
el.innerText = `Nowy ${++counter}`;
const div = document.querySelector(".test-cnt");
div.appendChild(el);
Natomiast metoda parentElement.insertBefore(newElement, element)
wstawia nowy element przed wskazany element:
<div class="test-cnt">
<strong>strong</strong>
<strong>strong</strong>
</div>
let counter = 0;
const el = document.createElement("div");
el.classList.add("element");
el.style.background = `hsl(${Math.random()*360}, 90%, 70%)`;
el.style.color = "#333";
el.innerText = `Nowy ${++counter}`;
const div = document.querySelector(".test-cnt");
const strong = div.querySelectorAll("strong")[1]; //pobieram 2 strong
div.insertBefore(el, strong);
Metody appendChild()
i insertBefore()
są klasykami, które zadziałają w każdej przeglądarce - stąd są bardzo często wykorzystywanymi do wstawiania elementów do HTML.
Jeżeli nie wspierasz już IE11 (prośba - nie rób tego), w dzisiejszych czasach polecam raczej skorzystać z ich młodszych braci w postaci 4 dodatkowych funkcji:
el.append(newElement) | wstawia tekst lub nowy element na koniec danego elementu |
---|---|
el.prepend(newElement) | wstawia tekst lub nowy element na początku danego elementu |
el.before(newElement) | wstawia tekst lub nowy element przed danym elementem |
el.after(newElement) | wstawia tekst lub nowy element za danym elementem |
const cnt = document.querySelector(".container");
let counter = 0;
const btnBefore = document.querySelector("#test-before");
btnBefore.addEventListener("click", () => {
const el = document.createElement("span");
el.innerHTML = "Przed " + ++counter;
cnt.before(el);
});
const btnAfter = document.querySelector("#test-after");
btnAfter.addEventListener("click", () => {
const el = document.createElement("span");
el.innerHTML = "Za " + ++counter;
cnt.after(el);
});
const btnPrepend = document.querySelector("#test-prepend");
btnPrepend.addEventListener("click", () => {
const el = document.createElement("span");
el.innerHTML = "Na początku " + ++counter;
cnt.prepend(el);
});
const btnAppend = document.querySelector("#test-append");
btnAppend.addEventListener("click", () => {
const el = document.createElement("span");
el.innerHTML = "Na końcu " + ++counter;
cnt.append(el);
});
W przeciwieństwie do insertBefore()
i appendChild()
powyższe metody możemy też użyć do wstawiania tekstów. Przekazany w ten sposób html także będzie wstawiony jako tekst:
element.append("Ten span <span></span> będzie zapisany jak tekst");
Ostatnią mini rzeczą odróżniającą appendChild()
i insertBefore()
od powyższych metod jest to, że ta pierwsza zwraca w rezultacie element, co daje nam możliwość odpalania kolejnych funkcji po kropce. Te nowsze (append(), prepend(), before(), after()) zwracają undefined, co powoduje, że pisanie "na skróty" nie zadziała.
const newEl = document.createElement("div");
document.body.appendChild(newEl).classList.add("element");
const newEl = document.createElement("div");
document.body.append(newEl).classList.add("element"); //błąd - Cannot read property "classList" of undefined
Tworzenie tekstu za pomocą createTextNode
Html składa się głównie z 2 bytów: elementów html i tekstów (ale też np. atrybutów).
Aby dodać tekst na stronę możemy używać właściwości innerText/textContent/innerHTML/outerHTML, ale też możemy utworzyć pojedynczy węzeł tekstowy za pomocą funkcji createTextNode:
const text = document.createTextNode("Lubię placki");
element.appendChild(text);
Spójrzmy na poniższy przykład:
<p>
Psy <strong>są</strong> fajne
</p>
Strong jest elementem. Słowo "Psy" jest węzłem tekstowym, tak samo jak słowo "fajne". Podobnie zresztą węzłem tekstowym jest słowo "są", które znajduje się w strong.
Co gdybyśmy w powyższym przykładzie chcieli dynamicznie zmieniać słowa "Psy" i "fajne", ale równocześnie nie moglibyśmy dodać dodatkowych elementów dla użycia innerText? Dzięki temu, że utworzymy te węzły za pomocą createTextNode
, bez problemu będziemy mogli się do nich odwoływać (to samo by było, gdybyśmy takie węzły pobrali za pomocą np. firstChild, nextSibling, previousSibling):
const p = document.querySelector("#testNodeText");
const btn = document.querySelector(".btn-textNodeTest");
const word1 = document.createTextNode("Psy"); //pierwsze słowo
p.append(word1);
p.append(" są "); //nie potrzebujemy tutaj referencji
const word2 = document.createTextNode("fajne"); //ostatnie słowo
p.append(word2);
btn.addEventListener("click", () => {
console.dir(word1); //wypiszemy sobie by zobaczyć co możemy użyć
word1.textContent = "Koty też";
word2.textContent = "super!";
});
Zbadaj poniższy paragraf, a następnie kliknij w przycisk:
insertAdjacentHTML i insertAdjacentElement
Funkcja el.insertAdjacentHTML(position, html) służy do wstawiania kawałka HTML na wybranej pozycji.
Pierwszy parametr określający miejsce może przyjąć jedną z wartości:
"beforebegin" | Wstawia element przed docelowym elementem. Działa podobnie do metody before(). |
---|---|
"afterbegin" | Wstawia element na początku dzieci. Działa podobnie do metody prepend(). |
"beforeend" | Wstawia element na końcu dzieci. Działa podobnie do metod append() lub appendChild(). |
"afterend" | Wstawia element za docelowym elementem. Działa podobnie do metody after(). |
const parent = document.querySelector("div");
parent.insertAdjacentHTML("beforebegin", "<strong>element 1</strong>");
parent.insertAdjacentHTML("afterbegin", "<strong>element 2</strong>");
parent.insertAdjacentHTML("beforeend", "<strong>element 3</strong>");
parent.insertAdjacentHTML("afterend", "<strong>element 4</strong>");
Istnieje też bardzo podobna w działaniu funkcja el.insertAdjacentElement(position, newEl), która służy do wstawiania nowych elementów na wybranej pozycji.
const parent = document.querySelector("div");
const el = document.createElement("strong");
parent.insertAdjacentElement("beforebegin", el);
Omawiane metody są podobne w działaniu do wcześniej wymienionych before, after, prepend, append. Zauważ jednak, że tutaj wstawiany element możemy podawać jako kod HTML, co czasami też się przydaje...
Klonowanie elementów
Za pomocą funkcji cloneNode(deep*) możemy wykonać kopię danego elementu.
Dodatkowy atrybut deep oznacza, czy dany element ma być klonowany wraz ze swoimi dziećmi.
<div class="to-clone">
<strong>Testowy</strong>
</div>
const el = document.querySelector(".to-clone");
const cloneEl1 = el.cloneNode();
console.log(cloneEl1); //<div class="to-clone"></div>
const cloneEl2 = el.cloneNode(true);
console.log(cloneEl2); //<div class="to-clone"><strong>Testowy</strong></div>
Po sklonowaniu elementu możemy na nim działać tak samo jakbyśmy go stworzyli od nowa, albo pobrali za pomocą dowolnej metody.
const btn = document.querySelector("button");
const targetDiv = document.querySelector(".target");
let nr = 0;
btn.addEventListener("click", () => {
nr++;
const el = document.querySelector(".to-clone");
const clone = el.cloneNode(true);
targetDiv.appendChild(clone);
})
Niestety klonowany za pomocą cloneNode()
jest tylko kod html elementu - bez podpiętych pod niego zdarzeń. Powoduje to, że nie da się tej metody swobodnie stosować na dowolnym elemencie, bo potencjalnie stracimy podpiętą pod niego logikę.
Niestety nie ma łatwego sposobu na obejście tego problemu. Albo napiszemy dodatkową logikę, albo podepniemy zdarzenia raz jeszcze, albo... użyjemy jQuerowego odpowiednika clone(), który pozwala klonować elementy wraz z eventami.
Usuwanie elementów
Element, który istnieje w drzewie dom, możemy z niego usunąć na kilka sposobów.
Możemy tutaj skorzystać z parentElement.removeChild(element) lub element.remove() (gdy nie interesuje nas IE11):
<div class="div-test-remove">
<span>Element do usunięcia</span>
</div>
const div = document.querySelector("div")
const el = div.querySelector("span");
const btn = document.querySelector("button");
btn.addEventListener("click", () => {
div.removeChild(el);
//lub
el.parentElement.removeChild(el);
//lub najprościej
el.remove();
});
Aby usunąć wszystkie dzieci danego elementu - czyli go wyczyścić, powinniśmy wykonać pętlę po jego dzieciach i wszystkie pousuwać:
const ul = document.querySelector("#list");
while (ul.firstChild) {
ul.removeChild(ul.firstChild); //lub div.firstChild.remove()
}
Jeżeli chcemy usuwać elementy za pomocą pętli for, wtedy róbmy ją od końca, ponieważ po usunięciu danego elementu, przestawi nam się indeks w kolekcji.
const li = document.querySelectorAll("#list li");
for (let i=li.length-1; i<=0; i--) {
li[i].remove();
}
Ale tak naprawdę powyższe pętle sprawdzą się tylko w przypadkach, kiedy chcemy dodatkowo coś zrobić z usuwanymi elementami (np. odpiąć im zdarzenia)... Jeżeli naszym celem jest tylko opróżnić dany element, możemy postąpić o wiele prościej:
const ul = document.querySelector("#list");
//dowolne z poniższych
ul.innerHTML = "";
ul.textContent = "";
ul.innerText = "";
Usuwanie z html i pamięć
Metody removeChild() i remove() (ale też powyższe opróżnianie elementu) usuwają elementy z HTML, ale jeżeli taki element jest podstawiony pod zmienną, po usunięciu dalej będzie ona przetrzymywać dany element w pamięci.
Przy pojedynczych usuwanych elementach nie powinno to być raczej problemem.
Problem pojawi się, gdy takich elementów będziemy pobierać (lub tworzyć) i usuwać bardzo wiele (np. w interwale).
const btn = document.querySelector("button");
const elToDelete = document.querySelectorAll(".el-to-delete");
btn.addEventListener("click", () => {
for (const el of elToDelete) {
el.remove(); //usuwamy tylko z HTML
}
//elementy wciąż są w pamięci pod zmienną elToDelete
console.log(elToDelete);
})
Sprawdź też omawiany problem na przykładowej stronie: delete-element-memory-test.html
Zastępowanie elementów
Do zastępowania jednego elementu drugim użyjemy metody parent.replaceChild(newChild, oldChild):
<div class="div-test-replace">
<span>Jestem starym elementem</span>
</div>
<button type="button" class="button" id="replaceTest">
Zastąp spana nowym elementem
</button>
const div = document.querySelector(".div-test-replace")
const btn = document.querySelector("#replaceTest");
btn.addEventListener("click", e => {
const oldItem = div.querySelector("span");
const newItem = document.createElement("strong");
newItem.innerText = "Jestem nowym elementem";
div.replaceChild(newItem, oldItem);
});
Drugą metodą, z której możemy skorzystać jest element.replaceWith(otherEl):
const span = document.querySelector(".old");
const strong = document.createElement("strong");
strong.innerText = "Zamiennik";
span.replaceWith(strong);
Tworzenie fragmentów dokumentu za pomocą template
Do tworzenia fragmentów HTML możemy oczywiście korzystać z innerHTML (wraz z template strings), ale równie ciekawym pomysłem jest skorzystanie z elementu template
. Element taki służy do przetrzymywania w swoim wnętrzu fragmentów kodu.
Po co taki zabieg? Bardzo często przy pracy ze stronami, gdzie całe widoki są generowane po stronie serwera, chcemy by kod danych elementów na których pracujemy za pomocą Javascript był generowany po stronie serwera. Dzięki temu unikamy dodatkowego miejsca potencjalnych błędów gdy zmienia się logika biznesowa. Backendowcy raczej nie będą nam generować innerHTML wewnątrz naszych spakowanych plików Javascript, dlatego dobrym miejscem staje się HTML, z którego my będziemy zaciągać fragmenty na których potem zaczniemy działać.
Wstawiony do HTML element template
nie jest parsowany przez przeglądarkę, co powoduje, że jest niewidoczny dla reszty strony, dzięki czemu staje się idealnym schowkiem do przetrzymywania fragmentów kodu HTML.
<body>
<p>Treść strony</p>
<template id="elementTemplate">
<h2 class="card-title">
</h2>
<div class="card-text">
</div>
<footer class="card-footer">
</footer>
</template>
</body>
//sprawdzam czy przeglądarka wspiera template
const list = document.querySelector(".list");
if ("content" in document.createElement("template")) {
const template = document.querySelector("#elementTemplate");
const clone = template.content.cloneNode(true);
const title = clone.querySelector(".card-title");
const text = clone.querySelector(".card-text");
const footer = clone.querySelector(".card-footer");
title.innerText = "Przykładowy tytuł";
text.innerText = "Lorem ipsum dolor sit amet consectetur"
footer.innerText = "copyright"
list.append(clone);
}
Jak widzisz użycie tego elementu jest bajecznie proste, bo sprowadza się do sklonowania jego zawartości.
Zamiast klonować można też pobrać ją jako innerHTML, dzięki czemu moglibyśmy w prosty sposób wypełniać ją odpowiednimi wartościami:
<body>
<p>Jakaś treść strony</p>
<template id="elementTemplate">
<div>
<a href="show.php?date={{date}}">
{{date}}
</a>
</div>
<div>
<a href="mailto: {{email}}">
{{email}}
</a>
</div>
</template>
</body>
const html = document.querySelector("#elementTemplate").innerHTML;
let newHTML = "";
newHTML = newHTML.replace(/\{\{email\}\}/g, "mojemail@gmail.com");
newHTML = newHTML.replace(/\{\{date\}\}/g, "2010-10-05");
const newEl = document.createElement("div");
newEl.innerHTML = newHTML;
list.append(newEl);
Tak naprawdę świat nie kończy się na elemencie template
. Istnieją popularne silniki szablonów takie jak Mustache, Handlebars, z których warto korzystać gdy tworzymy klasyczne strony na których mocno działamy Javascriptem.
Charakteryzują się one o wiele większymi możliwościami niż powyższy element. Zawartość takiej templatki może być tutaj dynamicznie generowana na podstawie danych. Możemy więc stosować tutaj pętle, instrukcje warunkowe itp. Fajna rzecz.
Trening czyni mistrza
Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania