天天看点

【angular】primeng源码分析, 自定义表单元素checkbox组件,实现ngModel双向数据绑定及FormControl表单验证功能(超实用)

背景:最近项目准备使用angular7,想快速入手,最好的方式就是自己动手封装一些常用的组件,在此可以直接参考primeng,重构一些常用的组件。

目的:封装一个checkbox组件

废话不多说,跟着下面的步骤,完整的跑通一个组件的开发。

checkbox.component.html

<div
  [ngStyle]="style"
  [ngClass]="'app-chkbox app-widget'"
  [class]="styleClass">
  <div class="app-helper-hidden-accessible">
    <input #cb type="checkbox"
      [attr.id]="inputId"
      [name]="name"
      [value]="value"
      [checked]="checked"
      [disabled]="disabled"
      (focus)="onFocus($event)"
      (blur)="onBlur($event)"
      [ngClass]="{'app-state-focus':focused}"
      (change)="handleChange($event)"
      [attr.tabindex]="tabindex">
  </div>
  <div class="app-chkbox-box app-widget app-corner-all app-state-default"
    (click)="onClick($event,cb,true)"
    [ngClass]="{'app-state-active':checked,'app-state-disabled':disabled,'app-state-focus':focused}">
    <span class="app-chkbox-icon app-clickable"
      [ngClass]="{'pi pi-check':checked}"></span>
  </div>
</div>
<label
  (click)="onClick($event,cb,true)"
  [class]="labelStyleClass"
  [ngClass]="{'app-chkbox-label': true, 'app-label-active':checked, 'app-label-disabled':disabled, 'app-label-focus':focused}"
  *ngIf="label"
  [attr.for]="inputId">{{label}}</label>
           
注解:
输入属性 @Input():根据需求配置;
ngClass控制样式的显示和移除;
事件(eventName):重点就是onClick事件;
HTML attribute 和 DOM property的区别;

checkbox.component.scss

$activeColor: #007ad9;
$normalColor: #a6a6a6;
$hoverColor: #212121;
.app-chkbox {
  cursor: pointer;
  display: inline-block;
  vertical-align: middle;
  margin-right: .25em;
  user-select: none;
  .app-chkbox-box {
    border: 1px solid $normalColor;
    background-color: #fff;
    width: 20px;
    height: 20px;
    line-height: 20px;
    border-radius: 2px;
    text-align: center;
    border-radius: 3px;
    transition: background-color .2s, border-color .2s, box-shadow .2s;
    &.app-state-active {
      border-color: $activeColor;
      background-color: $activeColor;
    }
    &:not(.app-state-disabled):hover {
      border-color: $hoverColor;
    }
  }
  .app-chkbox-icon {
    display: block;
  }
}

.app-chkbox-label {
  vertical-align: middle;
}
           

基础样式,不够完整,用心的同学可以自行补全;

checkbox.component.ts

import { Component, OnInit, EventEmitter, ChangeDetectorRef, forwardRef, OnChanges, SimpleChanges, Input, Output } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';

@Component({
  selector: 'app-checkbox',
  templateUrl: './checkbox.component.html',
  styleUrls: ['./checkbox.component.scss'],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CheckboxComponent),
    multi: true
  }]
})
export class CheckboxComponent implements OnInit, ControlValueAccessor {
  @Input() name: string;
  @Input() value: any;
  @Input() label: string;
  @Input() disabled: boolean;
  @Input() binary: boolean;
  @Input() tabindex: number;
  @Input() inputId: string;
  @Input() style: any;
  @Input() styleClass: string;
  @Input() labelStyleClass: string;
  @Output() change: EventEmitter<any>;
  formControl: FormControl;
  checked: boolean;
  focused: boolean;
  model: any;
  onModelChange: Function;
  onModelTouched: Function;

  constructor(
    private cd: ChangeDetectorRef
  ) {
    this.change = new EventEmitter<any>();
    this.onModelChange = () => {};
    this.onModelTouched = () => {};
    this.focused = false;
    this.checked = false;
  }

  ngOnInit() {
  }

  /**
   * template模板 源事件
   * @param event 组件
   * @param cb 表单元素
   * @param focus 触发focus事件
   */
  onClick(event: Event, checkbox: HTMLInputElement, focus: boolean): void {
    event.preventDefault();
    if (this.disabled) {
      return ;
    }
    this.checked = !this.checked;
    this.updateModel();
    if (focus) {
      checkbox.focus();
    }
  }

  /**
   * 更新model
   */
  updateModel() {
    if (!this.binary) {
      if (this.checked) {
        this.addValue();
      } else {
        this.removeValue();
      }
      this.onModelChange(this.model); // model-to-ui
      if (this.formControl) {
        this.formControl.setValue(this.model); // 触发writeValue
      }
    } else {
      this.onModelChange(this.checked);
    }
    this.change.emit(this.checked); // 外部获取的event事件
  }

  addValue() {
    if (this.model) {
      this.model = this.model.concat([this.value]);
    } else {
      this.model = [this.value]; // 第一次初始化
    }
  }

  removeValue() {
    this.model = this.model.filter((item) => {
      return item !== this.value;
    });
  }

  handleChange(event: any) {
    this.checked = event.target.checked;
    this.updateModel();
  }

  isChecked() {
    if (this.binary) { // 允许返回boolean值
      return this.model;
    } else {
      return this.model && this.model.indexOf(this.value) > -1;
    }
  }

  // 触发更新-内部状态
  onFocus(event) {
    this.focused = true;
  }

  onBlur(event) {
    this.focused = false;
    this.onModelTouched();
  }

  // Angular API 和 DOM 元素之间的桥梁
  writeValue(model) {
    this.model = model;
    this.checked = this.isChecked();
    this.cd.markForCheck();
  }

  registerOnChange(fn: any) {
    this.onModelChange = fn;
  }

  registerOnTouched(fn: any) {
    this.onModelTouched = fn;
  }

  setDisabledState(val: boolean) {
    this.disabled = val;
  }

}
           

重点说明:

providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CheckboxComponent),
    multi: true
  }]
           

组件类定义,providers声明,provide是必须的,Used to provide a ControlValueAccessor for form controls。

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  setDisabledState(isDisabled: boolean)?: void
}
           

Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM

定义一个接口,作为Angular表单API和DOM中的原生元素之间的桥梁

正是因为它,才实现了Angular ngModel及其form表单的接口

这里不多赘述,可以参考文档

官方文档地址

基础概念需要提前熟悉:EventEmitter,Input,Output, FormControl等等

组件类的方法遵循基本原则:

比如,单一性,拆得越细越好,一个方法就干一件事,便于复用:

更新值,创建 addValue

移除值, 创建removeValue

调用组件测试效果

doc.component.html

<form [formGroup]="myFormGroup" (ngSubmit)="onSubmit()">
    <div>
      <h3 class="first">Basic</h3>
      <div class="ui-g" style="width:250px;margin-bottom:10px">
          <div class="ui-g-12"><app-checkbox value="New York" [formControl]="myFormGroup.controls['checkList']" label="New York" [(ngModel)]="checkboxList" inputId="ny"></app-checkbox></div>
          <div class="ui-g-12"><app-checkbox value="San Francisco" [formControl]="myFormGroup.controls['checkList']" name="chbox" label="San Francisco" [(ngModel)]="checkboxList" inputId="sf"></app-checkbox></div>
          <div class="ui-g-12"><app-checkbox value="Los Angeles" [formControl]="myFormGroup.controls['checkList']" name="chbox" label="Los Angeles" [(ngModel)]="checkboxList" inputId="la"></app-checkbox></div>
      </div>
      <p [hidden]="myFormGroup.controls['checkList'].valid || (myFormGroup.controls['checkList'].pristine)" class="alert alert-danger">
        这是一个必填项
      </p>
      Selected Cities: <span *ngFor="let city of checkboxList" style="margin-right:10px">{{city}}</span>
    </div>
    <p>
      Form Value: {{ myFormGroup.value | json }}
    </p>
    <p>
      Form Status: {{ myFormGroup.status }}
    </p>
    <p>
      <button type="button" [ngStyle]="{'margin-right': '5px'}" (click)="setNormalValue()">setNormalValue</button>
      <button type="submit" [disabled]="myFormGroup.invalid">Submit</button>
    </p>
  </form>
           
  1. myFormGroup 检测,整个表单,可以把这个对象打印出来看结构;
  2. 验证基础要懂,几个常用的状态值: valid, invalid, pending, pristine, dirty , untouched, touched;

doc.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-doc',
  templateUrl: './doc.component.html',
  styleUrls: ['./doc.component.css']
})
export class DocComponent implements OnInit {
  checkboxList: string[];
  myFormGroup = new FormGroup({
    checkList: new FormControl('', {
      validators: Validators.required
    })
  });

  constructor() { }

  ngOnInit() {
    this.checkboxList = [];
  }
  // public updateSwitchValue(check: boolean) {
    // this.switchForm.controls.switchValue.setValue(!check);
  // }

  setNormalValue() {
    this.checkboxList = ['New York', 'Los Angeles'];
  }

  onSubmit() {
    console.log(this.switchForm);
  }

}
           
  1. validators基础要懂点;查看文档地址: 更多validators说明点我
  2. FormGroup,可以查看文档,响应式表单一章,就够了;
  3. 这里没有自定义验证规则,没有异步验证,文档都有示例参考,查看更多

最后,演示效果

【angular】primeng源码分析, 自定义表单元素checkbox组件,实现ngModel双向数据绑定及FormControl表单验证功能(超实用)