1

I have a component that displays some form fields depending of a given object. Normally, the component is called in a loop by a parent component that gives a new object each time, then, the child component shows the right element depending on the type.

Here is the StackBlitz example

But sometimes, the element could be an iterable, a repeatable element (in case of a FormArray). I have the following code:

<div [formGroup]="form">
 <ng-template #formTmpl
 let-field="field"
 let-index="index">
 <pre>index: {{ index }}</pre>
 <label [attr.for]="!!question.iterable ? question.key + index : question.key">{{ question.label }}</label>
 <div [ngSwitch]="question.controlType">
 <div [attr.formArrayName]="!!question.iterable ? question.key : null">
 <input *ngSwitchCase="'textbox'"
 [class]="isValid ? config.validClass : config.invalidClass"
 [formControlName]="!!question.iterable ? index : question.key"
 [placeholder]="question.placeholder"
 [id]="!!question.iterable ? question.key + index : question.key"
 [type]="question['type']">
 <select [id]="question.key"
 *ngSwitchCase="'dropdown'"
 [class]="isValid ? config.validClass : config.invalidClass"
 [formControlName]="question.key">
 <option value=""
 disabled
 *ngIf="!!question.placeholder"
 selected>{{ question.placeholder }}</option>
 <option *ngFor="let opt of question['options']"
 [value]="opt.key">{{ opt.value }}</option>
 </select>
 <textarea *ngSwitchCase="'textarea'"
 [formControlName]="question.key"
 [id]="question.key"
 [class]="isValid ? config.validClass : config.invalidClass"
 [cols]="question['cols']"
 [rows]="question['rows']"
 [maxlength]="question['maxlength']"
 [minlength]="question['minlength']"
 [placeholder]="question.placeholder"></textarea>
 </div>
 </div>
 <div class="errorMessage"
 *ngIf="!isValid">{{ question.label }} is required</div>
 </ng-template>
 <div *ngIf="question.iterable; else formTmpl">
 val {{questionArray.value | json}}
 <div *ngFor="let field of questionArray.controls; let i=index; last as isLast">
 <ng-container [ngTemplateOutlet]="formTmpl"
 [ngTemplateOutletContext]="{field: field, index: i}"></ng-container>
 <button *ngIf="question.iterable && isLast"
 type="button"
 (click)="addItem(question)">+</button>
 </div>
 </div>
</div>

for the moment, I'm testing the repeatability of an element on inputs only.

So for one element, it works well, but it can't get the reference to the optional directive: [attr.formArrayName]="!!question.iterable ? question.key : null"

If try the following (putting the input directly in the ng-container, without calling the ng-template:

 <div *ngIf="question.iterable; else formTmpl">
 val {{questionArray.value | json}}
 <div *ngFor="let field of questionArray.controls; let i=index; last as isLast">
 <input [class]="isValid ? config.validClass : config.invalidClass"
 [formControlName]="i"
 [placeholder]="question.placeholder"
 [id]="question.key"
 [type]="question['type']">
 <button *ngIf="question.iterable && isLast"
 type="button"
 (click)="addItem(question)">+</button>
 </div>
 </div>

the input field shows well, and the value of the Form is well bind to it.

What's strange is that the formControlName of each field inside the ng-template have their value bind well to the form.

The error I got at the form init is:

ERROR Error: Cannot find control with unspecified name attribute
 at _throwError (vendor.js:74769)
 at setUpControl (vendor.js:74593)
 at FormGroupDirective.addControl (vendor.js:78338)
 at FormControlName._setUpControl (vendor.js:78989)
 at FormControlName.ngOnChanges (vendor.js:78912)
 at checkAndUpdateDirectiveInline (vendor.js:59547)
 at checkAndUpdateNodeInline (vendor.js:70213)
 at checkAndUpdateNode (vendor.js:70152)
 at debugCheckAndUpdateNode (vendor.js:71174)
 at debugCheckDirectivesFn (vendor.js:71117)

Then, if I add a new element to the formArray, the new element appears but isn't bind, and this new errors shows:

ERROR Error: Cannot find control with name: '1'
 at _throwError (vendor.js:74769)
 at setUpControl (vendor.js:74593)
 at FormGroupDirective.addControl (vendor.js:78338)
 at FormControlName._setUpControl (vendor.js:78989)
 at FormControlName.ngOnChanges (vendor.js:78912)
 at checkAndUpdateDirectiveInline (vendor.js:59547)
 at checkAndUpdateNodeInline (vendor.js:70213)
 at checkAndUpdateNode (vendor.js:70152)
 at debugCheckAndUpdateNode (vendor.js:71174)
 at debugCheckDirectivesFn (vendor.js:71117)

So how can I make the field taking in account the formArrayName property ?

You can see below the attribute is present in the markup (but I thought it were ng-reflect-name normally), but it's not working anyway:

enter image description here

As final example, here's a screenshot of the form (notice the indexes above each repeatable inputs):

enter image description here

Here is the StackBlitz example

asked Jun 24, 2019 at 14:17
5
  • your code is hard to read and there are things difficult to figure out. what is the purpose of let-field="field"? where is question and questionArray defined? what is the structure of form in [formGroup]="form"? it would be great if you could provide a reproducible example in stackbliz. Commented Jun 24, 2019 at 14:55
  • You're right @ysf, sorry! There is some debug code I let in by mistake. I edited the post with a GitHub repo. Please make sure you're on the feature/add-iterable-field-option branch. ;) Commented Jun 24, 2019 at 15:12
  • I finally ended up adding a StackBlitz @ysf :stackblitz.com/github/zzminoxv Commented Jun 25, 2019 at 7:48
  • 1
    stackblitz link is not working. i was able to reproduce the error using github repo. honestly, i used to think that i had my share of reactive forms until i saw your code :) yesterday i had a bit struggle to understand what is going on in your code. today i am gonna take a look at it again. i ll let you know if i can come up with anything. Commented Jun 25, 2019 at 7:59
  • @ysf I'm so sorry, here's the right link ;) stackblitz.com/edit/github-c1vgys Thank you! Commented Jun 25, 2019 at 8:00

1 Answer 1

4

i think [attr.formArrayName]="!!question.iterable ? question.key : null" binding doesn't work because it is performed as attribute binding instead of input binding

however, when we convert it to input binding new errors occur for non-FormArray questions in your use case.

so, using good-old formControl directive instead of formControlName solves the problem.

[formControl]="form.get(question.iterable ? [question.key, index] : question.key)"

also with this approach you don't need [attr.formArrayName]="!!question.iterable ? question.key : null" binding anymore

here is a working demo

answered Jun 25, 2019 at 9:03
Sign up to request clarification or add additional context in comments.

3 Comments

[formControl]="form.get(question.iterable ? [question.key, index] : question.key)"
@yurzui thanks for the great tip. i updated my answer based on your comment.
Thank you very much to all of you, I didn't know it was possible to call formControl directly instead of classic inputs (formControlName, formArrayName, etc). That's really, really interesting! I even might make a getter to retrieve the formControl directly! ;) Thank you again!

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.