子组件上的 Angular 表单验证

Posted

技术标签:

【中文标题】子组件上的 Angular 表单验证【英文标题】:Angular Form validation on child components 【发布时间】:2017-12-31 11:15:39 【问题描述】:

我编写了一个动态表单,其中有一个主要部分和基于在主要部分(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 等都返回 undefined。

【问题讨论】:

你读过Angular template forms documentation这方面的内容吗? 不回答你的问题......但是,我真的建议这种情况下的模型驱动形式。在模板驱动的表单中嵌套组件并不容易。使用响应式表单实现起来更容易、更简洁:) @AJT_82 我看过响应式表单,但对我来说并不容易。每个小部件组件都有自己的一组字段,其中一些字段具有必需或自定义验证。我不知道如何将小部件组件中的字段添加到主窗体中。这篇 ***.com/questions/42531766/… 的帖子看起来很有希望,但它没有包含 TS。 反应式表单让您可以严格控制表单,包括验证等。我知道一开始它们很混乱,当我开始学习时,我的头撞墙了很长一段时间。但是,当您掌握了窍门时,它们就很棒! :) 这个应该让你开始,这篇文章至少在一开始对我有很大帮助:scotch.io/tutorials/… @AJT_82 找到了 scotch.io 教程,但对我没有用。原因之一是某些小部件没有表单字段。例如,用户可以从模式中选择联系人,然后将其添加到无序列表中。该列表需要包含多个项目才能有效。我决定在 ngswitch 中获取对小部件的引用,并让每个小部件实现一个 IWidgetComponent,该组件需要一个 isValid() 和一个在小部件更改时触发的输出。每次更改时,都会检查表单和小部件 isValid,如果为 true,则会保存表单。 【参考方案1】:

希望我不会太晚。我最近也用模板方法偶然发现了这个问题,因为反应形式不适合我需要做的......

问题与您的组件需要实现的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 object,或者您可以在form 对象的controls 属性下看到组件的控件。

【讨论】:

@AliAdravi 你可以看到我对反应形式***.com/a/53053333/6736888的其他答案@【参考方案2】:

决定在子组件上设置一个 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!");
    
  

【讨论】:

【参考方案3】:

对于未来的谷歌员工,

我遇到了类似的问题,尽管子组件较少,并且在挖掘了@penleychan 的上述主题后,我发现了一个小宝石,它为我解决了这个问题,而无需实现自定义指令。

import  ControlContainer, NgForm  from '@angular/forms';

@Component(
    ....
    viewProviders: [ provide: ControlContainer, useExisting: NgForm ],
)

这适用于我的嵌套表单。只需要添加到组件中, 哪些直接包含输入

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

【讨论】:

这对我有用 - 我花了很多时间研究这个,所以谢谢。父表单没有验证子组件 - 在我的场景中,为 ControlContainer 提供现有的 NgForm 实例就足以让验证从子组件传递到父表单。 请注意,您应该将上面的viewProviders 代码行添加到子组件,而不是父组件 这个简单的例子帮助了我! +1 这是我现在在互联网上最喜欢的东西!通常我在这种情况下所做的是使用所有样板和自定义NG_VALIDATORS 提供程序来实现ControlValueAccessor,这对于您只想将一种形式分解为几个较小的和平的情况实在是太多了。所以感谢您分享这颗宝石! 如何访问子组件html中的表单?我需要检查表单是否已提交?

以上是关于子组件上的 Angular 表单验证的主要内容,如果未能解决你的问题,请参考以下文章

如何从父组件 OnSubmit Angular 4 触发对子组件的验证?

如何从 Angular 1.5 组件中获取表单验证

angular 2 表单元素作为组件 - 验证不起作用

自定义 angular2 表单输入组件,在组件内具有两种方式绑定和验证

Angular 反应式表单自定义控件异步验证

Angular 2 表单验证器弄乱了取消按钮