前言
NG-NEST介绍
今天我们来看一个表单组件 Switch 开关是怎么实现的。主要是对自定义表单组件中需要用到 ControlValueAccessor 这个的说明。
一般实现一个自定义的表单组件我们需要2个步骤,注册NG_VALUE_ACCESSOR
提供者,实现ControlValueAccessor
接口
![e97a981461f53686b70327d70171a375.gif](https://i-blog.csdnimg.cn/blog_migrate/4f07f49cdd0ee7ffcb681a064b142f6f.gif)
功能分析
- 点击启用,再次点击关闭,根据状态展示不同的样式
- 可以通过
[(ngModel)]
双向绑定值 - 包含标签的不同布局方式
- 禁用样式
代码实现
先看文件 lib/ng-nest/ui/switch/switch.component.html :
<div
#switch
class="x-switch"
[class.x-flex]="justify || align || direction"
[class.x-checked]="value"
[class.x-disabled]="disabled"
>
<label *ngIf="label" [style.width]="labelWidth" [ngClass]="labelMap">{{ label }}</label>
<div class="x-switch-row">
<div class="x-switch-slider" (click)="switchClick()"></div>
</div>
</div>
这个比较简单,根据不同参数绑定了不同的样式,重点在对应的 switch.component.ts 文件:
@Component({
selector: `${XSwitchPrefix}`,
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [XValueAccessor(XSwitchComponent)]
})
export class XSwitchComponent extends XSwitchProperty implements OnInit {
@ViewChild('switch', { static: true }) switch: ElementRef;
writeValue(value: any) {
this.value = value;
this.cdr.detectChanges();
}
constructor(public renderer: Renderer2, private cdr: ChangeDetectorRef, public configService: XConfigService) {
super();
}
ngOnInit() {
this.setFlex(this.switch.nativeElement, this.renderer, this.justify, this.align, this.direction);
this.setClassMap();
}
setClassMap() {
XClearClass(this.labelMap);
this.labelMap[`x-text-align-${this.labelAlign}`] = this.labelAlign ? true : false;
}
switchClick() {
if (this.disabled) return;
this.value = !this.value;
if (this.onChange) this.onChange(this.value);
this.cdr.detectChanges();
}
formControlChanges() {
this.ngOnInit();
this.cdr.detectChanges();
}
}
从上往下一个一个来说明
- XValueAccessor 这个是对 NG_VALUE_ACCESSOR 的封装,用来在此组件中注册 NG_VALUE_ACCESSOR 提供者,实际返回的就是如下的代码:
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => XSwitchComponent),
multi: true
}
- 在 XSwitchProperty 这个属性类里面继承了 XControlValueAccessor 这个类,然后 XControlValueAccessor 这个里面又实现了 ControlValueAccessor 这个接口,此处有点绕,加上上面的 XValueAccessor 这个,刚好完成了我们最初说的2个步骤:
/**
* 抽象泛型类,并实现 ControlValueAccessor 接口
* 提供通用的基础属性以及功能
*/
export abstract class XControlValueAccessor<T> extends XFormProp implements ControlValueAccessor {
// 组件值验证,主要是空值以及正则验证
get invalid() {
return !XIsEmpty(this.value) && this.invalidPattern;
}
// 正则验证,单个以及多个,对应的 message 也可设置成多个
get invalidPattern(): boolean {
let result = false;
let index = 0;
if (Array.isArray(this.pattern)) {
for (const pt of this.pattern) {
result = !new RegExp(pt).test(this.value as any);
if (result) {
this.invalidIndex = index;
break;
}
index++;
}
} else {
result = !new RegExp(this.pattern).test(this.value as any);
}
return result;
}
// 必填验证
get requiredIsEmpty() {
return this.required && XIsEmpty(this.value);
}
// 验证信息
get invalidMessage(): string {
if (Array.isArray(this.message)) {
return this.message.length > this.invalidIndex ? this.message[this.invalidIndex] : '';
} else {
return this.message;
}
}
invalidIndex: number = 0;
labelMap: XClassMap = {};
// 接口 ControlValueAccessor 需要实现的功能---start
value: T;
onChange: (value: T) => void;
onTouched: () => void;
writeValue(value: T): void {
this.value = value;
}
registerOnChange(fn: (value: T) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(disabled: boolean) {
this.disabled = disabled;
}
// 接口 ControlValueAccessor 需要实现的功能---end
// 设置标签布局
setFlex(ele: Element, renderer: Renderer2, justify: XJustify, align: XAlign, direction: XDirection) {
if (!XIsEmpty(justify)) renderer.addClass(ele, `x-justify-${this.justify}`);
if (!XIsEmpty(align)) renderer.addClass(ele, `x-align-${this.align}`);
if (!XIsEmpty(direction)) renderer.addClass(ele, `x-direction-${this.direction}`);
}
}
- 接下来看 writeValue 方法,可以理解为对 XControlValueAccessor 中 writeValue方法的重写,使用 ngModel 设置初始或改变值的时候触发。
- 其它方法 setClassMap 样式设置,switchClick 切换点击事件。
- formControlChanges 这个用来在 Form 组件中做初始化。
总结
Switch 开关的功能比较简单,代码很少。主要说明库中是如何做自定义表单组件的,通过再次封装官方提供的接口到抽象类中,实际的组件只需要继承抽象类就可以了。在组件库中目前已有 16 种表单组件,通过此方法有效减少了重复代码。