typescript 子组件上的 Angular Form 验证

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/45304810/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-21 04:45:26  来源:igfitidea点击:

Angular Form validation on child components

formsangulartypescriptangular-components

提问by Guido Neele

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.

我编写了一个动态表单,其中有一个主要部分和基于在主要部分 (widget.type) 中选择的类型的子部分。显示和隐藏子部分是通过 ngSwitch 完成的。

HTML of the form looks like this:

表单的 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>

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.

我尝试向小部件组件添加 isValid() 方法,但无法获取组件的实例,可能是因为它们在 ngSwitch 中使用。@ContentChild、@ContentChildren、@ViewChild 等都返回未定义。

采纳答案by Guido Neele

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.

决定在子组件上有一个 isValid 方法,该方法指示小部件是否正确填写。只有当表单和小部件组件都有效时才能保存表单。

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.

所有小部件组件都实现了一个 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() { }

}

The parent html looks like this:

父 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>

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.

每次小部件更改时都会发出一个事件(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!");
    }
  }

回答by penleychan

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 ControlValueAccessorthat your component need to implement. However I couldn't get that working.

问题与ControlValueAccessor您的组件需要实现有关。但是我无法让它工作。

See: https://github.com/angular/angular/issues/9600

参见:https: //github.com/angular/angular/issues/9600

Solution provided by andreev-artem works well, and I also added my solution to wrap it inside ngModelGroupinstead of in the form's root object controlsproperty.

andreev-artem 提供的解决方案运行良好,我还添加了我的解决方案,将其包装在内部ngModelGroup而不是在form根对象controls属性中。

For your case you're not using ngModelGroupyou could just have this directive

对于您不使用的情况,您可以使用ngModelGroup此指令

@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:

用法:在您[(ngModel)]添加指令之前,在根元素的组件中。例子:

<div provide-parent-form> 
   <input name="myInput" [(ngModel)]="myInput"> 
</div>

Now if you output your form objectin your console or whatever you can see your component's controls under controlsproperty of your form's object.

现在,如果您form object在控制台或其他任何地方输出您的内容,您可以在's 对象的controls属性下看到您的组件控件form

回答by Lazy Coder

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.

我遇到了与此类似的问题,尽管子组件较少,并且在深入研究了@penleychan 的上述主题之后,我发现了一个小宝石,可以为我解决这个问题,而无需实现自定义指令。

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

https://github.com/angular/angular/issues/9600#issuecomment-522898551