OpenLMIS UI - Error behavior approaches

We currently have a multiple approaches on when the validations should be shown on our table forms (Requisition View, Proof of Delivery View, Shipment View, Adjustment, Issue and Receiver screens). All validation are fired with a slight delay due to the debounce setting we're using. This was done to improve the performance.

Current approaches

1. Standard Forms

Errors shows up only after the form is submitted. After the form has been submitted, but failed to do so because of errors, the errors for all inputs will be show. After that, validations will be fired every time the input is changed and error will be update (or cleared if the problem with the input has been fixed).

Level of effort to make standard: Small (removing a couple of directives which change how inputs inside tables behave compared to inputs inside form)

Example: Requisition Search page

2. Tables without form around them

The errors are show right after the input is changed or if it is focused out. Submitting the form causes all error to show up.

Level of effort to make standard: Medium (since we still would like these tables to be wrapped in forms - these are form after all - we would have to update a couple of directives to change the input behavior inside table - they errors should be shown immediately instead of waiting for submit)

Example: Product Grid, Adjustment, Issue, Receive screens

3. Tables with form around them

The errors won't be shown unless the row is left (focused out) or the form is submitted. After any of the actions has happened, the errors will be shown immediately. Leaving the row will cause all errors in that row to show up (even if the input wasn't touched). If the form also consist of inputs outside the table, their behavior will be the same as the one described in "Forms" approach.

Level of effort to make standard: Large (pagination would have to be updated to reflect this behavior - we don't currently have any screen with this approach and pagination; implementing this might be tricky as we would have to create a set of directives to track whether we have any invalid input that are actually showing a message - these are not equal)

Example: Proof of Delivery View, Shipment View

4. Tables with form around them (with highlighting headers)

Works the same as "Forms with table inside", but also highlights the column header.

Level of effort to make standard: Large (the amount of work needed for making the second option work, but can also require adjusting the directive for highlighting headers)

Example: Batch Approval screen

Recap

#OptionLevel of EffortUser Benefit

Performance Impact
Will the option slow down the experience for the user on 2G?

Screens which would change if this option is selected
1Mimic what the other forms do.SmallConsistency across the whole systemNone


  • View Proof of Delivery
  • View Requisition
  • View Shipment
  • Issue
  • Receive
  • Physical Inventory
  • Adjustments
  • Batch Approval
2Make errors appear after the fields has been touched.MediumInstant error feedbackNone
  • View Proof of Delivery
  • View Shipment
  • Batch Approval
3Make errors appear after we leave  (focus on something outside) the table row.LargeWhen fields within a lineItem are dependent on each other the error won’t show until they focus away from the LineItem (row)None
  • View Requisition
  • Issue
  • Receive
  • Physical Inventory
  • Adjustments

But, this approach will also require changing how pagination shows the page errors, it now is inconsistent with this approach and might require noticeable amount of work to make it happen

4Same as 3, but also highlight the table header.LargeWhen fields within a lineItem are dependent on each other the error won’t show until they focus away from the LineItem (row)None
  • View Proof of Delivery
  • View Requisition
  • View Shipment
  • Issue
  • Receive
  • Physical Inventory
  • Adjustments

Programming approaches

Currently we have a couple of different approaches on how to deal with validations from the coding side.

object.validate() method returning errors map

With this approach we're calling the method validate method and receive a map of errors for the specific object. If there are no errors an undefined is returned.

Pro: feels more DDD, the $errors object is not part of the domain object itself

Con: needs to be called for each input (multiple times for each digest cycle) unless we store the result outside the object

Level of effort to make standard: Large (most of our new code already uses this as the go to solution, but there is plenty of legacy code that would have to be updated to fit)

Example:

Proof of Delivery
/**
 * @ngdoc method
 * @methodOf proof-of-delivery.ProofOfDelivery
 * @name validate
 *
 * @description
 * Validates the Proof of Delivery and returns a map of errors if it is invalid.
 *
 * @return {Object} the map of errors if the Proof of Delivery is invalid, undefined
 *                  otherwise
 */
function validate() {
    var errors = {};

    verifyNotEmpty(errors, this.receivedBy, 'receivedBy');
    verifyNotEmpty(errors, this.deliveredBy, 'deliveredBy');
    verifyNotEmpty(errors, this.receivedDate, 'receivedDate');

    var lineItemsErrors = [];
    this.lineItems.forEach(function(lineItem) {
        var lineItemErrors = lineItem.validate();

        if (lineItemErrors) {
            lineItemsErrors.push(lineItemErrors);
        }
    });

    if (lineItemsErrors.length) {
        errors.lineItems = lineItemsErrors;
    }

    return angular.equals(errors, {}) ? undefined : errors;
}

object.validate() method updating object.$errors field

With this approach we're calling the validate method which updates a field called $errors which is a map of objects.

Pro: $errors (or errors) field can be reused, meaning we only have to fire the validate when we actually change an input value (can be nice performance boost)

Con: doesn't feel like the best DDD approach ($errors is not really part of the domain object)

Level of effort to make standard: Enormous (we're using this approach only with requisition line items)

Example:

RequisitionValidator
/**
 * @ngdoc method
 * @methodOf requisition-validation.requisitionValidator
 * @name validateLineItemField
 *
 * @description
 * Validates the field of the given requisition for the given column. Columns list is
 * necessary for validating calculations.
 *
 * @param  {Object}  lineItem the line item to be validated
 * @param  {Object}  column   the column to validate the line item for
 * @param  {Object}  columns  the list of columns used for validating the line item
 * @return {Boolean}          true of the line item field is valid, false otherwise
 */
function validateLineItemField(lineItem, column, requisition) {
    var name = column.name,
        error;

    if (lineItem[TEMPLATE_COLUMNS.SKIPPED]) return true;

    if (name === TEMPLATE_COLUMNS.TOTAL_LOSSES_AND_ADJUSTMENTS) return true;

    if (!column.$display) {
        return true;
    }

    if (column.$required) {
        error = error || nonEmpty(lineItem[name]);
    }

    if (validationFactory[name]) {
        error = error || validationFactory[name](lineItem, requisition);
    }

    if (shouldValidateCalculation(lineItem, column, requisition.template.columnsMap)) {
        error = error || validateCalculation(calculationFactory[name], lineItem, name);
    }

    if (column.$type === COLUMN_TYPES.NUMERIC && lineItem[name] > MAX_INTEGER_VALUE) {
        error = error || messageService.get('requisitionValidation.numberTooLarge');
    }

    return !(lineItem.$errors[name] = error);
}

OpenLMIS: the global initiative for powerful LMIS software