How to use Angular 2's FormBuilder between multiple components

13,895

Solution 1

For every one else with problem

Cannot find control with unspecified name attribute.

The problem is you forgot to define formControlName on your form input elements.

formControlName="url"

If you are facing No provider for NgControl after fixing inputs you have a problem with Ionic2 breaking changes in Fom handling. There is a chance you can fix your code by importing a new form component:

import { disableDeprecatedForms, provideForms } from '@angular/forms';

But you will probably still face more and more problems. To really fix your code:

  • update to the latest beta version
  • simplify your form to 2 simple inputs
  • rewrite your form and make it work
  • add rest of your elements

Good tutorial about FormBuilder and validation https://blog.khophi.co/ionic-2-forms-formbuilder-and-validation/

Solution 2

As a general note in Angular 2+, I found that adopting a consistent approach to specifying formGroupName, formArrayName, and formGroupNamesaved a great deal of time, and what remained of my sanity. I lost much time before I did.

Either use

formXXXName="literal value"

Or

formXXXName="{{value expression}}"

Or

[formXXXName]="'literal value'" (single tick within double tick)

Or

[formXXXName]="value expression"

I was continually mixing them, to my great sorrow.

Example from a recent project:

Case 1: Correct use of bracket notation.

    <label>Notes</label>
    <div class="panel panel-default">
        <div class="panel-body">
            <div [formArrayName]="'notes'">
                <div *ngFor="let note of outletForm.controls.notes.controls; let i=index">
                    <input type="text" [formControlName]="i" class="form-control" />
                </div>
            </div>
        </div>
    </div>

Case 2: Correct use of non-bracket notation.

    <label>Notes</label>
    <div class="panel panel-default">
        <div class="panel-body">
            <div formArrayName="notes">
                <div *ngFor="let note of outletForm.controls.notes.controls; let i=index">
                    <input type="text" formControlName="{{i}}" class="form-control" />
                </div>
            </div>
        </div>
    </div>

Please note in the above the use of "{{" and "}}" to compel Angular to treat what would normally be a literal string value (because of the use of the no-brackets syntax) as an EL expression.

Case 3: Incorrect use of bracket notation

    <label>Notes</label>
    <div class="panel panel-default">
        <div class="panel-body">
            <div [formArrayName]="notes">
                <div *ngFor="let note of outletForm.controls.notes.controls; let i=index">
                    <input type="text" formControlName="{{i}}" class="form-control" />
                </div>
            </div>
        </div>
    </div>

In this case, the value of formArrayName, "notes", is treated as an EL expression. Since my component has no such member, it evaluates to nothing. Therefore, the following error is sustained:

OutletFormComponent.html:153 ERROR Error: Cannot find control with unspecified name attribute
    at _throwError (forms.es5.js:1830)
    at setUpFormContainer (forms.es5.js:1803)
    at FormGroupDirective.addFormArray (forms.es5.js:4751)
    at FormArrayName.ngOnInit (forms.es5.js:5036)
    at checkAndUpdateDirectiveInline (core.es5.js:10793)
    at checkAndUpdateNodeInline (core.es5.js:12216)
    at checkAndUpdateNode (core.es5.js:12155)
    at debugCheckAndUpdateNode (core.es5.js:12858)
    at debugCheckDirectivesFn (core.es5.js:12799)
    at Object.eval [as updateDirectives] (OutletFormComponent.html:159)

Fortunately, the line number given at the very bottom of the stack points to the line with the problem.

Case 4: Incorrect use of non-bracketing notation

    <label>Notes</label>
    <div class="panel panel-default">
        <div class="panel-body">
            <div [formArrayName]="'notes'">
                <div *ngFor="let note of outletForm.controls.notes.controls; let i=index">
                    <input type="text" formControlName="i" class="form-control" />
                </div>
            </div>
        </div>
    </div>

In this case, the value of formControlName, "i", will be interpreted as a literal string, but we really intended to refer to the index variable 'i' on the containing *ngFor directive. The result of this error is:

OutletFormComponent.html:161 ERROR Error: Cannot find control with path: 'notes -> i'
    at _throwError (forms.es5.js:1830)
    at setUpControl (forms.es5.js:1738)
    at FormGroupDirective.addControl (forms.es5.js:4711)
    at FormControlName._setUpControl (forms.es5.js:5299)
    at FormControlName.ngOnChanges (forms.es5.js:5217)
    at checkAndUpdateDirectiveInline (core.es5.js:10790)
    at checkAndUpdateNodeInline (core.es5.js:12216)
    at checkAndUpdateNode (core.es5.js:12155)
    at debugCheckAndUpdateNode (core.es5.js:12858)
    at debugCheckDirectivesFn (core.es5.js:12799)

Thanks and kudos to all Angular2+ developers and experts.

Share:
13,895
Daniel Hair
Author by

Daniel Hair

BY DAY: Programming BY NIGHT: Family man FOR FUN: Do fun activities with my family "Whats the worth of a crown, if you don't wear it?"

Updated on June 19, 2022

Comments

  • Daniel Hair
    Daniel Hair almost 2 years

    I am trying to use FormBuilder in a page I have in Ionic 2.

    First, here is my environment details: Running on Windows 10, and running ionic --version gives me 2.0.0-beta.35

    Here is part of my package.json file:

    ...
    "@angular/common": "2.0.0-rc.3",
    "@angular/compiler": "2.0.0-rc.3",
    "@angular/core": "2.0.0-rc.3",
    "@angular/forms": "^0.3.0",
    "@angular/http": "2.0.0-rc.3",
    "@angular/platform-browser": "2.0.0-rc.3",
    "@angular/platform-browser-dynamic": "2.0.0-rc.3",
    "ionic-angular": "2.0.0-beta.10",
    "ionic-native": "1.3.2",
    "ionicons": "3.0.0"
    ...
    

    Second, here are the two main files involved:

    insight.ts

    import { Component } from '@angular/core';
    import {NavController, NavParams} from 'ionic-angular';
    import {
      REACTIVE_FORM_DIRECTIVES,
      FormBuilder,
      FormControl,
      FormGroup
    } from '@angular/forms';
    import { App, Insight } from '../../models';
    import { InsightProvider } from '../../providers/insight/insight.service';
    import { InsightImage, InsightLabel, InsightLink, InsightQuestion, InsightThought, InsightTodo, InsightVideo } from './shared';
    
    @Component({
      templateUrl: 'build/pages/insight/insight.html',
      directives: [REACTIVE_FORM_DIRECTIVES, InsightImage, InsightLabel, InsightLink, InsightQuestion, InsightThought, InsightTodo, InsightVideo],
      providers: [App, InsightProvider, FormBuilder]
    })
    export class InsightPage {
    
      canAdd: boolean;
      showDetails: boolean;
      newInsight: Insight;
      insightForm: FormGroup;
    
      constructor(private insightProvider: InsightProvider,
                  private params: NavParams) {
        this.insightForm = new FormGroup({
          type: new FormControl('', []),
          todo: new FormControl('', []),
          checked: new FormControl(false, []),
          imageUrl: new FormControl('', []),
          link: new FormControl('', []),
          url: new FormControl('', []),
          label: new FormControl('', []),
          question: new FormControl('', []),
          answer: new FormControl('', []),
          title: new FormControl('', []),
          details: new FormControl('', []),
        });
      }
    
      ngOnInit() {
        this.canAdd = false;
        this.showDetails = true;
      }
    
      addNewInsight() {
        if (this.newInsight.type) {
          this.insightProvider.createInsight(this.newInsight)
          .subscribe(response => {
            this.newInsight.setId(response.data.id);
            this.newInsight.title = '';
            console.log(response);
          });
        }
      }
    
      deleteClicked(index: number) {
        console.log('Clicked on ' + index);
        this.insightProvider.deleteInsight(this.newInsight)
        .subscribe(data => {
          console.log(data);
        });
      }
    
    
    }
    

    insight.html

    <form [ngFormModel]="insightForm" (ngSubmit)="createNewInsight()">
          <ion-item>
            <ion-label for="type">Insight Type</ion-label>
            <ion-select name="type" id="type" [formControl]="type">
              <ion-option value="label">Label</ion-option>
              <ion-option value="thought">Thought</ion-option>
              <ion-option value="link">Link</ion-option>
              <ion-option value="question">Question</ion-option>
              <ion-option value="todo">Todo</ion-option>
              <ion-option value="image">Image</ion-option>
              <ion-option value="video">Video</ion-option>
            </ion-select>
          </ion-item>
    
          <div [ngSwitch]="type">
              <insight-image [form]="insightForm" *ngSwitchCase="'image'"></insight-image>
              <insight-label [form]="insightForm" *ngSwitchCase="'label'"></insight-label>
              <insight-link [form]="insightForm" *ngSwitchCase="'link'"></insight-link>
              <insight-question [form]="insightForm" *ngSwitchCase="'question'"></insight-question>
              <insight-thought [form]="insightForm" *ngSwitchCase="'thought'"></insight-thought>
              <insight-todo [form]="insightForm" *ngSwitchCase="'todo'"></insight-todo>
              <insight-video [form]="insightForm" *ngSwitchCase="'video'"></insight-video>
          </div>
    
          <button type="submit" block primary text-center (click)="addNewInsight()" [disabled]="!newInsight.type">
            <ion-icon name="add"></ion-icon> Add Insight
          </button>
        </form>
    

    As you can see, I am trying to pass the FormGroup Object into multiple components so that I could use them.

    Here is an example of what one of the components look like (minimal version right now):

    <ion-item>
      <ion-label floating for="link">Link</ion-label>
      <ion-input type="text" name="link" id="link" [formControl]="link"></ion-input>
    </ion-item>
    
    <ion-item>
      <ion-label floating for="url">URL</ion-label>
      <ion-input type="text" id="url" name="url" [formControl]="url"></ion-input>
    </ion-item>
    

    The problem I am facing right now is this error:

    Error from FormBuilder and Ionic 2

    What I believe is happening is that the FormBuilder is looking for the given names I declare in my typescript file (such as todo, imageUrl, link, etc), but since it is in my other components, it errors out, thinking its not there.

    What could be the reason for this error? I have looked online and could not find related issues.

    FYI, the reason I am needing them in components and not in the same page, is because in the future, the functionality will be different for each input, thus needed to give each component a "Single Responsibility".

    Thanks in advance