本章将深入探讨自定义元素的高级特性,包括表单集成、扩展原生元素、生命周期管理等主题。
class MyInput extends HTMLElement {
static get formAssociated() {
return true;
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._value = '';
this._internals = this.attachInternals();
}
connectedCallback() {
this.render();
this.setupEvents();
}
render() {
this.shadowRoot.innerHTML = `
<style>
input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
input:focus {
outline: none;
border-color: #007bff;
}
.error {
color: red;
font-size: 12px;
margin-top: 4px;
}
</style>
<input type="text">
<div class="error"></div>
`;
}
setupEvents() {
const input = this.shadowRoot.querySelector('input');
input.addEventListener('input', (e) => {
this._value = e.target.value;
this._internals.setFormValue(this._value);
this.dispatchEvent(new Event('input', { bubbles: true }));
});
input.addEventListener('focus', () => {
this.dispatchEvent(new Event('focus', { bubbles: true }));
});
input.addEventListener('blur', () => {
this.dispatchEvent(new Event('blur', { bubbles: true }));
});
}
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.shadowRoot.querySelector('input').value = val;
this._internals.setFormValue(val);
}
get name() {
return this.getAttribute('name');
}
get form() {
return this._internals.form;
}
get validity() {
return this._internals.validity;
}
get validationMessage() {
return this._internals.validationMessage;
}
checkValidity() {
return this._internals.checkValidity();
}
reportValidity() {
return this._internals.reportValidity();
}
setCustomValidity(message) {
const errorDiv = this.shadowRoot.querySelector('.error');
if (message) {
this._internals.setValidity({ customError: true }, message);
errorDiv.textContent = message;
} else {
this._internals.setValidity({});
errorDiv.textContent = '';
}
}
formDisabledCallback(disabled) {
this.shadowRoot.querySelector('input').disabled = disabled;
}
formResetCallback() {
this.value = this.getAttribute('value') || '';
}
formStateRestoreCallback(state) {
this.value = state;
}
}
customElements.define('my-input', MyInput);
<form id="myForm">
<label>用户名:</label>
<my-input name="username" required></my-input>
<label>邮箱:</label>
<my-input name="email"></my-input>
<button type="submit">提交</button>
</form>
<script>
const form = document.getElementById('myForm');
form.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(form);
console.log('表单数据:', Object.fromEntries(formData));
});
</script>
class FancyButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => {
this.ripple();
});
}
connectedCallback() {
this.style.position = 'relative';
this.style.overflow = 'hidden';
}
ripple() {
const ripple = document.createElement('span');
ripple.style.cssText = `
position: absolute;
background: rgba(255,255,255,0.4);
border-radius: 50%;
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
`;
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = (event.clientX - rect.left - size / 2) + 'px';
ripple.style.top = (event.clientY - rect.top - size / 2) + 'px';
this.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
}
}
customElements.define('fancy-button', FancyButton, { extends: 'button' });
<button is="fancy-button">点击我</button>
class ColorInput extends HTMLInputElement {
static get observedAttributes() {
return ['color', 'format'];
}
constructor() {
super();
this.type = 'text';
}
connectedCallback() {
this.addEventListener('input', this.validateColor.bind(this));
}
validateColor() {
const value = this.value;
const format = this.getAttribute('format') || 'hex';
let isValid = false;
switch (format) {
case 'hex':
isValid = /^#([0-9A-F]{3}){1,2}$/i.test(value);
break;
case 'rgb':
isValid = /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/.test(value);
break;
case 'hsl':
isValid = /^hsl\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\)$/.test(value);
break;
}
this.style.borderColor = isValid ? 'green' : 'red';
}
}
customElements.define('color-input', ColorInput, { extends: 'input' });
class ReflectElement extends HTMLElement {
static get observedAttributes() {
return ['value', 'disabled', 'checked'];
}
constructor() {
super();
this._value = null;
this._disabled = false;
this._checked = false;
}
get value() {
return this._value;
}
set value(val) {
this._value = val;
this._reflectAttribute('value', val);
}
get disabled() {
return this._disabled;
}
set disabled(val) {
this._disabled = Boolean(val);
this._reflectBooleanAttribute('disabled', val);
}
get checked() {
return this._checked;
}
set checked(val) {
this._checked = Boolean(val);
this._reflectBooleanAttribute('checked', val);
}
_reflectAttribute(name, value) {
if (value === null || value === undefined) {
this.removeAttribute(name);
} else {
this.setAttribute(name, value);
}
}
_reflectBooleanAttribute(name, value) {
if (value) {
this.setAttribute(name, '');
} else {
this.removeAttribute(name);
}
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'value':
this._value = newValue;
break;
case 'disabled':
this._disabled = newValue !== null;
break;
case 'checked':
this._checked = newValue !== null;
break;
}
this.render();
}
render() {
// 渲染逻辑
}
}
class CounterElement extends HTMLElement {
constructor() {
super();
this._count = 0;
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
increment() {
this._count++;
this.render();
this.emitChange();
}
decrement() {
this._count--;
this.render();
this.emitChange();
}
emitChange() {
this.dispatchEvent(new CustomEvent('count-change', {
detail: { count: this._count },
bubbles: true,
composed: true
}));
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-block; }
button { padding: 8px 16px; margin: 0 4px; }
span { font-size: 18px; margin: 0 8px; }
</style>
<button id="dec">-</button>
<span>${this._count}</span>
<button id="inc">+</button>
`;
this.shadowRoot.getElementById('inc')
.addEventListener('click', () => this.increment());
this.shadowRoot.getElementById('dec')
.addEventListener('click', () => this.decrement());
}
}
customElements.define('counter-element', CounterElement);
<counter-element id="counter"></counter-element>
<script>
document.getElementById('counter').addEventListener('count-change', (e) => {
console.log('计数变化:', e.detail.count);
});
</script>
const eventOptions = {
bubbles: {
description: '事件是否冒泡',
default: false
},
composed: {
description: '事件是否穿透Shadow DOM',
default: false
},
cancelable: {
description: '事件是否可取消',
default: false
}
};
class EventTest extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `<button id="btn">点击</button>`;
this.shadowRoot.getElementById('btn').addEventListener('click', (e) => {
const event = new CustomEvent('button-click', {
detail: { timestamp: Date.now() },
bubbles: true,
composed: true
});
this.dispatchEvent(event);
});
}
}
const templateCache = new WeakMap();
class OptimizedElement extends HTMLElement {
static get template() {
if (!templateCache.has(this)) {
const template = document.createElement('template');
template.innerHTML = `
<style>
:host { display: block; }
</style>
<div class="content">
<slot></slot>
</div>
`;
templateCache.set(this, template);
}
return templateCache.get(this);
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(
this.constructor.template.content.cloneNode(true)
);
}
}
customElements.whenDefined('my-element').then(() => {
console.log('my-element 已定义');
});
async function waitForElement(name) {
await customElements.whenDefined(name);
return customElements.get(name);
}
const MyElement = await waitForElement('my-element');
const element = document.createElement('my-element');
if (element instanceof HTMLElement) {
console.log('元素已升级');
}
customElements.upgrade(element);
const defined = customElements.get('my-element');
🔧 进阶技巧
- 表单集成:使用formAssociated让自定义元素参与表单
- 事件穿透:composed: true让事件穿透Shadow DOM
- 模板缓存:使用WeakMap缓存模板提高性能
- 延迟注册:whenDefined处理异步定义
⚠️ 注意事项
- 扩展原生元素需要{ extends: 'tagname' }
- Safari对自定义内置元素支持有限
- 属性反射要注意性能影响
下一章将探讨 [Shadow DOM深入](file:///e:/db-w.cn/md_data/javascript/83_Shadow DOM深入.md),学习Shadow DOM的更多高级特性。