Если вы когда-либо сталкивались с разработкой веб-приложений, то наверняка знаете, как много времени уходит на повторяющиеся задачи: создавать кнопки, формы, выпадающие списки, таблицы. Ещё хуже, когда каждый проект требует своего подхода и инструментов, а поддерживать всё это становится настоящим кошмаром. В мире фронтенд-разработки настала эра стандартизации компонентов, и Web Components – это ответ на эти проблемы.

Web Components — это технология, которая позволяет создавать собственные HTML-теги с полным набором стилей и логики, которые можно переиспользовать в любом проекте. По сути, это набор спецификаций, которые позволяют разработчикам определять свои собственные компоненты, инкапсулировать стили и логику, и использовать их как обычные HTML-элементы.

Web Components состоят из трёх основных технологий:

  1. Custom Elements (Пользовательские элементы)
  2. Shadow DOM (Теневой DOM)
  3. HTML Templates (HTML-шаблоны)

Custom Elements – свои HTML-теги

Custom Elements — это то, что позволяет вам создавать свои собственные HTML-теги. Например, вместо того чтобы писать огромные и многословные разметки для одной и той же кнопки каждый раз, можно создать кастомный элемент <my-button> и использовать его везде:

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.innerHTML = `<button>Нажми меня</button>`;
  }
}

customElements.define('my-button', MyButton);

Теперь вы можете вставить <my-button> куда угодно в вашем приложении, и он будет работать, как обычный HTML-элемент.

Shadow DOM – инкапсуляция стилей

Представьте, что вы создали крутой компонент — допустим, кнопку с красивым стилем и анимацией. Но вот беда: другие стили на странице или стили, добавленные позже, могут легко поломать внешний вид этой кнопки. Или, наоборот, ваш стиль кнопки может неожиданно изменить вид других элементов на странице. Как этого избежать?

Здесь и приходит на помощь Shadow DOM — технология, которая позволяет изолировать стили и логику компонентов так, чтобы они не пересекались с остальным кодом страницы.

Shadow DOM — это способ создания собственного «мини-DOMа» внутри компонента. Все элементы и стили внутри Shadow DOM находятся в изоляции от остального документа.

Это означает, что:

  • Стили внутри Shadow DOM не влияют на элементы страницы.
  • Внешние стили не могут повлиять на элементы внутри Shadow DOM.

Таким образом, компоненты становятся независимыми и защищенными от «внешнего мира».

Пример работы с Shadow DOM

Давайте разберём простой пример. Мы создадим кастомный элемент — кнопку, и изолируем её стили с помощью Shadow DOM.

class MyButton extends HTMLElement {
  constructor() {
    super();

    // Создаём Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });

    // Вставляем HTML и стили в теневой DOM
    shadow.innerHTML = `
      <style>
        button {
          background-color: green;
          color: white;
          padding: 10px 20px;
          border: none;
          border-radius: 5px;
          cursor: pointer;
        }
      </style>
      <button>Нажми меня</button>
    `;
  }
}

customElements.define('my-button', MyButton);

Теперь давайте добавим этот компонент на страницу:

<my-button></my-button>

Что происходит? На странице появляется кнопка со стилями, которые мы задали внутри Shadow DOM. Даже если у вас на странице есть другой CSS, стили этой кнопки останутся нетронутыми.

Почему это полезно

  • Изоляция стилей: Стили внутри Shadow DOM изолированы от внешних стилей. Например, если у вас есть кнопка с глобальными стилями на странице, она не повлияет на вашу кастомную кнопку.
  • Чистота кода: Вам не нужно беспокоиться о том, что ваши компоненты будут конфликтовать с остальными элементами страницы. Все, что вы пишете внутри Shadow DOM, работает только там.
  • Легкость поддержки: Если проект становится большим, наличие независимых компонентов помогает избежать ситуаций, когда один стиль ломает другой, или изменения в одном компоненте случайно портят другие.

HTML Templates – шаблоны на стероидах

HTML-шаблоны (или HTML Templates) — это простой, но мощный инструмент, который позволяет вам готовить фрагменты HTML-кода заранее, не отображая их сразу на странице. Эти фрагменты можно «включить» тогда, когда это действительно нужно, что делает ваш код чище и более управляемым.

HTML-шаблон — это специальный элемент <template>, внутри которого вы можете написать HTML-разметку, но эта разметка не будет отображаться сразу на странице. Она как бы «спрятана» до тех пор, пока вы сами её не активируете.

Пример простого шаблона:

<template id="my-template">
  <p>Это текст внутри шаблона</p>
</template>

На странице этот текст не появится, потому что браузер игнорирует содержимое тега <template>, пока вы сами не решите его использовать.

Как использовать HTML-шаблоны

Чтобы воспользоваться этим шаблоном, нам нужно его «клонировать» и вставить куда-то в наш документ. Сделать это можно с помощью JavaScript.

Пример:

// Находим наш шаблон по ID
const template = document.getElementById('my-template');

// Клонируем содержимое шаблона
const templateContent = template.content.cloneNode(true);

// Вставляем клонированное содержимое в DOM
document.body.appendChild(templateContent);

После этого содержимое нашего шаблона появится на странице: текст «Это текст внутри шаблона» станет видимым.

Пример использования в компоненте

Теперь давайте посмотрим, как HTML-шаблоны могут помочь при создании Web Components. Допустим, мы хотим создать компонент карточки товара, но чтобы не дублировать код карточки каждый раз, мы можем использовать шаблон.

Создадим шаблон в HTML:

<template id="product-template">
  <div class="product">
    <h2>Название продукта</h2>
    <p>Описание продукта</p>
  </div>
</template>

И создадим компонент, который будет использовать этот шаблон:

class ProductCard extends HTMLElement {
  constructor() {
    super();

    // Создаем Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });

    // Находим шаблон и клонируем его содержимое
    const template = document.getElementById('product-template').content.cloneNode(true);

    // Вставляем шаблон в Shadow DOM
    shadow.appendChild(template);
  }
}

customElements.define('product-card', ProductCard);

Теперь, добавив этот компонент на страницу, мы можем использовать одну и ту же разметку из шаблона:

<product-card></product-card>

Каждый раз, когда вы будете использовать <product-card>, будет подтягиваться разметка из шаблона.

Почему это удобно

  • Чистый код: Шаблоны позволяют вам хранить разметку в одном месте, и при необходимости её многократно использовать, что избавляет вас от копипасты.
  • Динамическое создание контента: Вы можете изменять содержимое шаблона перед его вставкой на страницу. Например, меняя текст, добавляя изображения или другие элементы.
  • Отложенная загрузка: Поскольку шаблоны не отображаются до тех пор, пока вы их не используете, это может помочь с оптимизацией загрузки страницы.

Пример динамической вставки:

const template = document.getElementById('product-template').content.cloneNode(true);
template.querySelector('h2').textContent = 'Мой крутой продукт';
template.querySelector('p').textContent = 'Лучший продукт на рынке!';
document.body.appendChild(template);

Slots — Работа с контентом внутри компонентов

С помощью Shadow DOM мы уже научились изолировать стили и содержимое компонента. Но что если нам нужно сделать компонент, который может включать в себя динамическое содержимое? Например, создаём карточку с заголовком, но хотим, чтобы заголовок можно было менять каждый раз, когда компонент используется.

Вот здесь вступает в игру механизм Slots.

Пример работы со слотами

Создадим компонент карточки с заголовком и текстом:

class MyCard extends HTMLElement {
  constructor() {
    super();

    const shadow = this.attachShadow({ mode: 'open' });

    // HTML-шаблон с использованием <slot>
    shadow.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          padding: 20px;
          border-radius: 5px;
        }
      </style>
      <div class="card">
        <h2><slot name="title">Заголовок по умолчанию</slot></h2>
        <p><slot>Текст по умолчанию</slot></p>
      </div>
    `;
  }
}

customElements.define('my-card', MyCard);

Теперь при использовании компонента <my-card> мы можем динамически вставлять содержимое с помощью слотов:

<my-card>
  <span slot="title">Мой динамический заголовок</span>
  Текст, который я хочу вставить в карточку.
</my-card>
  • Тег <slot> внутри компонента обозначает место, куда будет вставляться пользовательский контент.
  • Атрибут slot=»title» указывает, что данный элемент попадет в слот с именем «title». Если мы не укажем слот для текста, он попадет в слот по умолчанию (без имени).

Это позволяет делать компоненты гибкими и настраиваемыми для разных сценариев использования.

Lifecycle — Управление поведением компонента

Каждый кастомный элемент (Custom Element) имеет свои методы жизненного цикла, которые помогают управлять поведением компонента в разные моменты его существования.

Вот основные методы жизненного цикла:

  • connectedCallback(): вызывается, когда компонент добавляется на страницу (например, когда браузер загружает элемент).
  • disconnectedCallback():  вызывается, когда компонент удаляется со страницы.
  • attributeChangedCallback(attrName, oldValue, newValue):  вызывается, когда изменяется атрибут элемента.
  • adoptedCallback(): вызывается, когда компонент перемещается в другой документ (например, если страница использует iframe).

Пример с connectedCallback() и disconnectedCallback()

Допустим, нам нужно, чтобы компонент делал что-то, когда он появляется на странице, и останавливал свои действия, когда его удаляют.

class MyTimer extends HTMLElement {
  constructor() {
    super();
    this.timerId = null;
  }

  connectedCallback() {
    // Запускаем таймер, когда элемент добавлен на страницу
    this.timerId = setInterval(() => {
      console.log('Таймер тикает!');
    }, 1000);
  }

  disconnectedCallback() {
    // Останавливаем таймер, когда элемент удалён со страницы
    clearInterval(this.timerId);
    console.log('Таймер остановлен!');
  }
}

customElements.define('my-timer', MyTimer);

Теперь, когда элемент <my-timer> появится на странице, начнёт работать таймер. Когда элемент удалится, таймер автоматически остановится. Это очень полезно для управления ресурсами и снижает вероятность утечек памяти.

Атрибуты и attributeChangedCallback()

Когда мы создаём кастомные элементы, они могут иметь свои собственные атрибуты, которые влияют на их поведение или внешний вид.

Представьте, что у нас есть компонент с текстом, цвет которого меняется в зависимости от значения атрибута.

class ColoredText extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        p {
          color: black;
        }
      </style>
      <p>Измените мой цвет!</p>
    `;
  }

  // Указываем, за изменениями каких атрибутов следить
  static get observedAttributes() {
    return ['color'];
  }

  // Вызывается при изменении атрибута
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'color') {
      this.shadowRoot.querySelector('p').style.color = newValue;
    }
  }
}

customElements.define('colored-text', ColoredText);

Теперь мы можем менять цвет текста через атрибут color:

<colored-text color="red"></colored-text>

При изменении атрибута color компонент автоматически обновит цвет текста.