Wednesday, May 23, 2012

In Place Editor Custom Binding for Knockout.js

I recently discovered knockout.js and was totally blown away. It does a great job of removing the need for (most) DOM manipulations which lets you focus on your data model and get complex UIs running with a minimal amount of code.  That said, those complex UIs are by default made up of simple widgets (input boxes, buttons, etc) and if you want more complex widgets (date pickers, inline editors, etc) you need to either find them elsewhere or roll your own.  Ryan Niemeyer gives a great explanation of how to create a date picker (or any custom binding) and I quickly incorporated it into my own site.  Since I also needed an inline editor I figured I would post my results so others could reuse and improve upon them.  The following code is very much based on the Custom Bindings article referenced above and the Jeditable inline editor.

The Goal:

The goal is to create something that looks like text but when you click it becomes editable.  I also wanted when you click off or hit enter for the text to be saved to the model.  Finally, I want hitting "Esc" to undo the latest change.  Here is a live example you can play with:

Click this:
Current value:

The Prerequisites:

The following code is based on:

The HTML:

The HTML is pretty simple:
Click this: <span data-bind="jeditable: name, jeditableOptions: {}"></span>
Current value: <span data-bind="text: name"></span>

If you're comfortable with Knockout.js this should look pretty familiar.  The first line uses a custom binding called "jeditable" to tie a model property called "name" to a span tag.  Knockout.js will be responsible for keeping the model and DOM in sync.  As a developer I just tell it to use the "jeditable" binding for this element and the "name" property.  There is also an optional "jeditableOptions" property which I can use to pass options to the jeditable binding.

The second line uses the standard text binding to display the current value of the "name" property in the span. Once again, as a developer, I don't have to think at all about what to update when the model changes. It all happens automatically.

The Model

The model can also be super simple:
     function myViewModel() {
      this.name = ko.observable("Innocent looking text");
     };
  
     ko.applyBindings(new myViewModel());  
In this case I just need a single property called "name" that I default to "innocent looking text".  I pass an instance of my model to Knockout as is the KO way and it handles all of the heavy lifting.  All that leaves is...

The Custom Binding

The Custom Binding is the reusable code that knows how to update the DOM when the model changes and update the model when the DOM (or control) changes:
ko.bindingHandlers.jeditable = {
     init: function(element, valueAccessor, allBindingsAccessor) {
         // get the options that were passed in
         var options = allBindingsAccessor().jeditableOptions || {};
         
         // "submit" should be the default onblur action like regular ko controls
         if (!options.onblur) {
          options.onblur = 'submit';
         }
         
         // set the value on submit and pass the editable the options
         $(element).editable(function(value, params) {
          valueAccessor()(value);
          return value;
         }, options);

         //handle disposal (if KO removes by the template binding)
         ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
             $(element).editable("destroy");
         });

     },
     
     //update the control when the view model changes
     update: function(element, valueAccessor) {
         var value = ko.utils.unwrapObservable(valueAccessor());
         $(element).html(value);
     }
 };

This is a little more complicated and requires some explanation.  The binding consists of two functions "init" and "update".  "init" is called only once, when the binding is initially evaluated.  Here we can create our control and pass it any options that the user specified.  We also listen for any changes so we can notify the model.  "update" is called every time the model changes and gives us a chance to alter the control to match the state of the model.

The work begins at line 4.  Here we check to see if the user passed in any options.  This gives the user the ability to customize the Jeditable control using any of the options allowed by the plugin.  One option allowed is "onblur".  This tells the control what to do when the user clicks outside of the control.  By default Jeditable will cancel any edits onblur but since the default Knockout.js controls will save onblur we follow this convention and set the default onblur value to be "submit" (that is, save) on lines 7-9.

Lines 12-15 are where we create the control with the specified options and tie it to the appropriate DOM node.  The function that we pass in is what gets called when the user saves a new value.  In this function we simply update the model with the new value and return that value so that Jeditable thinks everything is ok.

Lines 18-20 are just housekeeping.  That's where we clean up our control if Knockout.js tells us that the DOM node should be cleaned up.

Finally, it's time to update at lines 25-28.  Here we simply get a reference to the new value in the model and update the Jeditable control to display that new value.  And that's it!  Now we have a custom binding that can be reused many times throughout the site.

The Conclusion

Knockout.js bindings may seem a little scary at first but they're not that complicated and can be very powerful.  Even someone like me who is new to Knockout.js (and JQuery) can easily whip one up and reuse it throughout a site.  My only wish is that it was easier to distribute custom bindings so that some nice libraries could be written and shared across the 'net.  Please give me any feedback or improvements you have in the comments!





10 comments:

  1. I tried this out, but it does NOT update the ko object, only what's displayed. Tried updating an item, then linked a console.log(item.name) etc into a button so I could inspect it's value after updating the field. The items are custom objects which are inside an observableArray. SEE http://jsfiddle.net/Vg892/ for an example. I'll probly keep looking into it also.

    ReplyDelete
  2. Hi Anonymous,

    I think your issue is definitely the "target: save" parameter that you appended to the jeditable options because when I remove that it seems to work fine. In addition, if you add another line to your template, something like:
    <span data-bind="text: name"></span>
    Then you can see that the model is being updated when you hit enter. If you really want to execute a callback when your value is changed, maybe you can add a new parameter like "callback" and have the binding submit the before and after to the callback function. Or you could use the subscribe function that's built into knockout js.

    Thanks for reading and your feedback!
    Doug

    ReplyDelete
  3. Actually an update. I tried using the exact code above and it at least calls the callback in the ko.BindingHandlers.init method. However, when I try to set the value using valueAccessor()(value); I get an error. I then redid the code to look like this:

    $(element).editable(function(value, params) {
    var v = valueAccessor();
    v(value);
    return value;
    }, options);

    And when I put a breakpoint on the variable v above the returned value is the original text meaning valueAccessor() is returning a string so then the next linke tries to call string(string) which throws this error:
    Uncaught TypeError: string is not a function

    Any idea what's going on? What's valueAccessor() supposed to return? I thought it's supposed to return a function?

    ReplyDelete
  4. Ok figured it out...maybe it's a typo...or what not. Both your site and the Knockout.js site have to update a property like this:

    (Your site):
    valueAccessor()(value);

    (Knockout.js site):
    var value = valueAccessor();
    value(true);

    What actually prevents an error is to do:
    valueAccessor(value);

    Although it doesn't seem like it's updating the property on my view-model because I have another div displaying the value entered (like you do on your page above and the value is not being updated).

    *sigh* This can't be this difficult.

    ReplyDelete
  5. Ok...final note.

    Thanks for the tutorial it was very clear and very helpful. Sorry for all the blog posts...you can delete them if you want.

    In the end, my issue was that I was trying to attach editable to items within a knockout.js foreach loop. Things worked fine if I wasn't in the loop, but when I was, I was getting that error where valueAccessor was returning a string and not an observable. In the end, it was my own fault. I hadn't made the individual objects of my observableArray contain data elements that were observable themselves. So in the end I need an observable array of observable objects in order to assign editable properties to individual data items of each item in the list.

    Thanks!

    ReplyDelete
  6. Ok....last time I really promise, but I was having issues with getting any options to work and I figured out what was happening.

    There's one small mistake in the code above...in your update function, you call $(element).editable().html(value). Unfortunately, the editable framework is written so that whenever you call .editable() you are creating a new function/object, complete with all new settings. Since you're passing in no settings, it reverts to the default settings, thereby overriding the original settings the user may have passed in (as well as re-initializing the whole editable function on the element again). Since the Update function gets called when the HTMLelement's value is first read from the viewmodel, it's resetting all the editable settings so you lose anything other than what appears to be the tooltip.

    You can see the issue here...see how I try to pass an option and it doesn't get set. Also, the editable event remains single click and when I try to save the value, I get an error, because settings.target has been nulled out so it's trying to do editable's default URL callback: http://jsfiddle.net/X82aC/133/

    The solution is to just change the line the in update method from:
    $(element).editable().html(value);
    to
    $(element).html(value);

    This has the same effect (sets the elements value) but DOESN'T re-initialize the entire editable function bound to the element.

    You can try it in the jsfiddle above by commenting out the BAD and uncommenting the GOOD to see the correct/fixed behavior.

    Just thought I'd pass along the tip to any future readers since this was irksome!


    ReplyDelete
  7. Thanks Nate! I updated the page to reflect your fix. Thanks so much!

    ReplyDelete
  8. if i want to add a callback function to update the record in the database where can i add it? im kinda new to this.

    ReplyDelete
  9. how to do a function callback submit after edit a field ?

    ReplyDelete
  10. One more thing that I faced with. I need to update numeric fields and as a result I used knockout extender from their site that convert input to numeric and round it. View-model fields looks like:
    salary: ko.observable(i).extend({ numeric: 2 })
    In this case the extender works inside call
    >14 valueAccessor()(value);
    and modify it to 0 if alphabetic was entered. But the next line
    >15 return value;
    returns original input to jeditable and it updates DOM with it. This way we get '0' in view-model and some alphabetic on the screen.
    The fix that I applied is to return real value from view-model:
    >15 return ko.utils.unwrapObservable(valueAccessor());

    ReplyDelete