Unknown's avatar

Knockout options binding not refreshing – Breeze

I’m coding a detail screen with a drop down showing instances of child entities. The user can add a new instance of a child entity and the fields for the new instance appear for editing below. Alternatively the user can select an existing instance from the drop down and edit that.
When the user adds a new child entity, they need the dropdown focussed on the new child entity name and the fields for the new entity displayed for editing below.
The fields were appearing, but the drop down wasn’t refreshing.
Html for drop down:

    <div class="col-md-8">
      <div class="form-group">
        <label for="childItemList">Child items:</label>
        <select class="form-control" id="childItemList"
                data-bind="options: childItems, optionsText: 'name',value: $root.selectedChildItem, height:3"></select>
      </div>

    </div>

The binding context is a “with:parentEntity” binding. The solution was pretty simple, call “valueHasMutated” on the parent after the new child entity is created:

        addChildEntity = function () {
            var newChildEntity = datacontext.createChildEntity(parentEntity());
            parentEntity.valueHasMutated();
            selectedChildEntity(newChildEntity);
        },
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>