您的当前位置:首页浅析Angular19 自定义表单控件

浅析Angular19 自定义表单控件

2022-06-15 来源:比拉宠物

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--; }}

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 { }}

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 { }}

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 { }}

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>← 这是{{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}}; }}

3.6 初始化效果展示

 

小编还为您整理了以下内容,可能对您也有帮助:

Angular 自定义表单控件

Angular有两种类型的表单:响应式表单(reactive forms)、模板驱动表单(Template-driven forms)。
共同基础

实现自定义表单控件的核心在于: ControlValueAccessor ,组件需要实现这个接口的功能。

在组件实现 ControlValueAccessor 接口后,要能正常使用的话,还需要执行注册操作。

导入FormsMole

组件需要注入一个Provider去实现。

导入ReactiveFormsMole

Angular 自定义表单控件

Angular有两种类型的表单:响应式表单(reactive forms)、模板驱动表单(Template-driven forms)。
共同基础

实现自定义表单控件的核心在于: ControlValueAccessor ,组件需要实现这个接口的功能。

在组件实现 ControlValueAccessor 接口后,要能正常使用的话,还需要执行注册操作。

导入FormsMole

组件需要注入一个Provider去实现。

导入ReactiveFormsMole

Angular框架中使用ng-zorro-antd实现可编辑的table表格

想要实现可编辑的表单,需要考虑两个事件,一个是失焦点击事件,另外一个是当用户点击单元格的input时,可以让单元格的input进行编辑。

首先,我们需要在 mole.ts 模块文件导入我们需要使用的组件依赖

导入之后,我们开始写表格组件,根据视图的 status 状态,来控制 input 的可编辑性。

在这个模板中, userName 这个字段中,通过使用一个 status 来判断这个单元格的状态,点击输入框时,我们不需要去固定去一些状态的枚举去进行操作,直接取反即可 data.status = !data.status 。当 data.status 为 true 时,表示目前是编辑状态, false 则使用 span 标签只去显示。

输入框的 { standalone: true } 时不会发生(不会添加到 FormGroup 中)

使用async-validator如何编写Form组件(详细教程)

本篇文章主要介绍了使用 async-validator 编写 Form 组件的方法,现在分享给大家,也给大家做个参考。

前端开发中,表单的校验一个很常见的功能,一些 ui 库例如ant.design 与Element ui 都实现了有校验功能的 Form 组件。async-validator 是一个可以对数据进行异步校验的库,ant.design 与 Element ui 的 Form 组件都使用了 async-validator。本文就简单介绍一下 async-validator 的基本用法以及使用该库实现一个简单的有校验功能的 Form 组件。

1. async-validator 的基本用法async-validator 的功能是校验数据是否合法,并且根据校验规则给出提示信息。

下面演示一下 async-validator 的最基本用法。

import AsyncValidator from 'async-validator'

// 校验规则

const descriptor = {

username: [

{

required: true,

message: '请填写用户名'

},

{

min: 3,

max: 10,

message: '用户名长度为3-10'

}

]

}

// 根据校验规则构造一个 validator

const validator = new AsyncValidator(descriptor)

const data = {

username: 'username'

}

validator.validate(model, (errors, fields) => {

console.log(errors)

})当数据不符合校验规则时,在 validator.validate 的回调函数中,就可以得到相应的错误信息。

当 async-validator 中常见的校验规则无法满足需求时,我们可以编写自定义的校验函数来校验数据。一个简单的校验函数如下。

function validateData (rule, value, callback) {

let err

if (value === 'xxxx') {

err = '不符合规范'

}

callback(err)

}

const descriptor = {

complex: [

{

validator: validateData

}

]

}

const validator = new AsyncValidator(descriptor)async-validator 支持对数据异步校验,所以在编写自定义校验函数时,不管校验是否通过,校验函数中的 callback 都要调用。

2. 编写 Form 组件与 FormItem 组件现在知道了 async-validator 的使用方法,如何将这个库跟要编写的 Form 组件结合起来呢。

实现思路

用一张图描述一下实现思路。

Form 组件

Form 组件应该是一个容器,里面包含不定数量的 FormItem 或者其他元素。可以使用 Vue 内置的slot 组件来代表 Form 里面的内容。

Form 组件还需要知道包含了多少个需要校验的 FormItem 组件。一般情况下,父子组件的通信 是通过在子组件上绑定事件实现的,但是这里使用 slot,无法监听到子组件的事件。这里可以在 Form 组件上通过$on 监听事件,FormItem 挂载或者销毁前触发 Form 组件的自定义事件即可。

按照这个思路,我们先编写 Form 组件。

<template>

<form class="v-form">

<slot></slot>

</form>

</template>

<script>

import AsyncValidator from 'async-validator'

export default {

name: 'v-form',

componentName: 'VForm', // 通过 $options.componentName 来找 form 组件

data () {

return {

fields: [], // field: {prop, el},保存 FormItem 的信息。

formError: {}

}

},

computed: {

formRules () {

const descriptor = {}

this.fields.forEach(({prop}) => {

if (!Array.isArray(this.rules[prop])) {

console.warn(`prop 为 ${prop} 的 FormItem 校验规则不存在或者其值不是数组`)

descriptor[prop] = [{ required: true }]

return

}

descriptor[prop] = this.rules[prop]

})

return descriptor

},

formValues () {

return this.fields.rece((data, {prop}) => {

data[prop] = this.model[prop]

return data

}, {})

}

},

methods: {

validate (callback) {

const validator = new AsyncValidator(this.formRules)

validator.validate(this.formValues, (errors) => {

let formError = {}

if (errors && errors.length) {

errors.forEach(({message, field}) => {

formError[field] = message

})

} else {

formError = {}

}

this.formError = formError

// 让错误信息的顺序与表单组件的顺序相同

const errInfo = []

this.fields.forEach(({prop, el}, index) => {

if (formError[prop]) {

errInfo.push(formError[prop])

}

})

callback(errInfo)

})

}

},

props: {

model: Object,

rules: Object

},

created () {

this.$on('form.addField', (field) => {

if (field) {

this.fields = [...this.fields, field]

}

})

this.$on('form.removeField', (field) => {

if (field) {

this.fields = this.fields.filter(({prop}) => prop !== field.prop)

}

})

}

}

</script>FormItem 组件

FormItem 组件就简单很多,首先要向上找到包含它的 Form 组件。接下来就可以根据 formError 计算出对应的错误信息。

<template>

<p class="form-item">

<label :for="prop" class="form-item-label" v-if="label">

{{ label }}

</label>

<p class="form-item-content">

<slot></slot>

</p>

</p>

</template>

<script>

export default {

name: 'form-item',

computed: {

form () {

let parent = this.$parent

while (parent.$options.componentName !== 'VForm') {

parent = parent.$parent

}

return parent

},

fieldError () {

if (!this.prop) {

return ''

}

const formError = this.form.formError

return formError[this.prop] || ''

}

},

props: {

prop: String,

label: String

}

}

</script>FormItem 在 mounted 与 beforeDestroy 钩子中还需要触发 Form 组件的一些自定义事件。

<script>

export default {

// ...

methods: {

dispatchEvent (eventName, params) {

if (typeof this.form !== 'object' && !this.form.$emit) {

console.error('FormItem必须在Form组件内')

return

}

this.form.$emit(eventName, params)

}

},

mounted () {

if (this.prop) {

this.dispatchEvent('form.addField', {

prop: this.prop,

el: this.$el

})

}

},

beforeDestroy () {

if (this.prop) {

this.dispatchEvent('form.removeField', {

prop: this.prop

})

}

}

}

</script>最后新建一个 index.js 导出编写好的组件。

import VForm from './Form.vue'

import FormItem from './FormItem.vue'

export {

VForm,

FormItem

}3. 使用方式表单的校验函数是在 Form 组件中。通过$ref 可以访问到 Form 组件,调用 validate 函数,从而获取到相应的校验信息。

使用方法如下:

<template>

<v-form :model="formData" :rules="rules" ref="form">

<form-item label="手机号" prop="tel">

<input type="tel" maxlength="11" v-model.trim="formData.tel"/>

</form-item>

<button @click="handleSubmit">保存</button>

</v-form>

</template>

<script>

import { VForm, FormItem } from './common/Form'

export default {

data () {

return {

formData: {

tel: ''

},

rules: {

tel: [

{required: true, message: '您的手机号码未输入'},

{pattern: /^1[34578]d{9}$/, message: '您的手机号码输入错误'}

]

}

}

},

methods: {

handleSubmit () {

this.$refs.form.validate(errs => {

console.log(errs)

})

}

},

components: {

VForm,

FormItem

}

}

</script>完整的代码点击这里。

4. 总结本文简单介绍了一下 async-validator 的用法,并实现了一个有校验功能的 Form 组件。这里的实现的 Form 表单存在着很多的不足:(1) 仅仅是在提交表单时才去校验。(2) FormItem 组件也应该根据校验的结果调整 ui,给出相应的提示。所以,Form 组件更适合在交互较少的移动端使用。

大家可以根据这个实现思路,根据应用场景,编写定制化更高的 Form 组件。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

在Angular.js中使用Swiper插件如何解决不能滑动的问题

在Angular5中如何调用第三方js插件(详细教程)

在angular2中如何使用第三方js库(详细教程)

Top