Angular 对子组件进行表单验证
Angular Form validation on child components
我编写了一个动态表单,其中有一个主要部分和基于在主要部分 (widget.type) 中选择的类型的子部分。显示和隐藏子部分是使用 ngSwitch 完成的。
HTML 的形式如下所示:
<form class="widget-form cc-form" (ngSubmit)="saveChanges()" novalidate>
<div class="forms-group">
<label for="title" i18n="@@title">Titel</label>
<input class="form-control" id="title" name="title" type="text" [(ngModel)]="widget.title" required />
</div>
<div class="forms-group">
<label class="checkbox-label" for="show" i18n>
<input id="show" name="show" type="checkbox" [(ngModel)]="widget.show" /> <span>Titel tonen in app</span>
</label>
</div>
<div class="forms-group">
<label for="type" i18n="@@type">Type</label>
<select class="form-control" id="type" name="type" [(ngModel)]="widget.type" required>
<option value="text-widget" i18n="@@Text">Tekst</option>
<option value="tasklist-widget" i18n="@@Tasklists">Takenlijst</option>
<option value="image-widget" i18n="@@Text">Afbeelding(en)</option>
<option value="video-widget" i18n="@@Video">Youtube</option>
<option value="link-widget" i18n="@@Link">Link</option>
<option value="contacts-widget" i18n="@@Contacts">Contactpersonen</option>
<option value="attachment-widget" i18n="@@Attachments">Bijlage(n)</option>
</select>
</div>
<ng-container [ngSwitch]="widget.type">
<text-widget *ngSwitchCase="'text-widget'" [data]="widget"></text-widget>
<tasklist-widget *ngSwitchCase="'tasklist-widget'" [data]="widget"></tasklist-widget>
<image-widget *ngSwitchCase="'image-widget'" [data]="widget"></image-widget>
<video-widget *ngSwitchCase="'video-widget'" [data]="widget"></video-widget>
<link-widget *ngSwitchCase="'link-widget'" [data]="widget"></link-widget>
<contacts-widget *ngSwitchCase="'contacts-widget'" [data]="widget"></contacts-widget>
<attachment-widget *ngSwitchCase="'attachment-widget'" [data]="widget"></attachment-widget>
</ng-container>
</form>
每个小部件都是它自己的组件。
问题是表单验证只检查主要部分的输入而忽略了子部分(小部件组件)。如何确保小部件的输入字段包含在验证中?
我尝试将 isValid() 方法添加到小部件组件,但我无法获取组件的实例,可能是因为它们在 ngSwitch 中使用。 @ContentChild、@ContentChildren、@ViewChild 等都返回未定义。
希望我还不算太晚。我最近也用模板方法偶然发现了这个问题,因为反应形式不适合我需要做的...
问题与您的组件需要实现的 ControlValueAccessor
有关。但是我无法让它工作。
参见:https://github.com/angular/angular/issues/9600
andreev-artem 提供的解决方案效果很好,我还添加了我的解决方案将其包装在 ngModelGroup
中而不是 form
的根对象 controls
属性.
对于你的情况,你没有使用 ngModelGroup
你可以只使用这个指令
@Directive({
selector: '[provide-parent-form]',
providers: [
{
provide: ControlContainer,
useFactory: function (form: NgForm) {
return form;
},
deps: [NgForm]
}
]
})
export class ProvideParentForm {}
用法:在您的组件中的根元素之前 [(ngModel)]
添加指令。示例:
<div provide-parent-form>
<input name="myInput" [(ngModel)]="myInput">
</div>
现在,如果您在控制台或任何您可以在 form
对象的 controls
属性 下看到组件控件的地方输出 form object
。
决定在子组件上使用 isValid 方法来指示小部件是否已正确填写。只有当表单和widget组件都有效时才能保存表单。
所有小部件组件都实现 IWidgetComponent 接口,该接口需要更改的 EventEmitter 属性 和 isValid 方法。其中一个子部件组件如下所示。
@Component({
selector: 'video-widget',
templateUrl: './video.component.html',
styleUrls: ['./video.component.css'],
providers: [YouTubeIdExistsValidator]
})
export class VideoComponent implements OnInit, OnDestroy, IWidgetComponent {
@Input("data")
widget: IWidget;
@Output("change")
changed = new EventEmitter<any>();
video: any;
modelChanged: Subject<string> = new Subject<string>();
public isValid(): boolean {
return this.widget.youtube_id && this.widget.youtube_id !== "" && this.video ? true : false;
}
constructor(private youtubeService: YoutubeService) {
this.modelChanged
.debounceTime(500) // wait 500ms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value
.subscribe(youtube_id => this.getYoutubeVideo(youtube_id));
}
ngOnDestroy(): void {
this.widget.youtube_id = "";
}
getYoutubeVideo(youtube_id: string) {
this.youtubeService
.getById(youtube_id)
.subscribe((video) => {
this.video = video;
// Indicate that video was changed
this.changed.emit();
}, (error) => {
this.video = null;
});
}
youtubeIdChanged(youtube_id: string) {
this.modelChanged.next(youtube_id);
}
ngOnInit() { }
}
父 html 看起来像这样:
<form #widgetForm novalidate>
...
<ng-container [ngSwitch]="widget.type">
<text-widget #ref *ngSwitchCase="'text-widget'" [data]="widget" (change)="saveChanges()"></text-widget>
<tasklist-widget #ref *ngSwitchCase="'tasklist-widget'" [data]="widget" (change)="saveChanges()"></tasklist-widget>
<image-widget #ref *ngSwitchCase="'image-widget'" [data]="widget" (change)="saveChanges()"></image-widget>
<video-widget #ref *ngSwitchCase="'video-widget'" [data]="widget" (change)="saveChanges()"></video-widget>
<link-widget #ref *ngSwitchCase="'link-widget'" [data]="widget" (change)="saveChanges()"></link-widget>
<contacts-widget #ref *ngSwitchCase="'contacts-widget'" [data]="widget" (change)="saveChanges()"></contacts-widget>
<attachment-widget #ref *ngSwitchCase="'attachment-widget'" [data]="widget" (change)="saveChanges()"></attachment-widget>
</ng-container>
...
</form>
每次小部件更改时都会发出一个事件 (this.changed.emit()),这会触发父组件中的保存表单方法。在这种方法中,我检查表单和小部件是否有效,如果是则可以保存数据。
saveChanges() {
if (this.ref && this.ref.isValid() && this.widgetForm.valid) {
// save form
this.toastr.success("Saved!");
}
else {
this.toastr.warning("Form not saved!");
}
}
对于未来的 google 员工,
我遇到了与此类似的问题,尽管子组件较少,并且在深入了解@penleychan 的上述主题后,我发现了一些 gem 解决了这个问题,而无需实施自定义指令。
import { ControlContainer, NgForm } from '@angular/forms';
@Component({
....
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
This works for my nested form. Just needs to be added to components,
which ones directly contains inputs
https://github.com/angular/angular/issues/9600#issuecomment-522898551
我编写了一个动态表单,其中有一个主要部分和基于在主要部分 (widget.type) 中选择的类型的子部分。显示和隐藏子部分是使用 ngSwitch 完成的。
HTML 的形式如下所示:
<form class="widget-form cc-form" (ngSubmit)="saveChanges()" novalidate>
<div class="forms-group">
<label for="title" i18n="@@title">Titel</label>
<input class="form-control" id="title" name="title" type="text" [(ngModel)]="widget.title" required />
</div>
<div class="forms-group">
<label class="checkbox-label" for="show" i18n>
<input id="show" name="show" type="checkbox" [(ngModel)]="widget.show" /> <span>Titel tonen in app</span>
</label>
</div>
<div class="forms-group">
<label for="type" i18n="@@type">Type</label>
<select class="form-control" id="type" name="type" [(ngModel)]="widget.type" required>
<option value="text-widget" i18n="@@Text">Tekst</option>
<option value="tasklist-widget" i18n="@@Tasklists">Takenlijst</option>
<option value="image-widget" i18n="@@Text">Afbeelding(en)</option>
<option value="video-widget" i18n="@@Video">Youtube</option>
<option value="link-widget" i18n="@@Link">Link</option>
<option value="contacts-widget" i18n="@@Contacts">Contactpersonen</option>
<option value="attachment-widget" i18n="@@Attachments">Bijlage(n)</option>
</select>
</div>
<ng-container [ngSwitch]="widget.type">
<text-widget *ngSwitchCase="'text-widget'" [data]="widget"></text-widget>
<tasklist-widget *ngSwitchCase="'tasklist-widget'" [data]="widget"></tasklist-widget>
<image-widget *ngSwitchCase="'image-widget'" [data]="widget"></image-widget>
<video-widget *ngSwitchCase="'video-widget'" [data]="widget"></video-widget>
<link-widget *ngSwitchCase="'link-widget'" [data]="widget"></link-widget>
<contacts-widget *ngSwitchCase="'contacts-widget'" [data]="widget"></contacts-widget>
<attachment-widget *ngSwitchCase="'attachment-widget'" [data]="widget"></attachment-widget>
</ng-container>
</form>
每个小部件都是它自己的组件。
问题是表单验证只检查主要部分的输入而忽略了子部分(小部件组件)。如何确保小部件的输入字段包含在验证中?
我尝试将 isValid() 方法添加到小部件组件,但我无法获取组件的实例,可能是因为它们在 ngSwitch 中使用。 @ContentChild、@ContentChildren、@ViewChild 等都返回未定义。
希望我还不算太晚。我最近也用模板方法偶然发现了这个问题,因为反应形式不适合我需要做的...
问题与您的组件需要实现的 ControlValueAccessor
有关。但是我无法让它工作。
参见:https://github.com/angular/angular/issues/9600
andreev-artem 提供的解决方案效果很好,我还添加了我的解决方案将其包装在 ngModelGroup
中而不是 form
的根对象 controls
属性.
对于你的情况,你没有使用 ngModelGroup
你可以只使用这个指令
@Directive({
selector: '[provide-parent-form]',
providers: [
{
provide: ControlContainer,
useFactory: function (form: NgForm) {
return form;
},
deps: [NgForm]
}
]
})
export class ProvideParentForm {}
用法:在您的组件中的根元素之前 [(ngModel)]
添加指令。示例:
<div provide-parent-form>
<input name="myInput" [(ngModel)]="myInput">
</div>
现在,如果您在控制台或任何您可以在 form
对象的 controls
属性 下看到组件控件的地方输出 form object
。
决定在子组件上使用 isValid 方法来指示小部件是否已正确填写。只有当表单和widget组件都有效时才能保存表单。
所有小部件组件都实现 IWidgetComponent 接口,该接口需要更改的 EventEmitter 属性 和 isValid 方法。其中一个子部件组件如下所示。
@Component({
selector: 'video-widget',
templateUrl: './video.component.html',
styleUrls: ['./video.component.css'],
providers: [YouTubeIdExistsValidator]
})
export class VideoComponent implements OnInit, OnDestroy, IWidgetComponent {
@Input("data")
widget: IWidget;
@Output("change")
changed = new EventEmitter<any>();
video: any;
modelChanged: Subject<string> = new Subject<string>();
public isValid(): boolean {
return this.widget.youtube_id && this.widget.youtube_id !== "" && this.video ? true : false;
}
constructor(private youtubeService: YoutubeService) {
this.modelChanged
.debounceTime(500) // wait 500ms after the last event before emitting last event
.distinctUntilChanged() // only emit if value is different from previous value
.subscribe(youtube_id => this.getYoutubeVideo(youtube_id));
}
ngOnDestroy(): void {
this.widget.youtube_id = "";
}
getYoutubeVideo(youtube_id: string) {
this.youtubeService
.getById(youtube_id)
.subscribe((video) => {
this.video = video;
// Indicate that video was changed
this.changed.emit();
}, (error) => {
this.video = null;
});
}
youtubeIdChanged(youtube_id: string) {
this.modelChanged.next(youtube_id);
}
ngOnInit() { }
}
父 html 看起来像这样:
<form #widgetForm novalidate>
...
<ng-container [ngSwitch]="widget.type">
<text-widget #ref *ngSwitchCase="'text-widget'" [data]="widget" (change)="saveChanges()"></text-widget>
<tasklist-widget #ref *ngSwitchCase="'tasklist-widget'" [data]="widget" (change)="saveChanges()"></tasklist-widget>
<image-widget #ref *ngSwitchCase="'image-widget'" [data]="widget" (change)="saveChanges()"></image-widget>
<video-widget #ref *ngSwitchCase="'video-widget'" [data]="widget" (change)="saveChanges()"></video-widget>
<link-widget #ref *ngSwitchCase="'link-widget'" [data]="widget" (change)="saveChanges()"></link-widget>
<contacts-widget #ref *ngSwitchCase="'contacts-widget'" [data]="widget" (change)="saveChanges()"></contacts-widget>
<attachment-widget #ref *ngSwitchCase="'attachment-widget'" [data]="widget" (change)="saveChanges()"></attachment-widget>
</ng-container>
...
</form>
每次小部件更改时都会发出一个事件 (this.changed.emit()),这会触发父组件中的保存表单方法。在这种方法中,我检查表单和小部件是否有效,如果是则可以保存数据。
saveChanges() {
if (this.ref && this.ref.isValid() && this.widgetForm.valid) {
// save form
this.toastr.success("Saved!");
}
else {
this.toastr.warning("Form not saved!");
}
}
对于未来的 google 员工,
我遇到了与此类似的问题,尽管子组件较少,并且在深入了解@penleychan 的上述主题后,我发现了一些 gem 解决了这个问题,而无需实施自定义指令。
import { ControlContainer, NgForm } from '@angular/forms';
@Component({
....
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
This works for my nested form. Just needs to be added to components, which ones directly contains inputs
https://github.com/angular/angular/issues/9600#issuecomment-522898551