Strickland
  • Readme
  • Introduction
    • Validators
    • Validation
    • Validation Results
  • Extensibility
    • Validator Factories
    • Validation Context
    • Validation Result Props
    • Extensibility Pattern
    • formatResult
  • Built-In Validators
    • required
    • compare
    • min
    • max
    • range
    • minLength
    • maxLength
    • length
  • Composition
    • Arrays of Validators
      • every
      • all
      • some
    • Validating Array Elements
      • arrayElements
    • Validating Objects
      • objectProps
      • Advanced Object Validation
      • Nested Objects
      • Arrays of Objects
    • Composition Conventions
    • Composition and formatResult
  • Async Validation
    • Resolving Async Validation
    • Deferred Async Validation
    • Async Validator Arrays and Objects
    • Two-Stage Sync/Async Validation
    • Race Conditions
    • Automatic Race Condition Handling
    • Async Validation and formatResult
  • Form Validation
    • form
    • Async Form Validation
    • validateFields
    • emptyResults
    • updateFieldResults
  • Inspiration
  • Design Goals
  • Wrap-Up
  • Change Log
  • NPM
  • GitHub
Powered by GitBook
On this page

Was this helpful?

  1. Composition

Composition and formatResult

PreviousComposition ConventionsNextAsync Validation

Last updated 4 years ago

Was this helpful?

We learned about earlier. That validator wrapper provides an easy way to augment or transform the objectProps and arrayElements validation results to match the shape your application needs.

In the following example, formatResult is used to create a validationErrors validation result prop. The validationErrors value is flattened array of all validation errors existing in an object or array graph.

import validate, {arrayElements, formatResult} from 'strickland';

const withValidationErrors = (result) => {
    const validationErrors = [];

    function addErrorsFromObjectProps(resultObjectProps, parentPath) {
        if (resultObjectProps) {
            Object.keys(resultObjectProps)
                // for each prop, get that prop's result
                .map((propName) => ({
                    ...resultObjectProps[propName],
                    propName
                }))

                // recursively add errors
                .forEach(({propName, ...nestedResult}) => {
                    const propPath = [...parentPath, propName];

                    addErrorsFromResult({
                        ...nestedResult,
                        propPath
                    });
                });
        }
    }

    function addErrorsFromArrayElements(resultArrayElements, parentPath) {
        if (resultArrayElements) {
            // recursively add errors
            resultArrayElements.forEach((nestedResult, arrayElement) => {
                const propPath = [...parentPath, arrayElement];

                addErrorsFromResult({
                    ...nestedResult,
                    propPath
                });
            });
        }
    }

    function addErrorsFromResult(nestedResult) {
        if (!nestedResult.isValid) {
            const {
                objectProps,
                arrayElements,
                propPath = [],
                ...errorResult
            } = nestedResult;

            // omit the `objectProps` and `arrayElements`
            // result props but include `propPath`
            validationErrors.push({propPath, ...errorResult});

            addErrorsFromObjectProps(objectProps, propPath);
            addErrorsFromArrayElements(arrayElements, propPath);
        }
    }

    addErrorsFromResult(result);

    return {
        ...result,
        validationErrors
    };
};

const validateWithErrors = formatResult(withValidationErrors, {
    name: required(),
    addresses: [required(), minLength(1), arrayElements({
        addressType: required(),
        street: [required(), {
            number: required(),
            name: required()
        }],
        city: [required()],
        state: [required(), length(2, 2)],
        postal: [required(), length(5, 5)]
    })]
});

const data = {
    name: 'Marty',
    addresses: [
        {
            addressType: 'Home',
            street: {
                number: 9303,
                name: 'Lyon Drive'
            },
            city: 'Hill Valley',
            state: 'CA'
        },
        {
            addressType: 'Work'
        }
    ]
};

const result = validate(validateWithErrors, data);

expect(result).toMatchObject({
    validationErrors: [
        expect.objectContaining({
            value: expect.objectContaining({
                name: 'Marty',
                addresses: expect.any(Array)
            })
        }),
        expect.objectContaining({
            propPath: ['addresses']
        }),
        expect.objectContaining({
            propPath: ['addresses', 0],
            value: expect.objectContaining({
                addressType: 'Home'
            })
        }),
        expect.objectContaining({
            propPath: ['addresses', 0, 'postal'],
            required: true
        }),
        expect.objectContaining({
            propPath: ['addresses', 1],
            value: expect.objectContaining({
                addressType: 'Work'
            })
        }),
        expect.objectContaining({
            propPath: ['addresses', 1, 'street'],
            required: true
        }),
        expect.objectContaining({
            propPath: ['addresses', 1, 'city'],
            required: true
        }),
        expect.objectContaining({
            propPath: ['addresses', 1, 'state'],
            required: true
        }),
        expect.objectContaining({
            propPath: ['addresses', 1, 'postal'],
            required: true
        })
    ]
});

/*
    // Most result props are omitted for illustration
    // of the validationErrors contents
    result = {
        isValid: false,
        // objectProps,
        // value,
        validationErrors: [
            {
                value: {
                    name: 'Marty',
                    addresses: [/* ... */]
                })
            },
            {
                propPath: ['addresses']
            },
            {
                propPath: ['addresses', 0],
                value: {
                    addressType: 'Home'
                }
            },
            {
                propPath: ['addresses', 0, 'postal'],
                required: true
            },
            {
                propPath: ['addresses', 1],
                value: {
                    addressType: 'Work'
                }
            },
            {
                propPath: ['addresses', 1, 'street'],
                required: true
            },
            {
                propPath: ['addresses', 1, 'city'],
                required: true
            },
            {
                propPath: ['addresses', 1, 'state'],
                required: true
            },
            {
                propPath: ['addresses', 1, 'postal'],
                required: true
            }
        ]
    }
*/
formatResult