问题
I've written a dynamic form in which there is a main part and sub parts based on a type that's selected in the main part (widget.type). Showing and hiding the sub parts is done with an ngSwitch.
HTML of the form looks like this:
<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>
Every widget is it's own component.
The problem is that the form validation only checks the inputs from the main part and disregards the sub part (widget components). How can I make sure the input fields from the widgets are included in the validation?
I tried adding an isValid() method to the widget components but I couldn't get the instances of the components, probably because they are used in an ngSwitch. @ContentChild, @ContentChildren, @ViewChild etc. all returned undefined.
回答1:
For future googlers,
I had a similar issue to this, albeit with fewer child components and after digging through @penleychan's aforementioned thread on the subject I found a little gem that solved this for me without the need to implement a custom directive.
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
回答2:
Hope i'm not too late. I recently stumbled on this issue too with template approach since reactive form did not fit what I needed to do...
The issue is something to do with ControlValueAccessor
that your component need to implement. However I couldn't get that working.
See: https://github.com/angular/angular/issues/9600
Solution provided by andreev-artem works well, and I also added my solution to wrap it inside ngModelGroup
instead of in the form
's root object controls
property.
For your case you're not using ngModelGroup
you could just have this directive
@Directive({
selector: '[provide-parent-form]',
providers: [
{
provide: ControlContainer,
useFactory: function (form: NgForm) {
return form;
},
deps: [NgForm]
}
]
})
export class ProvideParentForm {}
Usage: In your component at the root element before you have [(ngModel)]
add the directive. Example:
<div provide-parent-form>
<input name="myInput" [(ngModel)]="myInput">
</div>
Now if you output your form object
in your console or whatever you can see your component's controls under controls
property of your form
's object.
回答3:
Decided to have an isValid method on the child component which indicates if the widget is filled out correctly. The form can only be saved when the form and widget component are both valid.
All widget components implement an IWidgetComponent interface which requires a changed EventEmitter property and an isValid method. One of the child widget components looks like this.
@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() { }
}
The parent html looks like this:
<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>
Each time the widget changes an event is emitted (this.changed.emit()) which triggers the save form method in the parent component. In this method I check if the form and widget are valid, if it is then the data may be saved.
saveChanges() {
if (this.ref && this.ref.isValid() && this.widgetForm.valid) {
// save form
this.toastr.success("Saved!");
}
else {
this.toastr.warning("Form not saved!");
}
}
来源:https://stackoverflow.com/questions/45304810/angular-form-validation-on-child-components