Розширення нативних елементів DOM за допомогою веб-компонентів

12 хв. читання

Веб-компоненти (Web Components) пропонують потужні функції прямо з платформи. Основні з них: справжня інкапсуляція JS та CSS; взаємодія між різними фреймворками; і, звичайно, стандартизована компонентна модель для полегшеного повторного використання UI компонентів.

Існує ще одна приваблива особливість веб-компонентів — налаштовувані вбудовані елементи (customized built-in elements). Їм не приділяється особлива увага через те, що вони поки що не доступні у багатьох браузерах. Деякі вендори взагалі говорять, що не будуть реалізовувати їх підтримку.

Що таке налаштовувані вбудовані елементи?

Налаштовувані вбудовані елементи є частиною специфікації Custom Elements. Вони дозволяють розробникам розширювати нативний елемент, переймати функціональність елементів, а також додавати нові можливості.

Уявіть, що ви хочете, щоб користувач підтверджував навігацію кожного разу, коли він натискає на посилання. Для цього ви могли б розширити нативний анкор елемент (<a>), перехопити клік й використати запит підтвердження, щоб запитати користувача, чи впевнений він у цьому. Це виглядало б якось так:

class ConfirmLink extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this._$a = null;
  }
  connectedCallback() {
    const href = this.getAttribute("href") || "#";
    this.shadowRoot.innerHTML = `
      <a href="${href}">
        <slot></slot>
      </a>
    `;
    this._$a = this.shadowRoot.querySelector("a");
    this._$a.addEventListener("click", e => {
      const result = confirm(`Are you sure you want to go to '${e.target.href}'?`);
      if (!result) e.preventDefault();
    });
  }
  static get observedAttributes() { return ["href"]; }
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      if (this._$a === null) return;
      this._$a.setAttribute("href", newValue);
    }
  }
}

customElements.define("confirm-link", ConfirmLink);

Якщо ви вже знайомі з Custom Elements, то помітите, що визначаючи наш кастомний елемент, ми робимо декілька речей інакше.

class ConfirmLink extends HTMLAnchorElement {
    ...
}
customElements.define("confirm-link", ConfirmLink, { 
    extends: "a" 
});

Замість HTMLElement ми розширюємо HTMLAnchorElement, тому що це інтерфейс, який реалізовується нативним анкор елементом (<a>). Потім, коли ми викличемо метод define() в Custom Elements API, то додамо третій аргумент, який визначає об'єкт з властивістю extends. Тут ми вказуємо ім'я тегу елемента, який хочемо розширити.

Зачекайте, а чому ми повинні вказувати ім'я тегу елемента, якщо ми тільки-но розширили інтерфейс HTMLAnchorElement? Хіба цього недостатньо, щоб зробити висновок, що ми розширюємо анкор елемент? На жаль, деякі елементи, такі як quote (<q>) і blockquote (<blockquote>) поділяють один і той самий інтерфейс —  (HTMLQuoteElement), тому недостатньо вказувати лише інтерфейс. Необхідно вказувати цей, на вигляд зайвий, третій аргумент методу define.

Ми додаємо прослуховувач подій (event listener) у під'єднаний колбек для перехоплення кліку на елементі хосту. Коли викликається подія, ми використовуємо діалог підтвердження, щоб попросити користувача підтвердження. В залежності від відповіді, ми або запобігаємо навігації за допомогою e.preventDefault(), або нічого не робимо — тобто дозволяємо навігацію.

connectedCallback() {
    this.addEventListener("click", e => {
        const result = confirm(
            `Are you sure you want to go to '${this.href}'?`
        );
        if (!result) e.preventDefault();
    });
}

Тоді все, що нам потрібно зробити, щоб використати цю нову функціональність, — додати атрибут is у нативний анкор елемент таким чином:

<a href="https://thewebplatformpodcast.com" is="confirm-link"></a>

Значення атрибуту is — ім'я тегу, яке ми вибрали для кастомного елементу. Цей анкор елемент потім успадкує функціональність, яку ми тільки но додали.

Навіщо нам потрібна здатність безпосередньо розширювати вбудовані елементи?

Можливість миттєво наслідувати вбудовані функції нативних елементів має очевидні переваги. Для відтворення багатьох функцій, наданих цими елементами, знадобиться багато роботи (і багато коду). Уявіть, що вам потрібно мати справу зі взаємодіями з клавіатурою, фокусною поведінкою, тонкощами того, як працює зміна подій, дозволяючи елементу правильно працювати у формі. Список можна продовжувати й продовжувати.

Але вам не потрібно безпосередньо розширювати вбудований елемент, щоб наслідувати його особливості або відтворити ту саму функціональність з анкор елемента вище. Ви могли б просто огорнути нативний елемент у нормальний (автономний) кастомний елемент, ось так:

class ConfirmLink extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this._$a = null;
  }
  connectedCallback() {
    const href = this.getAttribute("href") || "#";
    this.shadowRoot.innerHTML = `
      <a href="${href}">
        <slot></slot>
      </a>
    `;
    this._$a = this.shadowRoot.querySelector("a");
    this._$a.addEventListener("click", e => {
      const result = confirm(`Are you sure you want to go to '${e.target.href}'?`);
      if (!result) e.preventDefault();
    });
  }
  static get observedAttributes() { return ["href"]; }
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      if (this._$a === null) return;
      this._$a.setAttribute("href", newValue);
    }
  }
}

customElements.define("confirm-link", ConfirmLink);

За допомогою автономних кастомних елементів ми розширюємо інтерфейс HTMLElement і пропускаємо третій аргумент методу define().

class ConfirmLink extends HTMLElement {
    ...
}
customElements.define("confirm-link", ConfirmLink);

І замість використання нативного анкор елементу, ми просто використали наш кастомний елемент.

<confirm-link href="https://thewebplatformpodcast.com">Web Platform Podcast</confirm-link>

У конструкторі класу в цьому прикладі я створив Shadow Root для кастомного елементу за допомогою методу attachShadow().

this.attachShadow({ mode: "open" });

Це дозволяє мені залучити слотінг (slotting) функцію з Shadow DOM для проектування внутрішнього контенту кастомного елементу безпосередньо в обернутий анкор елемент, використовуючи елемент slot.

<a href="${href}">
    <slot></slot>
</a>
Розширення нативних елементів DOM за допомогою веб-компонентів
Текстовий вузол проеціюється у слот в анкор елементі

Це не дає нам вручну переміщати кастомні елементи innerHTML в анкор елемент, а також бере під свій контроль будь-які майбутні оновлення DOM.

Потім я передаю атрибут href до кастомного елемента анкор елементу:

static get observedAttributes() { return ["href"]; }
attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
        if (this._$a === null) return;
        this._$a.setAttribute("href", newValue);
    }
}

І під кінець я додаю функцію перехоплення кліків:

this._$a = this.shadowRoot.querySelector("a");
this._$a.addEventListener("click", e => {
    const result = confirm(
        `Are you sure you want to go to '${e.target.href}'?`
    );
    if (!result) e.preventDefault();
});

Примітка: будь ласка, не сприймайте цей кастомний елемент як приклад хорошого кастомного елемента — я пропустив багато гарних практик, щоб зберегти код коротким й сфокусуватися на тематиці цієї статті.

Ок, знадобилося трохи більше коду, але ми отримали ту ж саму функціональність, яку надавав нам налаштовуваний вбудований елемент. Тоді навіщо нам взагалі ці елементи?

1. Менше коду

Менше — це більше, особливо коли справа стосується коду. У прикладі налаштовуваного вбудованого елемента нам не потрібно було визначати шаблон для кастомного елемента, і не треба було передавати будь-які атрибути. Все, що потрібно зробити — тільки додати код, необхідний для додаткової функціональності.

2. Стилізація

Коли ми огортаємо нативний елемент, особливо в Shadow Root, будь-який наявний у застосунку CSS, що додає стилі к елементу, буде порушений.

a {
 color: red;
}

Можна не використовувати Shadow Root, щоб підвищити шанси наявного CSS застосувати правильний стиль, але тоді доведеться додати набагато більше JavaScript, щоб отримати потрібну функціональність. Крім того, якщо будь-який з CSS залежав від конкретної структури DOM, тоді додавання нашого кастомного елемента в будь-якому випадку зламало б стилі.

nav > a {
 color: red;
}

Коли ми розширюємо нативний анкор елемент за допомогою налаштовуваного вбудованого, HTML підпис залишається колишнім, і всі наявні CSS будуть функціонувати як звичайно. Якщо ви хочете застосувати до цих налаштовуваних вбудованих елементів інший стиль, тоді вам треба вказати їх, використовуючи у CSS атрибут is, на зразок цього:

a[is="confirm-link"] {
 color: orange;
}

3. Прогресивне покращення

Якщо ви завантажили наш приклад у браузер, який не підтримує кастомні елементи, ігноруйте поліфіли. Тоді користувачеві все одно буде представлений анкор елемент, який можна використовувати для навігації на іншу сторінку. Так, він не отримає додаткову функціональність, яку ми додали, але його досвід, як і раніше, буде задовільним.

З іншої сторони, коли ми завантажуємо приклад, де огортаємо анкор елемент у браузері без підтримки кастомних елементів, користувач нічого не побачить, і застрягне.

4. Деякі елементи мають не відтворювані можливості

У нашому прикладі огортання елемента ми можемо відтворити повні можливості анкор елемента, але функціональність певних елементів не отримати за допомогою їх огортання.

Візьмемо в ролі прикладу елемент шаблону. Його вміст залишається інертним (ігнорується й не відображається браузером) доки не буде активованим. Це дозволяє розробникам ховати частини HTML-розмітки, які не використовують ресурси браузера, поки вони їм дійсно не знадобляться. Без можливості розширювати нативний елемент шаблону, цю функціональність неможливо відтворити. Нам потрібно змусити користувачів огортати вміст, який вони надають нашому кастомному елементу, в елемент шаблону, щоб уникнути його автоматичного рендерингу.

<fancy-template>
 <template>
 <p>Don't render until needed.</p>
 </template>
</fancy-template>

Якщо ми розширимо елемент шаблону й додамо будь-яку функціональність, яка нам потрібна, все буде набагато простіше:

<template is="fancy-template">
 <p>Don't render until needed.</p>
</template>

5. Обмеження для дочірніх елементів

Існують також обставини, в яких огортання нативного елементу призведе до невалідної HTML-розмітки. Наприклад, елемент thead може містити тільки «нуль або більше <tr> елементів». Це означає, що щось на зразок цього буде не буде працювати:

<table>
 <thead>
 <fancy-tr></fancy-tr>
 </thead>
</table>

В той час як можна використовувати налаштовуваний вбудований елемент без подібних проблем:

<table>
 <thead>
 <tr is="fancy-tr"></tr>
 </thead>
</table>

Це всього лише один з багатьох прикладів, table, button та ul — лише декілька інших прикладів елементів, що мають специфічні дочірні елементи.

Необхідність налаштовуваних вбудованих елементів

Налаштовувані вбудовані елементи є не тільки рішенням для наслідування нативних функцій таким чином, щоб вирішити всі вище зазначені проблеми. Було також запропоновано інші рішення для стандартизації, такі як підхід в стилі міксинів і кастомні атрибути, але налаштовувані вбудовані елементи — єдине стандартизоване рішення, яке перевірялось та тестувалось протягом кількох років.

Кажуть, що атрибут is заплутаний, і я можу зрозуміти звідки береться ця думка. Оголошення класу (HTMLAnchorElement) та імені тегу (a) спершу може здатися заплутаним, і чесно кажучи, це зроблено не ідеально. Але як тільки ви зрозумієте, чому так робиться, стане набагато легше включитися у процес.

Але в цілому мені дуже подобається цей підхід. У нашому прикладі, де атрибут is додається до анкор елементу, він буквально зчитує:

«Цей анкор елемент є (is) елементом confirm-link». Це говорить розробнику значно більше, ніж споглядання незнайомого кастомного елементу (<confirm-link></confirm-link>), розміщеного в DOM. З одного погляду ви можете побачити, що його базова поведінка буде належати анкор елементу, і що у нього є додаткова функціональність зверху.

Можливість розширення нативних елементів дуже потужна, щоб її ігнорувати. Mozilla опублікувала «Наміри для реалізації: кастомні елементи», що включає налаштовувані вбудовані елементи, і вони доступні за замовчуванням у Chrome 67. Не всі вендори браузерів усвідомили користь цієї функції. Але до тих пір у нас є поліфіли.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація