องค์ประกอบที่กําหนดเองช่วยให้นักพัฒนาเว็บกําหนดแท็ก HTML ใหม่ ขยายแท็กที่มีอยู่ และสร้างคอมโพเนนต์เว็บที่นำกลับมาใช้ซ้ำได้
การใช้องค์ประกอบที่กำหนดเองช่วยให้นักพัฒนาเว็บสร้างแท็ก HTML ใหม่ เพิ่มแท็ก HTML ที่มีอยู่ หรือขยายคอมโพเนนต์ที่นักพัฒนาซอฟต์แวร์รายอื่นสร้างขึ้นได้ API เป็นรากฐานของคอมโพเนนต์เว็บ ซึ่งใช้แนวทางตามมาตรฐานเว็บในการสร้างคอมโพเนนต์ที่นํากลับมาใช้ซ้ำได้โดยใช้ JS/HTML/CSS พื้นฐานเท่านั้น ผลลัพธ์ที่ได้คือโค้ดน้อยลง โค้ดแบบโมดูล และการนำกลับมาใช้ซ้ำมากขึ้นในแอปของเรา
บทนำ
เบราว์เซอร์นี้มีเครื่องมือที่ยอดเยี่ยมสำหรับการวางโครงสร้างเว็บแอปพลิเคชัน ภาษานี้เรียกว่า HTML คุณอาจเคยได้ยินเกี่ยวกับเรื่องนี้ รูปแบบนี้เป็นแบบประกาศ พกพาได้ ได้รับการรองรับอย่างดี และใช้งานง่าย แม้ว่า HTML จะยอดเยี่ยมเพียงใด แต่คลังคำศัพท์และความสามารถในการขยายก็ยังมีข้อจำกัด มาตรฐาน HTML ฉบับปรับปรุงไม่เคยมีวิธีเชื่อมโยงลักษณะการทำงาน JS กับมาร์กอัปโดยอัตโนมัติมาก่อน… จนกระทั่งตอนนี้
องค์ประกอบที่กําหนดเองคือคําตอบในการทำให้ HTML ทันสมัย เติมเต็มส่วนที่ขาดหายไป และรวมโครงสร้างเข้ากับลักษณะการทํางาน หาก HTML ไม่สามารถแก้ปัญหาได้ เราสามารถสร้างองค์ประกอบที่กำหนดเองซึ่งแก้ปัญหาได้ องค์ประกอบที่กําหนดเองจะสอนเทคนิคใหม่ๆ แก่เบราว์เซอร์ไปพร้อมกับรักษาประโยชน์ของ HTML
การกําหนดองค์ประกอบใหม่
หากต้องการกำหนดองค์ประกอบ HTML ใหม่ เราต้องใช้ JavaScript
customElements
ส่วนกลางใช้เพื่อกำหนดองค์ประกอบที่กำหนดเองและสอนเบราว์เซอร์เกี่ยวกับแท็กใหม่ เรียกใช้ customElements.define()
ด้วยชื่อแท็กที่ต้องการสร้างและ JavaScript class
ที่ขยาย HTMLElement
พื้นฐาน
ตัวอย่าง - การกำหนดแผงลิ้นชักอุปกรณ์เคลื่อนที่ <app-drawer>
:
class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);
// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});
ตัวอย่างการใช้งาน
<app-drawer></app-drawer>
โปรดทราบว่าการใช้องค์ประกอบที่กำหนดเองนั้นไม่แตกต่างจากการใช้ <div>
หรือองค์ประกอบอื่นๆ คุณสามารถประกาศอินสแตนซ์ในหน้าเว็บ สร้างแบบไดนามิกใน JavaScript แนบโปรแกรมรับฟังเหตุการณ์ และอื่นๆ โปรดอ่านต่อเพื่อดูตัวอย่างเพิ่มเติม
การกําหนด JavaScript API ขององค์ประกอบ
ฟังก์ชันการทำงานขององค์ประกอบที่กําหนดเองจะกําหนดโดยใช้ ES2015
class
ซึ่งขยาย HTMLElement
การขยาย HTMLElement
จะช่วยให้มั่นใจได้ว่าองค์ประกอบที่กำหนดเองจะรับค่า DOM API ทั้งหมด และหมายความว่าพร็อพเพอร์ตี้/เมธอดใดๆ ที่คุณเพิ่มลงในคลาสจะกลายเป็นส่วนหนึ่งของอินเทอร์เฟซ DOM ขององค์ประกอบ โดยพื้นฐานแล้ว ให้ใช้คลาสเพื่อสร้าง JavaScript API สาธารณะสําหรับแท็ก
ตัวอย่าง - การกําหนดอินเทอร์เฟซ DOM ของ <app-drawer>
class AppDrawer extends HTMLElement {
// A getter/setter for an open property.
get open() {
return this.hasAttribute('open');
}
set open(val) {
// Reflect the value of the open property as an HTML attribute.
if (val) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
this.toggleDrawer();
}
// A getter/setter for a disabled property.
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
// Reflect the value of the disabled property as an HTML attribute.
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
// Can define constructor arguments if you wish.
constructor() {
// If you define a constructor, always call super() first!
// This is specific to CE and required by the spec.
super();
// Setup a click listener on <app-drawer> itself.
this.addEventListener('click', e => {
// Don't toggle the drawer if it's disabled.
if (this.disabled) {
return;
}
this.toggleDrawer();
});
}
toggleDrawer() {
// ...
}
}
customElements.define('app-drawer', AppDrawer);
ในตัวอย่างนี้ เราจะสร้างลิ้นชักที่มีพร็อพเพอร์ตี้ open
, disabled
และเมธอด toggleDrawer()
และยังแสดงพร็อพเพอร์ตี้เป็นแอตทริบิวต์ HTML ด้วย
ฟีเจอร์ที่น่าสนใจขององค์ประกอบที่กําหนดเองคือ this
ภายในคําจํากัดความของคลาสจะอ้างอิงถึงองค์ประกอบ DOM นั้นๆ กล่าวคืออินสแตนซ์ของคลาส ในตัวอย่างของเรา this
หมายถึง <app-drawer>
สิ่งนี้ (😉) เป็นวิธีที่องค์ประกอบจะแนบ click
Listener กับตนเองได้ และคุณไม่จํากัดเพียง Listener เหตุการณ์
DOM API ทั้งหมดมีอยู่ในโค้ดองค์ประกอบ ใช้ this
เพื่อเข้าถึงพร็อพเพอร์ตี้ขององค์ประกอบ ตรวจสอบรายการย่อย (this.children
) โหนดการค้นหา (this.querySelectorAll('.items')
) ฯลฯ
กฎในการสร้างองค์ประกอบที่กำหนดเอง
- ชื่อขององค์ประกอบที่กำหนดเองต้องมีขีดกลาง (-) ดังนั้น
<x-tags>
,<my-element>
และ<my-awesome-app>
จึงเป็นชื่อที่ถูกต้องทั้งหมด ส่วน<tabs>
และ<foo_bar>
นั้นไม่ถูกต้อง ข้อกำหนดนี้เพื่อให้โปรแกรมแยกวิเคราะห์ HTML สามารถแยกแยะองค์ประกอบที่กำหนดเองจากองค์ประกอบปกติได้ และยังรับประกันความเข้ากันได้แบบส่งต่อเมื่อมีการเพิ่มแท็กใหม่ใน HTML - คุณจดทะเบียนแท็กเดียวกันได้ไม่เกิน 1 ครั้ง การพยายามทำเช่นนั้น
จะแสดงข้อผิดพลาด
DOMException
เมื่อแจ้งเบราว์เซอร์เกี่ยวกับแท็กใหม่แล้ว ก็เสร็จสิ้น ไม่มีการดึงเงินคืน - องค์ประกอบที่กำหนดเองต้องปิดเองไม่ได้ เนื่องจาก HTML อนุญาตให้องค์ประกอบเพียงไม่กี่รายการเท่านั้นที่ปิดเองได้ เขียนแท็กปิดเสมอ
(
<app-drawer></app-drawer>
)
ความรู้สึกขององค์ประกอบที่กำหนดเอง
องค์ประกอบที่กําหนดเองสามารถกําหนดฮุกวงจรการทํางานพิเศษสําหรับการเรียกใช้โค้ดในช่วงเวลาที่น่าสนใจขององค์ประกอบ ซึ่งเรียกว่ารีแอ็กชัน ขององค์ประกอบที่กำหนดเอง
ชื่อ | เรียกใช้เมื่อ |
---|---|
constructor |
สร้างหรืออัปเกรดอินสแตนซ์ขององค์ประกอบ มีประโยชน์สำหรับการเริ่มต้นสถานะ ตั้งค่า Listener เหตุการณ์ หรือการสร้างขอบเขตเงา
ดูข้อจำกัดเกี่ยวกับสิ่งที่คุณทำได้ใน constructor ได้ที่ข้อกำหนดทางเทคนิค
|
connectedCallback |
มีการเรียกใช้ทุกครั้งที่มีการแทรกองค์ประกอบลงใน DOM มีประโยชน์สำหรับการเรียกใช้โค้ดการตั้งค่า เช่น การดึงข้อมูลทรัพยากรหรือการเรนเดอร์ โดยทั่วไปคุณควรพยายามเลื่อนเวลางานออกไปจนกว่าจะถึงเวลานี้ |
disconnectedCallback |
เรียกใช้ทุกครั้งที่นําองค์ประกอบออกจาก DOM มีประโยชน์สําหรับการเรียกใช้โค้ดล้าง |
attributeChangedCallback(attrName, oldVal, newVal) |
เรียกใช้เมื่อมีการเพิ่ม นําออก อัปเดต หรือแทนที่แอตทริบิวต์ที่สังเกตได้ หรือเรียกอีกอย่างว่าค่าเริ่มต้นเมื่อองค์ประกอบสร้างขึ้นโดยโปรแกรมแยกวิเคราะห์หรืออัปเกรด หมายเหตุ: เฉพาะแอตทริบิวต์ที่ระบุไว้ในพร็อพเพอร์ตี้ observedAttributes เท่านั้นที่จะได้รับ Callback นี้
|
adoptedCallback |
ระบบได้ย้ายองค์ประกอบที่กำหนดเองไปยัง document ใหม่แล้ว (เช่น บุคคลที่ชื่อ document.adoptNode(el) )
|
Callback ของรีแอ็กชันจะเป็นแบบซิงโครนัส หากมีผู้เรียก el.setAttribute()
ในองค์ประกอบของคุณ เบราว์เซอร์จะเรียก attributeChangedCallback()
ทันที
ในทํานองเดียวกัน คุณจะได้รับ disconnectedCallback()
ทันทีที่นําองค์ประกอบออกจาก DOM (เช่น ผู้ใช้เรียก el.remove()
)
ตัวอย่าง: การเพิ่มรีแอ็กชันองค์ประกอบที่กำหนดเองให้กับ <app-drawer>
class AppDrawer extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// ...
}
connectedCallback() {
// ...
}
disconnectedCallback() {
// ...
}
attributeChangedCallback(attrName, oldVal, newVal) {
// ...
}
}
กำหนดความรู้สึกหาก/เมื่อเหมาะสม หากองค์ประกอบมีความซับซ้อนมากพอและเปิดการเชื่อมต่อกับ IndexedDB ใน connectedCallback()
ให้ทําการล้างข้อมูลที่จําเป็นใน disconnectedCallback()
แต่โปรดระมัดระวัง คุณไม่สามารถคาดหวังได้ว่าองค์ประกอบจะถูกนําออกจาก DOM ในทุกกรณี ตัวอย่างเช่น ระบบจะไม่เรียกใช้ disconnectedCallback()
เลยหากผู้ใช้ปิดแท็บ
พร็อพเพอร์ตี้และแอตทริบิวต์
การสะท้อนพร็อพเพอร์ตี้ไปยังแอตทริบิวต์
เป็นเรื่องปกติที่พร็อพเพอร์ตี้ HTML จะแสดงค่ากลับไปยัง DOM เป็นแอตทริบิวต์ HTML เช่น เมื่อมีการเปลี่ยนแปลงค่าของ hidden
หรือ id
ใน JS
div.id = 'my-id';
div.hidden = true;
ค่าจะใช้กับ DOM แบบสดเป็นแอตทริบิวต์ดังนี้
<div id="my-id" hidden>
ซึ่งเรียกว่า "การสะท้อนพร็อพเพอร์ตี้ไปยังแอตทริบิวต์" พร็อพเพอร์ตี้เกือบทุกรายการใน HTML มีลักษณะการทำงานเช่นนี้ เหตุผล แอตทริบิวต์ยังมีประโยชน์สำหรับการกำหนดค่าองค์ประกอบแบบชัดเจน และ API บางอย่าง เช่น การช่วยเหลือพิเศษและตัวเลือก CSS จะอาศัยแอตทริบิวต์ในการทำงาน
การแสดงพร็อพเพอร์ตี้มีประโยชน์ในทุกที่ที่คุณต้องการทำให้การแสดง DOM ขององค์ประกอบซิงค์กับสถานะ JavaScript ขององค์ประกอบ เหตุผลหนึ่งที่คุณอาจต้องการแสดงพร็อพเพอร์ตี้คือเพื่อให้ใช้การจัดสไตล์ที่ผู้ใช้กำหนดได้เมื่อสถานะ JS เปลี่ยนแปลง
โปรดอ่าน<app-drawer>
ผู้ใช้คอมโพเนนต์นี้อาจต้องการทำให้คอมโพเนนต์ค่อยๆ หายไป และ/หรือป้องกันไม่ให้ผู้ใช้โต้ตอบเมื่อปิดใช้คอมโพเนนต์
app-drawer[disabled] {
opacity: 0.5;
pointer-events: none;
}
เมื่อพร็อพเพอร์ตี้ disabled
มีการเปลี่ยนแปลงใน JS เราต้องการให้เพิ่มแอตทริบิวต์นั้นลงใน DOM เพื่อให้ตัวเลือกของผู้ใช้ตรงกัน องค์ประกอบสามารถแสดงลักษณะการทำงานดังกล่าวได้โดยแสดงค่าในแอตทริบิวต์ที่มีชื่อเดียวกัน ดังนี้
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
// Reflect the value of `disabled` as an attribute.
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
this.toggleDrawer();
}
การสังเกตการเปลี่ยนแปลงแอตทริบิวต์
แอตทริบิวต์ HTML เป็นวิธีที่สะดวกสำหรับผู้ใช้ในการประกาศสถานะเริ่มต้น
<app-drawer open disabled></app-drawer>
องค์ประกอบสามารถตอบสนองต่อการเปลี่ยนแปลงแอตทริบิวต์ได้โดยการกำหนด attributeChangedCallback
เบราว์เซอร์จะเรียกใช้เมธอดนี้สําหรับการเปลี่ยนแปลงแอตทริบิวต์ทั้งหมดที่แสดงในอาร์เรย์ observedAttributes
class AppDrawer extends HTMLElement {
// ...
static get observedAttributes() {
return ['disabled', 'open'];
}
get disabled() {
return this.hasAttribute('disabled');
}
set disabled(val) {
if (val) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
}
// Only called for the disabled and open attributes due to observedAttributes
attributeChangedCallback(name, oldValue, newValue) {
// When the drawer is disabled, update keyboard/screen reader behavior.
if (this.disabled) {
this.setAttribute('tabindex', '-1');
this.setAttribute('aria-disabled', 'true');
} else {
this.setAttribute('tabindex', '0');
this.setAttribute('aria-disabled', 'false');
}
// TODO: also react to the open attribute changing.
}
}
ในตัวอย่างนี้ เรากําลังตั้งค่าแอตทริบิวต์เพิ่มเติมใน <app-drawer>
เมื่อแอตทริบิวต์ disabled
มีการเปลี่ยนแปลง แม้ว่าเราจะไม่ได้ดำเนินการในส่วนนี้ แต่คุณก็สามารถใช้ attributeChangedCallback
เพื่อทำให้พร็อพเพอร์ตี้ JS ซิงค์กับแอตทริบิวต์ของพร็อพเพอร์ตี้อยู่เสมอได้
การอัปเกรดองค์ประกอบ
HTML ที่ปรับปรุงแบบต่อเนื่อง
เราทราบแล้วว่าองค์ประกอบที่กำหนดเองจะกำหนดโดยการเรียกใช้ customElements.define()
แต่ก็ไม่ได้หมายความว่าคุณต้องกําหนด + ลงทะเบียนองค์ประกอบที่กําหนดเองทั้งหมดในครั้งเดียว
องค์ประกอบที่กำหนดเองสามารถใช้ได้ก่อนการบันทึกคำจำกัดความ
การปรับปรุงแบบเป็นขั้นเป็นฟีเจอร์ขององค์ประกอบที่กําหนดเอง กล่าวคือ คุณสามารถประกาศองค์ประกอบ <app-drawer>
จำนวนมากในหน้าเว็บและไม่เคยเรียกใช้ customElements.define('app-drawer', ...)
เลยจนกว่าจะถึงเวลาต่อมา เนื่องจากเบราว์เซอร์จะจัดการองค์ประกอบที่กำหนดเองที่เป็นไปได้แตกต่างกันไปเนื่องจากแท็กที่ไม่รู้จัก กระบวนการเรียก define()
และมอบหมายองค์ประกอบที่มีอยู่ให้กับคําจํากัดความของคลาสเรียกว่า "การอัปเกรดองค์ประกอบ"
หากต้องการทราบว่าชื่อแท็กได้รับการกําหนดแล้วหรือไม่ คุณสามารถใช้ window.customElements.whenDefined()
โดยจะแสดงผล "คำสัญญา" ที่ได้รับการแก้ไขเมื่อกำหนดองค์ประกอบแล้ว
customElements.whenDefined('app-drawer').then(() => {
console.log('app-drawer defined');
});
ตัวอย่าง - เลื่อนเวลาการทํางานจนกว่าชุดองค์ประกอบย่อยจะได้รับการอัปเกรด
<share-buttons>
<social-button type="twitter"><a href="...">Twitter</a></social-button>
<social-button type="fb"><a href="...">Facebook</a></social-button>
<social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');
let promises = [...undefinedButtons].map((socialButton) => {
return customElements.whenDefined(socialButton.localName);
});
// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
// All social-button children are ready.
});
เนื้อหาที่กําหนดโดยองค์ประกอบ
องค์ประกอบที่กำหนดเองจัดการเนื้อหาของตนเองได้โดยใช้ DOM API ภายในโค้ดองค์ประกอบ ความรู้สึกมีประโยชน์ในกรณีนี้
ตัวอย่าง - สร้างองค์ประกอบที่มี HTML เริ่มต้นบางส่วน
customElements.define('x-foo-with-markup', class extends HTMLElement {
connectedCallback() {
this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
}
// ...
});
การประกาศแท็กนี้จะสร้างสิ่งต่อไปนี้
<x-foo-with-markup>
<b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>
// สิ่งที่ต้องทำ: DevSite - นำตัวอย่างโค้ดออกเนื่องจากใช้เครื่องจัดการเหตุการณ์ในบรรทัด
การสร้างองค์ประกอบที่ใช้ Shadow DOM
Shadow DOM เป็นวิธีที่ช่วยให้องค์ประกอบเป็นเจ้าของ แสดงผล และจัดสไตล์กลุ่ม DOM ที่แยกจากส่วนที่เหลือของหน้า คุณยังซ่อนแอปทั้งแอปไว้ในแท็กเดียวได้ด้วย
<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>
หากต้องการใช้ Shadow DOM ในองค์ประกอบที่กําหนดเอง ให้เรียก this.attachShadow
ภายใน constructor
let tmpl = document.createElement('template');
tmpl.innerHTML = `
<style>:host { ... }</style> <!-- look ma, scoped styles -->
<b>I'm in shadow dom!</b>
<slot></slot>
`;
customElements.define('x-foo-shadowdom', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to the element.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
// ...
});
ตัวอย่างการใช้งาน
<x-foo-shadowdom>
<p><b>User's</b> custom text</p>
</x-foo-shadowdom>
<!-- renders as -->
<x-foo-shadowdom>
#shadow-root
<b>I'm in shadow dom!</b>
<slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>
ข้อความที่กําหนดเองของผู้ใช้
// TODO: DevSite - Code sample removed as it used inline event handlers
การสร้างองค์ประกอบจาก <template>
<template>
องค์ประกอบ
ช่วยให้คุณประกาศข้อมูลโค้ด DOM ที่แยกวิเคราะห์แล้ว ไม่มีการทํางานเมื่อโหลดหน้าเว็บ และสามารถเปิดใช้งานในภายหลังเมื่อรันไทม์ได้ ซึ่งเป็น API พื้นฐานอีกรายการในตระกูล Web Components เทมเพลตเป็นตัวยึดตำแหน่งที่เหมาะสำหรับการประกาศโครงสร้างขององค์ประกอบที่กำหนดเอง
ตัวอย่าง: การลงทะเบียนองค์ประกอบที่มีเนื้อหา Shadow DOM ที่สร้างขึ้นจาก <template>
<template id="x-foo-from-template">
<style>
p { color: green; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.</p>
</template>
<script>
let tmpl = document.querySelector('#x-foo-from-template');
// If your code is inside of an HTML Import you'll need to change the above line to:
// let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');
customElements.define('x-foo-from-template', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(tmpl.content.cloneNode(true));
}
// ...
});
</script>
โค้ดไม่กี่บรรทัดนี้มีประสิทธิภาพมาก มาทําความเข้าใจสิ่งสำคัญที่กำลังเกิดขึ้นกัน
- เรากําลังกําหนดเอลิเมนต์ใหม่ใน HTML:
<x-foo-from-template>
- Shadow DOM ขององค์ประกอบสร้างขึ้นจาก
<template>
- DOM ขององค์ประกอบจะอยู่ในองค์ประกอบนั้นๆ โดยตรงเนื่องจาก Shadow DOM
- CSS ภายในขององค์ประกอบมีขอบเขตอยู่ที่องค์ประกอบด้วย Shadow DOM
ฉันอยู่ใน Shadow DOM มาร์กอัปของฉันมีการประทับตราจาก <template>
// TODO: DevSite - Code sample removed as it used inline event handlers
จัดแต่งองค์ประกอบที่กำหนดเอง
แม้ว่าองค์ประกอบจะกำหนดการจัดรูปแบบของตัวเองโดยใช้ Shadow DOM แต่ผู้ใช้จะจัดรูปแบบองค์ประกอบที่กำหนดเองของคุณจากหน้าเว็บของตนได้ รูปแบบเหล่านี้เรียกว่า "สไตล์ที่ผู้ใช้กำหนด"
<!-- user-defined styling -->
<style>
app-drawer {
display: flex;
}
panel-item {
transition: opacity 400ms ease-in-out;
opacity: 0.3;
flex: 1;
text-align: center;
border-radius: 50%;
}
panel-item:hover {
opacity: 1.0;
background: rgb(255, 0, 255);
color: white;
}
app-panel > panel-item {
padding: 5px;
list-style: none;
margin: 0 7px;
}
</style>
<app-drawer>
<panel-item>Do</panel-item>
<panel-item>Re</panel-item>
<panel-item>Mi</panel-item>
</app-drawer>
คุณอาจสงสัยว่าความเฉพาะเจาะจงของ CSS ทำงานอย่างไรหากองค์ประกอบมีสไตล์ที่กําหนดภายใน Shadow DOM ในแง่ของความเฉพาะเจาะจง รูปแบบของผู้ใช้จะดีกว่า จะลบล้างการจัดรูปแบบที่องค์ประกอบกำหนดเสมอ ดูส่วนการสร้างองค์ประกอบที่ใช้ Shadow DOM
การจัดสไตล์องค์ประกอบที่ไม่ได้ลงทะเบียนล่วงหน้า
ก่อนที่องค์ประกอบจะอัปเกรด คุณสามารถกําหนดเป้าหมายองค์ประกอบนั้นใน CSS ได้โดยใช้พราซีคลาส :defined
ซึ่งมีประโยชน์สำหรับการกำหนดสไตล์คอมโพเนนต์ล่วงหน้า เช่น คุณอาจต้องการป้องกัน FOUC ของเลย์เอาต์หรือภาพอื่นๆ ด้วยการซ่อนองค์ประกอบที่ยังไม่ระบุและค่อย ๆ แสดงองค์ประกอบเหล่านั้นเมื่อมีการระบุ
ตัวอย่าง - ซ่อน <app-drawer>
ก่อนที่จะมีการกําหนด
app-drawer:not(:defined) {
/* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
display: inline-block;
height: 100vh;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
หลังจากกำหนด <app-drawer>
แล้ว ตัวเลือก (app-drawer:not(:defined)
)
ไม่ตรงกันอีกต่อไป
ขยายองค์ประกอบ
API องค์ประกอบที่กำหนดเองมีประโยชน์สำหรับการสร้างองค์ประกอบ HTML ใหม่ แต่ก็ยังมีประโยชน์สำหรับการขยายองค์ประกอบที่กำหนดเองอื่นๆ หรือแม้แต่ HTML ในตัวของเบราว์เซอร์ด้วย
การขยายองค์ประกอบที่กำหนดเอง
การขยายองค์ประกอบที่กำหนดเองรายการอื่นทำได้โดยการขยายคําจํากัดความของคลาส
ตัวอย่าง - สร้าง <fancy-app-drawer>
ที่ขยาย <app-drawer>
class FancyDrawer extends AppDrawer {
constructor() {
super(); // always call super() first in the constructor. This also calls the extended class' constructor.
// ...
}
toggleDrawer() {
// Possibly different toggle implementation?
// Use ES2015 if you need to call the parent method.
// super.toggleDrawer()
}
anotherMethod() {
// ...
}
}
customElements.define('fancy-app-drawer', FancyDrawer);
การขยายองค์ประกอบ HTML เนทีฟ
สมมติว่าคุณต้องการสร้าง <button>
ที่สวยหรูขึ้น แทนที่จะจำลองลักษณะการทำงานและฟังก์ชันการทำงานของ <button>
ตัวเลือกที่ดีกว่าคือการปรับปรุงองค์ประกอบที่มีอยู่อย่างต่อเนื่องโดยใช้องค์ประกอบที่กำหนดเอง
องค์ประกอบในตัวที่กําหนดเองคือองค์ประกอบที่กําหนดเองซึ่งขยายแท็ก HTML ในตัวแท็กใดแท็กหนึ่งของเบราว์เซอร์ ประโยชน์หลักของการขยายองค์ประกอบที่มีอยู่คือการรับฟีเจอร์ทั้งหมดขององค์ประกอบนั้น (พร็อพเพอร์ตี้ DOM, เมธอด, การช่วยเหลือพิเศษ) ไม่มีวิธีใดที่ดีกว่าในการเขียน Progressive Web App ไปกว่าการปรับปรุงองค์ประกอบ HTML ที่มีอยู่อย่างต่อเนื่อง
หากต้องการขยายองค์ประกอบ คุณจะต้องสร้างคําจํากัดความคลาสที่รับค่ามาจากอินเทอร์เฟซ DOM ที่ถูกต้อง เช่น องค์ประกอบที่กําหนดเองซึ่งขยายจาก <button>
ต้องรับค่าจาก HTMLButtonElement
แทนที่จะเป็น HTMLElement
ในทำนองเดียวกัน องค์ประกอบที่ขยาย <img>
จะต้องขยาย HTMLImageElement
ตัวอย่าง - การขยาย <button>
// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
constructor() {
super(); // always call super() first in the constructor.
this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
}
// Material design ripple animation.
drawRipple(x, y) {
let div = document.createElement('div');
div.classList.add('ripple');
this.appendChild(div);
div.style.top = `${y - div.clientHeight/2}px`;
div.style.left = `${x - div.clientWidth/2}px`;
div.style.backgroundColor = 'currentColor';
div.classList.add('run');
div.addEventListener('transitionend', (e) => div.remove());
}
}
customElements.define('fancy-button', FancyButton, {extends: 'button'});
โปรดทราบว่าการเรียก define()
จะเปลี่ยนแปลงเล็กน้อยเมื่อขยายองค์ประกอบเนทีฟ พารามิเตอร์ที่ 3 ซึ่งจําเป็นจะบอกเบราว์เซอร์ว่ากําลังขยายแท็กใด ซึ่งจําเป็นเนื่องจากแท็ก HTML จํานวนมากใช้อินเทอร์เฟซ DOM เดียวกัน <section>
, <address>
และ <em>
(และอื่นๆ) ทั้งหมดแชร์ HTMLElement
ทั้ง <q>
และ <blockquote>
แชร์ HTMLQuoteElement
เป็นต้น การระบุ {extends: 'blockquote'}
ช่วยให้เบราว์เซอร์ทราบว่าคุณกําลังสร้าง <blockquote>
ที่ปรับปรุงแล้วแทน <q>
ดูรายการอินเทอร์เฟซ DOM ทั้งหมดของ HTML ได้ในข้อกำหนด HTML
ผู้ใช้งานองค์ประกอบในตัวที่กำหนดเองจะใช้องค์ประกอบดังกล่าวได้หลายวิธี ซึ่งจะประกาศได้โดยเพิ่มแอตทริบิวต์ is=""
ในแท็กเนทีฟ ดังนี้
<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>
สร้างอินสแตนซ์ใน JavaScript
// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);
หรือใช้โอเปอเรเตอร์ new
ดังนี้
let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;
ต่อไปนี้เป็นตัวอย่างที่ขยาย <img>
ตัวอย่าง - การขยาย <img>
customElements.define('bigger-img', class extends Image {
// Give img default size if users don't specify.
constructor(width=50, height=50) {
super(width * 10, height * 10);
}
}, {extends: 'img'});
ผู้ใช้ประกาศคอมโพเนนต์นี้ดังนี้
<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">
หรือสร้างอินสแตนซ์ใน JavaScript
const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);
รายละเอียดอื่นๆ
องค์ประกอบที่ไม่รู้จักเทียบกับองค์ประกอบที่กําหนดเองซึ่งไม่ได้ระบุ
HTML มีความสะดวกสบายและยืดหยุ่นในการทำงาน เช่น ประกาศ <randomtagthatdoesntexist>
ในหน้าเว็บและเบราว์เซอร์ก็ยอมรับได้อย่างสมบูรณ์ เหตุใดแท็กที่ไม่ใช่มาตรฐานจึงทํางานได้ คำตอบคือข้อกำหนดของ HTML อนุญาต ระบบจะแยกวิเคราะห์องค์ประกอบที่ไม่ได้กำหนดโดยข้อกำหนดเป็น HTMLUnknownElement
แต่จะไม่ใช้กับองค์ประกอบที่กำหนดเอง ระบบจะแยกวิเคราะห์องค์ประกอบที่กําหนดเองที่เป็นไปได้เป็น HTMLElement
หากสร้างด้วยชื่อที่ถูกต้อง (มี "-") คุณสามารถตรวจสอบได้ในเบราว์เซอร์ที่รองรับองค์ประกอบที่กําหนดเอง เปิดคอนโซลโดยกด Ctrl+Shift+J (หรือ Cmd+Opt+J ใน Mac) แล้ววางบรรทัดโค้ดต่อไปนี้
// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true
// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true
เอกสารอ้างอิง API
customElements
ส่วนกลางระบุวิธีการที่เป็นประโยชน์ในการทำงานกับองค์ประกอบที่กำหนดเอง
define(tagName, constructor, options)
กำหนดองค์ประกอบที่กำหนดเองใหม่ในเบราว์เซอร์
ตัวอย่าง
customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});
get(tagName)
เมื่อระบุชื่อแท็กองค์ประกอบที่กําหนดเองที่ถูกต้อง ให้แสดงผลคอนสตรัคเตอร์ขององค์ประกอบ
แสดงผล undefined
หากไม่มีการบันทึกคําจํากัดความขององค์ประกอบ
ตัวอย่าง
let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();
whenDefined(tagName)
แสดงผลคำสัญญาที่ได้รับการแก้ไขเมื่อกำหนดองค์ประกอบที่กำหนดเอง หากมีการกำหนดองค์ประกอบไว้แล้ว ให้แก้ไขทันที ปฏิเสธหากชื่อแท็กไม่ใช่ชื่อองค์ประกอบที่กําหนดเองที่ถูกต้อง
ตัวอย่าง
customElements.whenDefined('app-drawer').then(() => {
console.log('ready!');
});
ประวัติและการรองรับเบราว์เซอร์
หากคุณติดตามเว็บคอมโพเนนต์ในช่วง 2-3 ปีที่ผ่านมา ก็จะทราบว่า Chrome 36 ขึ้นไปใช้ Custom Elements API เวอร์ชันที่ใช้ document.registerElement()
แทน customElements.define()
ซึ่งปัจจุบันถือว่าเป็นมาตรฐานเวอร์ชันที่เลิกใช้งานแล้ว เรียกว่า v0
customElements.define()
เป็นเทคโนโลยีที่มาแรงและผู้ให้บริการเบราว์เซอร์เริ่มนำมาใช้งาน เรียกว่าองค์ประกอบที่กำหนดเอง v1
หากคุณสนใจข้อกำหนด v0 เก่า ลองดูที่บทความ html5rocks
การสนับสนุนเบราว์เซอร์
Chrome 54 (status), Safari 10.1 (สถานะ) และ Firefox 63 (สถานะ) มี องค์ประกอบที่กำหนดเอง v1 Edge ได้เริ่มการพัฒนาแล้ว
หากต้องการใช้ฟีเจอร์ตรวจหาองค์ประกอบที่กำหนดเอง ให้ตรวจสอบว่ามีรายการต่อไปนี้หรือไม่
window.customElements
const supportsCustomElementsV1 = 'customElements' in window;
โพลีฟิลล์
เรามี Polyfill แบบสแตนด์อโลนสำหรับองค์ประกอบที่กำหนดเอง v1 จนกว่าจะมีการรองรับเบราว์เซอร์ในวงกว้าง อย่างไรก็ตาม เราขอแนะนำให้ใช้ webcomponents.js loader เพื่อโหลด polyfill ของ Web Components อย่างเหมาะสม ตัวโหลดจะใช้การตรวจหาฟีเจอร์เพื่อโหลดโพลีไฟล์ที่จำเป็นเท่านั้นแบบไม่พร้อมกันตามต้องการของเบราว์เซอร์
ติดตั้ง ดังนี้
npm install --save @webcomponents/webcomponentsjs
การใช้งาน:
<!-- Use the custom element on the page. -->
<my-element></my-element>
<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>
<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
function loadScript(src) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
WebComponents.waitFor(() => {
// At this point we are guaranteed that all required polyfills have
// loaded, and can use web components APIs.
// Next, load element definitions that call `customElements.define`.
// Note: returning a promise causes the custom elements
// polyfill to wait until all definitions are loaded and then upgrade
// the document in one batch, for better performance.
return loadScript('my-element.js');
});
</script>
บทสรุป
องค์ประกอบที่กำหนดเองมีเครื่องมือใหม่สำหรับกำหนดแท็ก HTML ใหม่ในเบราว์เซอร์และสร้างคอมโพเนนต์ที่นำมาใช้ใหม่ได้ เมื่อรวมเข้ากับองค์ประกอบพื้นฐานอื่นๆ ของแพลตฟอร์มใหม่ เช่น Shadow DOM และ <template>
เราก็เริ่มเห็นภาพรวมของ Web Components ดังนี้
- ใช้ได้กับทุกเบราว์เซอร์ (มาตรฐานเว็บ) สำหรับการสร้างและขยายคอมโพเนนต์ที่นำมาใช้ซ้ำได้
- ไม่ต้องใช้ไลบรารีหรือเฟรมเวิร์กเพื่อเริ่มต้นใช้งาน Vanilla JS/HTML FTW!
- มีรูปแบบการเขียนโปรแกรมที่คุ้นเคย เป็นเพียง DOM/CSS/HTML
- ทำงานร่วมกับฟีเจอร์อื่นๆ ของแพลตฟอร์มเว็บใหม่ได้ดี (Shadow DOM,
<template>
, CSS, พร็อพเพอร์ตี้ที่กำหนดเอง ฯลฯ) - ผสานรวมกับเครื่องมือสำหรับนักพัฒนาเว็บของเบราว์เซอร์อย่างแน่นหนา
- ใช้ประโยชน์จากฟีเจอร์การช่วยเหลือพิเศษที่มีอยู่