compelem
v0.21.2
Published
A modern, reactive, fast, lightweight and flexible lib for building web components
Maintainers
Readme
CompElem
一个现代化、响应式、快速、轻量的WebComponent开发库。为开发者提供丰富、灵活、可扩展的声明式接口
概览
CompElem 基于 Class 进行构建,该模型允许开发者使用装饰器进行声明式编码并可使用继承、扩展等高级特性,其核心特性包括:
- 类JSX的原生模板系统
- 丰富的装饰器及指令
- 可选的生命周期
- 原生插槽系统
- 响应式域样式
- ...
创建一个WebComponent总会从声明一个组件元素(CompElem子类)开始
const Slogan = ['complete', 'componentize', 'compact', 'companion']
@tag("page-test")
export class PageTest extends CompElem {
//////////////////////////////////// props
@prop arg:any
@state colorR = Math.random() * 255 % 255 >> 0;
@state colorG = Math.random() * 255 % 255 >> 0;
@state colorB = Math.random() * 255 % 255 >> 0;
@state rotation = 0
//////////////////////////////////// computed
@computed
get color() {
return `linear-gradient(90deg,rgb(${this.colorR},${this.colorG},${this.colorB}), rgb(${255 - this.colorR},${255 - this.colorG},${255 - this.colorB}));`
}
//////////////////////////////////// watch
@watch('rotation')
function(nv:number) {
console.log(nv)
}
//////////////////////////////////// styles
//静态样式
static get css(): Array<string | CSSStyleSheet> {
return [`:host{
font-size:16px;
}
h2,p,i,h3{
...
background-image:var(--test-color);
filter:hue-rotate(var(--test-hue-rotate));
}
...`];
}
//动态样式变量
get cssVars() {
return {
testColor: this.color,
testHueRotate: `${this.rotation}deg`
}
}
@query('i[name="text"]')
text: HTMLElement
sloganIndex = 0
//////////////////////////////////// lifecycles
mounted(): void {
setInterval(() => {
this.rotation += 1
}, 24);
setInterval(() => {
this.text.classList.add('hide')
setTimeout(() => {
this.text.innerHTML = Slogan[this.sloganIndex % 4]
this.sloganIndex++
this.text.classList.remove('hide')
}, 500);
}, 5000);
}
render(): Template {
return html`<div>
<i>Welcome to</i>
<br>
<h2>CompElem</h2>
<br>
<i>A modern, reactive, fast and lightweight library</i>
<br>
<i>for building</i>
<h3>Web Components</h3>
<p>
<comp-element> <i name="text">...</i> </comp-element>
</p>
${this.arg}
</div>`
}
}而后即可在HTML中直接使用,与使用一个原生元素如DIV没有任何区别
<body>
<page-test arg="args..."></page-test>
</body>当然,也可以直接嵌入其他UI库中只要引入编译后的js即可
APIs
视图模板
使用
render()函数定义组件视图模板render(): Template{ return html`<div>Hello CompElem</div>` } //对于无需内部样式的组件(如容器类)可返回null(或跳过定义该方法),此时不会创建Shadow DOM render(){ return null }视图模板-属性表达式
通过再视图模板中插入表达式可以实现动态视图,表达式通过在不同位置使用分为不同类型见(#### 指令类型)。其中属性表达式根据前缀分为
| 前缀 | 描述 | 示例 | | ---- | ------------------------------------------------------------- | -------------------------------------- | | @ | 事件属性,可用于任何标签 |
<div @click="${this.onClick}">| | . | 参数属性,仅用于给组件标签传递参数 |<l-input .value="${this.text}">| | ? | 可选属性,用于 disabled/readonly 等 toggle 类属性 |<input ?disabled="${this.disabled}">| | * | 引用属性,表达式求值后才会设置该属性。常用于 SVG 相关属性设置 |<circle *r="${this.r}">|引用属性可通过属性参数进行格式转换,如
render(): Template{ return html`<svg *view-box:camel="">...</svg>`// <svg viewBox=""> }支持格式包括:
- camel 驼峰式
- kebab 短横线
- snake 下划线
无视图
对于无体组件无需定义渲染函数,常用于layout、grid等结构控制相关组件。无视图组件仅支持global及host样式,如
static get hostCss(): string { return ` l-main{ display: block; flex: 1; overflow: hidden; } `; }同样,无视图组件的renderRoot/renderRoots/...等属性都为空
样式
使用静态函数定义组件样式或全局样式(如弹框),对于包裹在上级组件内的伪类样式(如:hover)时,可通过hostCss进行指定
static get css(): Array<string | CSSStyleSheet> { return []; } static get globalCss(): string { return ''; } static get hostCss(): string { return ''; }对于需要响应组件状态变化并动态更新样式时可以通过css变量进行变更,该方法会自动追踪内部所有响应状态
get cssVars() { return { testColor: this.color, testHueRotate: `${this.rotation}deg` } }属性
属性是由组件外部提供参数的响应变量,可通过
@prop注解定义@prop({ type: Boolean }) loading = false;//显式定义属性类型 @prop round = true;//通过默认值自动推断属性类型 @prop({ type: [Boolean,String] }) round = true;//多种类型使用数组定义 @prop({ type: Array }) datalist: Array<string>;//没有默认值必须显式指定属性类型 @prop({ type: [String, Number], sync: true }) //通过get/set设置属性 get value() { return this.__innerValue ?? '' } set value(v: any) { this.__innerValue = v if (isNil(v)) { this.__innerValue = ''; } }属性可以在组件内修改但默认不会同步父组件,除非显式指定
sync或自行 emitupdate:xxx事件 全部注解参数见PropOption状态
状态是仅由组件内部初始化的响应变量,可通过
@state注解定义@state hasLeft = false;//定义状态 @state({//自定义变化判断 hasChanged(nv: any[], ov: any[]) { return isEqual(nv , ov) } }) private nodes: Array<Record<string, any>>;状态仅能在组件内修改 全部注解参数见
StateOption状态监视
使用
@watch注解可以对属性/状态进行变化监视@prop width = "auto"; @watch("width", { immediate: true }) watchWidth(nv: string, ov: string, sourceName: string) { this.style.width = nv; }对于同类属性共享处理逻辑的监视,可批量处理
@watch(['height', 'minHeight', 'maxHeight'], { immediate: true }) watchHeight(nv: string, ov: string, sourceName: string) { this.style[srcName] = nv; }计算状态
计算状态会缓存 return 结果,只有当内部使用的响应属性/状态发生变化时才会重新计算 使用
@computed注解的 Getter,如@computed get hasHeader() { return !isEmpty(this.slots.header) || !!this.header }节点引用
使用
@query/all注解及ref属性//query @query('l-icon') iconEl: HTMLElement //ref refNode: RefObjectref可用于内部DOM被移出但仍需访问的场景,比如tooltip、toast、overlay等
divRef = createRef<HTMLDivElement>(); //视图片段 return html` <l-icon></l-icon> <div ref="${divRef}"></div>`;内置属性及函数
readonlyrootComponent 根组件引用readonlyparentComponent 父组件引用,可能为空readonlywrapperComponent 包装(组件所在视图归属)组件引用,可能为空readonlyrenderRoot/renderRoots 渲染根元素/渲染根元素列表,可能为空readonlyshadowRoot 阴影DOM,可能为空readonlyslots 插槽元素映射readonlyslotHooks 动态插槽钩子映射readonlycssSheets 组件样式对象列表readonlyglobalCssSheet 通过静态getter创建的全局样式表对象,所有实例共享readonlyattrs 组件特性readonlyprops 组件属性readonlyisMounted 组件是否已挂载readonlyisDestroyed 组件是否已销毁- emit(evName: string, arg: Record<string, any>, event?: Event) 抛出自定义事件
- nextTick(cbk: () => void) 下一帧执行函数
- forceUpdate() 强制更新一次视图
- insertStyleSheet(sheet: string | CSSStyleSheet): CSSStyleSheet 向组件ShadowDOM插入样式表
- destroy() 销毁组件
组件渲染流程
CompElem 组件既可以在 CompElem 环境内调用,也可以直接在原生环境调用,区别只是原生环境无法像组件传递类型参数。流程如下:
创建流程
| 功能 | | 生命周期 | | ------------------------------------------------------------ | --- | ----------- | | 1. 创建组件实例,完成类属性默认值设置(prop/state/...) | | | | 2. 初始化类全局样式(仅一次)及 实例样式(产生 styles 数组) | | | | 3. 创建 shadowRoot 并挂载组件样式 | | | | 4. 执行装饰器 create 回调 | | constructor | | 5. 绑定 parentComponent | | | | 6. 验证及初始化 props | | propsReady | | 7. 初始化 states | | | | 8. @computed | | | | 9. @watch(immediate) | | | | 10. 渲染 render 及依赖绑定 | | render | | 11. 绑定 renderRoot 及 renderRoots | | | | 12. 初始化插槽 slots | | | | 13. 执行装饰器 beforeMount 回调 ,如 @query | | beforeMount | | 14. 初始化动态 css | | mounted | | 15. 执行装饰器 mounted 回调 | | |
更新流程【普通】
| 功能 | 生命周期 | | ------------------------------ | ------------ | | 1. 父组件 props 变更【或】 | propsReady | | 1. 子组件 states 变更【或】 | | | 2. 执行@watch 注解 | | | 3. 合并变更内容并判断是否更新 | shouldUpdate | | 4. 更新依赖域指令(非 render) | | | 5. 更新动态 slot | | | 6. 执行 ref | | | 7. 执行@query 注解 | update |
更新流程【强制】
| 功能 | 生命周期 | | ------------------ | -------- | | 1. forceUpdate | | | 2. 渲染 render | render | | 3. 执行@query 注解 | update |
插槽 Slot
定义插槽
使用原生 <slot></slot> 标签来嵌入插槽
<slot></slot>
<!-- 命名插槽 -->
<slot name="content"></slot>插入节点(静态)
<l-tooltip>
<l-button>默认插槽内容</l-button>
<div slot="content">命名插槽内容</div>
</l-tooltip>插入节点(动态)
- 通过
slot指令 仅可在 CompElem 组件中编码,可使用所有CompElem指令并可通过args接收插槽参数
//仅可用于CompElem组件中
return html` <l-tooltip>
${slot(
(args: Record<string, any>) => html`
<div>我是动态内容-${args.data.id}</div>
`,
"content"
)}
</l-tooltip>`;- 通过元素注入 可在非CompElem环境中使用,比如将组件嵌入到React/Vue环境中使用
<!-- 在 .vue 中 -->
...
<l-column>
<!-- CompElem 会赋值组件的data属性为 data -->
<VueComp slot="cell" data-slot-data/>
</l-column>
...
<!-- 对于驼峰变量可通过短横线分隔,下例会向组件注入 dataList 变量 -->
...
<!-- CompElem 会赋值组件的dataList属性为 data -->
<VueComp slot="cell" data-slot-data-list="data"/>
...动态插槽的参数可在组件内的 slot 标签上通过 视图模板-参数表达式 .xx 注入参数
<slot .data="${this.row}"></slot>指令 Directive
指令用于分支/循环/动态插槽等结构及隐藏显示等
指令类型
不同的指令类型限制了指令仅能用于对应的插入点
|指令|插入位置|描述|示例|
|-------|-------|-------|-------|
|ATTR|特性|可用于任何特性值之中,仅能插入一个|attr="${xxx}"|
|PROP|属性|可用于任何属性值之中,必须是组件标签,仅能插入一个|.value="${xxx}"|
|TEXT|文本|标签体内,可插入多个|<div>${xx1}${xx2}${...}</div>|
|CLASS|样式类|用于 class 属性值中,仅能插入一个|class="a ${b}"|
|STYLE|样式规则|用于 style 属性值中,仅能插入一个|style="a:1;${b}"|
|SLOT|插槽|与 TEXT 类似,但标签必须是组件||
|TAG|标签|直接插入在节点标签上|<div a="b" ${show(..)}>|
内置指令
| 指令 | 类型 | 描述 | 示例 |
| ------- | --------- | ----------------------------------------------------------- | ------------------------------------------------------ |
| bind | TAG | 绑定属性/特性到标签上,根据标签类型及组件 prop 定义自动判断 | <div a="b" ${bind(obj)}> |
| show | TAG | 隐藏/显示标签(基于 display) | <div a="b" ${show(visible)}> |
| model | TAG | 双向绑定 | <div a="b" ${model(xx,modelPath?)}>
| sync | PROP | 类似Model,实现属性的同步跟踪 | <l-dialog .visible="${sync(this.visible)}"> |
| classes | CLASS | 绑定样式类属性,支持对象/数组/字符串。可以和静态字符混用 | <div class="otherClass ${classes(obj)}>" |
| styles | STYLE | 绑定样式规则属性,支持对象/字符串。可以和静态字符混用 | <div style="a:b;${styles(obj)}>" |
| forEach | TEXT/SLOT | 输出循环结构 | ...>${forEach(ary,(item)=>html...)}<... |
| ifTrue | TEXT/SLOT | 当条件为 true 时输出模板内容 | ...>${ifTrue(condition,()=>html...)}<... |
| ifElse | TEXT/SLOT | 当条件为 true/false 时输出对应模板内容 | ...>${ifElse(condition,()=>html``,()=>html``)}<... |
| when | TEXT/SLOT | 多条件分支,支持 switch/ifelse 两种模式 | ...>${when(condition,{c1:()=>html``,c2:...})}<... |
| slot | SLOT | 动态插槽 | ...>${slot((args) => html``)}<... |
| htmlD | TAG | 向指定元素插入HTML内容 | <div a="b" ${htmlD('<b>1</b>')}> |
| htmlC | TEXT/SLOT | 向指定文本位置插入指定HTML内容 | ...>${htmlC('<b>1</b>')}><... |
装饰器 Decorator
- @state 定义组件内状态属性。可选参数{prop},可指定 propName 初始化值
- @prop 定义父组件参数,默认不可修改。可选参数{type,required,sync,getter,setter}
设置 getter/setter 后,该属性的
@watch将会失效 - @query/queryAll 定义 CssSelector 查询结果
- @tag 自定义组件的标签名
- @watch 监控 state/prop 变更
- @computed 计算属性,仅在响应变量变更时更新缓存值
- @debounced 定义函数防抖
- @event 定义事件,支持修饰符
- @onced 定义一次性事件
- @throttled 定义节流函数
继承
部分指令可由子类继承不会覆盖,包括@state/@prop/@watch/@computed
事件
在CompElem中有三类不同事件,分别返回原生事件对象及自定义对象
- 原生事件 ——
<div @click="..."在原生元素上可以监听原生事件,监听器回调参数返回原生事件对象 - 扩展原生事件 ——
<div @resize="..."在原生元素/组件元素上都可以监听扩展原生事件,监听器回调参数返回自定义数据对象 - 组件事件 ——
<l-select @change="..."在组件元素上默认仅可监听组件事件,监听器回调参数返回自定义数据对象
组件原生事件
如果想要在组件上监听原生事件如click等,需要使用native关键字
<l-select @click.native="${...}"></l-select>此时click事件监听器参数返回原生事件对象
跨框架事件监听
如果组件要用于非 CompElem 框架时,需要为组件添加 emit-native属性,这样框架会将组件事件转为CustomEvent
<l-select @change="${...}" emit-native></l-select>事件修饰符
通过.号可使用修饰符修饰事件如 @scroll.throttle:100="${...}"
- 全部通用 ——
debounce/throttle/once可组合 - 原生通用 ——
stop/prevent/self可组合 - 鼠标 ——
left/right/middle不可组合 - 键盘 ——
ctrl/alt/shift/meta可组合esc/a/b/c/d...不可组合,多个key并列表示可选 - mutate ——
attr/child/char/tree可组合
部分修饰符支持参数,可使用冒号传参 ——
throttle:100 / debounce:100。支持列表如下
- throttle:wait
- debounce:wait
<!-- 示例 -->
<div @mutate.child.throttle:100="...">
<div @click.left.once="...">
<div @mousedown.stop.prevent="...">扩展原生事件
可直接用于DOM元素的扩展事件(非组件事件),支持列表如下
- resize 元素尺寸变化时触发
- outside 鼠标点击元素外部时触发,可通过修饰符限制鼠标点击类型
outside.[mousedown/up/click/dblclick],默认click - mutate 内容变化时触发。基于 Mutation Observer API
无视图组件事件
当组件无内部视图或想要监听组件自身元素时,可使用@event装饰器
class CustomButton{
...
@event('click.native', (comp) => comp)
onClick() {
this.emit('click')
}
}