public
Last active

Unobtrusive Knockout support library for jQuery

  • Download Gist
example.html
HTML
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
Choose a ticket class: <select id="tickets"></select>
 
<p id="ticketOutput"></p>
<script id="ticketTemplate" type="text/x-jquery-tmpl">
{{if chosenTicket}}
You have chosen <b>${ chosenTicket().name }</b>
($${ chosenTicket().price })
<button data-bind="click: resetTicket">Clear</button>
{{/if}}
</script>
<script type="text/javascript">
var viewModel = {
tickets: [
{ name: "Economy", price: 199.95 },
{ name: "Business", price: 449.22 },
{ name: "First Class", price: 1199.99 }
],
chosenTicket: ko.observable(),
resetTicket: function() { this.chosenTicket(null) }
};
 
$("#tickets").dataBind({
options: "tickets",
optionsCaption: "'Choose...'",
optionsText: "'name'",
value: "chosenTicket"
});
 
$("#ticketOutput").dataBind({ template: "'ticketTemplate'" });
 
ko.applyBindings(viewModel);
</script>
jquery.unobtrusive-knockout.js
JavaScript
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 35 36 37 38 39 40 41 42 43 44 45 46 47
/**
* @preserve Unobtrusive Knockout support library for jQuery
*
* @author Joel Thoms
* @version 1.0
*/
 
(function($) {
 
if (!$ || !$['fn']) throw new Error('jQuery library is required.');
 
/**
* Private method to recursively parse key value pairs into a string
*
* @param {Object} options Object to parse into a string.
* @return {string} The string value of the object passed in.
*/
function parse(options) {
var parsed = [];
for (var key in options) {
var val = options[key];
switch (typeof val) {
case 'string': parsed.push(key + ':' + val); break;
case 'object': parsed.push(key + ':{' + parse(val) + '}'); break;
case 'function': parsed.push(key + ':' + val.toString()); break;
}
}
return parsed.join(',');
}
 
/**
* jQuery extension to handle unobtrusive Knockout data binding.
*
* @param {Object} options Object to parse into a string.
* @return {Object} A jQuery object.
*/
$['fn']['dataBind'] = $['fn']['dataBind'] || function(options) {
return this['each'](function() {
var opts = $.extend({}, $['fn']['dataBind']['defaults'], options);
var attr = parse(opts);
if (attr != null && attr != '') {
$(this)['attr']('data-bind', attr);
}
});
};
 
})(jQuery);
jquery.unobtrusive-knockout.min.js
JavaScript
1 2 3 4 5 6 7 8
/*
Unobtrusive Knockout support library for jQuery
 
@author Joel Thoms
@version 1.0
*/
 
(function(a){function e(a){var b=[],c;for(c in a){var d=a[c];switch(typeof d){case "string":b.push(c+":"+d);break;case "object":b.push(c+":{"+e(d)+"}");break;case "function":b.push(c+":"+d.toString())}}return b.join(",")}if(!a||!a.fn)throw Error("jQuery library is required.");a.fn.dataBind=a.fn.dataBind||function(f){return this.each(function(){var b=a.extend({},a.fn.dataBind.defaults,f),b=e(b);b!=null&&b!=""&&a(this).attr("data-bind",b)})}})(jQuery);

Performance overhead?

there should be no performance overhead. once the code has executed, knockout works in the same exact way as it normally would.

But them this allows for unobtrusive data binding OUTSIDE templates, right? On templates this won't do anything...

Correct. The jQuery templates are not supported. Only native Knockout code. Additional code would have to be added to support the jQuery templates. The project doesn't require jQuery templates, so I haven't created any code that works with them.

Yeah, additional code per template framework. Can't think of an agnostic way of dealing with templates. Thanks, joelnet!

It might be possible to attach the data-binding after the template has been rendered. but again, I haven't been able to test it.

Some selectors don't seem to work or maybe if you can't bind the same element twice. In that case wouldn't it be good to add an overload dataBind(options, options2) or better yet, in pseudo-code, dataBind(params [] options) where the other options could be merged? That way you could share options between objects.

var sharedOptions = {
someBinding : "binding logic",
moreBindings : "more binding logic"
}

$("#obj1").dataBind( {
... // some options.
},
sharedOptions);

you are right. can't bind twice. no options to merge. this should be done outside the library.

There's a couple of options to do merging here:
http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically

Hi Joel,

The problem with all of the merge examples that I have come accross is that
that are destructive in nature;that is, they overwrite the arrays instead
of merge them. If you look at the merge function I put in the code you can
see that it merges arrays for scenarios like css and attr bindings when you
have multiples. Are you suggesting that merge should be done first and a
single options object would be better? My changes certainly don't do much
for your ultra-light-weight elegant code but they do provide a more
friendly developer experience. The only thing missing is that you should be
able to pass more options objects to the script so that you can develop
your own options library if needed, and maybe you could encapsulate that
functionality into another object(say $.combineOptions(op1,op2,op3)
removing the merge code from the jquery.unobtrusive.knockout.

I guess you are suggesting to write code as follows:

var options =
$.combineOptions(op1, myLib.opt2(firstName),myLib.customOptions);

...bindData(options)

This still doesn't make up for the missing ability to use arrays of
bindings. This is really important in my opinion, unless there is a more
elegant way of solving this shortcoming.

On Wed, May 23, 2012 at 12:01 PM, joelnet <
reply@reply.github.com

wrote:

you are right. can't bind twice. no options to merge. this should be
done outside the library.

There's a couple of options to do merging here:

http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically


Reply to this email directly or view it on GitHub:
https://gist.github.com/1006808

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

I'd create a custom combine function that can handle all of this that is separate from the data-bind.

Hi Joel,

Okay, somehow I didn't realize that code like this would work:

viewModel = {};
$(document).ready(function () {
    var settings = {
        css: { drag: "'true'", selectedElement: "'true'" }
    };
    $("#one").dataBind(settings);
    ko.applyBindings(viewModel);
});

I assumed that I could only put a single css object in the settings. So I
just have to write code to combine those objects as needed.

On Wed, May 23, 2012 at 2:58 PM, joelnet <
reply@reply.github.com

wrote:

I'd create a custom combine function that can handle all of this that is
separate from the data-bind.


Reply to this email directly or view it on GitHub:
https://gist.github.com/1006808

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

Ahhhh ok. I get where you were coming from now.

You can even do things like this:

$('#tickets').dataBind({
    event: {
        focus: function(evt) {
            alert('focus!');
        },
        change: function(evt) {
            alert('change!');
        }
    }
});

I must have had soemthing like this
$("tickets").dataBind({
css: { data : true},
css: { class2 : true}
});

where the second one won over the first.

I came up with this to combine objects I may have missed some points but I
will see if this works.

jQuery.combineOptions = function (first, second) {
var used = [];
var level = -1;
function internalCombineOptions(first, second) {
level += 1;
used[level] = [];

        for (var a1 in first) {

            // if second object is null we are finished.
            used[level].push(a1);
            if (second[a1] == null) {
                continue;
            } else {

                var ob2 = second[a1];
                if (typeof (first[a1]) != typeof (ob2)) {
                    throw new Error("Type mismatch");
                }

                if (typeof (first[a1]) == "object") {
                    // matching property.
                    return internalCombineOptions(first[a1],

second[a1]);
}
}
}
for (var a2 in second) {
if (used[level].indexOf(a2) < 0) {
first[a2] = second[a2];
}
}
level -= 1
return first;
}
return internalCombineOptions(first, second);
};
jQuery.combineOptions = function (first, second) {
var used = [];
var level = -1;
function internalCombineOptions(first, second) {
level += 1;
used[level] = [];

        for (var a1 in first) {

            // if second object is null we are finished.
            used[level].push(a1);
            if (second[a1] == null) {
                continue;
            } else {

                var ob2 = second[a1];
                if (typeof (first[a1]) != typeof (ob2)) {
                    throw new Error("Type mismatch");
                }

                if (typeof (first[a1]) == "object") {
                    // matching property.
                    return internalCombineOptions(first[a1],

second[a1]);
}
}
}
for (var a2 in second) {
if (used[level].indexOf(a2) < 0) {
first[a2] = second[a2];
}
}
level -= 1
return first;
}
return internalCombineOptions(first, second);
};

On Wed, May 23, 2012 at 3:24 PM, joelnet <
reply@reply.github.com

wrote:

Ahhhh ok. I get where you were coming from now.

You can even do things like this:

$('#tickets').dataBind({
event: {
focus: function(evt) {
alert('focus!');
},
change: function(evt) {
alert('change!');
}
}
});


Reply to this email directly or view it on GitHub:
https://gist.github.com/1006808

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

oops pasted twice

On Wed, May 23, 2012 at 3:13 PM, Jaime Weise jaime@ellev8.ca wrote:

Hi Joel,

Okay, somehow I didn't realize that code like this would work:

viewModel = {};
$(document).ready(function () {
    var settings = {
        css: { drag: "'true'", selectedElement: "'true'" }
    };
    $("#one").dataBind(settings);
    ko.applyBindings(viewModel);
});

I assumed that I could only put a single css object in the settings. So I
just have to write code to combine those objects as needed.

On Wed, May 23, 2012 at 2:58 PM, joelnet <
reply@reply.github.com

wrote:

I'd create a custom combine function that can handle all of this that is
separate from the data-bind.


Reply to this email directly or view it on GitHub:
https://gist.github.com/1006808

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

Hi Joel,

It seems that I have confirmed that the example you mentioned, "you can
even do this..." doesn't work.

$('#fullName').dataBind({
    value: 'SelectedCard().CardData.FullName',
    cardValueChanged : "SelectedCard().CardData.FullName",
    visible: "mode() == 'edit' && isMyCard()",
    valueUpdate : "'afterkeydown'",
    event : { load : function() {
        $("#fullName").addClass("formElement");
        $("#fullName").addClass("card-data");
        $("#fullName").addClass("fullName");
    }}
});

On Wed, May 23, 2012 at 4:01 PM, Jaime Weise jaime@ellev8.ca wrote:

oops pasted twice

On Wed, May 23, 2012 at 3:13 PM, Jaime Weise jaime@ellev8.ca wrote:

Hi Joel,

Okay, somehow I didn't realize that code like this would work:

viewModel = {};
$(document).ready(function () {
    var settings = {
        css: { drag: "'true'", selectedElement: "'true'" }
    };
    $("#one").dataBind(settings);
    ko.applyBindings(viewModel);
});

I assumed that I could only put a single css object in the settings. So I
just have to write code to combine those objects as needed.

On Wed, May 23, 2012 at 2:58 PM, joelnet <
reply@reply.github.com

wrote:

I'd create a custom combine function that can handle all of this that is
separate from the data-bind.


Reply to this email directly or view it on GitHub:
https://gist.github.com/1006808

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

what isn't working? what is "fullName"? Is that a text field? if so, I don't believe "load" is a valid event. Can you use chrome's element inspector to look inside "fullName" to see what the data-bind attribute generated is?

http://www.threetipsaday.com/2008/12/debug-inspect-google-chrome-inspector/

Thanks again Joel for your time,

I understand what you are saying and in my event handler load is a valid
event of my input.

This code works and is acceptable.

$('#fullName,#_title,#phone,#company').each(function() {

    var id = this.id;
    var propName = (id.charAt(0).toUpperCase() + id.slice(1));

    $(this).dataBind({
        value: 'SelectedCard().CardData.' + propName,
        cardValueChanged: "SelectedCard().CardData." + propName,
        visible: "mode() == 'edit' && isMyCard()",
        valueUpdate: "'afterkeydown'",
        event: { load: myfunc($(this)) }
    });
});

function myfunc(obj) {
obj.addClass("formElement");
obj.addClass("card-data");
obj.addClass(this.id);
}

It seems like the inline function doesn't play well.

By the way, I have been studying and working for 10 years and am looking
for work. Send anyone my way who might have a job for a Microsoft .NET web
application developer with excellent server and client-side development
experience. I live in Vancouver, Canada.

On Thu, May 24, 2012 at 1:02 PM, joelnet <
reply@reply.github.com

wrote:

what isn't working? what is "fullName"? Is that a text field? if so, I
don't believe "load" is a valid event. Can you use chrome's element
inspector to look inside "fullName" to see what the data-bind attribute
generated is?

http://www.threetipsaday.com/2008/12/debug-inspect-google-chrome-inspector/


Reply to this email directly or view it on GitHub:
https://gist.github.com/1006808

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

I don't believe the $(this) part will work. use the element inspector and you'll see why. The function get's converted into a string and will lose the "this" context.

Your're right, it doesn't work for the reasons you explained. But, the load
function works by nature because it only executes once and internally. The
binding leaves and empty ..., event: { }" stub in the data-bind attribute
that won't do anything. So, in really it doesn't work as you mentioned and
I have also decided that it doesn't make sense to bind an event such load.

    $(this).dataBind({
       ...
      event: { click: "alert($('#" + id + "').get(0).id)" }
    });

This seems to be the only way to pass an object. The expression has to be a
string.

On Thu, May 24, 2012 at 2:57 PM, joelnet <
reply@reply.github.com

wrote:

I don't believe the $(this) part will work. use the element inspector and
you'll see why. The function get's converted into a string and will lose
the "this" context.


Reply to this email directly or view it on GitHub:
https://gist.github.com/1006808

*
Jaime Weise
*
Technical Director

MOBILE 778 319 1739
OFFICE 604 568 2812
TOLL FREE 1 877 568 2812

EMAIL jaime@ellev8.ca
URL www.ellev8.ca

*

WARNING: This Electronic communication is protected pursuant to relevant
international treaties and domestic laws, and is legally privileged. The
information that is contained within this communication is intended ONLY
for the individual and/or entity who is the intended recipient, and YOU ARE
HEREBY ON NOTICE THAT any dissemination, distribution, copying, or other
use of this communication by anyone other than the intended recipient in an
unauthorized manner is strictly prohibited by law. IF YOU HAVE RECEIVED
THIS MESSAGE IN ERROR, PLEASE CONTACT THE SENDER IMMEDIATELY!
*

I think there's an inconsistency: instead of $.extend() need to use $['extend']() /sarcasm.

Is there a reason for this notation?

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.