knockout validation using breeze utility?
Asked Answered
F

6

5

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

Folium answered 1/12, 2012 at 18:35 Comment(2)
More generally, I'd like to see a toolkit to map any registered Breeze validator to KO validation. Jump right in :)Sapsucker
possible duplicate of Validation in Breeze and KnockoutSiderostat
U
8

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);
Univalent answered 10/1, 2013 at 16:55 Comment(1)
I'm trying to do something similar, but obviously doing something wrong. Can you see my question here and any suggestions you may have? #15736556Piefer
T
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

Therewith answered 13/3, 2013 at 19:39 Comment(0)
G
2

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!!

Granulate answered 6/9, 2013 at 12:56 Comment(0)
G
2

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>                        
Goggle answered 19/9, 2013 at 4:2 Comment(0)
I
0

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) {
};
Impartial answered 18/2, 2014 at 9:54 Comment(0)
H
0

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;
}
Heartfelt answered 15/7, 2015 at 9:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.