Unknown's avatar

Field validation with Knockout and Breeze

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:

date validation screenshot

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>

Leave a comment