2
votes

I have a component, form and business validators behind them. Additionally, have html which shows how the form is rendered.

The thing is, what if someone wants to utilize the formbuilder with same validators, however a totally different html rendering?

How to separate the form below, so formcontrols and validators, are separated and not strongly coupled to the html format?

Can we Export as a class, or Input/Output, or ControlValueAccessor?

Currently this is one component. Note: will need ability to handle more complex validators (eg, City/State or Zip code required, one or the other)

CustomerInfo.component:

Typescript

constructor(
    private formBuilder: FormBuilder,
    private cdr: ChangeDetectorRef
  ) {
    this.addressform = this.formBuilder.group({
      'firstName': [null, [Validators.maxLength(32)]],
      'lastName': [null, [Validators.maxLength(32)]],
      'phone': [null, [Validators.maxLength(32)]],
      'streetName': [null, [Validators.required, Validators.maxLength(64)]],
      'streetType': [null, [Validators.maxLength(8)]],
      'city': [null, [Validators.maxLength(32)]],
      'state': [null, [Validators.maxLength(16)]],
      'postalCode': [null, [Validators.maxLength(16)]],
    },  { validator: atLeastOneLocationRequired })

// City,State  or PostalCode Required

export function atLeastOneLocationRequired(group : FormGroup) : {[s:string ]: boolean} {
      var cityCtrl = group.controls.city;
      var stateCtrl = group.controls.state;
      var postalCodeCtrl = group.controls.postalCode;

      if (cityCtrl != undefined && stateCtrl != undefined && postalCodeCtrl != undefined)
        if (!(((cityCtrl.value && cityCtrl.value.length) && (stateCtrl.value && stateCtrl.value.length)) || (postalCodeCtrl.value && postalCodeCtrl.value.length)))
          return { invalid: true };
}

HTML The formbuilder will require a different html format later.

        <div class = "propertysitusaddress">
          <form [formGroup]="addressform">
            <div class = "propertysiteaddresscontainer column">
                <div class = "row title">
                    Address
                </div>  
                <div class = "row">
                    <app-input-textboxc formControlName = "firstName"> </app-input-textbox>
                    <app-input-textbox  formControlName = "lastName"> </app-input-textbox>
                    <app-input-textbox  formControlName = "phone"> </app-input-textbox>
                </div>
                <div class = "row">
                    <app-input-textbox  formControlName = "streetName"> </app-input-textbox>
                    <app-input-textbox  formControlName = "city"> </app-input-textbox>
                    <app-input-textbox  formControlName = "state"> </app-input-textbox>
                </div>
                <div class = "row">
                    <app-input-textbox  formControlName = "postalCode"> </app-input-textbox>
                </div>
            </div>

Its often mentioned to separate smart and presentational components in Angular.

1
Use an angular service to build the form. This method in the service takes in all the fields as parameters and constructs the form and return the form for use in various other components - saidutt
hi @saidutt can I use Input/Output, or Control Value Accessor, or Export as a class component? can you show in example, thanks - user12425844
Please refer the example below, feel free to post any further questions. Happy to answer - saidutt

1 Answers

1
votes

I implemented this in one of my projects.

Method #1

Create a custom generic model for form fields. In your case each field has a name and maxLength property. So your form field Model would look something like this.

export class FormFieldModel {
  name: string;
  label: string;
  fieldConfig: CustomFieldConfig;
}
export class CustomFieldConfig {
  /** whether the property field is editable or not within the form */
  editable: boolean;

  /** optional collection of validators for the form field */
  validators?: ValidatorFn[];

  /** whether the form field is required */
  required = true;

  maxlength?: number;

  /** the control type for the form field */
  type?: 'select' | 'date' | 'number' | 'text' | 'checkbox';

  patternValidationMessage?: string;

  options?: any[];
}

And now in each of your components where the form is necessary. Using the above model build an array of all the fields.

Example:

const fields = FormFieldModel[] = [
     {
      name: 'firstName',
      label: 'First Name',
      fieldConfig: {
       maxLength: 32
       // other custom props.
    },
   {
      name: 'lastName',
      label: 'Last Name',
      fieldConfig: {
       maxLength: 32
       // other custom props.
    }
 }

As mentioned earlier in the comments create a service to create the form or it can be also created at individual component level.

  createForm(formList: FormFieldModel []) {
    const form = this.formBuilder.group({ });
    formList.forEach(field => {
      const validators = field.formConfig ? field.formConfig.validators : undefined;
      form.addControl(field.name, this.formBuilder.control(field.value, validators));
    });
    this.addressForm= form; // if this method is at component level
    or
    return form; // if this method is at service layer. 
  }

This way, it will also help with your html. Now, in your html. You can loop over the fields.

<form [formGroup]="addressForm">
 <div *ngFor="let field in fields>
  <app-input-textbox  formControlName = "field.name"> </app-input-textbox> // you can have a custom component that takes in individual field object and display it accordingly.
</div>
</form>

Method #2:

Just take the FormGroup and Formbuilder and place into an export class. You can utilize your code, and call this class in new components.