天天看點

【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表單驗證功能(超實用)