1
votes

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?

2
@maxime1992, sincerely, I can't see the reason to create custom Form control, when is enough create a component and pass the formControl - Eliseo
@maxime1992 Thank you for the article reference. I feel that your solution is appropriate for the needs of your complex CAD app(which is pretty badass BTW), but overkill for me at the moment. - cyberthreat
@Eliseo "when is enough create a component and pass the formControl" I've replied to that in this section: dev.to/maxime1992/… - maxime1992
@maxime1992 That would be great. I look forward to seeing it. - cyberthreat

2 Answers

2
votes

@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 :)

1
votes

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)