Tuesday, May 22, 2012

Knockout js - Adding animation for any attribute binding

While documentation can be sparse for KnockoutJS, there still is an easy way to learn about how it works -  by reading the source code. In one of my current projects, I was able to quickly add a custom animation event to knockoutjs even when google was giving me sparse results.

I was working with a knockout-js based image gallery, and i was trying to implement a fade when the user changed images. knockoutjs.com had a basic example of animation here, but unfortunately, it was limited to only two use cases: a foreach loop (which has custom events that no other binding has), or a boolean on-off switch. Even worse, the boolean example puts UI code inside the model. I was hoping to add an animation to a "src" update, or more generally, any attr update.

Since a quick google search was being unhelpful, I switched to look in the knockout js source, particularly for the "attr" binding. Knockout is actually very nice behind-the-scenes, as it uses the same custom-binding syntax for all of it's internal bindings as well. Around line 2484, I found the "attr" binding:

var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
ko.bindingHandlers['attr'] = {
    'update': function(element, valueAccessor, allBindingsAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()) || {};
        for (var attrName in value) {
            if (typeof attrName == "string") {
                var attrValue = ko.utils.unwrapObservable(value[attrName]);

                // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
                // when someProp is a "no value"-like value (strictly null, false, or undefined)
                // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
                var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
                if (toRemove)
                    element.removeAttribute(attrName);

                // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
                // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
                // but instead of figuring out the mode, we'll just set the attribute through the Javascript
                // property for IE <= 8.
                if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
                    attrName = attrHtmlToJavascriptMap[attrName];
                    if (toRemove)
                        element.removeAttribute(attrName);
                    else
                        element[attrName] = attrValue;
                } else if (!toRemove) {
                    element.setAttribute(attrName, attrValue.toString());
                }
            }
        }
    }
};
BTW, knockout JS is (c) Steven Sanderson - http://knockoutjs.com/
Code is reposted as allowed under the MIT license (http://www.opensource.org/licenses/mit-license.php)

The easy way to add a fade would be to copy this to a custom binding, let's say called "fadeAttr".

ko.bindingHandlers['fadeAttr'] = {

Next, just wrap the for loop in a function, and call it from a traditional jquery fade:

var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
ko.bindingHandlers['fadeAttr'] = {
    'update': function(element, valueAccessor, allBindingsAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()) || {};
  
  var updateAttr = function (){
   for (var attrName in value) {
    if (typeof attrName == "string") {
     var attrValue = ko.utils.unwrapObservable(value[attrName]);

     // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
     // when someProp is a "no value"-like value (strictly null, false, or undefined)
     // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
     var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
     if (toRemove)
      element.removeAttribute(attrName);

     // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
     // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
     // but instead of figuring out the mode, we'll just set the attribute through the Javascript
     // property for IE <= 8.
     if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
      attrName = attrHtmlToJavascriptMap[attrName];
      if (toRemove)
       element.removeAttribute(attrName);
      else
       element[attrName] = attrValue;
     } else if (!toRemove) {
      element.setAttribute(attrName, attrValue.toString());
     }
    }
   }
  };
  
  if ($(element).is(":visible")){  
   $(element).fadeOut("fast", function (){
    updateAttr();
    $(element).fadeIn("fast");
   });
  }else{
   updateAttr();
  }
    }
};

Now you will have a fade effect whenever you update an attribute. If you wanted to get fancy, you could modify the params to pass in an animation, but I'll leave that as an exercise to the reader. Also, this could get cleaned up significantly if you only change one value, don't set classes, or don't support IE8.