Saturday, July 27, 2013

Avoiding Code in KnockoutJS Data Bindings

I was reviewing some KnockoutJS code that created a dialog box. It had this line for generating the buttons:



Granted, the Knockout documentation recommends using the anonymous function in the click handler, however, once I start seeing Javascript appearing in the template, I get concerned. Back in the day, we'd stuff our HTML full of PHP tags to dynamically render a page. At the time, we didn't really know better (or have another option). Over time, we've evolved to separating the presentation from the logic that binds data and attaches behavior to that content. Today, there's a plethora of templating languages each with their own vernacular and, generally, many programming like qualities. So, while the capability exists to still write code inline in the template, its definitely not considered a best-practice. Given that, my question becomes: How much code is too much?

Let me clarify that I'm not singling out Knockout when I ask this question. I've seen plenty of examples of this appear in Backbone-based applications too. Whenever your template engines allows for native Javascript expressions, it becomes very tempting to start sprinkling them throughout the layout. I only pick Knockout because there's a challenge to maintain a declarative syntax and avoid writing a lot of code that directly accesses the DOM. When something does need to execute code, the declaration needs to identify the correct context and establish proper scope such that the executing code can perform the necessary processing. So in the above example, the whole point of the anonymous function is to change the scope to the view model so selectionOption can use this to access its state and data elements. If we eliminate the anonymous function:



this becomes the data of the bound object (in this case the label for the button) and we lose access to the view model object. Fortunately, Knockout has (and heavily promotes) the ability to extend its binding language. Whenever you feel the need to code, make sure someone hasn't figured out a way to extend Knockout so you don't. The delegatedEvents plugin provides jQuery like delegated, live binding, fixes the scope, and makes the bound data available to the handler. Now you can go back to using the data binding to attach the selectOption handler from the view model. The custom binding, delegateHandler, will attempt to find the correct handler and trigger it with the correct scope and data:



Now, you have access to everything you may need in the handler function:


   function myViewModel() {

      this.options = ko.observableArray(['Save', 'Ok', 'Reset', 'Cancel']);

      this.selectOption= function( data, event ) {
         
         // this is the myViewModel instance
         // data is the button label (from the observableArray)
         // event the actual click event
         ...
      },

   }



Going one step further and completely eliminating the code from the layout, I used the afterRender callback in the template binding to allow the logic in the view model to attach handlers and establish state:









Now, all the event bindings and dynamic styling moves into the code:


   function myViewModel() {
      
      // Needed to bind scope in the rendered() handler
      var self = this;

      this.options = ko.observableArray(['Save', 'Ok', 'Reset', 'Cancel']);

      this.selectOption= function( event ) {
         
         var data = ko.dataFor(event.currentTarget);

         // this is the myViewModel instance
         // data is the button label (from the observableArray)
         // event the actual click event
         ...
      },

      this.rendered = function( elements ) {

         var $el = $(elements);
          
         // Mimic delegatedHandler and bind the click handlers at the container level
         $el.on( 'click', '.btn', $.proxy( self, 'clickButton' ) );
         
         // Move the css binding that added classes to the first button
         // And for fun, use jQuery UI to make an enhanced button
         $el.find('.btn')
            .button()
            .eq(0)
               .addClass('btn-primary', 'autofocus');

      }
   }



This does lose the bound data being passed as the first parameter of the selectOptions, but that can be found using ko.dataFor() based on the element that was clicked. Now, I know this is where most Knockout users will cry foul and tell me I've completely defeated the purpose of Knockout. Perhaps I have, so maybe we can meet somewhere in the middle. Let's enhance our button array to contain button view models that hold more details about the buttons other than the label:


   // Create observable data for a button
   function myButton(label, type, state) {

      this.label = ko.observable(label);
      this.type = ko.observable(type);
      this.state = ko.observable(state);

      this.selector = ko.computed(function() {
         return this.type() + ' ' + this.state();
      }, this);
   }

   function myButtonLine() {

      var self = this;
       
      // Create a bunch of buttons
      this.buttons = ko.observableArray([
         new myButton('Save', 'primary success', 'idle'),
         new myButton('Ok', 'success', 'idle'),
         new myButton('Reset', 'warn', 'idle'),
         new myButton('Cancel', 'error', 'idle')
      ]);

      this.clickButton = function( event ) {
         var data = ko.dataFor(event.currentTarget),
             oldLabel = data.label();
        
         // Change the label and state so the text/class updates
         data.label('Pow!').state('clicked');
         // Wait a bit and switch back
         setTimeout(function() { data.label(oldLabel).state('idle'); }, 1000);  

      },

      this.rendered = function( element ) {

         var $el = $(element);

         $el.on( 'click', '.btn', $.proxy( self, 'clickButton' ) );
      }
   }


   ko.applyBindings(new myButtonLine(), document.getElementById('ko-wrapper'));



Now, I'll bind the label to the html of the button and the calculated selector value as the class of the button:









Now the button will automatically change the label and classes based its state. This change is triggered by the click event which is bound as part of the rendering and not in the markup or directly on the button element. The final result works like this:



Even with Knockout, you have a choice about the placement of the code that defines the behavior of the elements being bound to the view model. My preference is to keep Javascript control logic inside the view model that is bound to the DOM and keep the markup as simple as possible. I think the data binding features of Knockout are excellent. My concern is that they are so powerful, its easy to start adding more and more logic into the markup when it probably belongs in the actual object definition. I believe in the end, this will lead to more readable, maintainable code that can be readily extended as more features are required. The final example still leverages the powerful binding capabilities of Knockout while keeping the code out of the markup.