17

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.

Guido Neele
  • 778
  • 1
  • 7
  • 20
  • have you read the [Angular template forms documentation](https://angular.io/guide/forms#track-control-state-and-validity-with-ngmodel) about this? – 0mpurdy Jul 25 '17 at 13:35
  • Not answering your question... but, I really suggest a model driven form for this scenario. Having nested components in a template driven form is not easy. It's much easier and cleaner to implement with a reactive form :) – AT82 Jul 25 '17 at 13:45
  • @AJT_82 I've had a look at reactive forms but to me it wasn't much easier. Each widget component has it's own set of fields with some of them having the required or custom validation. I couldn't figure out how the fields in the widget components could be added to the main form. This post https://stackoverflow.com/questions/42531766/angular-2-creating-reactive-forms-with-nested-components looks promising but it failed to included the TS. – Guido Neele Jul 25 '17 at 15:16
  • 1
    Reactive forms make you have tight control over your form, including validations etc. I know that they are confusing in the beginning, when I started learning I banged my head against the wall for quite some time. But when you get the hang of it, they are just great! :) This one should get you started, this article at least helped me a great deal in the beginning: https://scotch.io/tutorials/how-to-build-nested-model-driven-forms-in-angular-2 – AT82 Jul 25 '17 at 16:23
  • @AJT_82 Had found the scotch.io tutorial but it didn't work out for me. One of the reasons is that some of the widgets have no form fields. For instance a user can select contacts from a modal, which are then added to an unordered list. The list needs to have more than one item to be valid. I've decided to get a reference to the widget in the ngswitch and have each widget implement an IWidgetComponent that requires an isValid() and an output that triggers when the widget is changed. On each change the form and the widgets isValid is being checked, when true the form is saved. – Guido Neele Jul 26 '17 at 20:17
  • I struggled to get a reference to the widget within the NgSwitch but found out you could get a reference by assigning the same variable to all cases. https://stackoverflow.com/questions/38674651/angular2-template-reference-inside-ngswitch. Will post code later and then close the question. – Guido Neele Jul 26 '17 at 20:18

3 Answers3

37

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

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Lazy Coder
  • 1,157
  • 1
  • 8
  • 18
  • 1
    This worked for me - I spent a lot of time looking into this, so thanks. Child components were not being validated by the parent form - providing ControlContainer with the existing NgForm instance was enough in my scenario to get the validation passing from the child components to the parent. – Clay Feb 02 '20 at 21:51
  • 2
    Note that you should add the above `viewProviders` line of code to the child component, not the parent – Daniel Flippance Jul 07 '20 at 23:19
  • 1
    This simple example helped me! +1 – Travis L. Riffle Sep 17 '20 at 06:42
  • 1
    This is my favorite thing on the internet right now! Usually what I do in such cases is to implement `ControlValueAccessor` with all the boilerplate and custom `NG_VALIDATORS` providers, which for cases where you want to just break one form into few smaller peaces is just too much. So thank you for sharing this gem! – nyxz Oct 15 '20 at 08:46
  • How can I access the form in the child component html? I need to check if form has been submitted? – developer Jul 30 '21 at 15:21
  • This was the missing part for me. I wrote a customized timePicker component and the last thing I had to add was the validation. My timePicker was missing in the form located in the parent component. Until I found this answer. Great, thanks!!! – René Preisler Aug 04 '21 at 21:31
8

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.

penleychan
  • 5,370
  • 1
  • 17
  • 28
2

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!");
    }
  }
Guido Neele
  • 778
  • 1
  • 7
  • 20