Building Components Custom Elements 自定义元素

首先来写一个示例吧

Web Components 标准非常重要的一个特性是,它使开发者能够将HTML页面的功能封装为 custom elements(自定义标签),而往常,开发者不得不写一大堆冗长、深层嵌套的标签来实现同样的页面功能。这篇文章将会介绍如何使用HTML的custom elements。 - MDN

https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements

具体看MDN文档,(复制完应该就差不多了 然后去这里玩)

https://developers.google.com/web/fundamentals/web-components/

查看demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
<style for-run for-show>
.popup-info {
width: 100px;
height: 100px;
position: relative;
top: 100px;
left: 300px;
}
.popup-info1 {
width: 100px;
height: 100px;
position: relative;
top: 160px;
left: 600px;
}
.popup-info img {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.popup-info .info {
font-size: 20px;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 4px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 110px;
left: 0px;
z-index: 3;
}
.popup-info:hover .info{
opacity: 1;
}
</style>

<template for-run for-show>

<popup-info role="popup" img="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=4038933139,466219061&fm=26&gp=0.jpg" data-text="I Learn Js 💗"></popup-info>
<button id="remove">remove</button>
<button id="move">move</button>
<button id="toggle">toggle attribute</button>


<popup-info1 role="popup" img="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=4038933139,466219061&fm=26&gp=0.jpg" data-text="I Learn Js 💗"></popup-info1>

<button is="popup-button">popup-button!</button>

</template>

<script for-run for-show>
class popupInfo extends HTMLElement {
constructor() {
super()

}
static get observedAttributes() {
return ['role']
}
// 生命周期钩子
connectedCallback() {
// 创建 div
let div = document.createElement('div')
div.setAttribute('role', 'popup-info')

// 创建 img
let img = document.createElement('img')
let src = this.getAttribute('img')

// 创建info信息
let info = document.createElement('span')
let text = this.getAttribute('data-text')

// 设置img内容
img.src = src
img.alt = text

// 设置info内容
info.classList.add('info')
info.innerText = text

// 添加class
div.classList.add('popup-info')

// 添加内容 img info
div.appendChild(img)
div.appendChild(info)

this.appendChild(div)
}
disconnectedCallback() {
console.log('我没有了呀')
}
attributeChangedCallback(attrName, oldVal, newVal) {
console.log('当前的属性:', attrName)
console.log('当前的属性 old:', oldVal)
console.log('当前的属性 new:', newVal)
}
adoptedCallback() {
console.log('adoptedCallback 自定义元素被移入新的 document')
}
}

customElements.define('popup-info', popupInfo)


// 测试 自定义元素被移入新的 document
const testAdoptedCallback = ()=> {
// 创建 iframe
const createWindow = () => {
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe.contentWindow
}
let cw = createWindow()
// 创建 自定义元素
let cw1 = document.querySelector('popup-info')

// 创建的元素插入到 新创建的iframe
cw.document.body.appendChild(cw1)
}

const move = () => {
let move = document.querySelector('#move')
move.onclick = function() {
testAdoptedCallback()
}
}
move()

// 测试移除 popup-info
const remove = () => {
let remove = document.querySelector('#remove')
remove.onclick = function() {
document.querySelector('popup-info').remove()
}
}
remove()

// 测试属性被修改 toggle
const toggle = () => {
let i = 0
let toggle = document.querySelector('#toggle')
toggle.onclick = function() {
i++
document.querySelector('popup-info').setAttribute('role', `popup-info-change, ${i}`)
}
}
toggle()



class popupInfo1 extends popupInfo {
constructor() {
super()
}
connectedCallback() {
this.innerText = 'extends popupInfo'
}
}

customElements.define('popup-info1', popupInfo1)


class popupButton extends HTMLButtonElement {
constructor() {
super()
}
connectedCallback() {
this.innerText += ' extends'
}
}

customElements.define('popup-button', popupButton, {extends: 'button'})

</script>

定义新元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class popupInfo extends HTMLElement {
constructor() {
super()
}
static get observedAttributes() {
return ['role']
}
// 生命周期钩子
connectedCallback() {
// 创建 div
let div = document.createElement('div')
div.setAttribute('role', 'popup-info')

// 创建 img
let img = document.createElement('img')
let src = this.getAttribute('img')

// 创建info信息
let info = document.createElement('span')
let text = this.getAttribute('data-text')

// 设置img内容
img.src = src
img.alt = text

// 设置info内容
info.classList.add('info')
info.innerText = text

// 添加class
div.classList.add('popup-info')

// 添加内容 img info
div.appendChild(img)
div.appendChild(info)

this.appendChild(div)
}
disconnectedCallback() {
console.log('我没有了呀')
}
attributeChangedCallback(attrName, oldVal, newVal) {
console.log('当前的属性:', attrName)
console.log('当前的属性 old:', oldVal)
console.log('当前的属性 new:', newVal)
}
adoptedCallback() {
console.log('adoptedCallback 自定义元素被移入新的 document')
}
}

customElements.define('popup-info', popupInfo)

1
2
<popup-info role="popup" img="https://miro.medium.com/max/1440/1*LjR0UrFB2a__5h1DWqzstA.png" data-text="I Learn Js 💗"></popup-info>

定义一个自定义的元素 然后使用

扩展元素

扩展自定义元素

customElements 全局性用于定义自定义元素

customElements.define(),并使用 JavaScript class 扩展基础 HTMLElement

1
2
3
4
5
6
7
8
9
10
class popupInfo1 extends popupInfo {
constructor() {
super()
}
connectedCallback() {
this.innerText = 'extends popupInfo'
}
}

customElements.define('popup-info1', popupInfo1)

继承自定义元素好像效果有点不对(我还没找到为啥 希望大佬教教我)

有关创建自定义元素的规则

  • 自定义元素的名称必须包含短横线 (-)。因此, 等均为有效名称,而 则为无效名称….
  • 您不能多次注册同一标记…
  • 自定义元素不能自我封闭标签

教程里面更详细

和上面很相识, 扩展了自定义的元素

扩展原生 HTML 元素

1
2
3
4
5
6
7
8
9
10
class popupButton extends HTMLButtonElement {
constructor() {
super()
}
connectedCallback() {
this.innerText += ' extends'
}
}

customElements.define('popup-button', popupButton, {extends: 'button'})

要扩展元素,您需要创建继承自正确 DOM 接口的类定义

扩展原生元素时,对 define() 的调用会稍有不同。所需的第三个参数告知浏览器要扩展的标记。

这很有必要,因为许多 HTML 标记均使用同一 DOM 接口

例如 section address 和 em(以及其他)都使用 HTMLElement;q 和 blockquote 则使用 HTMLQuoteElement;等等。

指定 {extends: ‘blockquote’} 可让浏览器知道您创建的是 blockquote 而不是 q。有关 HTML DOM 接口的完整列表,请参阅 HTML 规范。

1
2
3
class popupButton extends HTMLButtonElement {}

customElements.define('popup-button', popupButton)

自定义元素响应

自定义元素可以定义特殊生命周期钩子

名称 描述
constructor 创建或升级元素的一个实例。用于初始化状态、设置事件侦听器或创建 Shadow DOM…
connectedCallback 元素每次插入到 DOM 时都会调用…
disconnectedCallback() 元素每次从 DOM 中移除时都会调用…
attributeChangedCallback(attrName, oldVal, newVal) 属性添加、移除、更新或替换…
adoptedCallback(attrName, oldVal, newVal) 自定义元素被移入新的 document…

https://developers.google.com/web/fundamentals/web-components/customelements#extendhtml

https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements#%E4%BD%BF%E7%94%A8%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0

上面写得很清晰 比我这个容易理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 生命周期钩子
connectedCallback() {
// 创建 div
let div = document.createElement('div')
div.setAttribute('role', 'popup-info')

// 创建 img
let img = document.createElement('img')
let src = this.getAttribute('img')

// 创建info信息
let info = document.createElement('span')
let text = this.getAttribute('data-text')

// 设置img内容
img.src = src
img.alt = text

// 设置info内容
info.classList.add('info')
info.innerText = text

// 添加class
div.classList.add('popup-info')

// 添加内容 img info
div.appendChild(img)
div.appendChild(info)

this.appendChild(div)
}

dom创建时候 创建内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
disconnectedCallback() {
console.log('我没有了呀')
}
...

// 测试移除 popup-info
const remove = () => {
let remove = document.querySelector('#remove')
remove.onclick = function() {
document.querySelector('popup-info').remove()
}
}
remove()

dom 移除执行的方法 可以移除事件什么的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
attributeChangedCallback(attrName, oldVal, newVal) {
console.log('当前的属性:', attrName)
console.log('当前的属性 old:', oldVal)
console.log('当前的属性 new:', newVal)
}
...
// 测试属性被修改 toggle
const toggle = () => {
let i = 0
let toggle = document.querySelector('#toggle')
toggle.onclick = function() {
i++
document.querySelector('popup-info').setAttribute('role', `popup-info-change, ${i}`)
}
}
toggle()

需要注意的是,如果需要在元素属性变化后,触发 attributeChangedCallback()回调函数,你必须监听这个属性。这可以通过定义observedAttributes() get函数来实现,observedAttributes()函数体内包含一个 return语句,返回一个数组,包含了需要监听的属性名称:

1
2
3
4
5

static get observedAttributes() {
return ['role']
}

监听属性变化 调用的方法 比如 role=”popup” 什么的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
adoptedCallback() {
console.log('adoptedCallback 自定义元素被移入新的 document')
}
...

// 测试 自定义元素被移入新的 document
const testAdoptedCallback = ()=> {
// 创建 iframe
const createWindow = () => {
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe.contentWindow
}
let cw = createWindow()
// 创建 自定义元素
let cw1 = document.querySelector('popup-info')

// 创建的元素插入到 新创建的iframe
cw.document.body.appendChild(cw1)
}

const move = () => {
let move = document.querySelector('#move')
move.onclick = function() {
testAdoptedCallback()
}
}
move()

自定义元素被移入新的 document

元素定义的内容

1
2
3
4
5
6
7
8
9
class popupInfo extends HTMLElement {
constructor() {
super()

}
connectedCallback() {
this.innerHTML = "xxxxxx";
}
}

以新内容覆盖元素的子项并非一种好的做法,因为这样做会不符合设想。
添加元素定义内容的更好做法是使用 shadow DOM,下一篇文章会写

1
2
3
4
5
6
7
8
9
10
11
class popupInfoShadow extends HTMLElement {
constructor() {
super()

let shadowRoot = this.attachShadow({mode: 'open'})
shadowRoot.innerHTML = "<span>hello</span>"
}

}

customElements.define('popup-info-shadow', popupInfoShadow)

通过 template 创建元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<info-template></info-template>

<template id="info-template">
<style>
p {
color: red;
}
</style>
<p>hello template</p>

</template>


<script>
class infoTemplate extends HTMLElement {
constructor(){
super()

let shadowRoot = this.attachShadow({mode: 'open'})
const t = document.querySelector('#info-template')
const instance = t.content.cloneNode(true)
shadowRoot.appendChild(instance)
}
}

customElements.define('info-template', infoTemplate)
  • 我们在 HTML 中定义新的元素:info-template
  • 元素的 Shadow DOM 使用 template 创建
  • 由于是 Shadow DOM,元素的 DOM 局限于元素本地
  • 由于是 Shadow DOM,元素的内部 CSS 作用域限于元素内

设置自定义元素样式

1
2
3
4
5
<style>
p {
...
}
</style>

:defined CSS 伪类 表示任何已定义的元素。这包括任何浏览器内置的标准元素以及已成功定义的自定义元素 (例如通过 CustomElementRegistry.define() 方法)。

https://developer.mozilla.org/zh-CN/docs/Web/CSS/:defined

1
app-drawer:not(:defined) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
info-template {
color:red;
opacity: 0;
}
aa-bb:not(:defined) {
color:red;
opacity: 0;
}
</style>

<info-template></info-template>
<info-template>123</info-template>

<aa-bb>123</aa-bb>

定义之前会隐藏起来哦

其他详情

未知元素与未定义的自定义元素

HTML 使用起来非常宽松和灵活。例如,在页面上声明 randomtagthatdoesntexist,浏览器将非常乐意接受它。为什么非标准标记可以生效?答案在于 HTML 规范允许这样。规范没有定义的元素作为 HTMLUnknownElement 进行解析。

自定义元素则并非如此。如果在创建时使用有效的名称(包含“-”),则潜在的自定义元素将解析为 HTMLElement。 您可以在支持自定义元素的浏览器中核实这一点。打开控制台:Ctrl+Shift+J(或者在 Mac 上,Cmd+Opt+J)并粘贴下列代码行:

1
2
3
4
5
// "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)

在浏览器中定义新的自定义元素。

1
2
3
customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

抽离出来可以更利于阅读

get(tagName)

1
2
3
4
5
6
7
8
9
10
11
12
let Drawer = customElements.get('app-drawer');
console.log(Drawer)
if (Drawer) {
let drawer = new Drawer();
}


let PopupInfo = customElements.get('popup-info');
console.log(PopupInfo)
if (PopupInfo) {
let popupInfo = new PopupInfo();
}

在给定有效自定义元素标记名称的情况下,返回元素的构造函数。

如果没有注册元素定义,则返回 undefined。

whenDefined(tagName)

1
2
3
4
5
6
7
8
9
customElements.whenDefined('popup-info').then(() => {
console.log('popup-info ready!');
});

customElements.whenDefined('app-drawer').then(() => {
console.log('app-drawer ready!');
}).catch(err => {
console.log('err', err)
})

如果定义了自定义元素,则返回可解析的 Promise。如果元素已定义,则立即得到解析。

如果标记名称并非有效自定义元素名称,则拒绝(好像也不会走catch)

历史记录和浏览器支持

如果您最近几年持续关注网络组件,您应知道 Chrome 36+ 实施的自定义元素 API 版本使用了 document.registerElement() 而不是 customElements.define()。

但前者是标准的弃用版本,称为 v0。customElements.define() 成为现行标准并逐步获得各大浏览器厂商的支持。这称为自定义元素 v1。

https://developers.google.com/web/fundamentals/web-components/customelements

浏览器支持

要检测自定义元素功能,检测是否存在 window.customElements:

1
const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

https://www.npmjs.com/package/@webcomponents/custom-elements

注:无法对 :defined CSS 伪类执行 polyfill。 (没测试过)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const supportsCustomElementsV1 = 'customElements' in window;
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}

if (!supportsCustomElementsV1) {
loadScript('https://unpkg.com/@webcomponents/custom-elements')
console.log('use polyfill')
} else {
// Native support.Good to go.
console.log('native')
}

总结:

自定义元素提供了一种新工具,可让我们在浏览器中定义新 HTML 标记并创建可重用的组件。
它们与 Shadow DOM 和 template 等新平台原语结合使用,我们可开始实现更多的可能

  • 创建和扩展可重复使用组件的跨浏览器(网络标准)
  • 无需库或框架即可使用。原生 JS/HTML 威武!
  • 提供熟悉的编程模型。仅需使用 DOM/CSS/HTML。
  • 与其他网络平台功能良好匹配(Shadow DOM、template、CSS 自定义属性等)
  • 与浏览器的 DevTools 紧密集成。
  • 利用现有的无障碍功能。
  • (我也是复制的 2333)

https://developers.google.com/web/fundamentals/web-components/customelements

https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements