flut1/knockout-validator

View on GitHub
src/lib/bindings/genericBindingHandler.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import * as ko from 'knockout';
import ValidatorFieldBinding from "./ValidatorFieldBinding";
import {bindings, SHORTHAND_BINDING_NAME} from "../const/bindings";
import elementMapper from "./elementMapper";
import * as find from 'lodash/find';

type BindingDescriptor = {
    value:any,
    binding:ValidatorFieldBinding
};

const ensureObservable = (value:any) => ko.isObservable(value) ? value : ko.observable(value);

const getAllBindingValues = (element:HTMLElement, allBindingsAccessor:ko.AllBindingsAccessor, bindingContext:ko.BindingContext<any>):{[name:string]:BindingDescriptor} =>
{
    const shorthandValues = allBindingsAccessor.get(SHORTHAND_BINDING_NAME) || {};
    const allBindings:{[name:string]:BindingDescriptor} = {};

    if(ko.isObservable(shorthandValues))
    {
        throw new TypeError(`Passing an observable for the '${SHORTHAND_BINDING_NAME}' binding is not allowed. 
        See the API docs for '${SHORTHAND_BINDING_NAME}' for more information.`);
    }

    bindings.forEach(binding =>
    {
        const bindingValue = allBindingsAccessor.get(binding.bindingName);
        if(bindingValue)
        {
            allBindings[binding.bindingName] = {
                value: bindingValue,
                binding
            };
        }
        else if(shorthandValues[binding.bindingShorthand])
        {
            allBindings[binding.bindingName] = {
                value: shorthandValues[binding.bindingShorthand],
                binding
            };
        }
        else if(binding.inheritFromContextProp && bindingContext[binding.inheritFromContextProp])
        {
            allBindings[binding.bindingName] = {
                value: bindingContext[binding.inheritFromContextProp],
                binding
            };
        }
        else if(binding.inheritFromAttribute && element.hasAttribute(binding.inheritFromAttribute))
        {
            allBindings[binding.bindingName] = {
                value: element.getAttribute(binding.inheritFromAttribute),
                binding
            };
        }

    });

    return allBindings;
};

export const getValueBindingKey = (element:HTMLElement):string =>
{
    const tagName = element.tagName.toLowerCase();
    const typeAttr = element.getAttribute('type');
    let valueBindingKey = 'value';

    if(tagName === 'input')
    {
        if(typeAttr === 'radio')
        {
            throw new Error(`Cannot apply validation bindings directly to radio buttons. Please use the
'validateRadio' binding to validate radio buttons.`);
        }
        else if(typeAttr === 'checkbox')
        {
            valueBindingKey = 'checked';
        }
    }
    else if(tagName === 'select')
    {
        if(element.hasAttribute('multiple'))
        {
            valueBindingKey = 'selectedOptions';
        }
    }
    else if(tagName !== 'textarea')
    {
        throw new Error(`Cannot apply validation bindings to unsupported <${tagName}> element.`);
    }

    return valueBindingKey;
};

export const createValueBinding = (element:HTMLElement, allBindingsAccessor:ko.AllBindingsAccessor, viewModel:any, bindingContext:ko.BindingContext<any>):ko.Observable<any> =>
{
    const hasValueBinding = allBindingsAccessor.has('value');
    const valueBindingKey = getValueBindingKey(element);
    if(valueBindingKey == 'selectedOptions' && hasValueBinding)
    {
        throw new Error(`Please do not use the value bindings with a multiple-option select element.
Use the selectedOptions binding instead.`);
    }

    let value = allBindingsAccessor.get(valueBindingKey);

    if(typeof value === 'undefined')
    {
        value = ko.observable('');
    }
    else
    {
        if(!ko.isWritableObservable(value))
        {
            throw new Error(`knockout-validator does not work on elements that have a "${valueBindingKey}" binding with a 
value that is not a writable observable.`);
        }
    }

    ko.bindingHandlers[valueBindingKey].init(element, () => value, allBindingsAccessor, viewModel, bindingContext);

    ko.computed({
        read: function()
        {
            var fieldValue = value();
            ko.unwrap(fieldValue);

            if(typeof ko.bindingHandlers[valueBindingKey].update == 'function')
            {
                ko.bindingHandlers[valueBindingKey].update(element, () => fieldValue, allBindingsAccessor, viewModel, bindingContext);
            }
        },
        // cast element because typings are incorrect
        disposeWhenNodeIsRemoved: <any> element
    });

    return value;
};

const injectContextValues = (
    element:any, bindingValues:{[name:string]:BindingDescriptor}, bindingContext:ko.BindingContext<any>
):ko.BindingHandlerControlsDescendant|void =>
{
    const addToContext:Array<any> = [];
    Object.keys(bindingValues).forEach(bindingName =>
    {
        const bindingDescriptor:BindingDescriptor = bindingValues[bindingName];
        if(bindingDescriptor.binding.inheritFromContextProp)
        {
            addToContext.push(bindingDescriptor);
        }
    });

    if(addToContext.length)
    {
        const innerBindingContext = bindingContext.extend(addToContext.reduce(
            function(context:any, bindingDescriptor:BindingDescriptor)
            {
                context[bindingDescriptor.binding.inheritFromContextProp] = ensureObservable(bindingDescriptor.value);
                return context;
            }, {})
        );
        ko.applyBindingsToDescendants(innerBindingContext, element);
        return {controlsDescendantBindings: true};
    }

    return null;
};

const initField = (element:any, id:string, bindingValues:{[name:string]:BindingDescriptor},
                   allBindingsAccessor:ko.AllBindingsAccessor, viewModel:any, bindingContext:ko.BindingContext<any>):void =>
{
    const field = elementMapper.createField(id, element);
    const value = createValueBinding(element, allBindingsAccessor, viewModel, bindingContext);
    field.value = value;
    Object.keys(bindingValues).forEach(bindingName =>
    {
        const bindingDescriptor:BindingDescriptor = bindingValues[bindingName];
        field[bindingDescriptor.binding.validatorFieldProp] = ko.unwrap(bindingDescriptor.value);
    });

    ko.utils.domNodeDisposal.addDisposeCallback(element, function()
    {
        field.dispose();
    });
};

export default (isInit:boolean, bindingName:string,
                element:any, valueAccessor:() => any, allBindingsAccessor:ko.AllBindingsAccessor, viewModel:any, bindingContext:ko.BindingContext<any>):ko.BindingHandlerControlsDescendant|void =>
{
    let id = elementMapper.getElementId(element);

    if(isInit)
    {
        if(!id)
        {
            id = elementMapper.setElementId(element);

            if(!elementMapper.getField(id))
            {
                const bindingValues = getAllBindingValues(element, allBindingsAccessor, bindingContext);

                if(bindingValues['validationName'])
                {
                    initField(element, id, bindingValues, allBindingsAccessor, viewModel, bindingContext);
                }

                return injectContextValues(element, bindingValues, bindingContext);
            }
        }
    }
    else if(id)
    {
        const field = elementMapper.getField(id);

        if(field)
        {
            if(bindingName === SHORTHAND_BINDING_NAME)
            {
                const shorthandValues = ko.toJS(valueAccessor());

                bindings.forEach(binding =>
                {
                    if(typeof shorthandValues[binding.bindingShorthand] !== 'undefined')
                    {
                        field[binding.validatorFieldProp] = shorthandValues[binding.bindingShorthand];
                    }
                });
            }
            else
            {
                const value = ko.unwrap(valueAccessor());
                const binding:ValidatorFieldBinding = find(
                    bindings,
                    (b:ValidatorFieldBinding) => b.bindingName === bindingName
                );
                if(binding)
                {
                    field[binding.validatorFieldProp] = value;
                }
            }
        }
    }

    return null;
};