Maxime
Maxime

Reputation: 2234

Access FormArray reference in ng-template

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

Upvotes: 1

Views: 1395

Answers (1)

ysf
ysf

Reputation: 4854

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

Upvotes: 4

Related Questions