Skip to content

Platform: FormGroup Layout Technical Design

kavya-b edited this page May 19, 2020 · 7 revisions

Summary

Forms are usually the central part of each business application where we need to be able to work with business entities (e.g: Purchase Order, Invoice,..) to accommodate any business process that is required. To make this happen we should be able to abstract form development to easy to use set of components which hides all the HTML/CSS details and outline proper design structure that each front-end developer can follow.

The main goal is:

  • Abstract form assembly into High-order component to boost developer experience and productivity
  • Built-in multi layouts supports so developer does not have to think how to position element on the page
  • In the future layout should configurable. Either declarative or pluggable by some Layout Manager.
  • Error handling
  • I18n (either built-in or delegated to the application)

In order to achieve something like this:

core vs platform

Form components structure can be broken down into 3 groups:

  • Form Group (actual Form Layout)
  • Form Field
  • FormControl (input widget)

FormGroup:

  • Manages global Error handling navigation
  • Responsible for layout (multizone)
  • Can pre-set common properties to be applied for whole forms

group

FormField:

  • Aggregates common behavior for input field (Errors, Hints, Labels,..)
  • If multi zone layout is enabled ability to specify where the field should appear (e.g.: left/right column)
  • Responsible for change detection and UI updates

group

FormControl:

  • Shares common properties among other input form controls
  • Notify its parent (FormField) about any state change.

group

To support above structure, we need to introduce common interface that each control needs implement:

export abstract class FormFieldControl<T> {

  /**
   * Each input control has always a value. Need to make sure we keep a convention for
   * input fields
   */
  value: T | null;

  /**
   * Need to have a way to set placeholder to the input
   */
  placeholder: string;


  /**
   * Sets id from FF to Input
   */
  id: string;


  editable: boolean;

  /**
   *
   * Form Field listen for all the changes happening inside the input
   */
  readonly stateChanges: Observable<void>;


  /**
   *  Each input should inject its own ngControl and we should retrieve it
   */
  readonly ngControl: NgControl | null;

  /** Whether the control is disabled. */
  readonly disabled: boolean;

  /**
   * Keeps track if the form element is in focus
   */
  readonly focused: boolean;

  /** Whether the control is in an error state. */
  readonly inErrorState: boolean;

  abstract onContainerClick(event: MouseEvent): void;

}

FormFieldControl is an abstract class that is used both to enforce the contract between the input widgets and FormField as well as it used as a provider identificator when registering control.

Register new provider under FormFieldControl identifier. E.g:

providers: [
    {provide:FormFieldControl, useExisting:InputComponent, multi:true }
]
  • Each control needs to use ChangeDetectionStrategy.OnPush strategy

  • If possible extract common behavior to the base class so other inputs controls can inherit this.

  • Each FormFieldControl needs to inject (ChangeDetectorRef )

  • Each FormFieldControl needs to inject:

    • ChangeDetectorRef
    • NgControl – access to THIS (ngModel, FormControl)
    • NgForm – In case this control is wrapped with
  • stateChanges is an instance of Subject that formField (parent) can subscribe to.

  • Each control is responsible to detect state changes from the NgControl and update error state accordingly.

Example

1. Simple form definition

To layout the form you use <fdp-form-group> and its <fdp-form-field> Current implementation supports 5 zone layout with 2 columns but it need sto be extended to be compliant with the spec.

    <fdp-form-group #fg [multiLayout]="true" [formGroup]="form" [object]="data" >
     
      <fdp-form-field #ffl1 [id]="'firstName'" hint="Hint for one " zone="zLeft"
                      [validators]="validators" rank="10">
        <fdp-input [formControl]="ffl1.formControl"></fdp-input>
      </fdp-form-field>

      <fdp-form-field #ffl2 [id]="'address'" required="true" hint="Hint for two "
                      zone="zRigth" rank="20">
        <fdp-input [formControl]="ffl2.formControl"></fdp-input>
     </fdp-form-field>

       <fdp-form-field #ffl3 [id]="'Description'" required="true" zone="zBottom" rank="20">
        <fdp-input [formControl]="ffl3.formControl"></fdp-input>
     </fdp-form-field>

Bindings:

  • useForm: Form will be wrapped with Form tag
  • multiLayout: Turn on the multilayout support otherwise its 1 column
  • formGroup: Ability to assign formGroup to comunicate with the FormAPI
  • noLabelLayout: Forms without labels
  • labelLayout: Vertical / Horizontal
  • object: This is jsut a convient way to initialize formControl values
    • This maps to id

FormField

  • id: Id that will be used as FormController name as well as input id
  • label: Can provide label for the field
  • hint: Help message
  • zone: In case of multizone layout it will move field to the correct position
  • columms: Specify how many columns the current fields occupies
  • rank: Fields can be ranked by number to keep their order
  • validators: Assign set of built-in or custom NG validators for each fields
  • required: Sets the asterix flag and adds default Validators.required
  • section: Each field can have section title
    • if section is provided field will be group by this section and sorted accordingly.

2. Form with i18n support

Since i18n can not be added to the library as of time writing this wiki, we need to find a way to provide some support for i18n. what we can do is to expose a ng-template or have bindings to be passed to the form.

FormGroup will pass angular's error object to the application and application can decide how to translate this string.

    <fdp-form-group #fg [multiLayout]="true" [formGroup]="form" [object]="data" >
     
      <fdp-form-field #ffl1 [id]="'firstName'" hint="Hint for one " zone="zLeft"
                      [validators]="validators" rank="10">
        <fdp-input [formControl]="ffl1.formControl"></fdp-input>
      </fdp-form-field>

      <fdp-form-field #ffl2 [id]="'address'" required="true" hint="Hint for two "
                      zone="zRigth" rank="20">
        <fdp-input [formControl]="ffl2.formControl"></fdp-input>
        
        <ng-template #i18n let-errros>
            <span *ngIf="errros.required">
                Value is required
            </span>
        
      </ng-template>

    </fdp-form-field>

Bindings:

  • i18Strings: Setts translation as binding
  • ng-template #i18n let-errros: Pushes the error error object to the application

i18n

Link to general support for i18n: Supporting internationalization in ngx/platform

As mentioned in the sections above, i18n for Forms and Form-related components will be handled via i18Strings and #i18n template ref bindings. Some more detail is specified in the guidelines above.

Special Usecase: No

The components using Form may have default labels specified, but Form Group Layout in itself is a wrapper and will not hold any default static strings.

Redesign Required: No


Notes

  • Common properties:

    • value: T | null
    • placeholder: string
    • size: 'cozy' | 'compact'
    • disabled: boolean
    • status: 'error' | 'warning' | void
    • name
    • id
    • focused: boolean
    • ngControl: NgControl | null
  • Events

    • stateChanges
  • Method

    • focus()
Clone this wiki locally