Knockout – Custom bind to any function

We’ve recently been using Knockout for a lot of our work.

Knockout is an MVVM library for JavaScript, which means that a model (i.e. an object which represents your data) can be bound to the UI in such a way that changes to the UI update the underlying model, and changes to the underlying model update any part of the UI to which the model is bound. Thus, a two-way binding exists between the UI and the underlying view-model.

There are obvious benefits to using Knockout but one which is probably overlooked by most developers is the ability to add your own custom bindings.

Let us explain…

When using Knockout, properties in the view-model are bound to the UI by means of a data-bind attribute added to DOM elements.

So let’s describe a basic Knockout view-model that we’ll use for our example.

function vmSimple() {
    this.ID = null;
    this.Description = ko.observable();
    this.Children = ko.observableArray([]);
}

Now let’s define a template to which this view-model is bound.

<div data-bind="attr: { id: ID }">
    <p class="description"></p>
    <!-- ko foreach Children -->
        <div class="child">
            // do something with each item in the Children collection
        </div>
    <!-- /ko -->
</div>

This will result in HTML resembling the following.

<div id="84">
    <p class="description">
        An example description.
    </p>
    <div class="child">
        I am the eldest child.
    </div>
    <div class="child">
        I am the youngest child.
    </div>
</div>

Now, what if we wanted to modify the exact value or format of one of those properties when it gets bound to the UI, or write out a value computed from one of those properties? Well, one example that Knockout offers is a computed observable. To implement this we’ll modify our model.

function vmSimple() {
    this.ID = null;
    this.Description = ko.observable();
    this.Children = ko.observableArray([]);
 
    <span class="highlight">this.NumberOfChildren = ko.computed(function() {
        return this.Children().length;
    });</span>
}

This now allows us to write out, or bind, a value from our view-model which was not primarily available.

<div data-bind="attr: { id: ID }">
    <p>
        This view-model contains <span class="highlight"><span data-bind="text: NumberOfChildren"></span></span> child items.
    </p>
</div>

This will result in HTML resembling the following.

<div id="84">
    <p>
        This example contains 2 child items.
    </p>
</div>

Another, perhaps more useful example of this might be to preface a value with a currency symbol. Let’s propose that a view-model has a property Price and another property Currency: we could add a computed observable which chooses whether to write out “$50” or “£50”. However, a slight downside to this is that the computed observable exists in every instance of that view-model (the number of view-models on your page could feasibly run into the thousands).

Ideally there would be a way to call any JavaScript function that you have defined in your client-side script. Of course, you could add any function you like as a Knockout custom binding, but if you wish to use this same function outside of the Knockout binding then you either have repetition or unnecessary overhead.

What I will show you is how we’ve gone half a step further and created a custom binding that allows us to call any function, whether it has been declared as a Knockout custom binding or not.

First of all, in one of our JavaScript files which is always served-up whenever we serve the Knockout script (we’ve implemented the ASP.NET Web Optimization Framework, so the following script is bundled together with our Knockout script, and this bundle is then referenced instead of the individual scripts) we have the following custom binding declared.

ko.bindingHandlers.call = {
    init: function(domElement, viewModelProp) {
              var callback = viewModelProp();
              if (callback && typeof (callback) === "function") {
                  return callback();
              }
              return null;
          }
};

Now let’s write a function which we can call via this custom binding:

function log(value) {
    console.log(value);
}

Now we can rewrite our HTML template.

<div data-bind="attr: { id: ID }">
    <p data-bind="<span class="highlight">call: function() { log(ID); }</span>">
        This view-model contains <span data-bind="text: NumberOfChildren"></span> child items.
    </p>
</div>

The value that we pass to the call binding handler takes the form of a callback. Going by the strict ECMA Script standards, this should be wrapped in a function() { ... } statement, but most modern browsers will actually let you get away with something more readable, such as the following.

<div data-bind="attr: { id: ID }">
    <p data-bind="<span class="highlight">call: log(ID)</span>">
        This view-model contains <span data-bind="text: NumberOfChildren"></span> child items.
    </p>
</div>

The function specified will then be called when the binding takes place, and in our trivial example, the ID property will be written the browser’s console.

There are a couple of points worth making here regarding the format of the parameters that we’re passing to the callback function: I’ve specified ID without parentheses because in our example view-model ID is not an observable property, and therefore ID is simply a value. However, for the sake of example, let’s tweak our view-model so that ID is now an observable:

function vmSimple() {
    this.ID = <span class="highlight">ko.observable();</span>
    this.Description = ko.observable();
    this.Children = ko.observableArray([]);
}

In order to have the value from this observable passed as a usable parameter we need to similarly tweak our template.

<div data-bind="attr: { id: ID }">
    <p data-bind="call: function() { log(<span class="highlight">ID()</span>); }">
        This view-model contains <span data-bind="text: NumberOfChildren"></span> child items.
    </p>
</div>

Also, remember that if the parameter is to be passed as a string then it should be wrapped in quote marks

<div data-bind="attr: { id: ID }">
    <p data-bind="call: function() { log(<span class="highlight">'Description()'</span>); }">
        This view-model contains <span data-bind="text: NumberOfChildren"></span> child items.
    </p>
</div>

So with the addition of this one, fairly simple Knockout custom binding handler, call we are now able to pass the view-model properties to any function in our client-side JavaScript and have the value processed during both the initial Knockout binding and any updates which the specified properties (if any) are involved in.




Ähnliche Beiträge


Leave a Reply

Your email address will not be published. Required fields are marked *



*