Naftis Naftis - 3 months ago 14
TypeScript Question

Angular2 beta: nesting form-based parent/child components and validating from parent

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"];
}
// ...
}

Answer

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

Comments