Angular响应式表单及封装表单控件

响应式表单也叫模型驱动型表单。

有三个重要元素FormControl,FormGroup和FormBuilder。还有一个FormArray。

验证器和异步验证器。

动态指定验证器。条件改变验证方式改变。

自定义FormControl。用于表单过于复杂之后,逻辑难以理清楚。把复杂问题拆成若干简单问题永远是万能钥匙。用于简化form表单自己的逻辑。

一、登录表单

多个validators:

Validators.compose([Validators.required, Validators.email])返回ValidatorFn。
动态指定validator: 
一开始可以不指定validator,在某些条件下动态指定validator:
 this.form.controls['email'].setValidators(this.validate);
查看errors:
<mat-error>{{form.controls['email'].errors | json}}</mat-error>
模板:
<form [formGroup]="form" (ngSubmit)="onSubmit(form,$event)">
    <mat-card class="example-card">
        <mat-card-header>
            <mat-card-title>登录:</mat-card-title>
        </mat-card-header>
        <mat-card-content>
            <mat-form-field class="example-full-width" class="full-width">
                <input type="text" formControlName="email" matInput placeholder="您的email" style="text-align: right">
                <mat-error>{{form.controls['email'].errors | json}}</mat-error>
            </mat-form-field>
            <mat-form-field class="example-full-width" class="full-width">
                <input type="password" formControlName="password"  matInput placeholder="您的密码" style="text-align: right">
            </mat-form-field>
            <button mat-raised-button type="submit" color="primary" [disabled]="!form.valid">登录</button>

        </mat-card-content>
        <mat-card-actions class="text-right">
            <p>还没有账户?<a routerLink="/register">注册</a></p>
            <p>忘记密码?<a href="">找回</a></p>
        </mat-card-actions>
    </mat-card>

    <mat-card class="example-card">
        <mat-card-header>
            <mat-card-title>每日佳句</mat-card-title>
            <mat-card-subtitle>满足感在于不断的努力,而不是现有成就。全心努力定会胜利满满。</mat-card-subtitle>
        </mat-card-header>
        <img mat-card-image src="/assets/images/quote_fallback.jpg" alt="">
        <mat-card-content>
            Satisfaction lies in the effort, not in the attainment. Full effort is full victory.
        </mat-card-content>
    </mat-card>
</form>
View Code

组件:

export class LoginComponent implements OnInit {
  form: FormGroup;
  constructor(private fb: FormBuilder) {
    // this.form = new FormGroup({
    //   email: new FormControl("wang@163.com", Validators.compose([Validators.required, Validators.email])),
    //   password: new FormControl("",Validators.required),
    // })

    //formBuilder不需要显示的new FormControl
    this.form = this.fb.group({
      email: ["wang@163.com", Validators.compose([Validators.required, Validators.email, this.validate]) ],
      password:["",Validators.required]

    })
  }

  ngOnInit(): void {
   
  }

  onSubmit(form: FormGroup, event: Event) {
    event.preventDefault();
    console.log(JSON.stringify(form.value));
    console.log(form.valid);
  }

  validate(c:FormControl):{[key:string]:any} | null{
    if(!c.value){
      return null;
    }
    const pattern=/^wang+/;
    if(pattern.test(c.value)){
      return null;
    }else{
      return {
        emailNotValid: 'The email must start with wang'
      }
    }
  }

}
View Code

 

 二、封装自定义表单控件

把注册表单中的图片列表抽成一个独立组件。

 

 

 ng g c shared/image-list-select生成组件

  •  实现ControlValueAccessor接口。实现writeValue(),registerOnChange()和registerOnTouched()
  • 定义一个providers,令牌NG_VALUE_ACCESSOR和NG_VALIDATORS。用useExisting加forwardRef。并且设置multi为true。

 在image-list-select中可以放开的属性有很多,有没有必要一一放开?需要权衡。

 如果想要充分的自由度的话,可以用transclude嵌入组件<ng-content></ng-content>。

 在image-list-select中隔离封装,放开有限的属性。

模板:

<div>
  <span>{{title}}</span>
  <mat-icon class="avatar" [svgIcon]="selected" *ngIf="useSvgIcon else imgSelect"> </mat-icon>
  <ng-template #imgSelect>
    <img [src]="selected" alt="image selected" class="cover">
  </ng-template>
</div>

<div class="scroll-container">
  <mat-grid-list [cols]="cols" [rowHeight]="rowHight">
    <mat-grid-tile *ngFor="let item of items; let i = index">
      <div class="image-container" (click)="onChange(i)">
        <mat-icon class="avatar" [svgIcon]="item" *ngIf="useSvgIcon else imgItem"></mat-icon>
        <ng-template #imgItem>
          <img [src]="item" alt="image item" [ngStyle]="{'width': itemWidth}">
        </ng-template>
        
        <div class="after">
          <div class="zoom">
            <mat-icon>checked</mat-icon>
          </div>
        </div>
      </div>
  
    </mat-grid-tile>
  </mat-grid-list>
</div>
View Code

组件:

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

@Component({
  selector: 'app-image-list-select',
  templateUrl: './image-list-select.component.html',
  styleUrls: ['./image-list-select.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ImageListSelectComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => ImageListSelectComponent),
      multi: true
    }
  ]
})
export class ImageListSelectComponent implements ControlValueAccessor {
  @Input() title = "选择"
  @Input() cols = 6;
  @Input() rowHight = '64px'
  @Input() items: string[] = [];
  @Input() useSvgIcon: boolean = false;
  @Input() itemWidth = '80px';
  selected: string = '';
  constructor() { }

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

  //写值,设置控件的值form中setValue设置初始值,通过表单控件的writeValue方法设值。
  writeValue(obj: any): void {
    this.selected = obj;
  }
  //控件view发生变化,把变化emit给表单
  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }
  //什么状态算touched,告诉表单
  registerOnTouched(fn: any): void {
  }

  onChange(i: number) {
    this.selected = this.items[i];
    this.propagateChange(this.selected); //变化通知表单
  }

  validate(c: FormControl): { [key: string]: any } | null {
    return this.selected ? null : {
      imageListInvalid: {
        valid: false
      }
    }
  }

}
View Code

1,UI布局 

图片鼠标划过去如果没有任何反应,用户会无法感知到有没有选中,所以

 <div class="image-container" (click)="onChange(i)">
        <mat-icon class="avatar" [svgIcon]="avator"></mat-icon>
        <div class="after">
          <div class="zoom">
            <mat-icon>checked</mat-icon>
          </div>
        </div>
</div>

让组件既处理icon又处理图片。用useSvgIcon控制,通过条件子句判断。

<mat-icon class="" [svgIcon]="selected" *ngIf="useSvgIcon else imgSelect"> </mat-icon>
<ng-template #imgSelect></ng-template>

list太多支持滚动。包在.scroll-container容器中。

.scroll-container {
    overflow-y: scroll;
    height: 200px;
}

 2,实现表单控件

可以通过 formContrlName来操作

 实现ControlValueAccessor接口。

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

  //写值,设置控件的值form中setValue设置初始值,通过表单控件的writeValue方法设值。
  writeValue(obj: any): void {
    this.selected = obj;
  }
  //控件view发生变化,把变化emit给表单
  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }
  //什么状态算touched,告诉表单
  registerOnTouched(fn: any): void {
    throw new Error('Method not implemented.');
  }

  onChange(i: number) {
    this.selected = this.items[i];
    this.propagateChange(this.selected); //变化通知表单
  }

providers中定义自己的provider,把自己注册进去。

包括NG_VALUE_ACCESSOR和NG_VALIDATORS。

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

validate(c: FormControl): { [key: string]: any } | null {
    return this.selected ? null : {
      imageListInvalid: {
        valid: false
      }
    }
  }

 3,调用/使用

在sharedModule中导出ImageListSelectComponent。

<app-image-list-select
          [useSvgIcon]="true"
          [cols]="6"
          [title]="'选择头像:'"
          [items]="items"
          formControlName="avatar">
</app-image-list-select>

 

 

 

 

 

 

2019-04-07

posted @ 2021-02-18 08:15  starof  阅读(462)  评论(0编辑  收藏  举报