GitHub использует Custom Elements повсеместно: <relative-time>, <details-dialog>, <clipboard-copy> — десятки компонентов в их дизайн-системе. YouTube, Salesforce, Google Maps — все они на Custom Elements. Это браузерный стандарт, работающий с React, Vue, Angular или без фреймворков.
Фреймворки создают компоненты, которые работают только внутри экосистемы. Custom Elements — браузерный стандарт: твой компонент работает в любом фреймворке или без него. Это критично для дизайн-систем, шаринга компонентов между командами, и для сред, где нет фреймворка.
connectedCallback вызывается при добавлении в DOMclass MyButton extends HTMLElement {
connectedCallback() {
// Вызывается когда элемент добавлен в DOM
this.innerHTML = '<button>Нажми меня</button>'
}
disconnectedCallback() {
// Вызывается при удалении из DOM — убираем слушатели!
console.log('Компонент удалён')
}
}
// Регистрируем — имя ДОЛЖНО содержать дефис
customElements.define('my-button', MyButton)
// Использование: <my-button></my-button>| Callback | Когда вызывается | Типичное использование |
|---|---|---|
| constructor | При создании экземпляра | Инициализация приватного состояния |
| connectedCallback | Добавлен в DOM | Рендеринг, подписка на события |
| disconnectedCallback | Удалён из DOM | Очистка таймеров, отписка |
| attributeChangedCallback | Атрибут изменился | Перерендеринг |
| adoptedCallback | Перенесён в другой документ | Редко нужен |
class UserCard extends HTMLElement {
// Список атрибутов для наблюдения
static get observedAttributes() {
return ['name', 'role']
}
attributeChangedCallback(name, oldValue, newValue) {
// Вызывается только для атрибутов из observedAttributes
console.log(`${name}: ${oldValue} → ${newValue}`)
if (this.isConnected) this.render()
}
render() {
const name = this.getAttribute('name') ?? 'Аноним'
const role = this.getAttribute('role') ?? 'user'
this.textContent = `${name} (${role})`
}
}
customElements.define('user-card', UserCard)
// <user-card name="Иван" role="admin"></user-card>В sandbox-среде симулируем жизненный цикл через базовый класс:
class BaseCustomElement {
constructor() {
this._attrs = {}
this._connected = false
this._innerHTML = ''
}
connect() {
this._connected = true
this.connectedCallback?.()
}
disconnect() {
this._connected = false
this.disconnectedCallback?.()
}
setAttribute(name, value) {
const old = this._attrs[name] ?? null
this._attrs[name] = String(value)
if (this.constructor.observedAttributes?.includes(name)) {
this.attributeChangedCallback?.(name, old, String(value))
}
}
getAttribute(name) {
return Object.prototype.hasOwnProperty.call(this._attrs, name)
? this._attrs[name]
: null
}
}Ошибка 1: Манипуляции с DOM в constructor
// НЕВЕРНО — элемент ещё не подключён к DOM
class Bad extends HTMLElement {
constructor() {
super()
this.innerHTML = '<p>Привет</p>' // Ошибка в некоторых браузерах!
}
}
// ВЕРНО — делай это в connectedCallback
class Good extends HTMLElement {
connectedCallback() {
this.innerHTML = '<p>Привет</p>'
}
}Ошибка 2: Забыть очистить ресурсы
class Timer extends HTMLElement {
connectedCallback() {
this._interval = setInterval(() => this.update(), 1000)
}
disconnectedCallback() {
clearInterval(this._interval) // Обязательно очищаем!
}
}Ошибка 3: Имя тега без дефиса
// НЕВЕРНО — браузер выбросит ошибку
customElements.define('mybutton', MyButton)
// ВЕРНО — обязательно наличие дефиса
customElements.define('my-button', MyButton)<ds-button>, <ds-input>, <ds-modal> — совместимы с любым фреймворком<relative-time>, <include-fragment>, <details-dialog>Симуляция Custom Elements: жизненный цикл, наблюдаемые атрибуты, компонент ProductCard с состоянием
// Симуляция Custom Elements (в браузере: class X extends HTMLElement)
// Базовый класс реализует жизненный цикл
class BaseCustomElement {
constructor() {
this._attrs = {}
this._connected = false
this._innerHTML = ''
}
connect() {
this._connected = true
this.connectedCallback?.()
}
disconnect() {
this._connected = false
this.disconnectedCallback?.()
}
setAttribute(name, value) {
const old = this._attrs[name] ?? null
this._attrs[name] = String(value)
const observed = this.constructor.observedAttributes
if (Array.isArray(observed) && observed.includes(name)) {
this.attributeChangedCallback?.(name, old, String(value))
}
}
getAttribute(name) {
return Object.prototype.hasOwnProperty.call(this._attrs, name)
? this._attrs[name]
: null
}
removeAttribute(name) {
const old = this._attrs[name] ?? null
delete this._attrs[name]
const observed = this.constructor.observedAttributes
if (Array.isArray(observed) && observed.includes(name)) {
this.attributeChangedCallback?.(name, old, null)
}
}
get isConnected() { return this._connected }
get innerHTML() { return this._innerHTML }
set innerHTML(v) { this._innerHTML = v }
}
// ===== ProductCard компонент =====
console.log('=== ProductCard ===')
class ProductCard extends BaseCustomElement {
static get observedAttributes() {
return ['name', 'price', 'currency', 'in-stock']
}
constructor() {
super()
this._renderCount = 0
console.log('[ProductCard] constructor')
}
connectedCallback() {
console.log('[ProductCard] connectedCallback')
// Значения по умолчанию
if (!this.getAttribute('currency')) this.setAttribute('currency', 'RUB')
this.render()
}
disconnectedCallback() {
console.log('[ProductCard] disconnectedCallback — очищаем ресурсы')
}
attributeChangedCallback(name, oldVal, newVal) {
console.log(` attr "${name}": ${JSON.stringify(oldVal)} → ${JSON.stringify(newVal)}`)
if (this._connected) this.render()
}
render() {
this._renderCount++
const name = this.getAttribute('name') ?? 'Товар'
const price = this.getAttribute('price') ?? '0'
const currency = this.getAttribute('currency') ?? 'RUB'
const inStock = this.getAttribute('in-stock') !== 'false'
this._innerHTML = `
<div class="product-card">
<h3>${name}</h3>
<div class="price">${parseFloat(price).toLocaleString('ru')} ${currency}</div>
<div class="stock ${inStock ? 'ok' : 'out'}">${inStock ? 'В наличии' : 'Нет в наличии'}</div>
</div>`
console.log(` [render #${this._renderCount}] ${name} | ${price} ${currency} | ${inStock ? 'в наличии' : 'нет'}`)
}
}
// --- Жизненный цикл ---
console.log('--- 1. Создание ---')
const card = new ProductCard()
console.log('\n--- 2. Подключение к DOM ---')
card.connect()
console.log('\n--- 3. Установка атрибутов ---')
card.setAttribute('name', 'Ноутбук Pro 15"')
card.setAttribute('price', '85000')
card.setAttribute('in-stock', 'true')
console.log('\n--- 4. Обновление цены ---')
card.setAttribute('price', '79000') // скидка!
console.log('\n--- 5. Товар закончился ---')
card.setAttribute('in-stock', 'false')
console.log('\n--- 6. Не наблюдаемый атрибут ---')
card.setAttribute('data-id', 'SKU-001') // attributeChangedCallback НЕ вызовется
console.log('\n--- 7. Чтение атрибутов ---')
console.log('name:', card.getAttribute('name'))
console.log('price:', card.getAttribute('price'))
console.log('data-id:', card.getAttribute('data-id')) // 'SKU-001' — хранится, но не отслеживается
console.log('\n--- 8. Отключение ---')
card.disconnect()
console.log('\n--- Итог ---')
console.log('Рендеров:', card._renderCount)
// ===== NotificationBadge компонент =====
console.log('\n=== NotificationBadge ===')
class NotificationBadge extends BaseCustomElement {
static get observedAttributes() { return ['count', 'max', 'hidden'] }
connectedCallback() {
if (!this.getAttribute('max')) this.setAttribute('max', '99')
this.render()
}
attributeChangedCallback(name, old, val) {
if (this._connected) this.render()
}
render() {
const count = parseInt(this.getAttribute('count') ?? '0')
const max = parseInt(this.getAttribute('max') ?? '99')
const hidden = this.getAttribute('hidden') === 'true'
const display = hidden ? '' : (count > max ? `${max}+` : String(count))
console.log(` Badge: ${hidden ? 'скрыт' : display || '0'}`)
this._innerHTML = `<span class="badge ${hidden ? 'hidden' : ''}">${display}</span>`
}
}
const badge = new NotificationBadge()
badge.connect()
badge.setAttribute('count', '5') // Badge: 5
badge.setAttribute('count', '42') // Badge: 42
badge.setAttribute('count', '150') // Badge: 99+
badge.setAttribute('hidden', 'true') // Badge: скрыт
badge.setAttribute('hidden', 'false') // Badge: 99+GitHub использует Custom Elements повсеместно: <relative-time>, <details-dialog>, <clipboard-copy> — десятки компонентов в их дизайн-системе. YouTube, Salesforce, Google Maps — все они на Custom Elements. Это браузерный стандарт, работающий с React, Vue, Angular или без фреймворков.
Фреймворки создают компоненты, которые работают только внутри экосистемы. Custom Elements — браузерный стандарт: твой компонент работает в любом фреймворке или без него. Это критично для дизайн-систем, шаринга компонентов между командами, и для сред, где нет фреймворка.
connectedCallback вызывается при добавлении в DOMclass MyButton extends HTMLElement {
connectedCallback() {
// Вызывается когда элемент добавлен в DOM
this.innerHTML = '<button>Нажми меня</button>'
}
disconnectedCallback() {
// Вызывается при удалении из DOM — убираем слушатели!
console.log('Компонент удалён')
}
}
// Регистрируем — имя ДОЛЖНО содержать дефис
customElements.define('my-button', MyButton)
// Использование: <my-button></my-button>| Callback | Когда вызывается | Типичное использование |
|---|---|---|
| constructor | При создании экземпляра | Инициализация приватного состояния |
| connectedCallback | Добавлен в DOM | Рендеринг, подписка на события |
| disconnectedCallback | Удалён из DOM | Очистка таймеров, отписка |
| attributeChangedCallback | Атрибут изменился | Перерендеринг |
| adoptedCallback | Перенесён в другой документ | Редко нужен |
class UserCard extends HTMLElement {
// Список атрибутов для наблюдения
static get observedAttributes() {
return ['name', 'role']
}
attributeChangedCallback(name, oldValue, newValue) {
// Вызывается только для атрибутов из observedAttributes
console.log(`${name}: ${oldValue} → ${newValue}`)
if (this.isConnected) this.render()
}
render() {
const name = this.getAttribute('name') ?? 'Аноним'
const role = this.getAttribute('role') ?? 'user'
this.textContent = `${name} (${role})`
}
}
customElements.define('user-card', UserCard)
// <user-card name="Иван" role="admin"></user-card>В sandbox-среде симулируем жизненный цикл через базовый класс:
class BaseCustomElement {
constructor() {
this._attrs = {}
this._connected = false
this._innerHTML = ''
}
connect() {
this._connected = true
this.connectedCallback?.()
}
disconnect() {
this._connected = false
this.disconnectedCallback?.()
}
setAttribute(name, value) {
const old = this._attrs[name] ?? null
this._attrs[name] = String(value)
if (this.constructor.observedAttributes?.includes(name)) {
this.attributeChangedCallback?.(name, old, String(value))
}
}
getAttribute(name) {
return Object.prototype.hasOwnProperty.call(this._attrs, name)
? this._attrs[name]
: null
}
}Ошибка 1: Манипуляции с DOM в constructor
// НЕВЕРНО — элемент ещё не подключён к DOM
class Bad extends HTMLElement {
constructor() {
super()
this.innerHTML = '<p>Привет</p>' // Ошибка в некоторых браузерах!
}
}
// ВЕРНО — делай это в connectedCallback
class Good extends HTMLElement {
connectedCallback() {
this.innerHTML = '<p>Привет</p>'
}
}Ошибка 2: Забыть очистить ресурсы
class Timer extends HTMLElement {
connectedCallback() {
this._interval = setInterval(() => this.update(), 1000)
}
disconnectedCallback() {
clearInterval(this._interval) // Обязательно очищаем!
}
}Ошибка 3: Имя тега без дефиса
// НЕВЕРНО — браузер выбросит ошибку
customElements.define('mybutton', MyButton)
// ВЕРНО — обязательно наличие дефиса
customElements.define('my-button', MyButton)<ds-button>, <ds-input>, <ds-modal> — совместимы с любым фреймворком<relative-time>, <include-fragment>, <details-dialog>Симуляция Custom Elements: жизненный цикл, наблюдаемые атрибуты, компонент ProductCard с состоянием
// Симуляция Custom Elements (в браузере: class X extends HTMLElement)
// Базовый класс реализует жизненный цикл
class BaseCustomElement {
constructor() {
this._attrs = {}
this._connected = false
this._innerHTML = ''
}
connect() {
this._connected = true
this.connectedCallback?.()
}
disconnect() {
this._connected = false
this.disconnectedCallback?.()
}
setAttribute(name, value) {
const old = this._attrs[name] ?? null
this._attrs[name] = String(value)
const observed = this.constructor.observedAttributes
if (Array.isArray(observed) && observed.includes(name)) {
this.attributeChangedCallback?.(name, old, String(value))
}
}
getAttribute(name) {
return Object.prototype.hasOwnProperty.call(this._attrs, name)
? this._attrs[name]
: null
}
removeAttribute(name) {
const old = this._attrs[name] ?? null
delete this._attrs[name]
const observed = this.constructor.observedAttributes
if (Array.isArray(observed) && observed.includes(name)) {
this.attributeChangedCallback?.(name, old, null)
}
}
get isConnected() { return this._connected }
get innerHTML() { return this._innerHTML }
set innerHTML(v) { this._innerHTML = v }
}
// ===== ProductCard компонент =====
console.log('=== ProductCard ===')
class ProductCard extends BaseCustomElement {
static get observedAttributes() {
return ['name', 'price', 'currency', 'in-stock']
}
constructor() {
super()
this._renderCount = 0
console.log('[ProductCard] constructor')
}
connectedCallback() {
console.log('[ProductCard] connectedCallback')
// Значения по умолчанию
if (!this.getAttribute('currency')) this.setAttribute('currency', 'RUB')
this.render()
}
disconnectedCallback() {
console.log('[ProductCard] disconnectedCallback — очищаем ресурсы')
}
attributeChangedCallback(name, oldVal, newVal) {
console.log(` attr "${name}": ${JSON.stringify(oldVal)} → ${JSON.stringify(newVal)}`)
if (this._connected) this.render()
}
render() {
this._renderCount++
const name = this.getAttribute('name') ?? 'Товар'
const price = this.getAttribute('price') ?? '0'
const currency = this.getAttribute('currency') ?? 'RUB'
const inStock = this.getAttribute('in-stock') !== 'false'
this._innerHTML = `
<div class="product-card">
<h3>${name}</h3>
<div class="price">${parseFloat(price).toLocaleString('ru')} ${currency}</div>
<div class="stock ${inStock ? 'ok' : 'out'}">${inStock ? 'В наличии' : 'Нет в наличии'}</div>
</div>`
console.log(` [render #${this._renderCount}] ${name} | ${price} ${currency} | ${inStock ? 'в наличии' : 'нет'}`)
}
}
// --- Жизненный цикл ---
console.log('--- 1. Создание ---')
const card = new ProductCard()
console.log('\n--- 2. Подключение к DOM ---')
card.connect()
console.log('\n--- 3. Установка атрибутов ---')
card.setAttribute('name', 'Ноутбук Pro 15"')
card.setAttribute('price', '85000')
card.setAttribute('in-stock', 'true')
console.log('\n--- 4. Обновление цены ---')
card.setAttribute('price', '79000') // скидка!
console.log('\n--- 5. Товар закончился ---')
card.setAttribute('in-stock', 'false')
console.log('\n--- 6. Не наблюдаемый атрибут ---')
card.setAttribute('data-id', 'SKU-001') // attributeChangedCallback НЕ вызовется
console.log('\n--- 7. Чтение атрибутов ---')
console.log('name:', card.getAttribute('name'))
console.log('price:', card.getAttribute('price'))
console.log('data-id:', card.getAttribute('data-id')) // 'SKU-001' — хранится, но не отслеживается
console.log('\n--- 8. Отключение ---')
card.disconnect()
console.log('\n--- Итог ---')
console.log('Рендеров:', card._renderCount)
// ===== NotificationBadge компонент =====
console.log('\n=== NotificationBadge ===')
class NotificationBadge extends BaseCustomElement {
static get observedAttributes() { return ['count', 'max', 'hidden'] }
connectedCallback() {
if (!this.getAttribute('max')) this.setAttribute('max', '99')
this.render()
}
attributeChangedCallback(name, old, val) {
if (this._connected) this.render()
}
render() {
const count = parseInt(this.getAttribute('count') ?? '0')
const max = parseInt(this.getAttribute('max') ?? '99')
const hidden = this.getAttribute('hidden') === 'true'
const display = hidden ? '' : (count > max ? `${max}+` : String(count))
console.log(` Badge: ${hidden ? 'скрыт' : display || '0'}`)
this._innerHTML = `<span class="badge ${hidden ? 'hidden' : ''}">${display}</span>`
}
}
const badge = new NotificationBadge()
badge.connect()
badge.setAttribute('count', '5') // Badge: 5
badge.setAttribute('count', '42') // Badge: 42
badge.setAttribute('count', '150') // Badge: 99+
badge.setAttribute('hidden', 'true') // Badge: скрыт
badge.setAttribute('hidden', 'false') // Badge: 99+Используя базовый класс `BaseCustomElement` из примера, реализуй компонент `CounterElement`. Требования: - Атрибуты: `count` (текущее значение), `step` (шаг, по умолчанию 1), `min` и `max` (ограничения, необязательные) - Методы: `increment()`, `decrement()`, `reset()` - При изменении `count` вызывает `render()`, который логирует состояние - Соблюдает границы min/max при increment/decrement
observedAttributes: ["count", "step", "min", "max"]. increment: next = count + step, clamped = max ? Math.min(next, parseInt(max)) : next. decrement: next = count - step, clamped = min ? Math.max(next, parseInt(min)) : next. reset: setAttribute("count", min ?? "0")