Skip to content

Latest commit

 

History

History
453 lines (374 loc) · 17.7 KB

FORM_ERRORS.md

File metadata and controls

453 lines (374 loc) · 17.7 KB

Form errors

Form errors

Handling the form errors in Otter context (container/presenter, localization ...), it's a bit different from creating a form in a component and do all the logic there.

Form error store

To have the possibility to display inline error messages in the form and also in error panels (on the top of the page, above submit button ...) the best match is to have a dedicated store for the form errors. In this way we can listen to the store state and display the errors anywhere in the page. The store is provided in @o3r/forms package. See Form Error Store for more details and state object model.

Error object model

The store model object is FormError. See below the form errors object models.

  • The FormError contains an identifier for each component which has a form inside, plus the errors associated to that form.
/** Form's error messages identified by form id */
export interface FormError {
  /** Id of the form containing the form field/fields */
  formId: string;

  /** Component's elements errors */
  errors: ElementError[];
}
  • ElementError This object contains all the errors associated to the html element. The identifier htmlElementId can be used as an anchor link to focus on the html element on which the validation has failed
/** Error messages of the html element identified by its id */
export interface ElementError {
  /** Id of the html element on which the validation has failed */
  htmlElementId: string;

  /** Element's error message objects */
  errorMessages: ErrorMessageObject[];
}
  • ErrorMessageObject
    • associated to an error message on a field.
    • It will contain:
      • translationKey for the error message
      • longTranslationKey used for a more detailed message on the same error
      • translationParams translations parameters
      • validationError original error object
/** The error object saved in the store for a specific element/formControl */
export interface ErrorMessageObject {
  /**
   *  Translation key of the short error message (e.g. used for inline errors)
   *  @example
   *  ```typescript
   *  translationKey = 'travelerForm.firstName.required'; // => corresponds to {'travelerForm.firstName.required': 'First name is required!'} in localization json;
   *  ```
   */
  translationKey: string;

  /**
   * Translation key of the long error message (e.g. used on a message panel)
   * @example
   * ```typescript
   * longTranslationKey = 'travelerForm.firstName.required.long'; // => corresponds to {'travelerForm.firstName.required.long': 'The first name in the registration form cannot be empty!'}
   * // in localization json;
   * ```
   *
   */
  longTranslationKey?: string;

  /** Translation parameters of the error message; Used in the short message but also in the long message if needed */
  translationParams?: { [key: string]: any };

    /**
   * Original error object defined by the corresponding validator
   * @note It's optional since custom errors don't need to provide the validation error
   * @example
   * ```typescript
   * {required: true}
   * ```
   * @example
   * ```typescript
   * {max: {max 12, actual: 31}}
   * ```
   */
  validationError?: {[key: string]: any};
}

Creating error object

The presenter has to implement the Validator or AsyncValidator in order to give us the possibility to define the error object which will be returned by the form. The error message structure will be defined in the implementation of validate method. As validate function should return a ValidationErrors object, which is a map of custom objects (with type any), we can prepare the returned object for the store of error messages. This will ease the process of adding the errors in the store. We have to make sure that we are providing the htmlElementId for the errors in the store which is matching the html field. For this, the presenter is receiving an id as input and for each field we are concatenating the id with the formControlName. As the container is setting a unique id we are sure that we have uniques html ids for the form fields. The object returned by the validate is the error object which is propagated to the container.

There are 2 types of validators (see Form Validation), 2 categories of error messages:

  • one for custom errors - set on the container
  • one for primitive errors - computed in the presenter.

Custom errors

They are returned by custom validators and have the type CustomErrors defined in @o3r/forms. This one is using customErrors key with an array of ErrorMessageObject which has to contain all the custom errors for a form control or group.

/**
 * The return of a custom validation
 */
export interface CustomErrors {
  /** The custom errors coming from a validation fn */
  customErrors: ErrorMessageObject[];
}

Error object model returned by the validator has to be compliant with the store model.

// Example of returned object by the custom validator
{customErrors: [{translationKey, longTranslationKey, translationParams}]};

Basic/primitive errors

The error object structure has to be created in the presenter because the basic validators are defined at presenter level (see FORM_VALIDATION).

Build error messages

We put in place a generic helper getFlatControlErrors in @o3r/forms. This gets a flattened list of all the errors in the form and it's descendants, concatenating the custom errors; The object returned by the helper has ControlFlatErrors type.

/**
 * Represents all errors (validation or custom ones) from a control.
 * Useful for working with form errors
 * @note The control may be form, therefore the controlName may be undefined
 */
export interface ControlFlatErrors {
  /** The name of a field. e.g firstName, cardNumber. If it's a form, should be undefined
   * @note For child fields, use [parentControlName].[fieldName]. e.g expiryDate.month
   */
  controlName?: string;
  /** List of customErrors (coming from custom validation) linked to the control */
  customErrors?: ErrorMessageObject[] | null;
  /** The list of flatten errors linked to the control */
  errors: FlatError[];
}

Example of validate method implementation

/// ----> in the presenter class
import { ControlFlatErrors, CustomFormValidation, FlatError, getFlatControlErrors } from '@o3r/forms';
...

export class FormsPocPresComponent implements OnInit, Validator, FormsPocPresContext, ControlValueAccessor, Configurable<FormsPocPresConfig>, OnDestroy {

    /**
     * Localization of the component
     */
    @Input()
    @Localization('./forms-poc-pres.localization.json')
    public translations: FormsPocPresTranslation;

    /** Object used to compute the ids of the form controls */
    @Input() id: string;

    componentSelector: string = 'o3r-forms-poc-pres';

    travelerForm: FormGroup;

    constructor(config: FormsPocPresConfig, private fb: FormBuilder, protected changeDetector: ChangeDetectorRef) {
      this.config = config;
      this.translations = translations;
      // Create the form with no initial values
      this.travelerForm = this.fb.group({
        firstName: null,
        lastName: null,
        dateOfBirth: null
      });
    }

  /**
   * Return the errors for the validators applied global to the form plus the errors for each field
   *
   * @inheritDoc
   */
  public validate(_control: AbstractControl): ValidationErrors | null {
    if (this.travelerForm.status === 'VALID') {
      return null;
    }

    const formErrors = getFlatControlErrors(this.travelerForm);  // ---> use the helper to get the flat list of errors for the form

    const errors = formErrors.reduce((errorsMap: ValidationErrors, controlFlatErrors: ControlFlatErrors) => {
      return {
        // ...errorsMap,
        [controlFlatErrors.controlName || 'global']: { // ---> use the 'global' key for the errors applied on the root form
          htmlElementId: `${this.id}${controlFlatErrors.controlName || ''}`, // ---> The html id of the element
          errorMessages: (controlFlatErrors.customErrors || []).concat( // ---> errors associated to the html element ( custom errors plus basic ones )
            controlFlatErrors.errors.map((error) => {
              // Translation key creation
              // As the primitive errors are linked to the presenter we use the component selector, the control name and the error key, to compute the translationKey
              // Ex: componentSelector= 'o3r-forms-poc-pres', controlName='firstName', error key {required: true} -> the error key is 'required'
              // translationKey = 'o3r-forms-poc-pres.firstName.required' or something like 'o3r-forms-poc-pres.firstName:required'
              const translationKey = `${this.componentSelector}.${controlFlatErrors.controlName}.${error.errorKey}`;
              return {
                translationKey,
                // Check if we have a long translation key in the defined translations associated to the presenter
                longTranslationKey: this.translations[`${translationKey}.long`] || undefined,
                validationError: error.validationError,
                translationParams: this.getTranslationParamsFromFlatErrors(controlFlatErrors.controlName || '', error) // ---> get the translation parameters for each control
              };
            })
          )
        }
      };
    }, {});

    return errors;
  }

  /**
   * Create the translation parameters for each form control error
   * @Note This is specific to the implementation of the form in each presenter
   */
  getTranslationParamsFromFlatErrors(controlName: string, error: FlatError) {
    switch (controlName) {
      case 'dateOfBirth': {
        switch (error.errorKey) {
          case 'max':
            return {max: error.errorValue.max};
          case 'min':
            return {min: error.errorValue.min};
          default:
            return {};
        }
      }
      case 'firstName': {
        switch (error.errorKey) {
          case 'maxlength':
            return {requiredLength: error.errorValue.requiredLength};
          default:
            return {};
        }
      }
      case 'dateOfBirth.month': {
        // Use case for form subcontrols
        switch (error.errorKey) {
          case 'max':
            return {maxMonthValue: error.errorValue.max};
          case 'min':
            return {minMonthValue: error.errorValue.min};
          default:
            return {};
        }
      }
      default:
        return {};
    }
  }
}

This is only an example of implementation. The translationKey and translationParams can be different implemented depending on the use cases.

Display inline error messages

Basic errors

///----> presenter template
<input type="date" formControlName="dateOfBirth" [id]="id + 'dateOfBirth'"></input>
<mat-error *ngIf="travelerForm.controls.dateOfBirth.errors?.max">
  // use the translation object for the translationKey and get the translationParams from the error object returned by 'date-inline-input'.
  {{translations.maxMonthInDate | o3rTranslate: {max: travelerForm.controls.dateOfBirth.errors?.max.max} }}
</mat-error>

Custom errors

///----> presenter template
<input type="date" formControlName="dateOfBirth" [id]="id + 'dateOfBirth'"></input>
<mat-error *ngFor="let customError of travelerForm.controls.dateOfBirth.errors?.customErrors">
  // translation key and params are already accessible in the error object returned by the custom validator
  {{customError.translationKey | o3rTranslate: customError.translationParams }}
</mat-error>

Add errors to the store

As we already defined the error message object, as the return of validate method in the presenter, we can get the error messages and add them to the store, in the container. Check the example below.

/// ---> in the container
...
/** The form object model */
traveler: Traveler;

/** The form control object bind to the presenter */
mainFormControl: FormControl;

// Inject the store of form error messages
constructor(config: FormsPocContConfig, private store: Store<FormErrorMessagesStore>) {
...
this.traveler = {firstName: '', lastName: 'TestUser', dateOfBirth: new utils.Date()};
this.mainFormControl = new FormControl(this.traveler);
}

/** submit function */
submitAction() {
  // ...
  const isValid = !this.mainFormControl.errors;
  if (!this.mainFormControl.errors) {
    // ---> Submit logic here
    // eslint-disable-next-line no-console
    console.log('CONTAINER: form status and errors', this.mainFormControl.status, this.mainFormControl.errors);
    // eslint-disable-next-line no-console
    console.log('CONTAINER: submit logic here', this.mainFormControl.value);
  } else {
    const errors: FormError = {
      formId: `${this.id}-my-form-example`,
      errors: Object.keys(this.mainFormControl.errors).map((controlName: string) => {
        const controlErrors = this.mainFormControl.errors![controlName];
        return {htmlElementId: controlErrors.htmlElementId, errorMessages: controlErrors.errorMessages};
      })
    };
    // Add the errors corresponding to travelerForm in the store
    this.store.dispatch(new UpsertFormErrorMessagesEntities({entities: [errors]}));
  }
  // Emit an event when the submit logic is done
  this.onSubmitted.emit(isValid);
}

In the example above we save the errors in the store when we execute the submit action. It can be done at valueChanges or statusChanges.

Errors translation definition

For the localization of the error messages we keep the same way we have today (LOCALIZATION), but we have specific places where to define the default translations of error messages.

Custom errors

Because the form validation depends on business logic and the custom validators are created in the container (see: Form Validation) we have to provide an error message for each validator and to ensure that the message is translatable. We have to add the default translation keys, corresponding to the custom validators in the container (container.localization.json file).

  // ---> in container class
  /**
   * Localization of the component
   */
  @Input()
  @Localization('./forms-poc-cont.localization.json')  // Here we will define the error messages translation keys
  public translations: FormsPocContTranslation;

Default values for the custom errors

// ----> forms-poc-cont.localization.json
...
"travelerForm.dateOfBirth.max": { // ---> travelerForm is the name we have chosen for the form
  "description": "Validator for date of birth month",
  "defaultValue": "Max value for the month should be {{ max }}"
},
"travelerForm.global": {  // ---> validator for the root (global) form
  "description": "This validator will check if the first name or last name will be 'TEST'",
  "defaultValue": "First name and Last name cannot be {{forbiddenName}}"
}
...

Primitive errors

These validators are defined and applied at presenter level, so we have to define the translation of the error messages here. Each possible validator should have a corresponding error message in presenter.localization.json file.

// ---> in presenter class
/**
 * Localization of the component
 */
@Input()
@Localization('./forms-pres-cont.localization.json')  // Here we will define the error messages translation keys
public translations: FormsPocPresTranslation;

Default values for the custom errors

// The first key is not related to forms
"o3r-forms-poc-pres.key.not.related.to.forms": {
  "description": "Test Value with a translation",
  "defaultValue": "This is a test value translated from the presenter"
},
...
"o3r-forms-poc-pres.firstName.required": {
  "description": "Required validator for the first name",
  "defaultValue": "The first name is required"
},
"o3r-forms-poc-pres.firstName.maxlength": {
  "description": "Maxlength validator for the first name",
  "defaultValue": "The first name should have a max length of {{max}} characthers"
},
...