Реализация HTML Templates и Slots для Web Components

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация HTML Templates и Slots для Web Components
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация HTML Templates и Slots для Web Components

<template> и <slot> — два HTML-элемента, которые дополняют Custom Elements и Shadow DOM. Templates позволяют описать структуру компонента прямо в HTML без исполнения. Slots дают возможность вставлять внешний контент внутрь Shadow DOM.

HTML Template

Содержимое <template> парсится браузером, но не рендерится и не исполняется. Изображения не загружаются, скрипты не выполняются, стили не применяются — до клонирования.

<!-- В HTML документе или компонентном файле -->
<template id="card-template">
  <style>
    .card {
      padding: 24px;
      border-radius: 12px;
      background: var(--card-bg, #fff);
      box-shadow: 0 2px 16px rgba(0,0,0,0.08);
    }

    .card__header {
      display: flex;
      align-items: center;
      gap: 12px;
      margin-bottom: 16px;
    }

    .card__avatar {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      object-fit: cover;
    }

    .card__body {
      line-height: 1.6;
    }
  </style>

  <div class="card">
    <div class="card__header">
      <img class="card__avatar" src="" alt="">
      <div class="card__meta">
        <slot name="name"><strong>Имя не указано</strong></slot>
        <slot name="role"><em>Роль не указана</em></slot>
      </div>
    </div>
    <div class="card__body">
      <slot>Описание не указано</slot>
    </div>
  </div>
</template>
class TeamCard extends HTMLElement {
  private shadow: ShadowRoot

  constructor() {
    super()
    this.shadow = this.attachShadow({ mode: 'open' })
  }

  connectedCallback() {
    // Получаем template и клонируем
    const template = document.getElementById('card-template') as HTMLTemplateElement
    const clone = template.content.cloneNode(true) as DocumentFragment

    // Устанавливаем данные из атрибутов
    const avatar = clone.querySelector('.card__avatar') as HTMLImageElement
    avatar.src = this.getAttribute('avatar') || '/placeholder.png'
    avatar.alt = this.getAttribute('name') || 'Фото'

    this.shadow.appendChild(clone)
  }
}

customElements.define('team-card', TeamCard)

Использование:

<team-card avatar="/team/anna.jpg">
  <strong slot="name">Анна Ковалёва</strong>
  <span slot="role">Lead Frontend Engineer</span>
  Специализируется на архитектуре React-приложений и WebGL-визуализациях.
</team-card>

Template внутри компонента (строковый подход)

Если template не нужен в HTML — используется programmatic создание:

// Создание template один раз при определении класса (не в constructor)
const template = document.createElement('template')
template.innerHTML = `
  <style>
    :host {
      display: inline-flex;
      align-items: center;
      gap: 8px;
    }

    .badge {
      padding: 4px 10px;
      border-radius: 100px;
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.03em;
    }

    :host([color="green"])  .badge { background: #d4edda; color: #155724; }
    :host([color="red"])    .badge { background: #f8d7da; color: #721c24; }
    :host([color="blue"])   .badge { background: #d1ecf1; color: #0c5460; }
    :host([color="yellow"]) .badge { background: #fff3cd; color: #856404; }
  </style>

  <span class="badge">
    <slot></slot>
  </span>
`

class StatusBadge extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })
    // Клонируем template — не пересоздаём DOM каждый раз
    shadow.appendChild(template.content.cloneNode(true))
  }
}

customElements.define('status-badge', StatusBadge)

Преимущество: template создаётся один раз, клонирование быстрее повторного innerHTML.

Slots: именованные и дефолтные

<template id="dialog-template">
  <style>
    .dialog-backdrop {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.5);
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 1000;
    }

    .dialog {
      background: #fff;
      border-radius: 16px;
      padding: 0;
      max-width: 480px;
      width: 90%;
      overflow: hidden;
    }

    .dialog__header {
      padding: 20px 24px;
      border-bottom: 1px solid #eee;
      font-weight: 700;
      font-size: 18px;
    }

    .dialog__body {
      padding: 24px;
    }

    .dialog__footer {
      padding: 16px 24px;
      border-top: 1px solid #eee;
      display: flex;
      justify-content: flex-end;
      gap: 12px;
    }

    /* Если footer slot пуст — скрываем */
    .dialog__footer:not(:has(slot[name="footer"] ~ *)):empty {
      display: none;
    }
  </style>

  <div class="dialog-backdrop">
    <div class="dialog" role="dialog" aria-modal="true">
      <div class="dialog__header">
        <!-- Именованный slot с fallback-контентом -->
        <slot name="title">Диалог</slot>
      </div>
      <div class="dialog__body">
        <!-- Дефолтный slot — весь контент без slot атрибута -->
        <slot></slot>
      </div>
      <div class="dialog__footer">
        <!-- Опциональный footer slot -->
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</template>
<modal-dialog id="confirm-modal">
  <span slot="title">Подтвердить удаление</span>

  <p>Это действие нельзя отменить. Вы уверены?</p>

  <div slot="footer">
    <button onclick="document.getElementById('confirm-modal').close()">
      Отмена
    </button>
    <button onclick="handleDelete()">Удалить</button>
  </div>
</modal-dialog>

Slotchange — реакция на изменение слотов

class DynamicList extends HTMLElement {
  private shadow: ShadowRoot
  private countEl!: HTMLElement

  constructor() {
    super()
    this.shadow = this.attachShadow({ mode: 'open' })
    this.shadow.innerHTML = `
      <style>
        .list-header { display: flex; justify-content: space-between; }
        .count { opacity: 0.5; font-size: 14px; }
      </style>
      <div class="list-header">
        <slot name="heading"></slot>
        <span class="count"></span>
      </div>
      <slot></slot>
    `
    this.countEl = this.shadow.querySelector('.count')!
  }

  connectedCallback() {
    const defaultSlot = this.shadow.querySelector('slot:not([name])')!

    // Слушаем изменение контента в дефолтном слоте
    defaultSlot.addEventListener('slotchange', () => {
      const items = (defaultSlot as HTMLSlotElement).assignedElements()
      this.countEl.textContent = `${items.length} элементов`
    })
  }
}

Программный доступ к слотам

// Получить все элементы в слоте
const slot = this.shadow.querySelector('slot[name="items"]') as HTMLSlotElement
const assigned = slot.assignedElements()
// или вместе с текстовыми узлами:
const nodes = slot.assignedNodes({ flatten: true })

// Проверить, заполнен ли слот
const hasContent = slot.assignedElements().length > 0
this.shadow.querySelector('.footer')?.toggleAttribute('hidden', !hasContent)

Template с id и data-атрибутами

Шаблоны для списков, где каждый элемент клонируется с данными:

// Рендер списка через template
function renderProductList(products: Product[], container: HTMLElement) {
  const template = document.getElementById('product-item-tpl') as HTMLTemplateElement

  // DocumentFragment для батч-вставки
  const fragment = document.createDocumentFragment()

  products.forEach((product) => {
    const clone = template.content.cloneNode(true) as DocumentFragment

    ;(clone.querySelector('.product-name') as HTMLElement).textContent = product.name
    ;(clone.querySelector('.product-price') as HTMLElement).textContent =
      new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' })
        .format(product.price)
    ;(clone.querySelector('.product-img') as HTMLImageElement).src = product.image

    const addBtn = clone.querySelector('.add-to-cart') as HTMLButtonElement
    addBtn.dataset.id = String(product.id)

    fragment.appendChild(clone)
  })

  container.innerHTML = ''
  container.appendChild(fragment)  // Один DOM insert
}

Сроки

Пара компонентов с templates и slots — 1 день. Полная компонентная система (5–8 компонентов) с программным доступом к слотам, slotchange-обработчиками и документацией — 1 неделя.