knockout validation using breeze utility?

2019-02-07 07:20发布

问题:

Has anyone written a utility that will convert Breeze metadata (captured from entity framework data attributes) into knockout validation extensions (using knockout.validation)?

回答1:

I have made an function that reads the metadata from an entity and adds validation rules.

app.domain.indicador = (function () {
"use strict";
var constructor = function () {...}
var initializer = function indicadorInitializer(entity) {
    var entityType = entity.entityType;
    if (entityType) {
        console.log(entityType);
        for (var i = 0; i < entityType.dataProperties.length; i++) {
            var property = entityType.dataProperties[i];
            console.log(property);
            var propertyName = property.name;

            var propertyObject = entity[propertyName];
            if (!property.isNullable) {
                propertyObject.extend({ required: true });
            }
            if (property.maxLength) {
                propertyObject.extend({ maxLength: property.maxLength });
            }
        }

        for (var i = 0; i < entityType.foreignKeyProperties.length; i++) {
            var property = entityType.foreignKeyProperties[i];
            console.log(property);
            var propertyName = property.name;

            var propertyObject = entity[propertyName];
            if (!property.isNullable) {
                propertyObject.extend({ required: true });
            }
            if (property.maxLength) {
                propertyObject.extend({ maxLength: property.maxLength });
            }
            //Bussines rule
            propertyObject.extend({ notEqual: 0 });
        }
    }
};
return {
    constructor: constructor,
    initializer: initializer
};
})();

I use the function as initializer:

store.registerEntityTypeCtor("Indicador", domain.indicador.constructor, domain.indicador.initializer);

It's just a start but for the time is useful for me.

Update:

I changed the way I add validation. I share it here in case it is useful to someone:

Helper object:

app.validatorHelper = (function (breeze) {
var foreignKeyInvalidValue = 0;

function addDataTypeRules(dataType, property) {
    switch (dataType) {
        case breeze.DataType.DateTime:
            //TODO: implement my function to validate dates. This validator is too permissive
            property.extend({ date: true });
            break;
        case breeze.DataType.Int64:
        case breeze.DataType.Int32:
        case breeze.DataType.Int16:
            //it's needed to accept negative numbers because of the autogenerated keys
            property.extend({ signedDigit: true });
            break;
        case breeze.DataType.Decimal:
        case breeze.DataType.Double:
        case breeze.DataType.Single:
            property.extend({ number: true });
            break;
    }
};

function addValidationRules(entity) {
    var entityType = entity.entityType;
    if (entityType) {
        for (var i = 0; i < entityType.dataProperties.length; i++) {
            var property = entityType.dataProperties[i];
            //console.log(property);
            var propertyName = property.name;
            var propertyObject = entity[propertyName];

            addDataTypeRules(property.dataType, propertyObject);

            if (!property.isNullable) {
                propertyObject.extend({ required: true });
            }
            if (property.maxLength) {
                propertyObject.extend({ maxLength: property.maxLength });
            }
        }

        for (var i = 0; i < entityType.foreignKeyProperties.length; i++) {
            var property = entityType.foreignKeyProperties[i];
            //console.log(property);
            var propertyName = property.name;
            var propertyObject = entity[propertyName];

            addDataTypeRules(property.dataType, propertyObject);

            if (!property.isNullable) {
                propertyObject.extend({ required: true });
                //Bussiness Rule: 0 is not allowed for required foreign keys
                propertyObject.extend({ notEqual: foreignKeyInvalidValue });
            }
            if (property.maxLength) {
                propertyObject.extend({ maxLength: property.maxLength });
            }
        }
    }
};

return {
    addValidationRules: addValidationRules
};
})(breeze);

The custom validator:

(function (ko) {
ko.validation.rules['signedDigit'] = {
    validator: function (value, validate) {
        if (!validate) return true;
        return ko.validation.utils.isEmptyVal(value) || (validate && /^-?\d+$/.test(value));
    },
    message: 'Please enter a digit'
};

ko.validation.registerExtenders();
})(ko);

Using the helper at the initializer:

app.domain.valorIndicador = (function (vHelper) {
"use strict";
var constructor = function () {
};

var initializer = function indicadorInitializer(entity) {
    vHelper.addValidationRules(entity);
};

return {
    constructor: constructor,
    initializer: initializer
};
})(app.validatorHelper);

And setting the initializer:

store.registerEntityTypeCtor("ValorIndicador", domain.valorIndicador.constructor, domain.valorIndicador.initializer);


回答2:

A simple way to bind validation errors from breezejs using knockout.

We can subscribe to validationErrorsChanged event from the entityAspect:

function subscribeValidation() {
    return self.entity().entityAspect.validationErrorsChanged.subscribe(function (validationChangeArgs) {
                validationChangeArgs.added.forEach(function (item) { addError(item); });
                validationChangeArgs.removed.forEach(function (item) { self.validationErrors.remove(item); });
    });
}

this.hasError = function (propertyName) {
    var array = self.validationErrors();
    var match = array.filter(function (item) {
        return item.propertyName == propertyName;
    });
    if (match.length > 0) {
        return true;
    } else return false;
};


function addError(item) {
    self.validationErrors.remove(function (i) {
        return i.propertyName == item.propertyName;
    });

    self.validationErrors.push(item);
}

Finally we can bind to the messages on the UI (I'm using Twitter boostrap css classes)

<div class="control-group" data-bind="css: { 'error': hasError('Nome') }">
    <label class="control-label">Nome</label>
    <div class="controls">
        <input type="text" class="input-xxlarge" data-bind="value: model().Nome">
        <span class="help-inline" data-bind="text: getErrorMessage('Nome')"></span>
    </div>
</div>

See the full gist here



回答3:

I've searched this before as I started using breeze with knockout and then I had the exact same question about how to validate stuff, and how to show validation inline.

Considering that breeze already has validation built in, I decided to write a custom Knockout Binding to show the validation result every time the observable value changes and it was quite easy afterall:

Here's the custom binding:

    ko.bindingHandlers.breezeValidate = {
    init: function (element, valueAccessor, allBindingsAccessor, context) {
        var isOk = context.entityAspect.validateProperty(valueAccessor());
        var errors = context.entityAspect.getValidationErrors(valueAccessor());
        var message = "";
        if (errors.length > 0)
            message = errors[0].errorMessage;
        $(element).html(message);
    },

    //update the control when the view model changes
    update: function (element, valueAccessor, allBindingsAccessor, context) {
        debugger;
        this.init(element, valueAccessor, allBindingsAccessor, context)
    }
};

And the usage is like this:

<span data-bind="text: Name"></span>
<span data-bind="breezeValidate: 'Name'"></span>

This works because of this line:

var isOk = context.entityAspect.validateProperty(valueAccessor());

When breeze is requested to validate the property it ends up calling the observable and it gets registered by knockout, so every time it is changed, this binding will be invoked again and the error message will be updated accordingly.

I'm just showing the first validation message, but of course you can iterate thru all of them and even add a different styling to the element.

Hope this helps!!



回答4:

Not sure why people would want to use ko.validation - it just replicates the processing breeze's client side is doing anyway. And given the breeze developers hints that validation will get even more power soon, why bother.

So I started with Thiago Oliveira's great work. But I wanted to have the bare minimum of markup. By assuming the use of bootstrap classes & defaulting the validation property name from the previous element I could simplify most markup additions to:

<span class="help-inline" data-bind="breezeValidation: null"></span>

Win!

My ko.bindingHandler:

//Highlight field in red & show first validation message
//
//Outputs first validation message for 'propertyName' or if null: previous controls value binding
//Needs ancestor with 'control-group' class to set class 'error' for Bootstrap error display
//
//Example:
//<td class="control-group">
//    <input class="input-block-level text-right" data-bind="value: id" />
//    <span class="help-inline" data-bind="breezeValidation: null"></span>
//</td>
//
//Does not and cannot validate keys that already exist in cache. knockout write calls breeze which throws uncaught error

ko.bindingHandlers.breezeValidation = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        // This will be called when the binding is first applied to an element
        // Set up any initial state, event handlers, etc. here

        var $msgElement = $(element);
        var entity = viewModel;

        var propName = valueAccessor();
        if (propName === null) {
            //  $element.prev().data("bind") = "value: itemType"
            var prevBinds = $msgElement.prev().data("bind");
            if (!prevBinds) {
                $msgElement.text("Could not find prev elements binding value.");
                return;
            }
            var bindPhrases = prevBinds.split(/,/);
            for (var i = 0, j = bindPhrases.length; i < j; i++) {
                var bindPhrase = bindPhrases[i];
                if (utility.stringStartsWith(bindPhrase, 'value: ')) {
                    propName = bindPhrase.substr(7);
                    break;
                }
            }
        }

        if (!propName) {
            $msgElement.text("Could not find this or prev elements binding value.");
            return;
        }

        //var $groupElement = $msgElement.parent();      
        var $groupElement = $msgElement.closest(".control-group");
        if (!$groupElement.hasClass("control-group")) {
            $msgElement.text("Could not find parent with 'control-group' class.");
            return;
        }


        onValidationChange();               //fire immediately (especially for added)
                                            //... and anytime validationErrors are changed fire onValidationChnange
        entity.entityAspect.validationErrorsChanged.subscribe(onValidationChange);

        element.onchange = function () {
            //Should never have updates pushed from validation msgElement
            $msgElement.text("readonly error");
        };


        function onValidationChange() {
            var errors = entity.entityAspect.getValidationErrors(propName);
            var message = "";
            if (errors.length > 0) {
                message = errors[0].errorMessage;
            }

            if (message) {
                $groupElement.addClass('error');
            }
            else {
                $groupElement.removeClass('error');
            }

            $msgElement.text(message);
        }


    }
    //Not interested in changes to valueAccessor - it is only the fieldName.
    //update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
};

Example view simple implicit property usage:

<div class="control-group">
    <label class="control-label" for="editStatusNote">Status note:</label>
    <div class="controls">
        <input id="editStatusNote" type="text" data-bind="value: statusNote" />
        <span class="help-inline" data-bind="breezeValidation: null"></span>
    </div>
</div>

Example view explicit property usage:

<div class="control-group">
    <label class="control-label" for="editAmount">Amount:</label>
    <div class="controls">
        <div class="input-prepend">
            <span class="add-on">$</span>
            <input id="editAmount" class="input-small" type="text" data-bind="value: amount" />
        </div>
        <span class="help-inline" data-bind="breezeValidation: 'amount'"></span>
    </div>
</div>                        


回答5:

I updated breezeValidation to Bootstrap 3 and improved with multipath property support.

ko.bindingHandlers.breezeValidation = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        // This will be called when the binding is first applied to an element
        // Set up any initial state, event handlers, etc. here

        var $msgElement = $(element);
        var entity = viewModel;

        var propName = valueAccessor();
        if (propName === null) {
            //  $element.prev().data("bind") = "value: itemType"
            var prevBinds = $msgElement.prev().data("bind");
            if (!prevBinds) {
                $msgElement.text("Could not find prev elements binding value.");
                return;
            }
            var bindPhrases = prevBinds.split(/,/);
            for (var i = 0, j = bindPhrases.length; i < j; i++) {
                var bindPhrase = bindPhrases[i];
                if (bindPhrase.substr(0, 7) == 'value: ') {
                    propName = bindPhrase.substr(7);

                    entity = ko.utils.unwrapObservable(entity);
                    var propPath = propName.replace(/[()]/g, "").split('.'), i = 0;
                    var tempProp = entity[propPath[i]], links = propPath.length;
                    i++;
                    while (ko.utils.unwrapObservable(tempProp) && i < links) {
                        entity = ko.utils.unwrapObservable(tempProp);
                        tempProp = entity[propName = propPath[i]];
                        i++;
                    }

                    break;
                }
            }
        }

        if (!propName) {
            $msgElement.text("Could not find this or prev elements binding value.");
            return;
        }

        //var $groupElement = $msgElement.parent();      
        var $groupElement = $msgElement.closest(".form-group");
        if (!$groupElement.hasClass("form-group")) {
            $msgElement.text("Could not find parent with 'form-group' class.");
            return;
        }


        onValidationChange();               //fire immediately (especially for added)
        //... and anytime validationErrors are changed fire onValidationChnange
        entity.entityAspect.validationErrorsChanged.subscribe(onValidationChange);

        element.onchange = function () {
            //Should never have updates pushed from validation msgElement
            $msgElement.text("readonly error");
        };


        function onValidationChange() {
            var errors = entity.entityAspect.getValidationErrors(propName);
            var message = "";
            if (errors.length > 0) {
                message = errors[0].errorMessage;
            }

            if (message) {
                $groupElement.addClass('has-error');
            }
            else {
                $groupElement.removeClass('has-error');
            }

            $msgElement.text(message);
        }


    }
    //Not interested in changes to valueAccessor - it is only the fieldName.
    //update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
};


回答6:

Knockout validator can use breeze validation as a whole:

function addKoValidationRules(entity) {
    if (entity.koValidationRulesAdded) {
        return;
    }
    entity.entityType.dataProperties.forEach(function (property) {
        entity[property.name].extend({
            validation: {
                validator: function () {
                    // manual validation ensures subscription to observables which current field depends on
                    // entity is added to context for retrieving other properties in custom validators
                    entity.entityAspect.validateProperty(property.name, { entity: entity });
                    var errors = entity.entityAspect.getValidationErrors(property.name);
                    if (!errors.length) {
                        return true;
                    }
                    this.message = errors[0].errorMessage;
                    return false;
                },
                message: ''
            }
        });
    });
    entity.koValidationRulesAdded = true;
}