I’ve struggled a little with this today. I’m used to knockout validation, and I wanted to make a start using Breeze validation to compare two date fields bound using knockout.
This is what I wanted to achieve:
Here’s a reminder on how to make this work:
I’m basing my app on the nuget package “ProvenStyle.Durandal.StarterKit” v 0.0.7. This is pretty much brand new at the time of writing and I’ve found it to be a great starting point, together with these Breeze packages:
<package id="bootstrap" version="3.0.3" targetFramework="net451" /> <package id="Breeze.Client" version="1.4.11" targetFramework="net451" /> <package id="Breeze.Server.ContextProvider" version="1.4.11" targetFramework="net451" /> <package id="Breeze.Server.ContextProvider.EF6" version="1.4.11" targetFramework="net451" /> <package id="Breeze.Server.WebApi2" version="1.4.11" targetFramework="net451" /> <package id="Breeze.WebApi2.EF6" version="1.4.11" targetFramework="net451" />
As per the great John Papa’s advice in his 2013 course “Single Page Apps Jumpstart” on Pluralsight, I created a datacontext to handle things like the configuration of the Breeze EntityManager. In this code snippet from the datacontext module, I get the metadata and then call a function “initializeCustomValidators” which I’ve called from a module called “model”, ( again recommended by John P).
var primeData = function () { return manager.fetchMetadata() .then(function (data) { utils.getOptionSets(data.schema.enumType, optionSets); model.initializeCustomValidators(manager); Q.resolve(); }).fail(function (error) { Q.reject(error); }); };
The initializeCustomValidators function is just configuring one validator here for “end Date” (“end”). This works at the entity level and supplies a validator object with details of the “end” property:
function initializeCustomValidators(manager) {
// returns an endDateValidator with its parameters filled
var endDateValidator = new breeze.Validator(
"endDateValidator", // validator name
endDateValidationFn, // validation function
{ // validator context
messageTemplate: "'start and end date' must be present, start must be less or equal to end",
property: manager.metadataStore.getEntityType("ClientLicense").getProperty("end"),
propertyName: "end"
});
function endDateValidationFn(entity, context) {
var end = entity.getProperty("end");
var start = entity.getProperty("start");
if (end === undefined || start === undefined) {
return false;
}
return start <= end;
};
var clientLicenseType = manager.metadataStore.getEntityType("ClientLicense");
clientLicenseType.validators.push(endDateValidator);
};
It’s worth stepping through the “getValidationErrors in breeze.debug.js to see how this works, this helped me populate the validator correctly ( specifically looking at how the filter works, see below, this helped to supply the correct parameter for property, above).
proto.getValidationErrors = function (property) {
assertParam(property, "property").isOptional().isEntityProperty().or().isString().check();
var result = __getOwnPropertyValues(this._validationErrors);
if (property) {
var propertyName = typeof (property) === 'string' ? property : property.name;
result = result.filter(function (ve) {
return ve.property && (ve.property.name === propertyName || (propertyName.indexOf(".") != -1 && ve.propertyName == propertyName));
});
}
return result;
};
This suggests one way to listen for changes to the Breeze validation errors connection and update a ko observable to which I can bind. Very handy. I call this in the EntityType post-construction initializer as suggested by Ward:
function addhasValidationErrorsProperty(entity) {
var prop = ko.observable(false);
var onChange = function () {
var hasError = entity.entityAspect.getValidationErrors().length > 0;
if (prop() === hasError) {
// collection changed even though entity net error state is unchanged
prop.valueHasMutated(); // force notification
} else {
prop(hasError); // change the value and notify
}
};
onChange(); // check now ...
entity.entityAspect // ... and when errors collection changes
.validationErrorsChanged.subscribe(onChange);
// observable property is wired up; now add it to the entity
entity.hasValidationErrors = prop;
}
I call it like this from my model class:
function clientLicenseInitializer(clientLicense) {
addhasValidationErrorsProperty(clientLicense);
clientLicense.name = ko.computed(function() {
var orgName = clientLicense.crmOrganisationName() ?clientLicense.crmOrganisationName():'--';
var type = clientLicense.licenseType() ? clientLicense.licenseType():'--';
var hostingModel = clientLicense.hostingModel() ? clientLicense.hostingModel():'--';
return orgName + ', ' + type + ', ' + hostingModel;
});
}
data binding is done as suggested in Ward’s post:
<div class="row"> <div class="col-md-6"> <div class="form-group"> <label class="control-label">Start Date</label> <input data-bind='datepicker:start' class="form-control"> </div> </div> <div class="col-md-6"> <div class="form-group"> <label class="control-label">End Date</label> <input data-bind="datepicker:end" class="form-control"> <div data-bind="if: hasValidationErrors"> <pre data-bind="text: entityAspect.getValidationErrors('end')[0].errorMessage "></pre> </div> </div> </div> </div>