14 серпня 2023 р.

Атрибути та властивості

Коли браузер завантажує сторінку, він “читає” (іншими словами: “парсить”) HTML і генерує DOM об’єкти з нього. Для вузлів-елементів більшість стандартних атрибутів HTML автоматично стають властивостями об’єктів DOM.

Наприклад, якщо тег це <body id="page">, тоді об’єкт DOM матиме body.id="page".

Але представлення атрибутів через властивості не відбувається один до одного! У цій главі ми звернемо увагу на відмінності цих двох концепцій, щоб навчитись, як з ними працювати, коли вони співпадають і коли відрізняються.

DOM властивості

Ми вже бачили вбудовані властивості DOM. Їх багато. Але технічно ніхто нас не обмежує, і якщо їх недостатньо, ми можемо додати власні.

DOM вузли є звичайними об’єктами JavaScript. Ми можемо змінювати їх.

Наприклад, створімо нову властивість у document.body:

document.body.myData = {
  name: 'Цезар',
  title: 'Імператор'
};

alert(document.body.myData.title); // Імператор

Ми також можемо додати метод:

document.body.sayTagName = function() {
  alert(this.tagName);
};

document.body.sayTagName(); // BODY (значення "this" у методі є document.body)

Ми також можемо змінювати вбудовані прототипи, такі як Element.prototype і додати нові методи для всіх елементів:

Element.prototype.sayHi = function() {
  alert(`Привіт, Я ${this.tagName}`);
};

document.documentElement.sayHi(); // Привіт, Я HTML
document.body.sayHi(); // Привіт, Я BODY

Отже, властивості та методи DOM поводяться так само, як і звичайні об’єкти JavaScript:

  • Вони можуть мати будь-яке значення.
  • Вони чутливі до регістру (наприклад elem.nodeType, не elem.NoDeTyPe).

HTML атрибути

У HTML, теги можуть мати атрибути. Коли браузер аналізує HTML і створює DOM-об’єкти для тегів, він розпізнає стандартні атрибути та створює з них властивості DOM.

Отже, коли елемент має id або інший стандартний атрибут, створюється відповідна властивість. Проте цього не відбувається, якщо атрибут не є стандартним.

Наприклад:

<body id="test" something="non-standard">
  <script>
    alert(document.body.id); // test
    // нестандартний атрибут не створює DOM-властивості
    alert(document.body.something); // undefined
  </script>
</body>

Зверніть увагу, що стандартний атрибут для одного елемента може бути невідомим для іншого. Наприклад, "type" – це стандартний для <input> (HTMLInputElement), але не для <body> (HTMLBodyElement). Стандартні атрибути описані у специфікації для відповідного класу елемента.

Ось приклад:

<body id="body" type="...">
  <input id="input" type="text">
  <script>
    alert(input.type); // text
    alert(body.type); // undefined: DOM властивість не створена, тому що вона нестандартна
  </script>
</body>

Отже, якщо атрибут не є стандартним, то для нього не буде створено DOM-властивості. Чи є спосіб отримати доступ до таких атрибутів?

Звичайно. Всі атрибути доступні за допомогою наступних методів:

  • elem.hasAttribute(name) – перевіряє наявність атрибута.
  • elem.getAttribute(name) – отримує значення атрибута.
  • elem.setAttribute(name, value) – встановлює значення атрибута.
  • elem.removeAttribute(name) – видаляє атрибут.

Ці методи працюють із значеннями, записаними у HTML.

Також можна прочитати всі атрибути, використовуючи elem.attributes: це колекція об’єктів, які належать вбудованому класу Attr, і мають властивості name та value.

Ось демонстрація читання нестандартного атрибута:

<body something="non-standard">
  <script>
    alert(document.body.getAttribute('something')); // non-standard
  </script>
</body>

Атрибути HTML мають такі особливості:

  • Їх назва нечутлива до регістру (id – це те саме, що й ID).
  • Їхні значення завжди є рядками.

Ось розширена демонстрація роботи з атрибутами:

<body>
  <div id="elem" about="Elephant"></div>

  <script>
    alert( elem.getAttribute('About') ); // (1) 'Elephant', читання

    elem.setAttribute('Test', 123); // (2), запис

    alert( elem.outerHTML ); // (3), дивимося чи атрибут знаходиться в HTML (так)

    for (let attr of elem.attributes) { // (4) перерахування всіх атрибутів
      alert( `${attr.name} = ${attr.value}` );
    }
  </script>
</body>

Будь ласка, зверніть увагу:

  1. getAttribute('About') – перша літера написана великою, а в HTML вона пишеться маленькою. Але це не має значення: імена атрибутів нечутливі до регістру.
  2. Ми можемо присвоїти атрибуту будь-яке значення, але воно стане рядком. Тому тут ми отримуємо "123" як значення.
  3. Всі атрибути, включаючи ті, які ми встановлюємо, видно в outerHTML.
  4. Колекція attributes є ітерованою і має всі атрибути елемента (стандартні та нестандартні) у вигляді об’єктів з властивостями name та value.

Синхронізація властивостей і атрибутів

Коли стандартний атрибут змінюється, відповідна властивість автоматично оновлюється і, за деякими винятками, навпаки.

У наведеному нижче прикладі id модифікується як атрибут, і ми також можемо побачити, що властивість теж змінюється. А потім те ж саме навпаки:

<input>

<script>
  let input = document.querySelector('input');

  // атрибут => властивість
  input.setAttribute('id', 'id');
  alert(input.id); // id (оновлений)

  // властивість => атрибут
  input.id = 'newId';
  alert(input.getAttribute('id')); // newId (оновлений)
</script>

Але є винятки, наприклад, input.value синхронізується лише в одному напрямку, з атрибуту → до властивості, але не навпаки:

<input>

<script>
  let input = document.querySelector('input');

  // атрибут => властивість
  input.setAttribute('value', 'text');
  alert(input.value); // text

  // НІ властивість => атрибут
  input.value = 'newValue';
  alert(input.getAttribute('value')); // text (не оновлено!)
</script>

У прикладі вище:

  • Зміна атрибута value оновлює властивість.
  • Але зміна властивості не впливає на атрибут.

Ця “особливість” може стати в нагоді, оскільки дії користувача можуть призвести до змін value, а потім після них, якщо ми хочемо відновити “оригінальне” значення з HTML, то воно є в атрибуті.

Властивості DOM типізовані

Властивості DOM не завжди є рядками. Наприклад, властивість input.checked (для чекбоксів) має логічний(булевий) тип:

<input id="input" type="checkbox" checked> чекбокс

<script>
  alert(input.getAttribute('checked')); // значення атрибута: порожній рядок
  alert(input.checked); // значення властивостей є: true
</script>

Є й інші приклади. Атрибут style – це рядок, але властивість style є об’єктом:

<div id="div" style="color:red;font-size:120%">Привіт</div>

<script>
  // рядок
  alert(div.getAttribute('style')); // color:red;font-size:120%

  // об’єкт
  alert(div.style); // [object CSSStyleDeclaration]
  alert(div.style.color); // red
</script>

Більшість властивостей є рядками.

Досить рідко, властивість в DOM може відрізнятися від атрибута, навіть якщо тип властивості в DOM – це рядок. Наприклад, властивість href DOM завжди є повним URL, навіть якщо атрибут містить відносну URL-адресу або просто #hash.

Ось приклад:

<a id="a" href="#hello">посилання</a>
<script>
  // атрибут
  alert(a.getAttribute('href')); // #hello

  // властивість
  alert(a.href ); // повний URL у формі http://site.com/page#hello
</script>

Якщо нам потрібна властивість href або будь-якого іншого атрибуту саме так, як написано в HTML, ми можемо використовувати getAttribute.

Нестандартні атрибути, dataset

При написанні HTML ми використовуємо багато стандартних атрибутів. Але як щодо нестандартних, користувацьких? Спершу давайте побачимо, чи вони корисні, і для чого вони потрібні?

Іноді нестандартні атрибути використовуються для передачі користувацьких даних з HTML до JavaScript, або для “позначення” HTML-елементів для JavaScript.

Наприклад:

<!-- позначимо div, щоб показати поле "name" тут -->
<div show-info="name"></div>
<!-- і вік "age" тут -->
<div show-info="age"></div>

<script>
  // код знаходить елемент з позначкою і показує те, що запитується
  let user = {
    name: "Іван",
    age: 25
  };

  for(let div of document.querySelectorAll('[show-info]')) {
    // вставимо відповідну інформацію в поле
    let field = div.getAttribute('show-info');
    div.innerHTML = user[field]; // по-перше, значення "Іван" в div з "name", потім 25 в "age"
  }
</script>

Також вони можуть бути використані для стилізації елемента.

Наприклад, тут для замовлення використовується атрибут order-state:

<style>
  /* стилі покладаються на користувальницький атрибут "order-state" */
  .order[order-state="new"] {
    color: green;
  }

  .order[order-state="pending"] {
    color: blue;
  }

  .order[order-state="canceled"] {
    color: red;
  }
</style>

<div class="order" order-state="new">
  Нове замовлення.
</div>

<div class="order" order-state="pending">
  Замовлення очікується.
</div>

<div class="order" order-state="canceled">
  Скасоване замовлення.
</div>

Чому б використовувати атрибут краще ніж мати класи .order-state-new, .order-state-pending, .order-state-canceled?

Оскільки атрибут більш зручний для управління. Стан може бути легко змінено, наприклад:

// трохи простіше, ніж видалення старого/додавання нового класу
div.setAttribute('order-state', 'canceled');

Але може виникнути потенційна проблема з користувацькими атрибутами. Що робити, якщо ми використовуємо нестандартний атрибут для наших цілей, а потім стандарт вводить його та надає йому якусь функціональність? Мова HTML жива, вона зростає, а також з’являється більше атрибутів, які відповідають потребам розробників. У такому випадку можуть виникнути неочікувані наслідки.

Щоб уникнути конфліктів, існують data-* атрибути.

Всі атрибути, які починаються з “data-” зарезервовані для використання програмістами. Вони доступні у властивості dataset.

Наприклад, якщо elem має атрибут, що називається "data-about", то він доступний як elem.dataset.about.

Наприклад:

<body data-about="Elephants">
<script>
  alert(document.body.dataset.about); // Elephants
</script>

Атрибути, що складаються з декількох слів, такі як data-order-state стають записані за допомогою верблюжої нотації (camel-case): dataset.orderState.

Ось приклад переписаного “order state”:

<style>
  .order[data-order-state="new"] {
    color: green;
  }

  .order[data-order-state="pending"] {
    color: blue;
  }

  .order[data-order-state="canceled"] {
    color: red;
  }
</style>

<div id="order" class="order" data-order-state="new">
  A new order.
</div>

<script>
  // читання
  alert(order.dataset.orderState); // new

  // зміна
  order.dataset.orderState = "pending"; // (*)
</script>

Використання атрибутів data-* є валідним, безпечним способом передачі додаткових даних.

Зверніть увагу, що ми можемо не тільки читати, але й модифікувати data-атрибути. Тоді CSS відповідно оновлює зовнішній вигляд: у прикладі вище останнього рядка (*) змінює колір на синій.

Підсумки

  • Атрибути – це те, що написано в HTML.
  • Властивості – це те, що є в об’єктах DOM.

Невелике порівняння:

Властивості Атрибути
Тип Будь-яке значення, стандартні властивості мають типи, описані в специфікації Рядок
Назва Назва є чутливою до регістру Назва не чутлива до регістру

Методи роботи з атрибутами:

  • elem.hasAttribute(name) – перевірити наявність.
  • elem.getAttribute(name) – отримати значення.
  • elem.setAttribute(name, value) – встановити значення.
  • elem.removeAttribute(name) – видалити атрибут.
  • elem.attributes – це колекція всіх атрибутів.

Для більшості ситуацій краще використовувати властивості DOM. Ми повинні посилатися на атрибути лише тоді, коли DOM властивості не підходять нам, коли нам потрібні саме атрибути, наприклад:

  • Нам потрібен нестандартний атрибут. Але якщо він починається з data-, то ми повинні використовувати dataset.
  • Ми хочемо прочитати значення “як написано” у HTML. Значення DOM властивості може бути іншим, наприклад, властивість href завжди є повною URL-адресою, і ми можемо бажати отримати “оригінальне” значення.

Завдання

важливість: 5

Напишіть код, щоб вибрати елемент з атрибутом data-widget-name з документа та прочитати його значення.

<!DOCTYPE html>
<html>
<body>

  <div data-widget-name="menu">Виберіть жанр</div>

  <script>
    /* ваш код */
  </script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>

  <div data-widget-name="menu">Виберіть жанр</div>

  <script>
    // отримаємо його
    let elem = document.querySelector('[data-widget-name]');

    // прочитаємо значення
    alert(elem.dataset.widgetName);
    // чи
    alert(elem.getAttribute('data-widget-name'));
  </script>
</body>
</html>
важливість: 3

Зробіть всі зовнішні посилання помаранчевими, змінюючи властивість style.

Посилання є зовнішнім, якщо:

  • В його href є ://
  • Але не починається з http://internal.com.

Приклад:

<a name="list">список</a>
<ul>
  <li><a href="http://google.com">http://google.com</a></li>
  <li><a href="/tutorial">/tutorial.html</a></li>
  <li><a href="local/path">local/path</a></li>
  <li><a href="ftp://ftp.com/my.zip">ftp://ftp.com/my.zip</a></li>
  <li><a href="http://nodejs.org">http://nodejs.org</a></li>
  <li><a href="http://internal.com/test">http://internal.com/test</a></li>
</ul>

<script>
  // налаштування style для одного посилання
  let link = document.querySelector('a');
  link.style.color = 'orange';
</script>

Результат повинен бути:

Відкрити пісочницю для завдання.

По-перше, нам потрібно знайти всі зовнішні посилання.

Є два способи.

Перше – знайти всі посилання, використовуючи document.querySelectorAll('a'), а потім відфільтровувати те, що нам потрібно:

let links = document.querySelectorAll('a');

for (let link of links) {
  let href = link.getAttribute('href');
  if (!href) continue; // немає атрибуту

  if (!href.includes('://')) continue; // немає протоколу

  if (href.startsWith('http://internal.com')) continue; // внутрішня

  link.style.color = 'orange';
}

Будь ласка, зверніть увагу: ми використовуємо link.getAttribute('href'). Не link.href тому, що нам потрібна властивість з HTML.

…Інший, простий спосіб полягає в тому, щоб додати перевірки до селектора CSS:

// шукати всі посилання, які мають :// у href
// але href не починається з http://internal.com
let selector = 'a[href*="://"]:not([href^="http://internal.com"])';
let links = document.querySelectorAll(selector);

links.forEach(link => link.style.color = 'orange');

Відкрити рішення в пісочниці.

Навчальна карта