Angular19 自定義表單控件


 1 需求

  當開發者需要一個特定的表單控件時就需要自己開發一個和默認提供的表單控件用法相似的控件來作為表單控件;自定義的表單控件必須考慮模型和視圖之間的數據怎么進行交互

 

2 官方文檔 -> 點擊前往

  Angular為開發者提供了ControlValueAccessor接口來輔助開發者構建自定義的表單控件,開發者只需要在自定義表單控件類中實現ControlValueAccessor接口中的方法就可以實現模型和視圖之間的數據交互

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

  2.1 writeValue  

writeValue(obj: any): void

    該方法用於將值寫入到自定義表單控件中的元素;

    這個參數值(obj)是使用這個自定義表單控件的組件通過模板表單或者響應式表單的數據綁定傳過來的;

    在自定義表單控件的類中只需要將這個值(obj)賦值給一個成員變量即可,自定義表單控件的視圖就會通過屬性綁定顯示出來這個值

  2.2 registerOnChange

registerOnChange(fn: any): void

    自定義表單控件的數據發生變化時會觸發registerOnChange方法,該方用於如何處理自定義表單控件數據的變化;

    registerOnChange方法接收的參數(fn)其實是一個方法,該方法負責處理變化的數據

    當自定義控件數據變化時就會自動調用fn執行的方法,但是通常的做法是自定義一個方法 propagateChange 讓自定義的方法指向fn,這樣當數據變化時只需要調用 propagateChange 就可以對變化的數據進行處理

  2.3 registerOnTouched

registerOnTouched(fn: any): void

    表單控件被觸摸時會觸發registerOnTouched方法,具體細節待更新......2018-1-31 11:18:33

  2.4 setDisabledState

setDisabledState(isDisabled: boolean)?: void

    待更新......2018-1-31 11:19:30

 

3 編程步驟

  3.1 創建自定義表單控件組件   

<div>
  <h4>當前計數為:{{countNumber}}</h4>
  <br />
  <div>
    <button md-icon-button (click)="onIncrease()">
      <span>增加</span>
      <md-icon>add</md-icon>
    </button>
    <span style="margin-left: 30px;"></span>
    <button md-icon-button (click)="onDecrease()">
      <span>減少</span>
      <md-icon>remove</md-icon>
    </button>
  </div>
</div>
HTML
import { Component, OnInit } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.scss']
})
export class CounterComponent implements OnInit {

  countNumber: number = 0;

  constructor() { }

  ngOnInit() {
  }

  onIncrease() {
    this.countNumber++;
  }

  onDecrease() {
    this.countNumber--;
  }

}
TS

    3.1.1 功能描述

      點擊增加按鈕時當前計數會增加1,點擊減少按鈕時當前計數會剪1

      

    3.1.2 直接在其他組件中使用時會報錯

      

      報錯信息如下:

        

        錯誤信息是說我們我們使用的組件<app-counter>還不是一個表單控件

  3.2 如何讓<app-counter>組件變成一個表單控件組件

    3.2.1 實現 ControlValueAccessor 接口

      

export class CounterComponent implements OnInit, ControlValueAccessor {

  countNumber: number = 0;

  constructor() { }

  ngOnInit() {
  }

  onIncrease() {
    this.countNumber++;
  }

  onDecrease() {
    this.countNumber--;
  }

    /**將數據從模型傳輸到視圖 */
  writeValue(obj: any): void {
  }

  /**將數據從視圖傳播到模型 */
  registerOnChange(fn: any): void {

  }

  registerOnTouched(fn: any): void {

  }

  setDisabledState?(isDisabled: boolean): void {

  }

}
View Code

    3.2.2 指定依賴信息providers

      

import { Component, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.scss'],
  providers: [
    {
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => CounterComponent),
        multi: true
    }
  ]
})
export class CounterComponent implements OnInit, ControlValueAccessor {

  countNumber: number = 0;

  constructor() { }

  ngOnInit() {
  }

  onIncrease() {
    this.countNumber++;
  }

  onDecrease() {
    this.countNumber--;
  }

    /**將數據從模型傳輸到視圖 */
  writeValue(obj: any): void {
  }

  /**將數據從視圖傳播到模型 */
  registerOnChange(fn: any): void {

  }

  registerOnTouched(fn: any): void {

  }

  setDisabledState?(isDisabled: boolean): void {

  }

}
View Code

    3.2.3 待修復bug

      雖然可以正常運行,但是表單控件中的元素接受不到使用表單控件那個組件中表單模型傳過來的數據,表單控件變化的數據也無法回傳到使用表單控件那個組件中的表單模型中去;簡而言之,就是模型和視圖之間無法進行數據交互

   3.3 實現模型和視圖的數據交互

    3.3.1 模型到視圖

      重構自定義表單控件類中的 writeValue 方法

      技巧01:writeValue 方法中的參數是使用自定義表單控件的那個組件通過表單的數據綁定傳進來的

        

    3.3.2 視圖到模型

      》自定義一個方法來處理自定義表單控件中的變化數據    

propagateChange = (_: any) => {};

       》重構自定義表單控件類中的 registerOnChange 方法

  /**將數據從視圖傳播到模型 */
  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

      》在數據變化的地方調用那個自定義的方法

        

  3.4 自定義表單控件組件代碼匯總

<div>
  <h4>當前計數為:{{countNumber}}</h4>
  <br />
  <div>
    <button md-icon-button (click)="onIncrease()">
      <span>增加</span>
      <md-icon>add</md-icon>
    </button>
    <span style="margin-left: 30px;"></span>
    <button md-icon-button (click)="onDecrease()">
      <span>減少</span>
      <md-icon>remove</md-icon>
    </button>
  </div>
</div>
HTML
import { Component, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.scss'],
  providers: [
    {
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => CounterComponent),
        multi: true
    }
  ]
})
export class CounterComponent implements OnInit, ControlValueAccessor {

  countNumber: number = 0;

  propagateChange = (_: any) => {};

  constructor() { }

  ngOnInit() {
  }

  onIncrease() {
    this.countNumber++;
    this.propagateChange(this.countNumber);
  }

  onDecrease() {
    this.countNumber--;
    this.propagateChange(this.countNumber);
  }

  /**將數據從模型傳輸到視圖 */
  writeValue(obj: any): void {
    this.countNumber = obj;
  }

  /**將數據從視圖傳播到模型 */
  registerOnChange(fn: any): void {
    /**fn其實是一個函數,當視圖中的數據改變時就會調用fn指向的這個函數,從而達到將數據傳播到模型的目的 */
    this.propagateChange = fn;  // 將fn的指向賦值給this.propagateChange,在需要將改變的數據傳到模型時只需要調用this.propagateChange方法即可
  }

  registerOnTouched(fn: any): void {

  }

  setDisabledState?(isDisabled: boolean): void {

  }

}
TS

  3.5 使用自定義表單控件的那個組件的代碼匯總

    技巧01:如果自定義表單控件和使用自定義表單控件的組件都在不在同一個模塊時需要對自定義表單控件對應組件進行導出和導入操作

    

<div class="panel panel-primary">
  <div class="panel-heading">面板模板</div>
  <div class="panel-body">
    <h3>面板測試內容</h3>
  </div>
  <div class="panel-footer">2018-1-22 10:22:20</div>
</div>

<div class="panel-primary">
  <div class="panel-heading">自定義提取表單控件</div>
  <div class="panel-body">
    <form #myForm=ngForm>
      <app-counter name="counter" [(ngModel)]="countNumber">
      </app-counter>
    </form>
    <h6>綠線上是自定義提取的表單控件顯示的內容</h6>
    <hr style="border: solid green 2px" />
    <h6>綠線下是使用自定義表單控件時表單的實時數據</h6>
    <h3>表單控件的值為:{{myForm.value | json}}</h3>
  </div>
  <div class="panel-footer">2018-1-31 10:09:17</div>
</div>

<div class="panel-primary">
  <div class="panel-heading">提取表單控件</div>
  <div class="panel-body">
    <form #form="ngForm">
      <p>outerCounterValue value: {{outerCounterValue}}</p>
      <app-exe-counter name="counter" [(ngModel)]="outerCounterValue"></app-exe-counter>
      <br />
      <button md-raised-button type="submit">Submit</button>
      <br />
      <div>
        {{form.value | json}}
      </div>
    </form>
  </div>
  <div class="panel-footer">2018-1-27 21:51:45</div>
</div>

<div class="panel panel-primary">
  <div class="panel-heading">ngIf指令測試</div>
  <div class="panel-body">
    <button md-rasied-button (click)="onChangeNgifValue()">改變ngif變量</button>
    <br />
    <div *ngIf="ngif; else ngifTrue" >
      <h4 style="background-color: red; color: white" >ngif變量的值為true</h4>
    </div>
    <ng-template #ngifTrue>
      <h4 style="background-color: black; color: white">ngif變量的值為false</h4>
    </ng-template>
  </div>
  <div class="panel-footer">2018-1-27 16:58:17</div>
</div>

<div class="panel panel-primary">
  <div class="panel-heading">RXJS使用</div>
  <div class="panel-body">
    <h4>測試內容</h4>
  </div>
  <div class="panel-footer">2018-1-23 21:14:49</div>
</div>

<div class="panel panel-primary">
  <div class="panel-heading">自定義驗證器</div>
  <div class="panel-body">
    <form (ngSubmit)="onTestLogin()" [formGroup]="loginForm">
      <md-input-container>
        <input mdInput placeholder="請輸入登錄名" formControlName="username" />
      </md-input-container>
      <br />
      <md-input-container>
        <input mdInput placeholder="請輸入密碼" formControlName="userpwd" />
      </md-input-container>
      <br />
      <button type="submit" md-raised-button>登陸</button>
    </form>
  </div>
  <div class="panel-footer">2018-1-23 11:06:01</div>
</div>

<div class="panel panel-primary">
  <div class="panel-heading">響應式表單</div>
  <div class="panel-body">
    <form [formGroup]="testForm">
      <md-input-container>
        <input mdInput type="text" placeholder="請輸入郵箱" formControlName="email" />
        <span mdSuffix>@163.com</span>  
      </md-input-container>
      <br />
      <md-input-container>
        <input mdInput type="password" placeholder="請輸入密碼" formControlName="password" />
      </md-input-container>
    </form>
    <hr />
    <div>
      <h2>表單整體信息如下:</h2>
      <h4>表單數據有效性:{{testForm.valid}}</h4>
      <h4>表單數據為:{{testForm.value | json}}</h4>
      <h4>獲取單個或多個FormControl:{{testForm.controls['email'] }}</h4>
      <hr />
      <h2>email輸入框的信息如下:</h2>
      <h4>有效性:{{testForm.get('email').valid}}</h4>
      <h4>email輸入框的錯誤信息為:{{testForm.get('email').errors | json}}</h4>
      <h4>required驗證結果:{{testForm.hasError('required', 'email') | json}}</h4>
      <h4>minLength驗證結果:{{ testForm.hasError('minLength', 'email') | json }}</h4>
      <h4>hello:{{ testForm.controls['email'].errors | json }}</h4>
      <hr />
      <h2>password輸入框啊的信息如下:</h2>
      <h4>有效性:{{testForm.get('password').valid}}</h4>
      <h4>password輸入框的錯誤信息為:{{testForm.get('password').errors | json }}</h4>
      <h4>required驗證結果:{{testForm.hasError('required', 'password') | json}}</h4>
    </div>
    <div>
      <button nd-rasied-button (click)="onTestClick()">獲取數據</button>
      <h4>data變量:{{data}}</h4>
    </div>
  </div>
  <div class="panel-footer">2018-1-22 15:58:43</div>
</div> 

<div class="panel panel-primary">
  <div class="panel-heading">利用響應式編程實現表單元素雙向綁定</div>
  <div class="panel-body">
    <md-input-container>
      <input mdInput  placeholder="請輸入姓名(響應式雙向綁定):" [formControl]="name"/>
    </md-input-container>
    <div>
      姓名為:{{name.value}}
    </div>
  </div>
  <div class="panel-footer">2018-1-22 11:12:35</div>
</div> -->

<div class="panel panel-primary">
  <div class="panel-heading">模板表單</div>
  <div class="panel-body">
    <md-input-container>
      <input mdInput placeholder="隨便輸入點內容" #a="ngModel" [(ngModel)]="desc" name="desc" />
      <button type="button" md-icon-button mdSuffix (click)="onTestNgModelClick()">
        <md-icon>done</md-icon>
      </button>
    </md-input-container>
    <div>
      <h3>名為desc的表單控件的值為:{{ a.value }}</h3>
    </div>
  </div>
  <div class="panel-footer">2018-1-22 10:19:31</div>
</div>

<div class="panel panel-primary">
  <div class="panel-heading">md-chekbox的使用</div>
  <div calss="panel-body">
    <div>
        <md-checkbox #testCheckbox color="primary" checked="true">測試</md-checkbox>
    </div>
    <div *ngIf="testCheckbox.checked">
      <h2>測試checkbox被選中啦</h2>
    </div>
  </div>
  <div class="panel-footer">2018-1-18 14:02:20</div>
</div>  

<div class="panel panel-primary">
  <div class="panel-heading">md-tooltip的使用</div>
  <div class="panel-body">
    <span md-tooltip="重慶火鍋">鼠標放上去</span>
  </div>
  <div class="panel-footer">2018-1-18 14:26:58</div>
</div>


<div class="panel panel-primary">
  <div class="panel-heading">md-select的使用</div>
  <div class="panel-body">
    <md-select placeholder="請選擇目標列表" class="fill-width" style="height: 40px;">
      <md-option *ngFor="let taskList of taskLists" [value]="taskList.name">{{taskList.name}}</md-option>
    </md-select>
  </div>
  <div class="panel-footer">2018-1-18 14:26:58</div>
</div>  

<div class="panel panel-primary">
  <div class="panel-heading">ngNonBindable指令的使用</div>
  <div class="panel-body">
    <h3>描述</h3>
    <p>使用了ngNonBindable的標簽,會將該標簽里面的元素內容全部都看做時純文本</p>
    <h3>例子</h3>
    <p>
      <span>{{taskLists | json }}</span>
      <span ngNonBindable>&larr; 這是{{taskLists | json }}渲染的內容</span>
    </p>
  </div>
  <div class="panel-footer">2018-1-19 09:34:26</div>
</div>
HTML

    

import { Component, OnInit, HostListener, Inject} from '@angular/core';
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Http } from '@angular/http';
import { QuoteService } from '../../service/quote.service';

@Component({
  selector: 'app-test01',
  templateUrl: './test01.component.html',
  styleUrls: ['./test01.component.scss']
})
export class Test01Component implements OnInit {

  countNumber: number = 9;

  outerCounterValue: number = 5;  

  ngif = true;

  loginForm: FormGroup;

  testForm: FormGroup;
  data: any; 

  name: FormControl = new FormControl();

  desc: string = 'hello boy';

  taskLists = [
    {label: 1, name: '進行中'},
    {label: 2, name: '已完成'}
  ];

  constructor(
    private formBuilder: FormBuilder,
    private http: Http,
    @Inject('BASE_CONFIG') private baseConfig,
    private quoteService: QuoteService
  ) {}

  ngOnInit() {
    this.testForm = new FormGroup({
      email: new FormControl('', [Validators.required, Validators.minLength(4)], []),
      password: new FormControl('', [Validators.required], [])
    });

    this.name.valueChanges
    .debounceTime(500)
    .subscribe(value => alert(value));

    this.loginForm = this.formBuilder.group({
      username: ['', [Validators.required, Validators.minLength(4), this.myValidator], []],
      userpwd: ['', [Validators.required, Validators.minLength(6)], []]
    });

    this.quoteService.test()
    .subscribe(resp => console.log(resp));

  }

  onChangeNgifValue() {
    if (this.ngif == false) {
      this.ngif = true;
    } else {
      this.ngif = false;
    }
  }

  @HostListener('keyup.enter')
  onTestNgModelClick() {
    alert('提交');
    
  }

  onTestClick() {
    // this.data = this.testForm.get('email').value;
    // console.log(this.testForm.getError);
    console.log(this.testForm.controls['email']);
  }

  onTestLogin() {
    console.log(this.loginForm.value);
    if (this.loginForm.valid) {
      console.log('登陸數據合法');
    } else {
      console.log('登陸數據不合法');
      console.log(this.loginForm.controls['username'].errors);
      console.log(this.loginForm.get('userpwd').errors);
    }
  }

  myValidator(fc: FormControl): {[key: string]: any} {
    const valid = fc.value === 'admin';
    return valid ? null : {myValidator: {requiredUsername: 'admin', actualUsername: fc.value}};
  }

}
TS

  3.6 初始化效果展示

    

   3.7 參考文檔

    點擊前往

    

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM