不添加任何依賴來構建本身的定製組件帶有樣式,擁有交互功能而且在各自文件中優雅組織的 HTML 標籤javascript
https://developer.mozilla.org...css
Web Components是一套不一樣的技術,容許您建立可重用的定製元素(它們的功能封裝在您的代碼以外)而且在您的web應用中使用它們。html
示例java
https://github.com/mdn/web-co...node
polyfillgit
https://www.webcomponents.org...github
https://github.com/webcompone...web
https://unpkg.com/browse/@web...ajax
npm install @webcomponents/webcomponentsjsnpm
<!-- load webcomponents bundle, which includes all the necessary polyfills --> <script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script> <!-- load the element --> <script type="module" src="my-element.js"></script> <!-- use the element --> <my-element></my-element>
Web Component 是一系列 web 平臺的 API,它們能夠容許你建立全新可定製、可重用而且封裝的 HTML 標籤定製的組件基於 Web Component 標準構建,能夠在如今瀏覽器上使用,也能夠和任意與 HTML 交互的 JavaScript 庫和框架配合使用。
它賦予了僅僅使用純粹的JS/HTML/CSS就能夠建立可重用組件的能力。若是 HTML 不能知足需求,咱們能夠建立一個能夠知足需求的 Web Component。
舉個例子,你的用戶數據和一個 ID 有關,你但願有一個能夠填入用戶 ID 而且能夠獲取相應數據的組件。HTML 多是下面這個樣子:
<user-card user-id="1"></user-card>
HTML 和 DOM 標準定義了四種新的標準來幫助定義 Web Component。這些標準以下:
web 開發者能夠經過定製元素建立新的 HTML 標籤、加強已有的 HTML 標籤或是二次開發其它開發者已經完成的組件。這個 API 是 Web Component 的基石。
HTML 模板定義了新的元素,描述一個基於 DOM 標準用於客戶端模板的途徑。模板容許你聲明標記片斷,它們能夠被解析爲 HTML。這些片斷在頁面開始加載時不會被用到,以後運行時會被實例化。
Shadow DOM 被設計爲構建基於組件的應用的一個工具。它能夠解決 web 開發的一些常見問題,好比容許你把組件的 DOM 和做用域隔離開,而且簡化 CSS 等等。
HTML 模板(HTML Templates)容許你建立新的模板,一樣的,HTML 引用(HTML imports)容許你從不一樣的文件中引入這些模板。經過獨立的HTML文件管理組件,能夠幫助你更好的組織代碼。
定製元素的名稱必須包含一個短橫線。因此 <my-tabs> 和 <my-amazing-website> 是合法的名稱, 而 <foo> 和 <foo_bar> 不行。在 HTML 添加新標籤時須要確保向前兼容,不能重複註冊同一個標籤。
定製元素標籤不能是自閉合的,由於 HTML 只容許一部分元素能夠自閉合。須要寫成像 <app-drawer></app-drawer> 這樣的閉合標籤形式。
建立組件時可使用繼承的方式。舉個例子,若是想要爲兩種不一樣的用戶建立一個 UserCard,
你能夠先建立一個基本的 UserCard 而後將它拓展爲兩種特定的用戶卡片。
Google web developers’ article https://developers.google.com...
組件元素是類的實例,就能夠在這些類中定義公用方法。這些公用方法能夠用來容許其它定製組件/腳原本和這些組件產生交互,而不是隻能改變這些組件的屬性。
能夠經過多種方式定義私有方法。我傾向於使用(當即執行函數),由於它們易寫和易理解。
(function() {})();
爲了防止新的屬性被添加,須要凍結你的類。這樣能夠防止類的已有屬性被移除,或者已有屬性的可枚舉、可配置或可寫屬性被改變,一樣也能夠防止原型被修改。
class MyComponent extends HTMLElement { ... } const FrozenMyComponent = Object.freeze(MyComponent); customElements.define('my-component', FrozenMyComponent);
凍結類會阻止你在運行時添加補丁而且會讓你的代碼難以調試。
鑑於 服務器的根路徑的配置不統一import 可使用絕對路徑
import 的 js 內部不能夠再次 import ,會出現路徑錯誤
<script type="module" async> import 'https://xxx/button.js'; </script>
聲明一個類,定義元素如何表現。這個類須要繼承 HTMLElement 類
connectedCallback — 每當元素插入 DOM 時被觸發。disconnectedCallback — 每當元素從 DOM 中移除時被觸發。
attributeChangedCallback — 當元素上的屬性被添加、移除、更新或取代時被觸發。
若是須要在元素屬性變化後,觸發 attributeChangedCallback()回調函數,你必須監聽這個屬性。
這能夠經過定義observedAttributes() get函數來實現
observedAttributes()函數體內包含一個 return語句,返回一個數組,包含了須要監聽的屬性名稱:
static get observedAttributes() { return ['disabled','icon','loading'] } constructor(){}
該段代碼處於構造函數的上方。
在 UserCard 文件夾下建立 UserCard.js:
class UserCard extends HTMLElement { constructor() { super(); this.addEventListener("click", e => { this.toggleCard(); }); } toggleCard() { console.log("Element was clicked!"); } } customElements.define("user-card", UserCard);
customElements.define('user-card', UserCard) 函數調用告知 DOM 咱們已經建立了一個新的定製元素叫 user-card它的行爲被 UserCard 類定義。
如今能夠在咱們的 HTML 裏使用 user-card 元素了。
UserCard.html
<template id="user-card-template"> <div> <h2> <span></span> ( <span></span>) </h2> <p>Website: <a></a></p> <div> <p></p> </div> <button class="card__details-btn">More Details</button> </div> </template> <script src="/UserCard/UserCard.js"></script>
在類名前加了一個 card__ 前綴,避免意外的樣式覆蓋在較早版本的瀏覽器中,咱們不能使用 shadow DOM 來隔離組件 DOM
UserCard.css
.card__user-card-container { text-align: center; display: inline-block; border-radius: 5px; border: 1px solid grey; font-family: Helvetica; margin: 3px; width: 30%; } .card__user-card-container:hover { box-shadow: 3px 3px 3px; } .card__hidden-content { display: none; } .card__details-btn { background-color: #dedede; padding: 6px; margin-bottom: 8px; }
UserCard.html 文件的最前面引入這個 CSS 文件:
<template id="user-card-template"> <link rel="stylesheet" href="/UserCard/UserCard.css"> <div> <h2> <span></span> ( <span></span>) </h2> <p>Website: <a></a></p> <div> <p></p> </div> <button class="card__details-btn">More Details</button> </div> </template> <script src="/UserCard/UserCard.js"></script>
constructor 方法是元素被實例化時調用connectedCallback 方法是每次元素插入 DOM 時被調用。
connectedCallback 方法在執行初始化代碼時是頗有用的,好比獲取數據或渲染。
在 UserCard.js 的頂部,定義一個常量 currentDocument。它在被引入的 HTML 腳本中是必要的,容許這些腳本有途徑操做引入模板的 DOM。像下面這樣定義:
const currentDocument = document.currentScript.ownerDocument;
定義 connectedCallback 方法把克隆好的模板綁定到 shadow root 上
// 元素插入 DOM 時調用 connectedCallback() { const shadowRoot = this.attachShadow({ mode: "open" }); // 選取模板而且克隆它。最終將克隆後的節點添加到 shadowDOM 的根節點。 // 當前文檔須要被定義從而獲取引入 HTML 的 DOM 權限。 const template = currentDocument.querySelector("#user-card-template"); const instance = template.content.cloneNode(true); shadowRoot.appendChild(instance); // 從元素中選取 user-id 屬性 // 注意咱們要像這樣指定卡片: // <user-card user-id="1"></user-card> const userId = this.getAttribute("user-id"); // 根據 user ID 獲取數據,而且使用返回的數據渲染 fetch(`https://jsonplaceholder.typicode.com/users/${userId}`) .then(response => response.text()) .then(responseText => { this.render(JSON.parse(responseText)); }) .catch(error => { console.error(error); }); }
render(userData) { // 使用操做 DOM 的 API 來填充卡片的不一樣區域 // 組件的全部元素都存在於 shadow dom 中,因此咱們使用了 this.shadowRoot 這個屬性來獲取 DOM // DOM 只能夠在這個子樹種被查找到 this.shadowRoot.querySelector(".card__full-name").innerHTML = userData.name; this.shadowRoot.querySelector(".card__user-name").innerHTML = userData.username; this.shadowRoot.querySelector(".card__website").innerHTML = userData.website; this.shadowRoot.querySelector(".card__address").innerHTML = `<h4>Address</h4> ${userData.address.suite}, <br /> ${userData.address.street},<br /> ${userData.address.city},<br /> Zipcode: ${userData.address.zipcode}`; } toggleCard() { let elem = this.shadowRoot.querySelector(".card__hidden-content"); let btn = this.shadowRoot.querySelector(".card__details-btn"); btn.innerHTML = elem.style.display == "none" ? "Less Details" : "More Details"; elem.style.display = elem.style.display == "none" ? "block" : "none"; }
既然組件已經完成,咱們就能夠把它用在任意項目中了。爲了繼續教程,咱們須要建立一個 index.html 文件
<html> <head> <title>Web Component</title> </head> <body> <user-card user-id="1"></user-card> <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script> <link rel="import" href="./UserCard/UserCard.html"> </body> </html>
構建3個組件。第一個組件是人員列表。
第二個組件將顯示咱們從第一個組件中選擇的人的信息。
父組件將協調這些組件,並容許咱們獨立開發子組件並將它們鏈接在一塊兒。
建立一個components包含全部組件的目錄。每一個組件都有本身的目錄,其中包含組件的HTML模板,JS和樣式表。
僅用於建立其餘組件且未重用的組件將放置在該組件目錄中
src/ index.html components/ PeopleController/ PeopleController.js PeopleController.html PeopleController.css PeopleList/ PeopleList.js PeopleList.html PeopleList.css PersonDetail/ PersonDetail.js PersonDetail.html PersonDetail.css
PeopleList.html
<template id="people-list-template"> <style> .people-list__container { border: 1px solid black; } .people-list__list { list-style: none } .people-list__list > li { font-size: 20px; font-family: Helvetica; color: #000000; text-decoration: none; } </style> <div class="people-list__container"> <ul class="people-list__list"></ul> </div> </template> <script src="/components/PeopleController/PeopleList/PeopleList.js"></script>
PeopleList.js
(function () { const currentDocument = document.currentScript.ownerDocument; function _createPersonListElement(self, person) { let li = currentDocument.createElement('LI'); li.innerHTML = person.name; li.className = 'people-list__name' li.onclick = () => { let event = new CustomEvent("PersonClicked", { detail: { personId: person.id }, bubbles: true }); self.dispatchEvent(event); } return li; } class PeopleList extends HTMLElement { constructor() { // If you define a constructor, always call super() first as it is required by the CE spec. super(); // A private property that we'll use to keep track of list let _list = []; //使用defineProperty定義此對象的prop,即組件。 //每當設置列表時,調用render。 這種方式當父組件設置一些數據時 //在子對象上,咱們能夠自動更新子對象。 Object.defineProperty(this, 'list', { get: () => _list, set: (list) => { _list = list; this.render(); } }); } connectedCallback() { // Create a Shadow DOM using our template const shadowRoot = this.attachShadow({ mode: 'open' }); const template = currentDocument.querySelector('#people-list-template'); const instance = template.content.cloneNode(true); shadowRoot.appendChild(instance); } render() { let ulElement = this.shadowRoot.querySelector('.people-list__list'); ulElement.innerHTML = ''; this.list.forEach(person => { let li = _createPersonListElement(this, person); ulElement.appendChild(li); }); } } customElements.define('people-list', PeopleList); })();
在該render方法中,咱們須要使用建立人名列表/<li/>。咱們還將CustomEvent爲每一個元素建立一個。每當單擊該元素時,其id將在DOM樹中向上傳播事件。
咱們建立了PeopleList一個按名稱列出人員的組件。咱們還想建立一個組件,當在該組件中單擊人名時,該組件將顯示人員詳細信息PersonDetail.html
<template id="person-detail-template"> <link rel="stylesheet" href="/components/PeopleController/PersonDetail/PersonDetail.css"> <div class="card__user-card-container"> <h2 class="card__name"> <span class="card__full-name"></span> ( <span class="card__user-name"></span>) </h2> <p>Website: <a class="card__website"></a></p> <div class="card__hidden-content"> <p class="card__address"></p> </div> <button class="card__details-btn">More Details</button> </div> </template> <script src="/components/PeopleController/PersonDetail/PersonDetail.js"></script>
PersonDetail.css
.card__user-card-container { text-align: center; border-radius: 5px; border: 1px solid grey; font-family: Helvetica; margin: 3px; } .card__user-card-container:hover { box-shadow: 3px 3px 3px; } .card__hidden-content { display: none; } .card__details-btn { background-color: #dedede; padding: 6px; margin-bottom: 8px; }
/components/PeopleController/PersonDetail/PersonDetail.js
(function () { const currentDocument = document.currentScript.ownerDocument; class PersonDetail extends HTMLElement { constructor() { // If you define a constructor, always call super() first as it is required by the CE spec. super(); // Setup a click listener on <user-card> this.addEventListener('click', e => { this.toggleCard(); }); } // Called when element is inserted in DOM connectedCallback() { const shadowRoot = this.attachShadow({ mode: 'open' }); const template = currentDocument.querySelector('#person-detail-template'); const instance = template.content.cloneNode(true); shadowRoot.appendChild(instance); } // 建立API函數,以便其餘組件可使用它來填充此組件 // Creating an API function so that other components can use this to populate this component updatePersonDetails(userData) { this.render(userData); } /// 填充卡的功能(能夠設爲私有) // Function to populate the card(Can be made private) render(userData) { this.shadowRoot.querySelector('.card__full-name').innerHTML = userData.name; this.shadowRoot.querySelector('.card__user-name').innerHTML = userData.username; this.shadowRoot.querySelector('.card__website').innerHTML = userData.website; this.shadowRoot.querySelector('.card__address').innerHTML = `<h4>Address</h4> ${userData.address.suite}, <br /> ${userData.address.street},<br /> ${userData.address.city},<br /> Zipcode: ${userData.address.zipcode}` } toggleCard() { let elem = this.shadowRoot.querySelector('.card__hidden-content'); let btn = this.shadowRoot.querySelector('.card__details-btn'); btn.innerHTML = elem.style.display == 'none' ? 'Less Details' : 'More Details'; elem.style.display = elem.style.display == 'none' ? 'block' : 'none'; } } customElements.define('person-detail', PersonDetail); })()
updatePersonDetails(userData)以便在單擊Person組件時可使用此函數更新此PeopleList組件。咱們也可使用屬性完成此操做
HTML導入已從標準中刪除,預計將被模塊導入替換PeopleController.html
<template id="people-controller-template"> <link rel="stylesheet" href="/components/PeopleController/PeopleController.css"> <people-list id="people-list"></people-list> <person-detail id="person-detail"></person-detail> </template> <link rel="import" href="/components/PeopleController/PeopleList/PeopleList.html"> <link rel="import" href="/components/PeopleController/PersonDetail/PersonDetail.html"> <script src="/components/PeopleController/PeopleController.js"></script>
PeopleController.css
#people-list { width: 45%; display: inline-block; } #person-detail { width: 45%; display: inline-block; }
PeopleController.js
(function () { const currentDocument = document.currentScript.ownerDocument; function _fetchAndPopulateData(self) { let peopleList = self.shadowRoot.querySelector('#people-list'); fetch(`https://jsonplaceholder.typicode.com/users`) .then((response) => response.text()) .then((responseText) => { const list = JSON.parse(responseText); self.peopleList = list; peopleList.list = list; _attachEventListener(self); }) .catch((error) => { console.error(error); }); } function _attachEventListener(self) { let personDetail = self.shadowRoot.querySelector('#person-detail'); //Initialize with person with id 1: personDetail.updatePersonDetails(self.peopleList[0]); self.shadowRoot.addEventListener('PersonClicked', (e) => { // e contains the id of person that was clicked. // We'll find him using this id in the self.people list: self.peopleList.forEach(person => { if (person.id == e.detail.personId) { // Update the personDetail component to reflect the click personDetail.updatePersonDetails(person); } }) }) } class PeopleController extends HTMLElement { constructor() { super(); this.peopleList = []; } connectedCallback() { const shadowRoot = this.attachShadow({ mode: 'open' }); const template = currentDocument.querySelector('#people-controller-template'); const instance = template.content.cloneNode(true); shadowRoot.appendChild(instance); _fetchAndPopulateData(this); } } customElements.define('people-controller', PeopleController); })()
調用API來獲取用戶的數據。 這將採用咱們以前定義的2個組件,填充PeopleList組件,並將此數據的第一個用戶提供爲PeopleDetail組件的初始數據。在父組件中監視PersonClicked事件,以便咱們能夠相應地更新PersonDetail對象。 所以,在上面的文件中建立2個私有函數
建立一個名爲index.html的新HTML文件
<html> <head> <title>Web Component Part 2</title> </head> <body> <people-controller></people-controller> <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script> <link rel="import" href="./components/PeopleController/PeopleController.html"> </body> </html>
HTML中的元素具備屬性; 這些是配置元素或以各類方式調整其行爲以知足用戶所需條件的其餘值。使用如下屬性建立一個組件UserCard:username,address和is-admin(布爾值告訴咱們用戶是否爲admin)。
觀察這些屬性以進行更改並相應地更新組件。
定義屬性
<user-card username="Ayush" address="Indore, India" is-admin></user-card>
使用JavaScript中的DOM API來使用getAttribute(attrName)和setAttribute(attrName,newVal)方法來獲取和設置屬性。
let myUserCard = document.querySelector('user-card') myUserCard.getAttribute('username') // Ayush myUserCard.setAttribute('username', 'Ayush Gupta') myUserCard.getAttribute('username') // Ayush Gupta
自定義元素規範v1定義了一種觀察屬性更改並對這些更改採起操做的簡便方法。 在建立咱們的組件時,咱們須要定義兩件事:觀察到的屬性:要在屬性更改時獲得通知,必須在初始化元素時定義觀察到的屬性列表,方法是在返回屬性名稱數組的元素類上放置一個靜態的observeAttributes getter。
attributeChangedCallback(attributeName,oldValue,newValue,namespace):在元素上更改,追加,刪除或替換屬性時調用的生命週期方法。 它僅用於觀察屬性。
構建UserCard組件,它將使用屬性進行初始化,而且咱們的組件將觀察對其屬性所作的任何更改。在項目目錄中建立index.html文件。
還可使用如下文件建立UserCard目錄:UserCard.html,UserCard.css和UserCard.js。
UserCard.js
(async () => { const res = await fetch('/UserCard/UserCard.html'); const textTemplate = await res.text(); const HTMLTemplate = new DOMParser().parseFromString(textTemplate, 'text/html') .querySelector('template'); class UserCard extends HTMLElement { constructor() { ... } connectedCallback() { ... } // Getter to let component know what attributes // to watch for mutation static get observedAttributes() { return ['username', 'address', 'is-admin']; } attributeChangedCallback(attr, oldValue, newValue) { console.log(`${attr} was changed from ${oldValue} to ${newValue}!`) } } customElements.define('user-card', UserCard); })();
建立組件時,咱們將爲它提供一些初始值,它將用於初始化組件。
<user-card username="Ayush" address="Indore, India" is-admin="true"></user-card>
在connectedCallback中,咱們將使用這些屬性並定義與每一個屬性相對應的變量。
connectedCallback() { const shadowRoot = this.attachShadow({ mode: 'open' }); const instance = HTMLTemplate.content.cloneNode(true); shadowRoot.appendChild(instance); // You can also put checks to see if attr is present or not // and throw errors to make some attributes mandatory // Also default values for these variables can be defined here this.username = this.getAttribute('username'); this.address = this.getAttribute('address'); this.isAdmin = this.getAttribute('is-admin'); } // Define setters to update the DOM whenever these values are set set username(value) { this._username = value; if (this.shadowRoot) this.shadowRoot.querySelector('#card__username').innerHTML = value; } get username() { return this._username; } set address(value) { this._address = value; if (this.shadowRoot) this.shadowRoot.querySelector('#card__address').innerHTML = value; } get address() { return this._address; } set isAdmin(value) { this._isAdmin = value; if (this.shadowRoot) this.shadowRoot.querySelector('#card__admin-flag').style.display = value == true ? "block" : "none"; } get isAdmin() { return this._isAdmin; }
更改觀察到的屬性時,將調用attributeChangedCallback。 因此咱們須要定義當這些屬性發生變化時會發生什麼。 重寫函數以包含如下內容:
attributeChangedCallback(attr, oldVal, newVal) { const attribute = attr.toLowerCase() console.log(newVal) if (attribute === 'username') { this.username = newVal != '' ? newVal : "Not Provided!" } else if (attribute === 'address') { this.address = newVal !== '' ? newVal : "Not Provided!" } else if (attribute === 'is-admin') { this.isAdmin = newVal == 'true'; } }
<template id="user-card-template"> <h3 id="card__username"></h3> <p id="card__address"></p> <p id="card__admin-flag">I'm an admin</p> </template>
使用2個輸入元素和一個複選框建立index.html文件,併爲全部這些元素定義onchange方法以更新組件的屬性。 一旦屬性更新,更改也將反映在DOM中。
<html> <head> <title>Web Component</title> </head> <body> <input type="text" onchange="updateName(this)" placeholder="Name"> <input type="text" onchange="updateAddress(this)" placeholder="Address"> <input type="checkbox" onchange="toggleAdminStatus(this)" placeholder="Name"> <user-card username="Ayush" address="Indore, India" is-admin></user-card> <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script> <script src="/UserCard/UserCard.js"></script> <script> function updateAddress(elem) { document.querySelector('user-card').setAttribute('address', elem.value); } function updateName(elem) { document.querySelector('user-card').setAttribute('username', elem.value); } function toggleAdminStatus(elem) { document.querySelector('user-card').setAttribute('is-admin', elem.checked); } </script> </body> </html>
在上一篇文章中,咱們爲子組件建立了一個API,以便父組件可使用此API初始化並與它們交互。在這種狀況下,若是咱們已經有一些配置,但願直接提供而不使用父/其餘函數調用,將沒法作到。使用屬性,咱們能夠很是輕鬆地提供初始配置。而後能夠在構造函數或connectedCallback中提取此配置以初始化組件。
更改屬性以與組件交互可能會有點單調乏味。假設您要將大量json數據傳遞給組件。這樣作須要將json表示爲字符串屬性,並在組件使用時進行解析。
僅使用屬性:這是咱們在本文中看到的方法。咱們使用屬性來初始化組件以及與外部世界進行交互。僅使用已建立的函數:這是咱們在本系列的第2部分中看到的方法,咱們使用咱們爲它們建立的函數初始化並與組件交互。
使用混合方法:應該使用IMO。在這種方法中,咱們使用屬性初始化組件,而且對於全部後續交互,只需使用對其API的調用。
modal.js
class Modal extends HTMLElement { constructor() { super(); this._modalVisible = false; this._modal; this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style> /* The Modal (background) */ .modal { display: none; position: fixed; z-index: 1; padding-top: 100px; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); } /* Modal Content */ .modal-content { position: relative; background-color: #fefefe; margin: auto; padding: 0; border: 1px solid #888; width: 80%; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); -webkit-animation-name: animatetop; -webkit-animation-duration: 0.4s; animation-name: animatetop; animation-duration: 0.4s } /* Add Animation */ @-webkit-keyframes animatetop { from {top:-300px; opacity:0} to {top:0; opacity:1} } @keyframes animatetop { from {top:-300px; opacity:0} to {top:0; opacity:1} } /* The Close Button */ .close { color: white; float: right; font-size: 28px; font-weight: bold; } .close:hover, .close:focus { color: #000; text-decoration: none; cursor: pointer; } .modal-header { padding: 2px 16px; background-color: #000066; color: white; } .modal-body {padding: 2px 16px; margin: 20px 2px} </style> <button>Open Modal</button> <div class="modal"> <div class="modal-content"> <div class="modal-header"> <span class="close">×</span> <slot name="header"><h1>Default text</h1></slot> </div> <div class="modal-body"> <slot><slot> </div> </div> </div> ` } connectedCallback() { this._modal = this.shadowRoot.querySelector(".modal"); this.shadowRoot.querySelector("button").addEventListener('click', this._showModal.bind(this)); this.shadowRoot.querySelector(".close").addEventListener('click', this._hideModal.bind(this)); } disconnectedCallback() { this.shadowRoot.querySelector("button").removeEventListener('click', this._showModal); this.shadowRoot.querySelector(".close").removeEventListener('click', this._hideModal); } _showModal() { this._modalVisible = true; this._modal.style.display = 'block'; } _hideModal() { this._modalVisible = false; this._modal.style.display = 'none'; } } customElements.define('pp-modal',Modal);
index.html
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="./modal.js"></script> </head> <body> <h2>Modal web component with vanilla JS.</h2> <pp-modal> <h1 slot="header">Information Box</h1> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </pp-modal> </body> </html>
(function () { class MidociLayOut extends HTMLElement { static get observedAttributes() { return ['acitve-title', 'active-sub-title'] } constructor() { super() this.attachShadow({mode: 'open'}) this.shadowRoot.innerHTML = ` <style> </style> <div class="wrapper"> </div> ` this._a = '' } connectedCallback() { } disconnectedCallback() { } attributeChangedCallback(attr, oldVal, newVal) { // const attribute = attr.toLowerCase() // if (attribute === 'descriptions') { // console.log(1) // this.render(newVal) // } } } const FrozenMidociLayOut = Object.freeze(MidociLayOut); customElements.define('midoci-lay-out', FrozenMidociLayOut); })()
效果
體驗
web components polyfill 兼容舊版本瀏覽器的支持插件https://www.webcomponents.org...
源碼
(function () { const selectListDemo = [ {name: 'test1', value: 1}, {name: 'test2', value: 2}, {name: 'test3', value: 3} ] class MidociSelect extends HTMLElement { static get observedAttributes() { return ['acitve-title', 'active-sub-title'] } constructor() { super() this.attachShadow({mode: 'open'}) this.shadowRoot.innerHTML = ` <style> :host{ --themeColor:rgb(24,144,255); box-sizing: border-box; font-size: 14px; --borderColor:#eee; } .wrapper{ position: relative; display: inline-flex; align-items: center; padding-left: 10px; width: 95px; height: 36px; border: 1px solid var(--borderColor); color: #333; border-radius: 2px; user-select: none; transition: .3s cubic-bezier(.12, .4, .29, 1.46); outline:none } .wrapper:hover{ border: 1px solid var(--themeColor); transition: .3s cubic-bezier(.12, .4, .29, 1.46); } .title{ } .arrow-out{ position: absolute; right: 12px; top: 50%; transform: translateY(0px) rotateX(0deg); transition: .3s cubic-bezier(.12, .4, .29, 1.46); } .wrapper.flip>.arrow-out{ transform: translateY(-3px) rotateX(180deg); transition: .3s cubic-bezier(.12, .4, .29, 1.46); } .arrow{ display: flex; width: 6px; height:6px; border: none; border-left: 1px solid #333; border-bottom: 1px solid #333; transform: translateY(-50%) rotateZ(-45deg); transition: .3s cubic-bezier(.12, .4, .29, 1.46); } .wrapper:hover .arrow{ border-left: 1px solid var(--themeColor); border-bottom: 1px solid var(--themeColor); transition: .3s cubic-bezier(.12, .4, .29, 1.46); } .list{ z-index: 100; position: absolute; top: 130%; left: 0; background-color: #fff; box-shadow: 0 0 10px rgba(0,0,0,0.1); visibility: hidden; min-width: 100%; border-radius: 3px; transform: scale(0); transform-origin: top; transition: .3s cubic-bezier(.12, .4, .29, 1.46); } .wrapper.flip>.list{ visibility: visible; transform: scale(1); transition: .3s cubic-bezier(.12, .4, .29, 1.46); } .item{ display: flex; align-items: center; padding-left: 10px; width: 95px; height: 36px; color: #333; border-radius: 2px; user-select: none; background-color: #fff; transition: background-color .3s ease-in-out; } .item:hover{ background-color: rgba(24,144,255,0.1); transition: background-color .3s ease-in-out; } </style> <div class="wrapper" tabindex="1"> <span class="title">1</span> <span class="arrow-out"> <span class="arrow"></span> </span> <div class="list" > <div class="item">1</div> <div class="item">2</div> <div class="item">3</div> <div class="item">4</div> </div> </div> ` this._wrapperDom = null this._listDom = null this._titleDom = null this._list = [] this._arrowFlip = false this._value = null this._name = null } connectedCallback() { this._wrapperDom = this.shadowRoot.querySelector('.wrapper') this._listDom = this.shadowRoot.querySelector('.list') this._titleDom = this.shadowRoot.querySelector('.title') this.initEvent() this.list = selectListDemo } disconnectedCallback() { this._wrapperDom.removeEventListener('click', this.flipArrow.bind(this)) this._wrapperDom.removeEventListener('blur', this.blurWrapper.bind(this)) this.shadowRoot.querySelectorAll('.item') .forEach((item, index) => { item.removeEventListener('click', this.change.bind(this, index)) }) } attributeChangedCallback(attr, oldVal, newVal) { // const attribute = attr.toLowerCase() // if (attribute === 'descriptions') { // console.log(1) // this.render(newVal) // } } set list(list) { if (!this.shadowRoot) return this._list = list this.render(list) } get list() { return this._list } set value(value) { this._value = value } get value() { return this._value } set name(name) { this._name = name } get name() { return this._name } initEvent() { this.initArrowEvent() this.blurWrapper() } initArrowEvent() { this._wrapperDom.addEventListener('click', this.flipArrow.bind(this)) } initChangeEvent() { this.shadowRoot.querySelectorAll('.item') .forEach((item, index) => { item.addEventListener('click', this.change.bind(this, index)) }) } change(index) { this.changeTitle(this._list, index) let changeInfo = { detail: { value: this._value, name: this._name }, bubbles: true } let changeEvent = new CustomEvent('change', changeInfo) this.dispatchEvent(changeEvent) } changeTitle(list, index) { this._value = list[index].value this._name = list[index].name this._titleDom.innerText = this._name } flipArrow() { if (!this._arrowFlip) { this.showList() } else { this.hideList() } } showList() { this._arrowFlip = true this._wrapperDom.classList = 'wrapper flip' } hideList() { this._arrowFlip = false this._wrapperDom.classList = 'wrapper' } blurWrapper() { this._wrapperDom.addEventListener('blur', (event) => { event.stopPropagation() this.hideList() }) } render(list) { if (!list instanceof Array) return let listString = '' list.forEach((item) => { listString += ` <div class="item" data-value="${item.value}">${item.name}</div> ` }) this._listDom.innerHTML = listString this.changeTitle(list, 0) this.initChangeEvent() } } const FrozenMidociSelect = Object.freeze(MidociSelect); customElements.define('midoci-select', FrozenMidociSelect); })()
注意:若是父元素高度過低,須要關閉父元素的 overflow 屬性,不然會遮蓋 下拉列表
<script type="module" async> import './MidociSelect.js' </script> <midoci-select></midoci-select> <script> const list = [ {name: '全平臺', value: 1}, {name: '東券', value: 2}, {name: '京券', value: 3} ] window.onload=function(){ document.querySelector('midoci-select').list=list console.log(document.querySelector('midoci-select').value) console.log(document.querySelector('midoci-select').name) document.querySelector('midoci-select').addEventListener('change', (event) => { console.log('選中的 value:', event.detail.value) console.log('選中的 name:', event.detail.name) }) } </script>
https://github.com/WangShuXia...