# Web Components
[toc]
# 1、基础介绍
Web Component 是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的 web 应用中使用它们。
作为开发者,我们都知道尽可能多的重用代码是一个好主意。这对于自定义标记结构来说通常不是那么容易 — 想想复杂的 HTML(以及相关的样式和脚本),有时您不得不写代码来呈现自定义 UI 控件,并且如果您不小心的话,多次使用它们会使您的页面变得一团糟。
Web Components 旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。
- Custom element(自定义元素):一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。
- Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML template(HTML 模板):
<template>
和<slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
实现 web component 的基本方法通常如下所示:
- 创建一个类或函数来指定 web 组件的功能,如果使用类,请使用 ECMAScript 2015 的类语法 (参阅类 (opens new window)获取更多信息)。
- 使用
CustomElementRegistry.define()
方法注册您的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。 - 如果需要的话,使用
Element.attachShadow()
方法将一个 shadow DOM 附加到自定义元素上。使用通常的 DOM 方法向 shadow DOM 中添加子元素、事件监听器等等。 - 如果需要的话,使用
<template>
和<slot>
定义一个 HTML 模板。再次使用常规 DOM 方法克隆模板并将其附加到您的 shadow DOM 中。 - 在页面任何您喜欢的位置使用自定义元素,就像使用常规 HTML 元素那样。
# 2、简单示例
<body>
<work-count class="word-count-container"></work-count>
<script>
class WordCount extends HTMLElement {
constructor() {
super();
const p = document.createElement('p');
p.textContent = 'word count';
// 这里的this就是自定义元素实例,可以把他看做一个常规的DOM元素
this.appendChild(p);
}
}
window.customElements.define('work-count', WordCount);
</script>
</body>
渲染结果
<work-count class="word-count-container">
<p>word count</p>
</work-count>
CustomElementRegistry.define()
方法用来注册一个 custom element,该方法接受以下参数:
- 表示所创建的元素名称的符合 [
DOMString
]标准的字符串。注意,custom element 的名称不能是单个单词,且其中[必须要有短横线]。 - 用于定义元素行为的类。
可选参数
,一个包含extends
属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
# 2.1 组件生命周期
上面只是一个简单的例子,我们能做的不只这些。在构造函数中,我们可以设定一些生命周期的回调函数,在特定的时间,这些回调函数将会被调用。
connectedCallback
:当 custom element 首次被插入文档 DOM 时,被调用。disconnectedCallback
:当 custom element 从文档 DOM 中删除时,被调用。adoptedCallback
:当 custom element 被移动到新的文档时,被调用。attributeChangedCallback
: 当 custom element 增加、删除、修改自身属性时,被调用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<custom-square id="CustomSquareRef" l="100" c="red"></custom-square>
<script>
class CustomSquare extends HTMLElement {
static get observedAttributes() {
return ['c', 'l'];
}
constructor() {
super();
const div = document.createElement('div');
const style = document.createElement('style');
div.textContent = 'square';
this.appendChild(style);
this.appendChild(div);
}
connectedCallback() {
console.log('connectedCallback');
updateStyle(this);
}
/* disconnectedCallback()和adoptedCallback()回调函数只是简单地将消息发送到控制台,提示我们元素什么时候从 DOM 中移除、或者什么时候移动到不同的页面: */
disconnectedCallback() {}
adoptedCallback() {}
/* 每当元素的属性变化时,`attributeChangedCallback()`回调函数会执行。正如它的属性所示,我们可以查看属性的名称、旧值与新值,以此来对元素属性做单独的操作。
需要注意的是,如果需要在元素属性变化后,触发attributeChangedCallback()回调函数,你必须监听这个属性。
这可以通过定义observedAttributes() get 函数来实现,observedAttributes()函数体内包含一个 return 语句,返回一个数组,包含了需要监听的属性名称:
*/
attributeChangedCallback(name, oldValue, newValue) {
console.log('attributeChangedCallback');
console.log(name, oldValue, newValue);
updateStyle(this);
}
}
function updateStyle(el) {
const style = el.querySelector('style');
style.textContent = `
div {
width: ${el.getAttribute('l')}px;
height: ${el.getAttribute('l')}px;
background-color: ${el.getAttribute('c')};
}
`;
}
window.customElements.define('custom-square', CustomSquare);
const CustomSquareRef = document.querySelector('#CustomSquareRef');
setTimeout(() => {
CustomSquareRef.setAttribute('l', '200');
CustomSquareRef.setAttribute('c', 'green');
}, 1000);
</script>
</body>
</html>
# 3、自定义元素类型
两种 custom elements:
Autonomous custom elements 是独立的元素,它不继承其他内建的 HTML 元素。你可以直接把它们写成 HTML 标签的形式,来在页面上使用。例如
<popup-info>
,或者是document.createElement("popup-info")
这样。class CustomSquare extends HTMLElement {}
Customized built-in elements 继承自基本的 HTML 元素。在创建时,你必须指定所需扩展的元素,使用时,需要先写出基本的元素标签,并通过
is
(opens new window) 属性指定 custom element 的名称。例如<p is="word-count">
, 或者document.createElement("p", { is: "word-count" })
。<body> <!-- 1、使用时,需要使用is属性 --> <p is="work-count"></p> <script> // 2、类继承自 HTMLParagraphElement class WordCount extends HTMLParagraphElement { constructor() { super(); const p = document.createElement('p'); p.textContent = 'word count'; this.appendChild(p); } connectedCallback() { console.log('connectedCallback'); } attributeChangedCallback() { console.log('attributeChangedCallback'); } } // 3、第三个参数表明自己继承自p window.customElements.define('work-count', WordCount, { extends: 'p' }); </script> </body>
# 4、shadow DOM
Web components 的一个重要属性是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。
Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。
这里,有一些 Shadow DOM 特有的术语需要我们了解:
- Shadow host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上。
- Shadow tree:Shadow DOM 内部的 DOM 树。
- Shadow boundary:Shadow DOM 结束的地方,也是常规 DOM 开始的地方。
- Shadow root: Shadow tree 的根节点。
你可以使用同样的方式来操作 Shadow DOM,就和操作常规 DOM 一样——例如添加子节点、设置属性,以及为节点添加自己的样式(例如通过 element.style
属性),或者为整个 Shadow DOM 添加样式(例如在 `` (opens new window) 元素内添加样式)。不同的是,Shadow DOM 内部的元素始终不会影响到它外部的元素(除了 :focus-within
(opens new window)),这为封装提供了便利。
注意,不管从哪个方面来看,Shadow DOM 都不是一个新事物——在过去的很长一段时间里,浏览器用它来封装一些元素的内部结构。以一个有着默认播放控制按钮的 `` (opens new window) 元素为例。你所能看到的只是一个 <video>
标签,实际上,在它的 Shadow DOM 中,包含了一系列的按钮和其他控制器。Shadow DOM 标准允许你为你自己的元素(custom element)维护一组 Shadow DOM。
# 4.1 基本用法
可以使用 Element.attachShadow()
(opens new window) 方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode
属性,值可以是 open
或者 closed
:
let shadow = elementRef.attachShadow({ mode: "open" });
let shadow = elementRef.attachShadow({ mode: "closed" });
open
表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用 Element.shadowRoot
(opens new window) 属性:
let myShadowDom = myCustomElem.shadowRoot;
如果你将一个 Shadow root 附加到一个 Custom element 上,并且将 mode
设置为 closed
,那么就不可以从外部获取 Shadow DOM 了——myCustomElem.shadowRoot
将会返回 null
。浏览器中的某些内置元素就是如此,例如<video>
,包含了不可访问的 Shadow DOM。
如果你想将一个 Shadow DOM 附加到 custom element 上,可以在 custom element 的构造函数中添加如下实现(目前,这是 shadow DOM 最实用的用法):
<body>
<style>
p {
color: red;
}
</style>
<p>outer paragraph</p>
<work-count class="word-count-container"></work-count>
<script>
class WordCount extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'closed' });
// 这里的p元素并不会被外部的样式所影响
const p = document.createElement('p');
p.textContent = 'word count';
shadow.appendChild(p);
}
}
window.customElements.define('work-count', WordCount);
</script>
</body>
# 4.2 为 shadow DOM 添加样式
# 4.2.1 通过style标签添加
class WordCount extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'closed' });
// 这个样式是私有的, 不会影响外部的元素
const style = document.createElement('style');
style.textContent = `
p {
color: green;
font-size: 20px;
}
`;
const p = document.createElement('p');
p.textContent = 'word count';
shadow.appendChild(style);
shadow.appendChild(p);
}
}
window.customElements.define('work-count', WordCount);
# 4.2.2 通过link引用外部样式
class WordCount extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'closed' });
// 这个样式是私有的, 不会影响外部的元素
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
const p = document.createElement('p');
p.classList.add('p-count');
p.textContent = 'word count';
shadow.appendChild(linkElem);
shadow.appendChild(p);
}
}
window.customElements.define('work-count', WordCount);
WARNING
请注意,因为link
元素不会打断 shadow root 的绘制,因此在加载样式表时可能会出现未添加样式内容(FOUC),导致闪烁。
# 5、使用 templates and slots
使用 template
和 slot
元素可以创建一个可以用来灵活填充 Web 组件的 shadow DOM 的模板。
当您必须在网页上重复使用相同的标记结构时,使用某种模板而不是一遍又一遍地重复相同的结构是有意义的。以前这是可行的,但 HTML template
元素使它更容易实现 (这在现代浏览器中得到了很好的支持)。此元素及其内容不会在 DOM 中呈现,但仍可使用 JavaScript 去引用它。
<template id="MyCard">
<div>username:</div>
</template>
<script>
const myCardTemplate = document.querySelector('#MyCard');
const content = myCardTemplate.content;
document.body.appendChild(content);
</script>
# 5.1 在Web Components 中使用
模板(Template)本身就是有用的,而与 web 组件(web component)一起使用效果更好。
<body>
<hello-world></hello-world>
<hello-world />
<template id="hello">
<h1>hello, world!</h1>
</template>
<script>
window.customElements.define(
'hello-world',
class extends HTMLElement {
constructor() {
super();
const helloTemp = document.querySelector('#hello');
const content = helloTemp.content.cloneNode(true);
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(content);
}
}
);
</script>
</body>
要注意的关键是我们使用
Node.cloneNode()
(opens new window) 方法添加了模板的拷贝到阴影的根结点上。
因为我们添加了模板的内容到 shadow DOM,所以我们可以加入一些样式信息到模板的 style
标签里,这些样式信息稍后会封装到自定义的元素中。如果只给它添加到一个标准的 DOM 中是不起作用的。
<body>
<hello-world></hello-world>
<hello-world />
<template id="hello">
<style>
h1.title {
color: red;
margin: 0;
text-align: center;
}
</style>
<h1 class="title">hello, world!</h1>
</template>
<script>
window.customElements.define(
'hello-world',
class extends HTMLElement {
constructor() {
super();
const helloTemp = document.querySelector('#hello');
const content = helloTemp.content.cloneNode(true);
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(content);
}
}
);
</script>
</body>
# 5.1.1 在template中使用style
<template id="hello">
<style>
:host {
display: block;
background-color: #ccc;
}
h1.title {
color: red;
margin: 0;
text-align: center;
}
</style>
<h1 class="title">
<slot>hello, world!</slot>
</h1>
</template>
这里面的style只会对内部元素生效, 不会影响外部的DOM。
TIP
上面代码中,<template>
样式里面的:host
伪类,指代自定义元素本身。
# 5.2 使用槽 (slots) 添加灵活度
尽管到这一步已经挺好了,但是元素仍旧不是很灵活。我们只能在里面放一点文本,甚至没有普通的 p 标签管用!
插槽由其name
属性标识,并且允许您在模板中定义占位符,当在标记中使用该元素时,该占位符可以填充所需的任何 HTML 标记片段。
查看下面的例子
<hello-world>
<span slot="text">HELLO, WORLD!</span>
</hello-world>
<hello-world />
<template id="hello">
<style>
h1.title {
color: red;
margin: 0;
text-align: center;
}
</style>
<h1 class="title">
<slot name="text">hello, world!</slot>
</h1>
</template>
<script>
window.customElements.define(
'hello-world',
class extends HTMLElement {
constructor() {
super();
const helloTemp = document.querySelector('#hello');
const content = helloTemp.content.cloneNode(true);
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(content);
}
}
);
</script>
在模板中定义slot标签<slot name="text">hello, world!</slot>
。 使用的时候填写填充内容<span slot="text">HELLO, WORLD!</span>
。
不设置slot的name属性的话, 就是默认插槽。
TIP
将能被插入到槽中的元素视为 Slotable
(en-US) (opens new window); 称已经插入到槽中的元素为 slotted.
# 6、传递attr给组件
除了可以通过slot来动态添加自定义内容时,通过元素的attr属性也能达到部分动态效果。
<person-card username="xdyuan" sex="男"></person-card>
// 获取内部的username元素
const userNameEl = content.querySelector('.username');
// 通过this.getAttribute('username')获取外部attr
userNameEl.textContent = this.getAttribute('username') || '';
完整例子如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<template id="person_temp">
<style>
.username {
color: red;
}
</style>
<h4>人员基本信息</h4>
<ul>
<li class="username"></li>
<li class="sex"></li>
</ul>
</template>
<person-card username="xdyuan" sex="男"></person-card>
<script>
class PersonCard extends HTMLElement {
constructor() {
super();
const content = document.querySelector('#person_temp').content.cloneNode(true);
// 获取内部的username元素
const userNameEl = content.querySelector('.username');
// 通过this.getAttribute('username')获取外部attr
userNameEl.textContent = this.getAttribute('username') || '';
const sexEl = content.querySelector('.sex');
sexEl.textContent = this.getAttribute('sex') || '';
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(content);
}
}
window.customElements.define('person-card', PersonCard);
</script>
</body>
</html>
配合生命周期, 查看2.1章节的demo。可以做到当外部attr变更时, 同步渲染变动的内容。
查看下面的例子, 我们使用一个get属性get label()
, 来保证我们每次都获取到最新的label属性值。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<template id="button_temp">
<button class="xd-button"></button>
</template>
<my-button label="click me"></my-button>
<script>
class MyButton extends HTMLElement {
constructor() {
super();
const content = document.querySelector('#button_temp').content.cloneNode(true);
const shadowRoot = this.attachShadow({ mode: 'closed' });
this._shadowRoot = shadowRoot;
this._shadowRoot.appendChild(content);
this.$button = this._shadowRoot.querySelector('.xd-button');
}
get label() {
return this.getAttribute('label') || '按钮';
}
static get observedAttributes() {
return ['label'];
}
attributeChangedCallback(name, oldVal, newVal) {
this.render();
}
render() {
this.$button.textContent = this.label;
}
}
window.customElements.define('my-button', MyButton);
const btn = document.querySelector('my-button');
setTimeout(() => {
btn.setAttribute('label', 'new click');
}, 1000);
</script>
</body>
</html>
不过这种往dom元素添加attr属性的方式在遇到需要传递数组和对象时,就不是很合适。通常我们可以采用下面的办法
get list() {
return this.getAttribute('list');
}
set list(newList) {
this.setAttribute('list', newList);
}
const btn = document.querySelector('my-button');
btn.list = [1, 2];
# 7、与用户互动
给封装的元素添加用户交互,可以通过attr或者prop的方式传递一个函数进去。但是最佳实践是通过addEventListener
给组件添加事件,在组件内部可以通过CustomEvent
触发事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<template id="button_temp">
<style>
.my-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: none;
margin: 0;
transition: 0.1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
}
.my-button:hover,
.my-button:focus {
color: #409eff;
border-color: #c6e2ff;
background-color: #ecf5ff;
}
.my-button:active {
color: #3a8ee6;
border-color: #3a8ee6;
outline: none;
}
.my-button.my-button--disabled {
color: #c0c4cc;
cursor: not-allowed;
background-image: none;
background-color: #fff;
border-color: #ebeef5;
}
</style>
<button class="my-button">
<slot></slot>
</button>
</template>
<my-button id="my_button">点我</my-button>
<script>
class MyButton extends HTMLElement {
constructor() {
super();
const _this = this;
const content = document.querySelector('#button_temp').content.cloneNode(true);
const shadowRoot = this.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(content);
this.$clickHandler = this.getAttribute('on-click');
this.$buttonEl = shadowRoot.querySelector('.my-button');
this.$onClick = function (e) {
const clickEvent = new CustomEvent('on-click', {
detail: e,
bubbles: true,
cancelable: true
});
// window[_this.$clickHandler](e);
_this.dispatchEvent(clickEvent);
};
this.$buttonEl.addEventListener('click', this.$onClick);
}
disconnectedCallback() {
this.$buttonEl.removeEventListener('click', this.$onClick);
}
}
window.customElements.define('my-button', MyButton);
</script>
<script>
const my_button = document.querySelector('#my_button');
my_button.addEventListener('on-click', e => {
console.log(e);
});
</script>
</body>
</html>
# 8、尝试封装一个下拉框组件
MySelect.js
const template = document.createElement('template');
template.innerHTML = `
<style>
.my-select {
position: relative;
width: 100%;
height: 30px;
box-sizing: border-box;
}
.trigger-btn {
width: 100%;
height: 100%;
border-radius: 2px;
border: 1px solid #eee;
box-sizing: border-box;
font-size: 16px;
line-height: 30px;
padding: 0 8px;
}
.select__input {
display: block;
height: 0;
position: absolute;
overflow: hidden;
border: none;
outline: none;
padding: 0;
}
.my-select--focus .trigger-btn {
border-color: #409eff;
box-sizing: border-box;
}
.select-options-wrapper {
position: absolute;
box-sizing: border-box;
left: 0;
right: 0;
top: 34px;
margin: 0;
padding: 0;
list-style: none;
overflow: auto;
max-height: 0px;
transition: max-height 0.5s ease;
border-radius: 2px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.select-item {
height: 28px;
box-sizing: border-box;
padding: 0 8px;
/* transition: all 0.3s; */
background-color: #fff;
color: #000;
font-size: 14px;
line-height: 28px;
}
.select-item.select-item--active {
color: #409eff;
}
.select-item:hover {
background-color: #eee;
}
</style>
<div class="my-select">
<div class="trigger-btn"></div>
<input class="select__input" type="text" />
<ul class="select-options-wrapper"></ul>
</div>`;
class MySelect extends HTMLElement {
constructor() {
super();
const content = template.content.cloneNode(true);
console.log(template, content);
const shadowRoot = this.attachShadow({ mode: 'closed' });
this._shadowRoot = shadowRoot;
this._shadowRoot.appendChild(content);
this.collapse = false;
this.optionElList = [];
this.$innerInput = this._shadowRoot.querySelector('.select__input');
this.inputBlurHandler = e => {
setTimeout(() => {
this.collapse = false;
this.triggerOptionsWrapper(this.collapse);
}, 100);
};
this.$innerInput.addEventListener('blur', this.inputBlurHandler);
this.$optionsWrapper = this._shadowRoot.querySelector('.select-options-wrapper');
this.transitionendHandler = e => {
e.stopPropagation();
};
this.$optionsWrapper.addEventListener('transitionend', this.transitionendHandler);
this.$triggerBtn = this._shadowRoot.querySelector('.trigger-btn');
this.triggerBtnClickHandler = e => {
this.collapse = !this.collapse;
this.triggerOptionsWrapper(this.collapse);
};
this.$triggerBtn.addEventListener('click', this.triggerBtnClickHandler);
this.selectItemClickHandler = e => {
e.stopPropagation();
const liEl = e.target;
const value = liEl.dataset.value;
const changeEvent = new CustomEvent('on-change', {
detail: value,
bubbles: true,
cancelable: true
});
// window[_this.$clickHandler](e);
this.dispatchEvent(changeEvent);
// this.triggerBtnClickHandler();
};
this.$optionsWrapper.addEventListener('click', this.selectItemClickHandler);
}
get options() {
const optionsStr = this.getAttribute('options') || '[]';
return JSON.parse(optionsStr);
}
get value() {
return this.getAttribute('value') || '[]';
}
static get observedAttributes() {
return ['options', 'value'];
}
connectedCallback() {}
disconnectedCallback() {
this.$optionsWrapper.removeEventListener('transitionend', this.transitionendHandler);
this.$optionsWrapper.removeEventListener('click', this.selectItemClickHandler);
this.$triggerBtn.removeEventListener('click', this.triggerBtnClickHandler);
}
attributeChangedCallback(name, oldVal, newVal) {
console.log('attributeChangedCallback', name, oldVal, newVal);
if (name === 'options') {
this.renderOptions();
}
if (name === 'value') {
this.setActiveLiEl();
}
}
renderOptions() {
this.optionElList = [];
this.options.forEach(option => {
const { label, value } = option;
const li = document.createElement('li');
li.classList.add('select-item');
li.setAttribute('data-value', value);
li.setAttribute('data-label', label);
li.textContent = label;
this.optionElList.push(li);
this.$optionsWrapper.appendChild(li);
});
this.setActiveLiEl();
}
setActiveLiEl() {
this.optionElList.forEach(liEl => {
liEl.classList.remove('select-item--active');
const value = liEl.dataset.value;
if (value == this.value) {
liEl.classList.add('select-item--active');
this.$triggerBtn.textContent = liEl.dataset.label;
}
});
}
triggerOptionsWrapper(status) {
console.log('triggerOptionsWrapper', status);
console.trace();
if (status) {
this.$optionsWrapper.style.maxHeight = '140px';
// this.$optionsWrapper.style.overflow = 'hidden';
this.$innerInput.focus();
} else {
this.$optionsWrapper.style.maxHeight = '0';
}
}
}
window.customElements.define('my-select', MySelect);
使用
<div style="width: 300px">
<my-select
id="select1"
value=""
options='[{"value":1,"label":"11"},{"value":2,"label":"22"},{"value":3,"label":"33"},{"value":4,"label":"44"},{"value":5,"label":"55"},{"value":6,"label":"66"}]'
>
</my-select>
</div>
<script src="./MySelect.js"></script>
<script>
const select1 = document.querySelector('#select1');
let selectValue = '2';
select1.setAttribute('value', selectValue);
select1.addEventListener('on-change', event => {
selectValue = event.detail;
select1.setAttribute('value', selectValue);
});
</script>
ruanyifeng (opens new window)、MDN (opens new window)、web-components-tutorial (opens new window)、https://web.dev/custom-elements-v1/ (opens new window)
← WebWorker 的使用 剪切板操作 →