Designing nested reactive forms from nested classes

给你一囗甜甜゛ 提交于 2020-01-21 19:40:46

问题


I have the following classes:

class License {
   name:string;
   .. lots of other fields.
   nameAttributes:NameAttributes[];
}

class nameAttributes:NameAttributes{
   index:number;
   attribute:string;
}

I know I can created the form like this, but it requires that I manually create each field(control) and every time the licences class changes, I have to update the class and this formGroup with the new fields.

       this.licenseForm = formBuilder.group({
          name:['name value'],
          otherFields:['their values'],
          nameAttributes: this.formBuilder.array([
            this.formBuilder.group({
              index:[1],
              attribute:["attr1"]
            }),
            this.formBuilder.group({
              index:[2],
              attribute:["attr2"]
            })
          ])
      });

I would prefer if I could just pass the license class into formBuilder and it would create the necessary FormGroups automatically and make them available based on their name, so the following would create two groups, a "license" group and a nested "nameAttributes" group with all of the associated controls for license and nameAttributes.

      this.licenseForm = formBuilder.group({
        license:formBuilder.group(license)
      });

Am I missing something or is this just not possible without some serious class introspection code?


回答1:


If your object has data, of course you can do it

Take a look to this stackblitz

You has a function like

  createForm(data:any):FormGroup
  {
    const group=new FormGroup({});
    for (let key in data)
    {
      if (Array.isArray(data[key])) //if is an array
      {                             //first create the formArray
        group.addControl(key,new FormArray([this.createForm(data[key][0])]))
        for (let i=1;i<data[key].length;i++)  //after add the rest of the elements
          (group.get(key) as FormArray).push(this.createForm(data[key][i]))
      }
      else
      {
        if (typeof(data[key])=="object") //if is an object we add a formGroup
          group.addControl(key,this.createForm(data[key]))
        else  //add a simple control
          group.addControl(key,new FormControl(data[key]))
      }
    }
    return group
  }

And call it as

this.form=this.createForm(this.obj)



回答2:


@cyberthreat as promised, here's a version with ngx-sub-form.

First of all, here's the link of a live demo: https://stackblitz.com/edit/angular-td2iew

Now let's see how it's built:

app.component.ts

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public licenseUpdated(license: License): void {
    console.log(JSON.stringify(license, null, 2));
  }
}

app.component.html

<app-license-form (licenseUpdated)="licenseUpdated($event)"></app-license-form>

Nothing too fancy here but notice the fact that from this component we absolutely don't care how we get the data of the license. We just want to be warned as soon as there's an update.

license-form.component.ts

@Component({
  selector: 'app-license-form',
  templateUrl: './license-form.component.html',
  styleUrls: ['./license-form.component.css']
})
export class LicenseFormComponent extends NgxRootFormComponent<License> {
  @DataInput()
  @Input('license')
  public dataInput: License | null | undefined;

  @Output('licenseUpdated') public dataOutput: EventEmitter<License> = new EventEmitter();

  protected getFormControls(): Controls<License> {
    return {
      name: new FormControl(null, [Validators.required]),
      nameAttributes: new FormControl(null, [Validators.required]),
    };
  }
}

license-form.component.html

<form [formGroup]="formGroup">
    <div>
        Name<br>
        <input type="text" [formControlName]="formControlNames.name">
  </div>

  <div>
    License attributes<br>
    <app-license-attributes-form [formControlName]="formControlNames.nameAttributes"></app-license-attributes-form>
  </div>

  <button class="save" (click)="manualSave()">Save form</button>
  (look at your console to see when the form is saved!)
</form>

<div class="values">
  Form group values
  <pre>{{ formGroupValues | json }}</pre>
</div>

Here, if you're not familiar with ngx-sub-form I'd invite you to read that blog post: https://dev.to/maxime1992/building-scalable-robust-and-type-safe-forms-with-angular-3nf9 and/or the README on the project: https://github.com/cloudnc/ngx-sub-form

The important thing to notice here is that we only care about primitive values and everything else is broken down into sub form components! Also, everything there is type safe (ts and html!) regarding the form.

license-attributes-form.component.ts

interface LicenseAttributesForm {
  attributes: NameAttribute[];
}

@Component({
  selector: 'app-license-attributes-form',
  templateUrl: './license-attributes-form.component.html',
  styleUrls: ['./license-attributes-form.component.css'],
  providers: subformComponentProviders(LicenseAttributesFormComponent)
})
export class LicenseAttributesFormComponent extends NgxSubFormRemapComponent<NameAttribute[], LicenseAttributesForm> {
  protected getFormControls(): Controls<LicenseAttributesForm> {
    return {
      attributes: new FormArray([], [Validators.required]),
    };
  }

  protected transformToFormGroup(obj: NameAttribute[]): LicenseAttributesForm {
    return {
      attributes: obj ? obj : [],
    };
  }

  protected transformFromFormGroup(formValue: LicenseAttributesForm): NameAttribute[] | null {
    return formValue.attributes;
  }

  public addAttribute(): void {
    (this.formGroupControls.attributes as FormArray).push(
      this.createFormArrayControl(
        'attributes',
        {
          index: null,
          attribute: null
        }
      )
    );
  }

  public removeAttribute(index: number): void {
    (this.formGroupControls.attributes as FormArray).removeAt(index);
  }

  public createFormArrayControl(
    key: any,
    value: any,
  ): FormControl {
    return new FormControl(value, [Validators.required]);
  }
}

license-attributes-form.component.html

<div [formGroup]="formGroup">
  <button (click)="addAttribute()">Add an attribute</button>

  <div
    class="attribute"
    formArrayName="attributes"
    *ngFor="let attribute of formGroupControls.attributes.controls; let i = index"
  >
    <app-license-attribute-form [formControl]="attribute"></app-license-attribute-form>

    <button (click)="removeAttribute(i)">Delete</button>
  </div>
</div>

And finally the last one

license-attribute-form.component.ts

@Component({
  selector: 'app-license-attribute-form',
  templateUrl: './license-attribute-form.component.html',
  styleUrls: ['./license-attribute-form.component.css'],
  providers: subformComponentProviders(LicenseAttributeFormComponent)
})
export class LicenseAttributeFormComponent extends NgxSubFormComponent<NameAttribute> {
  protected getFormControls(): Controls<NameAttribute> {
    return {
      index: new FormControl(null, [Validators.required]),
      attribute: new FormControl(null, [Validators.required]),
    };
  }
}

license-attribute-form.component.html

<form [formGroup]="formGroup">
    <div>
        Index<br>
        <input type="number" [formControlName]="formControlNames.index">
  </div>

  <div>
    Attribute<br>
    <input type="text" [formControlName]="formControlNames.attribute">
  </div>
</form>

I'd really encourage you to have a look on Stackblitz and play around with that demo it'll be the easier way to understand and discover I think :)



来源:https://stackoverflow.com/questions/56671796/designing-nested-reactive-forms-from-nested-classes

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!