4

I'm trying to implement in Angular2 (beta 0 with TS in the Plunker) a scenario with 2 nested forms, each represented by a component.

The parent component is Word, which represents the word in a fake dictionary. The children components are WordSense's, each representing a sense of the parent word.

Both the components use model-driven forms, and the child form binds its model's values to form controls using ngModel. This way, the parent component can easily pass its word's senses down to the children components, and 2-way bindings do the rest.

Simple custom validators are attached to both forms. Among other things, I'd like to disable the submit button not only when the word form is invalid, but also when any of its senses is invalid. To this end, I added an isValid property to the model being edited, and code to observe changes in the sense form: whenever a change occurs, I check the form's valid property and set the model's property accordingly. I could then easily add a check at the level of the parent component in the view and in the code so I can post only when both forms are OK.

To support custom validation and additional logic, I switched my initial code from template-based to model-based forms; yet, as soon as I launch the refactored code I get several No Directive annotation found errors, and I'm not sure about their meaning.

Probably I'm missing something obvious, but I'm a newbie here. Could anyone give a suggestion? You can find a repro at this plunker: http://plnkr.co/edit/v9Dj5j5opJmonxEeotcR. Here is some essential code from it:

a) the parent component:

@Component({
    selector: "word",
    directives: [FORM_DIRECTIVES, FORM_PROVIDERS, WordSense],
    templateUrl: `
<div>
    <form [ngFormModel]="wordForm"
          (ngSubmit)="onSubmit(wordForm.value)"
          role="form">

          <div class="form-group"
               [class.has-error]="!lemma.valid && lemma.touched">
            <label for="lemma">lemma</label>
            <input type="text" id="lemma"
                   maxlength="100" required spellcheck="false"
                   class="form-control"
                   placeholder="lemma"
                   [ngFormControl]="wordForm.controls['lemma']">
             <div *ngIf="lemma.hasError('required') && lemma.touched"
                  class="text-danger small">lemma required</div>
             <div *ngIf="lemma.hasError('lemmaValidator') && lemma.touched"
                  class="text-danger small">invalid lemma</div>
          </div>
            ...
          <div class="form-group">
            <table class="table table-bordered">
              <tbody>
              <tr *ngFor="#s of senses">
                <td>
                    <word-sense [sense]="s" [ranks]="ranks" [fields]="fields"></word-sense>
                </td>
              </tr>
              </tbody>
            </table>
          </div>
            ...              
          <button type="submit"
                  [ngClass]="{disabled: !wordForm.valid}"
                  class="btn btn-primary btn-sm">save</button>
     </form>
</div>
    `,
    inputs: [
        "word"
    ]
})
export class Word {
    private _word: IWordModel;

    public set word(value: IWordModel) {
        this._word = value;
        this.setFormValues();
    }
    public get word() {
        return this._word;
    }
    // ...

    // form
    public wordForm: ControlGroup;
    public lemma: Control;
    public language: Control;
    public class: Control;
    public ranks: IPair<number>[];
    public senses: ISenseViewModel[];
    public fields: IFieldModel[];

    constructor(private formBuilder:FormBuilder) {
        // ...
        this.senses = [
            this.createSense()
        ];
        // ...            
        // build the form
        this.wordForm = this.formBuilder.group({
            "lemma": ["", Validators.compose([Validators.required, LemmaValidator.isValidLemma])],
            "language": ["eng", Validators.required],
            "class": ["s.", Validators.required],
        });
        this.lemma = <Control> this.wordForm.controls["lemma"];
        this.language = <Control> this.wordForm.controls["language"];
        this.class = <Control> this.wordForm.controls["class"];
        // ...
    }
}

b) the child component:

@Component({
    selector: "word-sense",
    directives: [FORM_DIRECTIVES],
    template: `
    <form class="form-inline" role="form" [ngFormModel]="senseForm">

    <div class="form-group" 
         [class.has-error]="!definitionCtl.valid">
        <input type="text" 
               class="form-control"
               placeholder="definition"
               [ngFormControl]="definitionCtl"
               [(ngModel)]="sense.definition">
    </div>

    <div class="form-group"
         [class.has-error]="!yearCtl.valid">
        <input type="number"
               class="form-control"
               placeholder="date"
               [ngFormControl]="yearCtl"
               [(ngModel)]="sense.year">
    </div>
    ...         
</form>
    `,
    inputs: [
        "sense",
        "ranks",
        "fields"
    ]
})
export class WordSense {
    // model being edited
    public sense: ISenseViewModel;
    // lookup data
    public ranks: IPair<number>[];
    public fields: IFieldModel[];
    public field: IFieldModel;
    // form
    public senseForm: ControlGroup;
    public definitionCtl: Control;
    public yearCtl: Control;
    public rankCtl: Control;
    public fieldsCtl: Control;

    constructor(private formBuilder: FormBuilder) {
        this.senseForm = this.formBuilder.group({
            "definition": ["", Validators.required],
            "year": [0, Validators.compose([Validators.required, YearValidator.isValidYear])],
            "rank": [{value: 2, label: "media"}, Validators.required],
            "fields": [""]
        });
        this.definitionCtl = <Control> this.senseForm.controls["definition"];
        this.yearCtl = <Control> this.senseForm.controls["year"];
        this.rankCtl = <Control> this.senseForm.controls["rank"];
        this.fieldsCtl = <Control> this.senseForm.controls["fields"];
    }
    // ...
}
Naftis
  • 4,393
  • 7
  • 63
  • 91
  • Possible duplicate of [angular 2 form level validation using components](http://stackoverflow.com/questions/34980344/angular-2-form-level-validation-using-components) – Mike Argyriou Apr 26 '16 at 08:59

1 Answers1

0

To have more readable errors, you can change Angular2 .min.js files to the .dev.js ones.

Doing this, you have now the following error:

No Directive annotation found on FormBuilder

In fact, the problem is that you set the FORM_PROVIDERS into the directives attribute of your component. So it tries to use providers as directives but they aren't...

@Component({
  selector: "word",
  directives: [FORM_DIRECTIVES, FORM_PROVIDERS, WordSense], <-----
  templateUrl: `
    <div>
    (...)
  `
})
export class ...

Removing it should fix your problem:

@Component({
  selector: "word",
  directives: [FORM_DIRECTIVES, WordSense], <-----
  templateUrl: `
    <div>
    (...)
  `
})
export class ...

Another problem is that you use templateUrl instead of template for your Word component:

@Component({
  selector: "word",
  directives: [FORM_DIRECTIVES,WordSense],
  templateUrl: ` <----------
  `
  (...)

You should use this instead:

@Component({
  selector: "word",
  directives: [FORM_DIRECTIVES,WordSense],
  template: ` <----------
  `
  (...)

Here is the refactored plunkr: http://plnkr.co/edit/x0d5oiW1J9C2JrJG8NdT?p=preview.

Hope it helps you, Thierry

superjos
  • 12,189
  • 6
  • 89
  • 134
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Thank you! Sadly, another issue: in my app, the word component has its word model bound to another parent, which can set it: just a plain [word]="word". When the word property is changed, I get exceptions like "the expression has changed after it was checked" (referring to the valid property). Race condition? I found only this: http://stackoverflow.com/questions/35019829/formbuilder-control-causing-expression-has-changed-after-it-was-checked-except. Could you imagine any solution? – Naftis Feb 04 '16 at 20:29
  • Given that comments do not seem fit for this, which seems a different kind of error, I made a new post about this issue here: http://stackoverflow.com/questions/35219844/angular2-beta3-expression-has-changed-after-it-was-checked-in-updating-form . – Naftis Feb 05 '16 at 08:53
  • @thierry you did not change the code in the last example – select Feb 19 '16 at 20:50