Właściwości elementów

Jeżeli pobierzesz jakikolwiek element ze strony, będziesz mógł w konsoli wyświetlić jego właściwości i metody.
Najlepiej od razu odpal debuggera (F12) i zostaw konsolę widoczną, bo jeszcze nam się przyda.

Otwórz debuggera i kliknij w poniższy przycisk:


const btn = document.querySelector(".button");
console.log(btn);
console.dir(btn);

Obiekt taki ma bardzo dużo właściwości i metod. Część z nich przechowuje pozycję na stronie, typ elementu, jego rozmiary, inne są wykorzystywane dla zdarzeń itp.
Nie wszystkie będziesz non stop używał. Jeżeli trafi się taka konieczność, wiedz po prostu gdzie szukać tego co cię interesuje (wypisując pobrany element w konsoli za pomocą console.dir i badając odpowiednie właściwości).

Poniżej skupimy się na właściwościach i metodach, które są wykorzystywane najczęściej.

innerHTML

Właściwość innerHTML umożliwia odczyt i ustawianie html, jaki jest we wnętrzu danego elementu:


<button class="button">
    <span>Kliknij mnie</span>
</button>

const btn = document.querySelector(".button");
console.log( btn.innerHTML ); //<span>Kliknij mnie</span>

btn.innerHTML = "<span>Nie klikaj mnie!</span>"

Otwórz debuggera i kliknij w poniższy przycisk:

Za pomocą innerHTML możemy oczywiście wstawiać całe kawałki dokumentu:


<div class="test-cnt">Tutaj zaraz coś wstawimy</div>
<button class="button" type="button">Wstaw element</button>

const btn = document.querySelector(".button");

btn.addEventListener("click", () => {
    document.querySelector(".test-cnt").innerHTML = `
        <div class="module">
            <h2>Przykładowy tytuł</h2>
            <p>To jest <strong>dynamicznie wstawiony html</strong></p>
            <button class="button">klik</button>
        </div>
    `;
});
Tutaj zaraz coś wstawimy

Wstawianie na stronę nowy HTML tak jak powyżej z pewnością jest bardzo wygodne i nie raz będziesz z tego korzystał. Warto się jednak tutaj na moment zatrzymać, ponieważ nie zawsze będzie to takie proste.

Wyobraź sobie, że wstawiasz tak jak powyżej kawałek HTML, który pobierasz z jakiegoś miejsca. Dla przykładu może to być twój własny serwer, z którego dynamicznie za pomocą Javascript (fetch) pobierzesz kod wstawianego elementu. Może to też być kod HTML, który wpisuje do formularza użytkownik (a może tylko jego mała część - np. samo imię), albo w końcu ukryty element zagnieżdżony w kodzie twojej własnej strony (np. w formie template).

We wszystkich takich przypadkach gdy odpowiednio nie zabezpieczysz swojego skryptu, możesz narazić użytkownika na atak XSS. Złośliwy program, skrypt z jakiegoś dodatku do przeglądarki, lub użytkownik łobuz mogą zmodyfikować wstawiany kod (a przecież nie jest to wcale takie trudne - wystarczy prosta zmiana za pomocą debuggera) tak, że zostanie wstawiona zupełnie inna rzecz niż chciałeś. Przykładowo zamiast powyższego elementu .module zostanie wstawiony iframe z oszukaną treścią lub inny złośliwy skrypt np. rejestrujący naciskane przez użytkownika klawisze. Możesz to sprawdzić na prostym mini teście.

Przy bardziej realnych projektach warto w takich sytuacjach utrudnić zadanie potencjalnemu łobuzowi.
Możemy tutaj wykorzystać dostępny w Javascript DOMParser, który zamieni nasz kawałek html na odpowiedni dokument:


    const btn = document.querySelector("#test3");

    const html = `
        <div class="module">
            <h2>Przykładowy tytuł</h2>
            <p>To jest <strong>dynamicznie wstawiony html</strong></p>
            <button class="button">klik</button>
        </div>
    `;

    btn.addEventListener("click", () => {
        const parser = new DOMParser();
        const doc = DOMParser.parseFromString(html, "text/html");

        console.log(doc); //parser tworzy cały dokument z naszym kodem
        const module = doc.querySelector(".module").cloneNode(true);
        document.querySelector("#test3div").append(module);
    });
    

Przy takiej zamianie dołączone do kodu skrypty nie są odpalane. Nie jest wcale ciężko to obejść, ale my możemy dodatkowo zrobić pętlę po wszystkich elementach i usunąć wszystkie potencjalnie niebezpieczne:


    btn.addEventListener("click", () => {
        const parser = new DOMParser();
        const doc = DOMParser.parseFromString(html, "text/html");

        for (let el of doc.querySelectorAll("*")) {
            if (["SCRIPT", "IFRAME", "OBJECT"].includes(el.nodeName)) {
                el.remove();
            }
        }
        const module = doc.querySelector(".module").cloneNode(true);
        document.querySelector("#test3div").append(module);
    });
    

Powyższy przykład to tylko bardzo proste naprędce napisane rozwiązanie. Z pewnością nie rozwiąże każdej sytuacji. Innymi polecanymi rozwiązaniami są biblioteki DOMPurify oraz Sanitizer Api.

Czy to oznacza, że zawsze musisz tak kombinować? Hmm. Niekoniecznie?. Zasada jest prosta. Im bardziej poważny projekt (np. dla jakiegoś ministerstwa, lub tam gdzie pojawia się jakakolwiek wartość np. pieniężna), tym bardziej musisz uważać i stosować odpowiednio szczelne techniki. Natomiast na co dzień wystarczy zasada, że tekst wstawiamy za pomocą innerText/textContent, a przy użyciu innerHTML po prostu uważamy (częstokroć zamiast niej warto sięgać po metody takie jak document.createElement). Ale znów - w 100% zabezpieczyć się nie zabezpieczysz, więc częstokroć temat jest zwyczajnie pomijany...

Na koniec mała uwaga. Starajcie się nie dodawać do innerHTML tekstu jak poniżej:


//ja dodałem słowo "lorem", ale to może być dowolny tekst lub html
document.body.innerHTML += 'lorem'

Taka operacja faktycznie doda nowy html do strony, ale też odepnie wszystkie zdarzenia dla elementów mieszczących się w danym elemencie (ale i w jego dzieciach). A tego raczej nie chcemy.

innerText i textContent

Właściwości innerText i textContent działają podobnie do powyższej innerHTML, z tym, że zwracają lub ustawiają sam tekst (bez znaczników HTML):


const btn = document.querySelector(".btn");

console.log(btn.innerHTML); //<span>Kliknij mnie</span>
console.log(btn.innerText); ///"Kliknij mnie"
console.log(btn.textContent); //"Kliknij mnie"

Obie właściwości pozwalają nam zwrócić i ustawić tekst elementu. Rozróżnia je to, że innerText działa na sparsowanym tekście, natomiast textContent na oryginalnym, jaki wpisaliśmy do pliku HTML.

Co to oznacza w praktyce? Gdy pobierasz tekst za pomocą innerText, zostanie on pobrany z zaaplikowanymi stylami. Fragmenty tekstu, które zostały ukryte za pomocą display:none i visibility:hidden nie zostaną zwrócone. Dodatkowo tekst taki nie będzie miał wcięć, a wszystkie napotkane znaczniki <br> i nowe linie w HTML zostaną zwrócone jako przejście do nowej linii.

Pobierając ten sam tekst za pomocą textContent zwróci nam tekst oryginalny, jaki napisaliśmy w pliku HTML z wszystkimi wcięciami i enterami które zrobiliśmy za pomocą tabulatora.


<div>
    <span style="display: none">Ukryty</span>
    <span>Obok mnie są dwa niewidoczne spany</span>
    <span style="visibility: hidden">Niewidoczny</span>
</div>

<div>
    Turlał goryl po górach kolorowe korale
    Góral tarł na tarze kartofle wytrwale
</div>
Ukryty Obok mnie są dwa niewidoczne spany Niewidoczny
Turlał goryl po górach kolorowe korale Góral tarł na tarze kartofle wytrwale

To samo tyczy się wstawiania tekstu do elementu. Ustawiając wieloliniowy tekst za pomocą textContent zostanie on po prostu wstawiony do elementu w oryginalnej postaci, natomiast ten sam tekst wstawiany za pomocą innerText automatycznie doda nam <br>


const text = `
    Mała muszka spod Łopuszki
    chciała mieć różowe nóżki
    Różdżką nóżki czarowała
    lecz wciąż nóżki czarne miała
`;

const div = document.querySelector("#addTextTest");

const btn1 = document.querySelector("#textContent");
btn1.addEventListener("click", () => {
    div.textContent = text.trim();
});

const btn2 = document.querySelector("#innerText");
btn2.addEventListener("click", () => {
    div.innerText = text.trim();
});

tagName

Właściwość tagName zwraca nam nazwę elementu np. h2, a, p itp.


const btn = document.querySelector(".button");
console.log(btn.tagName);

Do czego to może się przydać? Przykładowo można pobrać wiele elementów na raz, a następnie tylko dla określonych coś zmieniać:


const elements = document.querySelectorAll("body *");

for (const el of elements) {
    if (el.tagName === "STRONG") {
        el.style.border = "1px solid red";
    }
}

Dla elementów pobranych ze strony HTML nazwy znaczników są zwracane dużymi literami. Dla strony - np. xhtml już nie zawsze będzie to regułą.

Praca z atrybutami

Do pracy z atrybutami danego elementu (np. src dla img) możemy skorzystać z poniższych metod:

el.getAttribute(name) pobiera wartość danego atrybutu lub zwraca null jeżeli takiego nie ma
el.setAttribute(name, value) ustawia nową wartość atrybutu
el.hasAttribute(name) zwraca true/false w zależności czy element ma atrybut o danej nazwie
el.removeAttribute(name) usuwa atrybut o danej nazwie
el.toggleAttribute(name) dodaje/usuwa dany atrybut

<a href="http://google.pl"> Wyszukaj </a>

const link = document.querySelector("a");

const href = link.getAttribute("href"); //"http://google.pl"
const target = link.getAttribute("target"); //null

link.setAttribute("target", "_blank");
if (link.hasAttribute("target")) {
    console.log(link.getAttribute("target")); //"_blank"
}

const btn = document.querySelector("button");
const input = document.querySelector("input");

btn.addEventListener("click", () => {
    input.toggleAttribute("readonly");

    if (input.hasAttribute("readonly")) {
        btn.innerText = "edytuj";
    } else {
        btn.innerText = "zakończ";
    }
})

dataset

Atrybuty możemy podzielić na standardowe dostępne dla danych znaczników (np. src, alt, title, class itp.), oraz nasze własne. Te drugie powinny zaczynać się od słowa data- np. data-text, data-direction.

Własne atrybuty możemy obsługiwać za pomocą powyższych metod, ale możemy dla nich skorzystać z właściwości dataset. Jest to obiekt, którego kolejne właściwości są budowane na bazie niestandardowych atrybutów:


<div class="module"
    data-type="important"
    data-position="top"
    data-my-custom-data="Przykładowy tekst"
> ... </div>

const tooltip = document.querySelector(".module");

console.log(tooltip.dataset.type); //"important"
console.log(tooltip.dataset.position); //"top"
console.log(tooltip.dataset.myCustomData); //"Przykładowy tekst"

Przy podawaniu nazwy danego atrybutu pomijamy początek data-, a myślniki w nazwie zamieniamy na zapis camelCase.


tooltip.dataset.myCustomData = "nowa wartość"; //utworzy w elemencie atrybut data-my-custom-data="nowa wartość"
tooltip.dataset.style = "primary"; //utworzy atrybut data-style="primary"
tooltip.dataset.moduleSize = "big" //utworzy atrybut data-module-size="big"

//to samo możemy uzyskać za pomocą getAttribute i setAttribute
tooltip.setAttribute("data-custom-data", "nowa wartość");
tooltip.setAttribute("data-style", "primary");
tooltip.setAttribute("data-module-size", "big");

Charakterystyczną rzeczą jest to, że atrybuty w znacznikach html zawsze są tekstem. Tak więc cokolwiek byśmy nie przetrzymywali w dataset, staje się to tekstem:


<div data-nr1="16" data-nr2="30"> ... </div>

console.log(typeof div.dataset.nr1, typeof div.dataset.nr2) //"string", "string"
console.log(div.dataset.nr1 + div.dataset.nr2); //"1630"

Niestandardowe atrybuty idealnie nadają się więc do przechowywania tekstów dynamicznie wstawianych na stronę, kawałków html, które zaraz wmontujemy w stronę, czy dodatkowych wartości służących do stylowania za pomocą atrybutów.


div.dataset.type = "error";

div[data-type="error"] {
    color: red;
}

Przykładem (jednym z setek) mogą być tooltipy w popularnym frameworku Bootstrap. Dzięki dodatkowych atrybutom określana jest tutaj pozycja dymka.

bootstrap tooltipy

Przy liczbach także nie powinno być większych problemów, ponieważ łatwo możemy je konwertować:


console.log( +div.dataset.nr1 + +div.dataset.nr2 ); //46

Jeżeli z jakiś powodów chcielibyśmy przechowywać w atrybutach wartości tablic czy obiektów, musimy je skonwertować na format JSON:


//bez konwersji - źle
div.dataset.table = [1,2,3,4,5];
div.dataset.ob = {name : "Marcin"};

console.log(div.dataset.table); //"1,2,3,4,5"
console.log(div.dataset.ob); //"[object Object]"

//z konwersją - lepiej
div.dataset.table = JSON.stringify( [1,2,3,4,5] );
div.dataset.ob = JSON.stringify( {name : "Marcin"} );

console.log( JSON.parse(div.dataset.table) ); //["1,2,3,4,5"]
console.log( JSON.parse(div.dataset.ob) ); //{name : "Marcin"}

Tak skonwertowane obiekty nie mogą zawierać żadnych funkcji, dlatego dataset raczej nie nadaje się do przechowywania bardzo skomplikowanych danych, a tylko prostych tekstów lub wartości wykorzystanych do stylowania.

Atrybuty i właściwości

HTML to tak naprawdę kawałek tekstu, który napisaliśmy w dowolnym edytorze. Przeglądarka analizuje taki tekst, przetwarza, a następnie dla każdego elementu w HTML tworzy jego odzwierciedlenie w postaci odpowiedniego obiektu:


<a id="link" href="https://google.pl" target="_blank"> było - zamykam temat </a>

const link = document.querySelector("#link");
console.dir(link);

Obiekt taki automatycznie dostał wiele właściwości. Wśród nich są te, które odpowiadają atrybutom z html - np. id, href ale też takie które element danego typu potencjalnie może mieć. W powyższym przykładzie mamy link, więc ma on dla przykładu atrybut href. Nasz obiekt w JS także będzie miał taką właściwość.

html i js

<img src="obrazek.jpg" alt="obrazek" title="Fajny obrazek">

const img = document.querySelector("img");

img.src //"obrazek.jpg"
img.alt //"obrazek"
img.title //"Fajny obrazek"

Niektóre wartości są dodatkowo przeliczane:


<a id="link1" href="./kontakt.html"> kontakt </a>
<a id="link2" href="https://google.pl"> było. wygogluj sobie </a>

const link1 = document.querySelector("#link1");
console.log( link1.href ); //file:///C:/Users/kartofelek/Desktop/kontakt.html

const link2 = document.querySelector("#link1");
console.log( link2.href ); //https://google.pl

Atrybuty i właściwości są ze sobą scalone. Oznacza to, że przykładowo zmieniając atrybut href, zmienimy też właściwość href i vice wersa.


<a id="link" href="https://google.pl"> było - zamykam temat </a>

const a = document.querySelector("a");

a.href = "strona.html"
console.log(a.getAttribute("href")); //strona.html

const a = document.querySelector("a");

a.setAttribute("href", "strona.html"); //strona.html
console.log(a.getAttribute("href")); //file:///C:/Users/kartofelek/Desktop/test/strona.html
console.log(a.href);

Tu pojawia się też pewna ciekawostka.

Dla inputów w formularzach istnieją dwie właściwości - defaultValue i value.

Wchodząc na stronę z formularzem, część pól może być już domyślnie wypełniona. Mają one wtedy ustawiony atrybuty value np:


<input type="text" value="Piotrek">

Atrybut ten jest zmapowany z właściwością defaultValue, co oznacza, że jego wartość możemy odczytywać ale i zmienić na dwa sposoby:


input.setAttribute("value", "nowa-wartość");
input.defaultValue = "nowa-wartość"

Wartość value natomiast zawiera zawsze aktualną wartość pola. Po wejściu na stronę wartość ta będzie identyczna jak defaultValue. Gdy jednak użytkownik zacznie ręcznie edytować dane pole, właściwość value automatycznie zaktualizuje swoją wartość, natomiast atrybut (a co za tym idzie defaultValue) pozostanie bez zmian.

Dla nas oznacza to tyle, że jeżeli chcemy odczytać początkową wartość pola - użyjmy defaultValue. Natomiast jeżeli chcemy pobrać aktualną wartość pola - value. Podobna sytuacja będzie się dziać w przypadku checkboxów i radio gdzie mamy właściwości defaultChecked i checked.

Dla bardzo dociekliwych mechanizm ten opisany jest w dokumentacji.

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.