Custom Elements
自定义元素 判断当前浏览器是否支持自定义元素
if( 'customElements' in window )
customElements 是一个浏览器window下的全局变量工具
第二个参数接收一个class类,这个class类要继承元素构造器 class extends HTMLElement { }
继承其他web Components
假如我们要二次封装别人的 Web Components
,我们可以定义自定义元素名后,继承原 Web Components
用 customElements.get( ) 获取已经定义的原 Web Components
如👇
customElements.define('my-p', class extends customElements.get('origin-ui-p') {
constructor() {
super()
// 此时写入的this操作将是基于原组件做的额外操作
this.onclick = () => alert('基于原组件的额外点击事件')
}
})
为什么强制要求命名带-
浏览器解析html,从上往下执行,我们常常要求在dom元素前先引入css资源,在dom元素后引入js资源,是为了让页面在无视js先渲染出内容
那么当出现了js定义的自定义元素标签时,js还能写在dom元素后面吗? 是可以的
html遇到带 -
的标签,并且不是原生标签将会跳过渲染并认为是未定义的标签,而不是报错 而不带 -
的标签将一律认为是原生标签,如果是不认识的标签将会报错
伪类选择器 选中未定义的自定义标签
上面说到浏览器会跳过渲染未定义的标签 这里有个伪类选择器可以选中已定义的元素标签
:defined { }
,进行样式编写
那么假如我们要选中未定义的元素标签(自定义元素) :not(:defined) {}
这样我们就可以给这个 Web Components
做一个加载效果
<html>
<head>
<meta charset="utf-8">
<title>demo</title>
<style>
body { display: grid; place-items: center; }
:not(:defined) {
width: 120px;
height: 20px;
background: gray linear-gradient(60deg, transparent, transparent 20%, white 40%, transparent 60%) 0/300%;
border-radius: 4px;
animation: loading 2s infinite;
}
@keyframes loading {
to { background-position: 300% 0 }
}
</style>
</head>
<body>
<my-p></my-p>
<script>
setTimeout(()=>{
customElements.define('my-p', class extends HTMLElement {
constructor() {
super()
this.innerHTML = '<p>Web Components</p>'
}
})
},3000)
</script>
</body>
</html>
效果如👇
customElements.whenDefined
👆 已经用到了
customElements
上的defind
方法创建定义自定义标签,它还有其他的几个方法可以使用 因为不算常用,暂时先不仔细研究,官方文档也不算详细,先有个印象
自定义标签生命周期
customElements.define('my-p', class extends HTMLElement {
// 相当于vue3的setup
constructor() {
super()
this.innerHTML = '<p>Web Components</p>'
}
// 相当于vue的mounted
connectedCallback(){
console.log('connectedCallback')
}
// 相当于vue3的unmounted
disconnectedCallback(){
console.log('disconnectedCallback')
}
adoptedCallback(){
console.log('adoptedCallback')
}
})
connectedCallback
当 WebComponents 第一次被挂在到 dom 上是触发的钩子,并且只会触发一次。类似 Vue 中的 mounted React 中的 useEffect(() => {}, []),componentDidMount。
disconnectedCallback
当自定义元素与文档 DOM 断开连接时被调用。
adoptedCallback
adopted: 收养、采用 当自定义元素被移动到新文档时被调用
如在 dom操作运行我们移动iframe的元素到主文档document Document.adoptNode( ) -MDN 效果类似剪切 此时剪切的dom元素是自定义元素标签的话,将会触发自定义标签的adoptedCallback
生命周期
attributeChangedCallback
attributeChangedCallback 也是自定义自定义属性的生命周期,但是我们单独来看 字面意思,属性变化时触发 类似vue的watch 注意它不是元素的prop 注意它的触发时机比
connectedCallback
挂载要早,可以用oldVal
有没有值判断是否未挂载
本生命周期起作用的前提是先定义需要监听的属性名static get observedAttributes()
,因为标签上的预置属性是非常多的 这样的写法就会非常像vue的watch生效需要先在data声明
👇 实现 input
属性的 placeholder
<html>
<head>
<title>demo</title>
</head>
<body>
<my-input placeholder="请输入"></my-input>
<script>
customElements.define('my-input', class extends HTMLElement {
// 类似vue的watch监听前需要在data中声明
static get observedAttributes() { return ['placeholder'] }
// 这个set是为了实现直接操作dom.placeholder = xx 也生效
// 不加这个方法,则js只能通过dom.setAttribute()实现修改
set placeholder(val) {
this.querySelector('input').setAttribute('placeholder',val)
}
constructor() {
super()
this.innerHTML = '<input>'
}
// 查看attributeChangedCallback 和 connectedCallback 顺序
connectedCallback() {
console.log('connectedCallback')
}
// 可以用 oldVal 有没有值判断是否未挂载
attributeChangedCallback(key, oldVal, newVal) {
console.log('触发attributeChangedCallback', key, oldVal, newVal)
if(key === 'placeholder') {
this.querySelector('input').setAttribute('placeholder',newVal)
}
}
})
</script>
</body>
</html>
继承其他原生元素dom
👆 的placeholder示例中,我们需要手动选中内部的真实 input 标签来设置placeholder 我们现在希望我们的自定义标签就是input标签,它自带着placeholder功能
<html>
<head>
<title>demo</title>
</head>
<body>
<input is="my-input" placeholder="请输入"></input>
<script>
customElements.define('my-input', class extends HTMLInputElement {
constructor() {
super()
this.disabled = true
}
}, { extends: 'input' })
</script>
</body>
</html>
👆的自定义标签定义的时候,不再继承HTMLElement
而是 HTMLInputElement
其他具体的标签都是类似的做法,他们的本质都是继承自 HTMLElement
并且需要写上第3个参数, { extends: 'input' }
注意这种写法,safari
不支持 Safari不支持build-in自定义元素的兼容处理 引入 polyfill
的 custom-elements-builtin
js库来兼容safari,但是还有些细微操作,详见👆的文章