1
votes

TL;DR: Combining nested components with nested reactive forms, appears to be problematic. Each nested component must build the entire form hierarchy from a combination of [formGroupName], [formArrayName] and [formControlName] directives.

Detail: Given a key-value pair is modeled as:

{
    "key": string,
    "value": string
}

A single key-value pair and a list of key-value pairs could be modeled as:

{
  "one": {
    "key": "Key A",
    "value": "Value A"
  },
  "many": [
    {
      "key": "Key A",
      "value": "Value A"
    },
    {
      "key": "Key B",
      "value": "Value B"
    },
    {
      "key": "Key C",
      "value": "Value C"
    }
  ]
}

It looks like a combination of Angular Reactive Forms and nested @Component should make this trivial.

I created the following component hierarchy and project structure:

│   app.component.html
│   app.component.ts
│   app.module.ts
│
├───key-value
│       key-value.component.html
│       key-value.component.ts
│
└───key-value-list
        key-value-list.component.html
        key-value-list.component.ts

The application component, app.component.ts, defines a model and a corresponding FormGroup built using FormBuilder:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  public form: FormGroup;
  public model = {
    one: {
      key: 'Key A',
      value: 'Value A'
    },
    many: [
      {
        key: 'Key A',
        value: 'Value A'
      },
      {
        key: 'Key B',
        value: 'Value B'
      },
      {
        key: 'Key C',
        value: 'Value C'
      }
    ]
  };

  constructor(private readonly fb: FormBuilder) { }

  public ngOnInit(): void {
    const items: FormGroup[] = this.model.many.map(pair => {
      return this.fb.group(pair);
    });

    this.form = this.fb.group({
      one: this.fb.group(this.model.one),
      many: this.fb.array(items)
    });
  }
}

The model introduced at the beginning should be easily identifiable. form contains two controls, FormGroup and FormArray for key-value and key-value-list respectively. FormArray is a list of FormGroups.

app.component.html provides the top-level form and the name of the control group the key-value pair should belong to:

<key-value [parentForm]="form" name="one"></key-value>
<pre>{{ form.value | json }}</pre>

Form values are piped out for debugging and formatted (<pre>). The key-value.component.ts accepts two Input() values and does very, very little (all the work is done in the view):

@Component({
  selector: 'key-value',
  templateUrl: './key-value.component.html'
})
export class KeyValueComponent {
  @Input() public parentForm: FormGroup;
  @Input() public name: string;
}

The work is done in key-value.component.html using the two Input() values to construct a hierarchy of controls:

<div [formGroup]="parentForm">
    <div [formGroupName]="name">
        <mat-form-field>
            <input matInput formControlName="key" placeholder="Key">
        </mat-form-field>
        <mat-form-field>
            <input matInput formControlName="value" placeholder="Value">
        </mat-form-field>
    </div>
</div>

The list view is similar:

<div [formGroup]="parentForm">
    <div [formArrayName]="name">
        <div *ngFor="let c of parentForm.get(name).controls; let i=index;" [formGroupName]="i">
            <mat-form-field>
                <input matInput formControlName="key" placeholder="Key">
            </mat-form-field>
            <mat-form-field>
                <input matInput formControlName="value" placeholder="Value">
            </mat-form-field>
        </div>
    </div>
</div>

This works. But I'd like to re-use key-value component in the key-value-list component, but I'm hitting a wall. Assuming my application view becomes:

<div [formGroup]="parentForm">
    <div [formArrayName]="name">
        <div *ngFor="let c of parentForm.get(name).controls; let i=index;" [formGroupName]="i">
            <key-value [parentForm]="??????" name="??????"></key-value>
        </div>
    </div>
</div>

This seems a reasonable start, but I don't know what the inside of the loop should be.

1
Why don't you make the sub-component a control value accessor? angular.io/api/forms/ControlValueAccessorjonrsharpe
On key-value? And. Should that be necessary? It may be possible, but it feels a little involved for merely rendering a list of editable components?Jack
Why any? Isn't each one representing a single key-value? Perhaps you could edit to clarify, there's a lot going on in your question.jonrsharpe
Sorry, I made a bad typo. any-value should have been key-value (fixed previous comment too).Jack
I don't have a solution for you but I do want to raise couple of questions: 1. The way you construct your one is to make it as a FormGroup and hello-component is used to render a FormGroup with 2 controls: key and value'. However, your many` is a FormArray of FormControls, not FormArray of FormGroups. Hence, I do not think it is possible to "re-use" hello-component in hello-list 2. As you "re-use" hello, you also "re-render" the parentForm in your hello template.Chau Tran

1 Answers

3
votes

Actually, you don't need to pass both, FormGroup instance and FormGroupName to the child component, either one will do. Arrays are a bit trickier than single control but something like this should work:

<div [formGroup]="parentForm">
    <div [formArrayName]="name">
        <div *ngFor="let c of parentForm.get(name).controls; let i=index;">
            <key-value [parentForm]="c"></key-value>
            <!--<key-value [name]="i"></key-value> this should also work-->
        </div>
    </div>
</div>