前言
Web Components 允许开发者创建可复用的自定义元素,由于是浏览器的原生组件,能够无缝对接到各种前端框架。 这个系列将介绍如何使用原生html+js+css开发 Web Components原生组件并作为组件库的一员使用 。
1.目录准备
我们的项目使用以下文件目录结构来组织UI组件库
ui-component/
├── component1/
│ └── component1.html
│ └── index.js
├── component2/
│ └── component2.html
│ └── index.js
└── index.js
其中,ui-component 是 UI 组件的根目录,下面的 component1、component2 和 component3 是不同的 UI 组件目录,每个组件目录下的 index.js 文件包含该组件的js实现代码, html文件则是该组件的html模板。ui-component目录下的index.js 文件则是UI组件的导出文件。
如果是vue项目,可以在main.js中直接引入ui-component目录下的index.js,也可以打包成插件使用。
2. 第一个自定义组件--Input组件
第一个自定义组件就用input吧,最常的form元素之一。
在ui-component下新建对应目录,userInput.html建立模板内容。(虽然简单的组件的html部分可以在js文件中直接拼接进去,但是复杂一点的组件需要一定的html和css,完全采用js中拼接字符串比较麻烦,所以抽取专门的模板文件)。
<style>
.user-input{
border: 0;
border-bottom: 1px solid #D7D7D7;
min-width: 30px;
width: 100%;
height: 26px;
line-height: 26px;
font-size: 14px;
}
.user-input:focus{
outline: none;
border-bottom: 1px solid #1296db;
}
</style>
<div><input type="text" class="user-input"/></div>
在userInput.js下自定义组件,将userInput.html引入:
import templatePanel from './userInput.html'
const template = document.createElement('template')
template.innerHTML = templatePanel
export default class InputNumber extends HTMLElement {
static observedAttributes = ['name','value'];
constructor() {
super();
// 将userInput.html作为模板内容引入到自定义组件中
this._shadowRoot.appendChild(template.content.cloneNode(true));
}
static get observedAttributes() {
return ['name','value'];
}
attributeChangedCallback(name, oldValue, newValue) {
}
connectedCallback() {
}
}
if (!customElements.get('user-input')) {
// Register
customElements.define('user-input', UserInput);
}
记得在index.js中导出userInput
import './userInput/userInput.js'
此时可以像正常html标签一样直接使用,如<user-Input></user-Input>,也可以在js文件中通过new userInput()创建一个自定义元素,然后通过dom方法append到对应元素下。
3.进一步扩展
此时一个简单的input组件已经可以使用了,但是作为组件库的一员,它还需要继续扩展如下功能:
- 监听属性的变化,同步更新内容
- 将元素标记为一个表单关联的自定义元素,能在form中像原生input一样使用
- 对外抛出的事件
3.1监听属性变化
在 Web Components 里,我们可以借助 `observedAttributes` 静态方法与 `attributeChangedCallback` 生命周期方法来监听属性的变化。
observedAttributes :此方法返回一个数组,数组中的元素为需要监听的属性名。在上述示例中,return['name','value'] 表示要监听 'name','value' 属性的变化。
attributeChangedCallback :
当被监听的属性发生变化时,该方法会被调用。它接收三个参数:name(发生变化的属性名)、oldValue(属性的旧值)和 newValue(属性的新值)。比如当value值从外部改变时,需要修改对应dom中的值,这里为了避免每次都刷新dom的消耗,处理为值跟上一次不一样才更新dom。
attributeChangedCallback(name, oldValue, newValue) {
// 当观察的属性发变化时调用
if (newValue === oldValue && newValue !== 'undefined') {
return;
}
if (name === 'value) {
this.value= (newValue === "undefined" || newValue === undefined) ? "" : newValue;
this._shadowRoot.querySelector(".user-input").value = this.value;
}
}
3.2将元素标记为一个表单关联的自定义元素
代码如下
static formAssociated = true;
constructor() {
...
this.value;
this.value_;
// 获得访问内部表单控件 API 的能力
this.internals_ = this.attachInternals();
this._shadowRoot = this.attachShadow({mode: 'open'});
this._shadowRoot.appendChild(template.content.cloneNode(true));
}
// 表单控件通常暴露一个“value”属性
get value() {
return this.value_;
}
set value(v) {
this.value_ = v;
}
// 提供它们有助于确保与浏览器提供的控件保持一致。
get form() {
return this.internals_.form;
}
3.3对外抛出的事件
当input组件的值变化时需要通知外面,这里我们正常监听input元素的input事件,blur事件和enter事件。 connectedCallback() 是在 custom element 首次被插入文档 DOM 时被调用的。这个回调函数通常用于执行一些初始化操作,比如添加事件监听器、请求数据等等。在这个时候,元素已经被添加到了文档中,可以访问到 DOM 和其他元素。
connectedCallback() {
// 当元素被插入到DOM时调用
// 监听输入值,失去焦点时触发handleChange
this._shadowRoot.querySelector(".user-input").addEventListener('blur', this.handleChange);
// 输入回车触发handleChange
this._shadowRoot.querySelector(".user-input").addEventListener('keyup',this.handleEnter);
this._shadowRoot.querySelector(".user-input").addEventListener('input', this.handleChange);
}
handleChange =(e) => {
this.value = e.target.value;
this.internals_.setFormValue(this.value_);
this.dispatchEvent(new UIEvent('change'));
}
此时<user-Input></user-Input>就可以像一个原生input一样使用,也可以在form表单中和原生的input一样获取值。
当然还可以继续扩展,比如表单中的验证规则,不同类型的input等等...这里就不继续展开了