Friday 22 November 2013

Knockout value binding and valueUpdate

I'm part of a team writing a Single Page Application, a misnomer if ever there was one. The client code uses knockout.js to bind data to the HTML. The application supports desktop browsers (from IE8 and up) but also has to run on mobile devices, in particular on iOS. Recently we've hit a couple of iOS issues, one involving SignalR and one involving knockout. Both of these appear to be bugs (features) introduced in iOS7.

The knockout problem appears when you use the valueUpdate:'afterkeydown' parameter on the value binding and has been reported here. As this affected us I needed to find a workaround (not the point of this post) and had to understand how valueUpdate works (which is the point of this post).

Id you read the knockout documentation around valueUpdate it says

If your binding also includes a parameter called valueUpdate, this defines additional browser events KO should use to detect changes besides the change event. The following string values are the most commonly useful choices:
  • "keyup" - updates your view model when the user releases a key
  • "keypress" - updates your view model when the user has typed a key. Unlike keyup, this updates repeatedly while the user holds a key down
  • "afterkeydown" - updates your view model as soon as the user begins typing a character. This works by catching the browser’s keydown event and handling the event asynchronously.
which I took to mean "you can only use these three properties, which now that I've re-read it, isn't what it says at all.

You can pass the name of any event to valueUpdate and an event handler will be added to the control for that event, the controls value will then be update when that event fires. Furthermore you can make this event handling asynchronous by prepending the 5 letters 'a', 'f', 't', 'e' and 'r' to the event name, i.e. afterKeyDown, afterInput etc. You can also pass an array of events to listen on to valueUpdate, something like: valueUpdate:['propertychange','input']. The code that handles valueUpdate looks something like this (in knockout 2.3)

var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
var propertyChangedFired = false;
if (requestedEventsToCatch) {
    if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
        requestedEventsToCatch = [requestedEventsToCatch];
    ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
    eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
}

var valueUpdateHandler = function() {
    // update the value
    // ...
}

// Workaround for https://github.com/SteveSanderson/knockout/issues/122
// IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list
// CODE NOT SHOWN HERE

ko.utils.arrayForEach(eventsToCatch, function(eventName) {
    // The syntax "after" means "run the handler asynchronously after the event"
    // This is useful, for example, to catch "keydown" events after the browser has updated the control
    // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
    var handler = valueUpdateHandler;
    if (ko.utils.stringStartsWith(eventName, "after")) {
        handler = function() { setTimeout(valueUpdateHandler, 0) };
        eventName = eventName.substring("after".length);
    }
    ko.utils.registerEventHandler(element, eventName, handler);
});

The code shown here does two things. The first is to take the list of events passed to valueUpdate and create an array of event names. The second is to bind those events to the control. It's the binding code that's interesting. There is an event handler (the function is shown but the code in that function has been elided) called valueUpdateHandler. In the arrayForEach loop, this handler is bound to the event unless the name of the event starts with 'after'.

In this latter case a new function is defined that simply calls setTimeout and this function is bound to the handler. The setTimout has a timeout of 0 and the valueUpdateHandler as the callback function. This means that when the event fires the timeout code is executed and this immediately queues the real event handler to run. This event handler then runs when the main thread is available

This means that any event that the control supports can be bound to the valueUpdate and any event that is bound can be run asynchronously. Which is pretty cool.

(We ended up binding afterinput along with afterpropertychange (to support IE8))

No comments:

Post a Comment