Skip to content

Suite Forms

When developing Suite Applications, we need to make sure that the UI is standard across them all.

In order to make sure all forms look the same, the Suite provides a set of Standard Form Controls for the most common scenarios, like text, password, date, multi-selection, etc. Basically, instead of using plain inputs, you must use the Suite's Form Controls.

When the Suite's Standard Controls are not enough, you can quickly create your own Custom Form Control.

In addition, in order to build complex forms quickly, the Suite provides helpers for building FormGroups and FormArrays.

Concepts

Before we dive in, let's define some concepts. These are all Angular's Concepts. The Suite integrates with them seamlessly.

FormControl

A FormControl is the most basic unit inside a form which has a Value. Think of them as the leafs of the tree. They are your Text, Password inputs, etc.

FormControls have validation status and a list of errors.

HTML
<input type="text" [FormControl]="myControl" />
TypeScript
public myControl = this.fb.control('', [Validators.Required]);

The FormControl's value would be a string containing the text the user typed.

FormGroup

A FormGroup is a set of FormControls grouped together. The FormGroup usually represents a subset of a complete Form.

It's value is an object made from the values of all the FormControl's it is grouping.

HTML
1
2
3
4
<div [FormGroup]="myGroup">
    <input type="text" formControlName="displayName" />
    <input type="text" formControlName="loginId" />
</div>
TypeScript
1
2
3
4
public myGroup = this.fb.group({
    displayName: ['', Validators.Required],
    loginId: ['', Validators.Required]
});

The FormGroup's value would be an object, where each key is the FormControl's name and the value is the FromControl's value.

JavaScript
1
2
3
4
{
    displayName: "John",
    loginId: "johnDoe",
}

The FormGroup has validation status and errors, both of which are compiled from its inner controls. Meaning that, if a FormControl is invalid, the FormGroup will be invalid. This is what allows us to handle validation of an entire Form or subset of a Form in a single place.

FormArray

The FormArray is a collection of AbstractControls. An AbstractControl is either a FormControl, a FormGroup or a FormArray.

That means that a FormArray is a set of either FormControls, FormGroups or FormArrays.

FormArrays are used when you have a Form that allows you to "add elements" to it. For example, a Form that allows you to add as many Guests as you want and for each of them you have to fill a set of inputs.

Usually we see FormArrays of FormGroups. That's what allows you to know that the entire list is valid or not. You can also know which of the FormGroups is invalid and the errors.

HTML
1
2
3
4
5
6
7
8
<div [FormArray]="myArray">
    <div *ngFor="let control of myArray.controls" [formGroupName]="i">
        <input type="text" formControlName="displayName" />
        <input type="text" formControlName="loginId" />
    </div>
</div>

<button (click)="addItem()">Add Item</button>
TypeScript
1
2
3
4
5
6
7
addItem() {
    const group = this.fb.group({
        displayName: ['', Validators.Required],
        loginId: ['', Validators.Required],
    });
    this.myArray.push(group)
}

The FormArray value would belike this:

JavaScript
1
2
3
4
5
6
[
    {
        displayName: 'John',
        loginId: 'johnDoe'
    }
];

Form

There is no class for "implementing a form". We simply use the <form> element in combination with a [FormGroup]. Cause, basically a Form is just a Group of Controls.

The cool thing about the <form> element is that it has the (submit) event that we can use to know when the form has been submitted. In combination with making the entire form a FormGroup, we can know if it is valid or not.

Suite Standard FormControls

Now you know the building blocks. The Suite includes a set of what we call Standard Form Controls that implement the basic HTML inputs and some more complex that we usually see when building enterprise applications.

Text FormControl

The text FormControl is the replacement of the input type="text".

HTML
1
2
3
4
5
6
<its-form-control
    text
    label="LBL_CREATE_USER_FORM_TRADE"
    formControlName="trade"
>
</its-form-control>

Password FormControl

The password FormControl is the replacement of the input type="password". It includes the "toggle password visibility" feature.

HTML
1
2
3
4
5
6
<its-form-control
    password
    label="LBL_CREATE_USER_FORM_PASSWORD"
    formControlName="password"
>
</its-form-control>

Other FormControls

Yeah that's it for now.. We are working on building more!

Custom FormGroup

Usually, we wanna split a huge Form into multiple Components in order to simplify it. We will quickly realize that we have no clear way of "making our component behave like a FormGroup".

So.. We could simple use an @Input() FormGroup, but what about formControlName? It starts to get tricky. For that purpose the Suite includes a FormGroupComponent that we can use to make our component behave like a group.

We can run the below command to generate a component which extends from FormGroupComponent:

Bash
1
2
3
yarn nx workspace-generator form-component service-order \
    --form-type group \
    --project admin-center-users-ui

Then we need to initialize the group in ngOnInit:

TypeScript
@Component({
    selector: 'admin-service-order-form',
    templateUrl: './service-order-form.component.html',
    styleUrls: ['./service-order-form.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ServiceOrderFormComponent
    extends FormGroupComponent
    implements OnInit
{
    ngOnInit() {
        super.ngOnInit();

        if (!this.formGroup.value.trade) {
            this.formGroup.setControl(
                'trade',
                new FormControl('', [Validators.required])
            );
        }
    }
}

As you can see, the FormGroupComponent already includes the formGroup which is bind properly from the caller side.

We still need to add the FormControls to it, so we do that in the OnInit.

Then, we can just use our Component as a FormGroup!

HTML
1
2
3
4
<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <admin-service-order-form formGroupName="serviceOrder">
    </admin-service-order-form>
</form>

Custom FormArray

FormArrays can be quickly created by extending the FormArrayComponent<TControl> class. We usually wanna use a FormGroup as TControl.

The base class gives us a formArray and formControls properties, which contains the FormArray instance and its list of Controls casted to TControl.

This allows us to quickly build components that behave as a FormArray!

We can run the below command to generate a component which extends from FormArrayComponent:

Bash
1
2
3
yarn nx workspace-generator form-component array-sample \
    --form-type array \
    --project admin-center-users-ui

We need to add methods for adding and removing, and specify the array's generic constraint:

TypeScript
@Component({
    selector: 'its-form-array-sample',
    templateUrl: './array-sample.component.html',
    styleUrls: ['./array-sample.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ArraySampleComponent
    extends FormArrayComponent<FormGroup>
    implements OnInit
{
    constructor(controlContainer: ControlContainer, private fb: FormBuilder) {
        super(controlContainer);
    }

    add() {
        const group = this.fb.group({
            displayName: ['', Validators.Required],
            loginId: ['', Validators.Required]
        });
        this.formArray.push(group);
    }

    remove(index: number) {
        this.formArray.removeAt(index);
    }
}
HTML
1
2
3
4
5
6
7
8
9
<ng-container *ngFor="let group of formControls; index as formGroupIndex">
    <ng-container [formGroupName]="formGroupIndex">
        <its-form-control text formControlName="displayName"></its-form-control>
        <its-form-control text formControlName="loginId"></its-form-control>
        <button (click)="remove(formGroupIndex)">Remove</button>
    </ng-container>
</ng-container>

<button (click)="add()">Add</button>

The usage would be like this:

TypeScript
1
2
3
public myForm = this.fb.group({
    myArray: this.fb.array([])
});
HTML
1
2
3
<form [formGroup]="myForm" (submit)="submit()">
    <its-form-array-sample formArrayName="myArray"></its-form-array-sample>
</form>

Custom FormControls

For developing custom FormControls, the Suite includes the FormControlComponent class and StandardFormControlComponent class.

That being said, before building a FormControl please contact the Suite Team, since we may want to reuse your control. In the case you feel your control is not reusable, perhaps it should be a FormGroup instead; so talk to us and let's figure it out together!

In any case, the idea behind the FormControlComponent is that you get a control : FormControl field that you need to keep up to date. The class implements a ControlValueAccessor bind to that control.

We can run the below command to generate a component which extends from FormControlComponent:

Bash
1
2
3
yarn nx workspace-generator form-component control-sample \
    --form-type control \
    --project admin-center-users-ui

The schematic generates the below:

  1. Component that extends from FormControlComponent
  2. A constant for the control's type.
  3. The special selector we use. i.e: its-form-control[${TEXT_FORM_CONTROL_TYPE}].
  4. An angular module which declares the component and all of its dependencies. This is for tree-shaking.
  5. The module also registers the component with the FormControlRepositoryService which is required for using the component in a DynamicFormControl

The module should look like this:

TypeScript
@NgModule({
    declarations: [TextFormControlComponent],
    imports: [
        CommonModule,
        ReactiveFormsModule,
        StandardFormFieldModule, // only for standard controls.
        MatInputModule
    ],
    exports: [TextFormControlComponent]
})
export class TextFormControlModule {
    constructor(repo: FormControlRepositoryService) {
        repo.register({
            type: TEXT_FORM_CONTROL_TYPE,
            component: TextFormControlComponent
        });
    }
}

Your component should look like this:

TypeScript
1
2
3
4
5
6
7
8
export const TEXT_FORM_CONTROL_TYPE = 'text';
@Component({
    selector: `its-form-control[${TEXT_FORM_CONTROL_TYPE}]`,
    templateUrl: './text-form-control.component.html',
    styleUrls: ['./text-form-control.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TextFormControlComponent extends FormControlComponent {}

Then we just need to set the control's value somehow. In the case of a Text control we just bind it to the underlying input.

HTML
1
2
3
4
<mat-form-field itsStandardFormField>
    <mat-label>{{label}}</mat-label>
    <input matInput [placeholder]="placeholder" [formControl]="control" />
</mat-form-field>

Once we've done this, we can use our control like so:

HTML
<its-form-control text />

Standard Form Controls

When building Standard Form Controls, meaning the controls for the framework, we can use below schematic:

Bash
1
2
3
yarn nx workspace-generator form-component control-sample \
    --form-type standardControl \
    --project admin-center-users-ui

It is important to use the itsStandardFormField directive on the mat-form-field which will give it the standard look that we want for all Suite Controls.

Composite Form Controls

When building a Control with an object value, first think if it shouldn't be a FormGroup instead.

In some cases, a FormControl with an object value makes sense, like in the case of a multi-selection component, where the FormControl's value is an item (or a set of items) from the original data set.

Exposing Control to other Modules

When building Standard Form Controls, or any sort of reusable components we need to export it in order to be able to use it from external modules.

In the Suite Module that contains the form, we need to export TextFormControlModule and the TextFormControlComponent in the public-api.ts file and then import it into the module of the application where you want to use it.

Dynamic Form Control Directive

When building dynamic forms, it is useful to create a Form Control dynamically on runtime. Say you have a property controlType = "text" in a Component.

You can then use the dynamic form control like so:

HTML
<its-form-control [type]="controlType"> </its-form-control>