flut1/knockout-validator

View on GitHub
src/lib/KnockoutValidator.ts

Summary

Maintainability
A
25 mins
Test Coverage
import Disposable from 'seng-disposable';
import * as ko from 'knockout';
import ClassnameOptions  from './options/Classnames';
import Field from "./fields/Field";
import createBindings from "./bindings/createBindings";
import * as some from 'lodash/some';
import * as find from 'lodash/find';
import IValidatable from "./interface/IValidatable";
import scheduleKoResolve from "./utils/scheduleKoResolve";
import fieldPlaceholder from "./const/fieldPlaceholder";

createBindings(ko.bindingHandlers);

export default class KnockoutValidator extends Disposable implements IValidatable
{
    /**
     * Object containing the classnames that the validator will apply on HTML elements based on validation state.
     * These may be changed if different classnames are desired.
     */
    public classnames = new ClassnameOptions();

    /**
     * Timeout for async validation in ms. If validation takes longer than this value, it will throw a
     * warning and return false (or invalid). If 0, validation will never timeout.
     */
    public asyncValidationTimeout:number = 5000;

    /**
     * Knockout computed value that returns _true_ while validation of one or more field is in progress. Is
     * returned to false after validation has completed
     */
    public isValidating:ko.PureComputed<boolean>;

    /**
     * Knockout computed that returns _true_ when all groups and fields have been validated
     * and are valid. If one of the groups or fields are invalid, this returns _false_.
     *
     * **If not all groups or fields have been validated, returns _null_**
     */
    public isValid:ko.PureComputed<boolean>;

    /**
     * Knockout computed that returns _true_ when all fields have been validated. Otherwise,
     * returns _false_.
     */
    public isValidated:ko.PureComputed<boolean>;

    /**
     * Returns an array of Field instances representing the state of each field in the
     * validator.
     */
    public fields:ko.PureComputed<Array<Field>> = ko.pureComputed(() => this._fields());

    /**
     * Observable array of fields attached to this validator. The accessors of this fields are
     * exposed through the public [[fields]] property.
     */
    private _fields:ko.ObservableArray<Field> = ko.observableArray([]);

    constructor()
    {
        super();

        this._initComputed();
    }

    public validate():Promise<boolean>
    {
        const fields = this._fields();
        fields.forEach(field => field.isValid(null));

        return Promise.all(fields.map(field => field.validate())).then((results:Array<boolean>) =>
        {
            let isValid = true;
            for(let i = 0; isValid && i < results.length; i++)
            {
                if(!results[i])
                {
                    isValid = false;
                }
            }
            return scheduleKoResolve(isValid);
        });
    }

    public getField = (name:string) =>
    {
        return find(this._fields(), field => field.name === name) || fieldPlaceholder;
    };

    /**
     * Unregisters a field with this validator instance
     * @param field The field to remove
     */
    public detachField(field:Field):void
    {
        const index = this._fields().indexOf(field);
        if(index >= 0)
        {
            this._fields.splice(index, 1);
        }
    }

    public clearValidation():void
    {
        const fields:Array<Field> = this._fields();
        fields.forEach(field => field.isValid(null));
    }

    /**
     * Registers a field with this validator instance
     * @param field The field to add
     */
    public attachField(field:Field):void
    {
        if(this._fields().indexOf(field) < 0)
        {
            this._fields.push(field);
        }
    }

    /**
     * Returns a map of the values of all fields attached to this validator by name.
     * @param validatedOnly If true, will only include the validated fields in the returned
     * map.
     */
    public getValues(validatedOnly:boolean = false):{[name:string]:any}
    {
        let fields = this._fields();
        if(validatedOnly)
        {
            fields = fields.filter(field => field.isValidated());
        }
        return fields.reduce((values:{[name:string]:any}, field:Field) =>
        {
            values[field.name] = field.value();
            return values;
        }, {});
    }

    /**
     * Clears the computed values and subscriptions of this validator for garbage collection.
     * This method should always be called when the validator is no longer used to prevent
     * memory leaks.
     */
    public dispose():void
    {
        if(this.isValid)
        {
            this.isValid.dispose();
            this.isValid = null;
        }
        if(this.isValidated)
        {
            this.isValidated.dispose();
            this.isValidated = null;
        }
        if(this.isValidating)
        {
            this.isValidating.dispose();
            this.isValidating = null;
        }
        super.dispose();
    }

    private _initComputed():void
    {
        this.isValid = ko.pureComputed(() =>
        {
            const fields = this._fields();
            let isValid = true;
            for(let i = 0; i < fields.length; i++)
            {
                const fieldIsValid = fields[i].isValid();
                if(fieldIsValid === null)
                {
                    return null;
                }
                else if(!fieldIsValid)
                {
                    isValid = false;
                }
            }
            return isValid;
        }).extend({deferred : true});

        this.isValidated = ko.pureComputed(() => !some(this._fields(), field => !field.isValidated())).extend({deferred : true});
        this.isValidating = ko.pureComputed(() => some(this._fields(), field => field.isValidating())).extend({deferred : true});
    }
}