Реалізація 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 — використовується програмне створення:

// Створення 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('uk-UA', { style: 'currency', currency: 'UAH' })
        .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 тиждень.