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