Angular 动态表单:以编程方式更新值是一种不好的做法吗?
Angular dynamic forms: Is a bad practice to update values programmatically?
我已经实现了一个通用的动态表单组件:
@Component({
selector: 'dynamic-form',
templateUrl: './dynamic-form.component.html'
})
export class DynamicFormComponent implements OnInit, OnChanges {
@Input() inputs: InputBase<any>[] = [];
@Input() submitLabel: string;
@Input() globalValidator: ValidatorFn | undefined;
@Output() onSubmit: EventEmitter<any> = new EventEmitter<any>();
@Output() onChanges: EventEmitter<any> = new EventEmitter<any>();
@Output() onValid: EventEmitter<boolean> = new EventEmitter<boolean>();
private wasValid: boolean = false;
form: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit() {
//Let's get the needed controls to create a FormGroup
this.form = this.generateFormGroup();
}
ngOnChanges() {
this.form = this.generateFormGroup();
}
private generateFormGroup(): FormGroup {
let group: any = {};
this.inputs && this.inputs.forEach(ib => {
ib.addtoFormGroup(group);
});
let form: FormGroup;
if (this.globalValidator) {
form = this.fb.group(group, { validator: this.globalValidator });
} else {
form = this.fb.group(group);
}
form.valueChanges.subscribe(data => {
if (this.form.valid !== this.wasValid) {
this.wasValid = this.form.valid;
this.onValid.emit(this.wasValid);
}
this.onChanges.emit(this.form.value);
});
return form;
}
submit() {
this.onSubmit.emit(this.form.value);
}
isValid(): boolean {
return this.form.valid;
}
getValue() {
return this.form.value;
}
}
模板如下:
<form *ngIf="form" role="form" class="form-horizontal" (ngSubmit)="submit()" [formGroup]="form">
<div class=" row ibox-content">
<div class="col-md-12">
<div *ngFor="let input of inputs" class="form-group">
<df-input [input]="input" [form]="form" [options]="input.options"></df-input>
</div>
</div>
<div class="row col-md-12">
<button class="btn btn-primary pull-right" type="submit" [disabled]="!form.valid">{{submitLabel| translate}}</button>
</div>
</div>
</form>
如您所见,该组件接收一个包含多个 classes 的项目列表,这些项目扩展 InputBase
并使用它们生成 df-input
组件:该组件唯一有趣的部分class 我的问题是以下方法,它有助于填充将用于创建 FormGroup
的对象:
addtoFormGroup(groupConf: {[key:string]:any}) {
if (groupConf[this.key]) {
throw new Error('The form already has an input with the same name: '+ this.key);
}
groupConf[this.key]=[this.value,this.validators];
}
现在df-input组件如下(简化):
@Component({
selector: 'df-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.css']
})
export class InputComponent implements OnInit, OnChanges,DoCheck {
@Input() input: InputBase<any>;
@Input() form: FormGroup;
private wasValid: boolean = false;
differ: any;
constructor(private differs: KeyValueDiffers) {
}
ngOnInit() {
this.differ = this.differs.find(this.input).create();
if (this.input.controlType === 'dropdown') {
let configuration: InputDropdownConfiguration<any>= (this.input as InputDropdownConfiguration<any>);
if (configuration.options && configuration.options.length===1) {
if (this.form) {
(this.form.controls[this.input.key] as AbstractControl).setValue(configuration.options[0].value)
}
}
}
}
ngOnChanges() {
this.ngOnInit();
}
ngDoCheck() {
var changes = this.differ.diff(this.input);
}
get isValid(): boolean {
if (this.form && this.form.controls && this.form.controls[this.input.key]) {
return this.form.controls[this.input.key].valid || !this.form.controls[this.input.key].touched;
}
return true;
}
}
及其模板
<div [formGroup]="form" class="form-group" [class.has-error]="!isValid">
<label *ngIf="input.label" [attr.for]="input.key" class="control-label col-sm-{{labelWidth}}">
<ng-container *ngIf="input.isRequired">* </ng-container>{{input.label | translate}}
</label>
<div [ngSwitch]="input.controlType" class="col-sm-{{inputWidth}}">
<input *ngSwitchCase="'text'" class="form-control" [formControlName]="input.key" [type]="input.type"
[name]="input.key" [readonly]="input.readonly" placeholder="{{input.placeholder |translate }}">
<select *ngSwitchCase="'dropdown'" [formControlName]="input.key" class="form-control">
<option *ngFor="let opt of options" [selected]="input.value == opt.value" value="{{opt.value}}">{{opt.text | translate}}</option>
</select>
</div>
<div class="help-block col-md-10 col-md-offset-1" *ngIf="!isValid" [hidden]="!isValid">{{input.errorMsg | translate}}</div>
</div>
这是有效的,但我不确定如何扩展它以满足我的下一个要求:我需要一个 <select>
元素,它始终有一个最终的 "Other" 选项。
如果用户 select 选择此选项,则应出现输入文本并可输入更多信息。
这意味着两个元素,但我想然后表现得只有一个:如果 "other" 选项是 selected,那么表单模型应该忽略
select 并获取输入文本值,但我不想将这两个元素都添加到 formGroup。
我正在考虑不将属性 [formControlName]="input.key"
添加到任何元素,而是以编程方式,当 selected 选项更改时,
检查它是否是 "Other" 然后激活输入字段。无论如何,我都会将新值添加到带有
的表单中
form.controls[input.key].setValue(<new_value>)
这是一种不好的做法吗?我在更新我的组件状态时遇到了很多问题,我试图保持智能组件的方法,但这对我来说不是一件容易的事
您可能想要创建一个 Validation
服务或类似的东西,并将您所有的自定义验证器放在那里。
比如我有这个:
import { Injectable } from '@angular/core';
import { FormGroup, AbstractControl } from '@angular/forms';
import { shouldMatch } from './should-match';
@Injectable()
export class Validation {
shouldMatch(...props: string[]) {
return (group: FormGroup): any => shouldMatch(group, ...props);
}
}
应该-match.ts:
import { FormGroup } from '@angular/forms';
export function shouldMatch(group: FormGroup, ...props: string[]): any {
const ctrls = group.controls;
const len = props.length;
for (let i = 1; i < len; i++) {
if (ctrls[props[0]]) {
if (ctrls[props[0]].value !== ctrls[props[i]].value) {
return {invalid: true};
}
} else {
throw new Error(`The property '${props[0]}' passed to 'shouldMatch()' was not found on the form group.`);
}
}
return null;
}
我在需要两个密码匹配的地方使用它(它以字符串形式接收属性,代表表单组中的表单控件名称,它们都应该具有相同的值):
this.form = this.fb.group({
paswords: this.fb.group({
password: [null, Validators.required],
passwordConfirmation: [null, Validators.required]
}, {validator: this.validation.shouldMatch('password', 'passwordConfirmation')}
})
我已经实现了一个通用的动态表单组件:
@Component({
selector: 'dynamic-form',
templateUrl: './dynamic-form.component.html'
})
export class DynamicFormComponent implements OnInit, OnChanges {
@Input() inputs: InputBase<any>[] = [];
@Input() submitLabel: string;
@Input() globalValidator: ValidatorFn | undefined;
@Output() onSubmit: EventEmitter<any> = new EventEmitter<any>();
@Output() onChanges: EventEmitter<any> = new EventEmitter<any>();
@Output() onValid: EventEmitter<boolean> = new EventEmitter<boolean>();
private wasValid: boolean = false;
form: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit() {
//Let's get the needed controls to create a FormGroup
this.form = this.generateFormGroup();
}
ngOnChanges() {
this.form = this.generateFormGroup();
}
private generateFormGroup(): FormGroup {
let group: any = {};
this.inputs && this.inputs.forEach(ib => {
ib.addtoFormGroup(group);
});
let form: FormGroup;
if (this.globalValidator) {
form = this.fb.group(group, { validator: this.globalValidator });
} else {
form = this.fb.group(group);
}
form.valueChanges.subscribe(data => {
if (this.form.valid !== this.wasValid) {
this.wasValid = this.form.valid;
this.onValid.emit(this.wasValid);
}
this.onChanges.emit(this.form.value);
});
return form;
}
submit() {
this.onSubmit.emit(this.form.value);
}
isValid(): boolean {
return this.form.valid;
}
getValue() {
return this.form.value;
}
}
模板如下:
<form *ngIf="form" role="form" class="form-horizontal" (ngSubmit)="submit()" [formGroup]="form">
<div class=" row ibox-content">
<div class="col-md-12">
<div *ngFor="let input of inputs" class="form-group">
<df-input [input]="input" [form]="form" [options]="input.options"></df-input>
</div>
</div>
<div class="row col-md-12">
<button class="btn btn-primary pull-right" type="submit" [disabled]="!form.valid">{{submitLabel| translate}}</button>
</div>
</div>
</form>
如您所见,该组件接收一个包含多个 classes 的项目列表,这些项目扩展 InputBase
并使用它们生成 df-input
组件:该组件唯一有趣的部分class 我的问题是以下方法,它有助于填充将用于创建 FormGroup
的对象:
addtoFormGroup(groupConf: {[key:string]:any}) {
if (groupConf[this.key]) {
throw new Error('The form already has an input with the same name: '+ this.key);
}
groupConf[this.key]=[this.value,this.validators];
}
现在df-input组件如下(简化):
@Component({
selector: 'df-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.css']
})
export class InputComponent implements OnInit, OnChanges,DoCheck {
@Input() input: InputBase<any>;
@Input() form: FormGroup;
private wasValid: boolean = false;
differ: any;
constructor(private differs: KeyValueDiffers) {
}
ngOnInit() {
this.differ = this.differs.find(this.input).create();
if (this.input.controlType === 'dropdown') {
let configuration: InputDropdownConfiguration<any>= (this.input as InputDropdownConfiguration<any>);
if (configuration.options && configuration.options.length===1) {
if (this.form) {
(this.form.controls[this.input.key] as AbstractControl).setValue(configuration.options[0].value)
}
}
}
}
ngOnChanges() {
this.ngOnInit();
}
ngDoCheck() {
var changes = this.differ.diff(this.input);
}
get isValid(): boolean {
if (this.form && this.form.controls && this.form.controls[this.input.key]) {
return this.form.controls[this.input.key].valid || !this.form.controls[this.input.key].touched;
}
return true;
}
}
及其模板
<div [formGroup]="form" class="form-group" [class.has-error]="!isValid">
<label *ngIf="input.label" [attr.for]="input.key" class="control-label col-sm-{{labelWidth}}">
<ng-container *ngIf="input.isRequired">* </ng-container>{{input.label | translate}}
</label>
<div [ngSwitch]="input.controlType" class="col-sm-{{inputWidth}}">
<input *ngSwitchCase="'text'" class="form-control" [formControlName]="input.key" [type]="input.type"
[name]="input.key" [readonly]="input.readonly" placeholder="{{input.placeholder |translate }}">
<select *ngSwitchCase="'dropdown'" [formControlName]="input.key" class="form-control">
<option *ngFor="let opt of options" [selected]="input.value == opt.value" value="{{opt.value}}">{{opt.text | translate}}</option>
</select>
</div>
<div class="help-block col-md-10 col-md-offset-1" *ngIf="!isValid" [hidden]="!isValid">{{input.errorMsg | translate}}</div>
</div>
这是有效的,但我不确定如何扩展它以满足我的下一个要求:我需要一个 <select>
元素,它始终有一个最终的 "Other" 选项。
如果用户 select 选择此选项,则应出现输入文本并可输入更多信息。
这意味着两个元素,但我想然后表现得只有一个:如果 "other" 选项是 selected,那么表单模型应该忽略
select 并获取输入文本值,但我不想将这两个元素都添加到 formGroup。
我正在考虑不将属性 [formControlName]="input.key"
添加到任何元素,而是以编程方式,当 selected 选项更改时,
检查它是否是 "Other" 然后激活输入字段。无论如何,我都会将新值添加到带有
form.controls[input.key].setValue(<new_value>)
这是一种不好的做法吗?我在更新我的组件状态时遇到了很多问题,我试图保持智能组件的方法,但这对我来说不是一件容易的事
您可能想要创建一个 Validation
服务或类似的东西,并将您所有的自定义验证器放在那里。
比如我有这个:
import { Injectable } from '@angular/core';
import { FormGroup, AbstractControl } from '@angular/forms';
import { shouldMatch } from './should-match';
@Injectable()
export class Validation {
shouldMatch(...props: string[]) {
return (group: FormGroup): any => shouldMatch(group, ...props);
}
}
应该-match.ts:
import { FormGroup } from '@angular/forms';
export function shouldMatch(group: FormGroup, ...props: string[]): any {
const ctrls = group.controls;
const len = props.length;
for (let i = 1; i < len; i++) {
if (ctrls[props[0]]) {
if (ctrls[props[0]].value !== ctrls[props[i]].value) {
return {invalid: true};
}
} else {
throw new Error(`The property '${props[0]}' passed to 'shouldMatch()' was not found on the form group.`);
}
}
return null;
}
我在需要两个密码匹配的地方使用它(它以字符串形式接收属性,代表表单组中的表单控件名称,它们都应该具有相同的值):
this.form = this.fb.group({
paswords: this.fb.group({
password: [null, Validators.required],
passwordConfirmation: [null, Validators.required]
}, {validator: this.validation.shouldMatch('password', 'passwordConfirmation')}
})