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