A general rule I follow when using KnockoutJS is that there should be no DOM manipulation in the viewModel. The viewModel should be completely independent of any markup, while any changes to the DOM (via jQuery or otherwise) should be handled in the binding handler. This makes your viewModels much more portable and testable.
As I’m sure you’re aware if you’re reading this article(!), KnockoutJS’s binding handlers are applied to an element and have init
and update
functions that get called when a certain value in your viewModel changes. Within your init
function, you can set up various DOM-element-specific jQuery handlers, while within your update
function you can perform various DOM manipulations, trigger events etc., as well as reading from/updating your viewModel and much more.
A common situation I’ve come across a number of times is: say you have a big div
with plenty of buttons and links that are tied into external jQuery plugins and DOM elements and you want to perform certain actions when they’re clicked or when other changes happen in your viewModel. You don’t really want to have loads of binding handlers for each separate change that might happen in your viewModel, your codebase could get quite big quite quickly. What I’m about to propose is a structure of how to apply 1 binding handler to the entire div, then call various functions to manipulate the DOM outside of your update
binding handler function, via the viewModel.
So, I’ll start with the viewModel. I’m going to have an observable action
attribute and 2 functions linkClicked
and buttonClicked
. (Please bear in mind, this is a very simple example for illustration purposes, you wouldn’t really call viewModel functions linkClicked
etc.!) There’ll also be a resetAction
function, which will be explained shortly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | exampleViewModel = (function($) { var viewModel = function() { this.action = ko.observable(''); }; viewModel.prototype.resetAction = function() { this.action(''); }; viewModel.prototype.linkClicked = function() { this.action('jQLinkClicked'); // prepended "jQ" to the function name to help the reader later }; viewModel.prototype.buttonClicked = function() { this.action('jQButtonClicked'); }; //JS Module Pattern return viewModel; }(jQuery)); ko.applyBindings(new exampleViewModel(), $('#example')[0]); |
And now our HTML markup:
1 2 3 4 | <div id="example" data-bind="exampleBindingHandler: action()"> <a href="void(0)" data-bind="click: linkClicked"><br /> <button data-bind="click: buttonClicked"> </div> |
So now we can see that whenever we click either the link or the button, our action
attribute will be updated and thus trigger the update
function in the exampleBindingHandler
binding handler that’s applied to the div
. Let’s look at that binding handler now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | ko.bindingHandlers.exampleBindingHandler = { init: function(element, valueAccessor, allBindingsAccessor, viewModel) { // do whatever initial set up you need to do here, e.g. $('body').addClass('whatever'); }, update: function(element, valueAccessor, allBindingsAccessor, viewModel) { // so this will be called whenever our observable 'action' changes // get the value var action = valueAccessor(); // reset to empty viewModel.resetAction(); switch (action) { case 'jQLinkClicked': alert('link'); break; case 'jQButtonClicked': alert('button'); break; } } }; |
So you can see from the above how we can move from various different viewModel changes out to the binding handler and maniuplate the DOM in there. We read and save action
from the valueAccessor
, then reset it via the viewModel’s resetAction
function, just to keep things clean.
At this point we have very simple alert
s for each of our actions but of course in real life you’ll want to call your jQuery plugins, change the DOM etc. To keep things clean what we can do is have a simple JSON object with functions for each of the actions and within those functions do our heavy jQuery lifting, something along the lines of:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | var _ = { jQLinkClicked: function() { // e.g. $('.class').parent().remove(); }, jQButtonClicked: function() { // e.g. $.plugin.foo(); } } ko.bindingHandlers.exampleBindingHandler = { init: function(element, valueAccessor, allBindingsAccessor, viewModel) { // do whatever initial set up you need to do here, e.g. $('body').addClass('whatever'); }, update: function(element, valueAccessor, allBindingsAccessor, viewModel) { // so this will be called whenever our observable 'action' changes // get the value var action = valueAccessor(); // reset to empty viewModel.resetAction(); switch (action) { case 'jQLinkClicked': _.jQLinkClicked(); break; case 'jQButtonClicked': _.jQButtonCliked(); break; } } }; |
So, in summary:
- Have an observable
action
attribute in your viewModel - Apply the binding handler to your main
div
, with the observableaction
variable as thevalueAccessor
parameter - Set
action
accordingly in your viewModel - In your binding handler, figure out what jQuery/DOM manipulation etc. you want to do based on the value of
action
No Comments
Leave a reply
You must be logged in to post a comment.