不添加任何依赖来构建本身的定制组件带有样式,拥有交互功能而且在各自文件中优雅组织的 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...