function Cell(name, x, y) { this.name = name; this.x = x; this.y = y; this.showDetails = ko.observable(false); this.toggleShowDetails = function() { this.showDetails(!this.showDetails()); }.bind(this); } Cell.prototype.toString = function() { return "(" + this.x + "," + this.y + ")"; } var viewModel = { counter: 0, headers: ko.observableArray([]), rows: ko.observableArray([]), deleteRow: function(row) { viewModel.rows.remove(row); }, setDescription: function(header) { viewModel.description(header.description); }, clearDescription: function() { viewModel.description(""); }, description: ko.observable("") }; //setup a 20x20 grid of cells for (var i = 0; i < 15; i++) { var row = { name: i, cells: [] }; for (var j = 0; j < 15; j++) { row.cells.push(new Cell(viewModel.counter++, i, j)); } viewModel.rows.push(row); viewModel.headers.push({ name: i, description: "This is a description for column " + i }); } //binding to do event delegation for any event ko.bindingHandlers.delegatedEvent = { init: function(element, valueAccessor, allBindings, viewModel) { var eventsToHandle = valueAccessor() || {}; //if a single event was passed, then convert it to an array if (!$.isArray(eventsToHandle)) { eventsToHandle = [eventsToHandle]; } ko.utils.arrayForEach(eventsToHandle, function(eventOptions) { var realCallback = function(event) { var element = event.target; var options = eventOptions; //verify that the element matches our selector if ($(element).is(options.selector)) { //get real context var context = $(event.target).tmplItem().data; //if a string was passed for the function, then assume it is a function of the real context if (typeof options.callback === "string" && typeof context[options.callback] === "function") { return context[options.callback].call(context, event); } //if a function was passed, then give it the real context as a param return options.callback.call(viewModel, context, event); } } var realValueAccessor = function() { var result = {}; result[eventOptions.event] = realCallback; return result; } ko.bindingHandlers.event.init(element, realValueAccessor, allBindings, viewModel); }); } }; //binding to do event delegation for click events ko.bindingHandlers.delegatedClick = { init: function(element, valueAccessor, allBindings, viewModel) { var clicksToHandle = valueAccessor(); //if a single event was passed, then convert it to an array if (!$.isArray(clicksToHandle)) { clicksToHandle = [clicksHandle]; } ko.utils.arrayMap(clicksToHandle, function(options) { options.event = 'click'; return options; }); var realValueAccessor = function() { return clicksToHandle; } ko.bindingHandlers.delegatedEvent.init(element, realValueAccessor, allBindings, viewModel) } } //binding to do event delegation for click events ko.bindingHandlers.delegatedClickLong = { init: function(element, valueAccessor, allBindings, viewModel) { var clicksToHandle = valueAccessor(); //if a single event was passed, then convert it to an array if (!$.isArray(clicksToHandle)) { clicksToHandle = [clicksHandle]; } ko.utils.arrayForEach(clicksToHandle, function(options) { //build a function that will call our function with the correct context var realValueAccessor = function() { return function(event) { var element = event.target; //verify that the element matches our selector if ($(element).is(options.selector)) { //get real context var context = $(event.target).tmplItem().data; //if a string was passed for the function, then assume it is a function of the real context if (typeof options.callback === "string" && typeof context[options.callback] === "function") { return context[options.callback].call(context, event); } //if a function was passed, then give it the real context as a param return options.callback.call(viewModel, context, event); } } } //call real click binding's init ko.bindingHandlers.click.init(element, realValueAccessor, allBindings, viewModel); }); } } ko.applyBindings(viewModel);
<div data-bind="text: description"> </div> <table data-bind="template: 'rowsTmpl', delegatedClick: [{callback: 'toggleShowDetails', selector: 'td a' }, { callback: deleteRow, selector: 'button' }]"></table> <script id="rowsTmpl" type="text/html"> <thead data-bind="delegatedEvent: [{ event: 'mouseover', callback: setDescription, selector: 'th'}, { event: 'mouseout', callback: clearDescription, selector: 'th'}]"> <tr data-bind="template: { name: 'headTmpl', foreach: headers }"><th></th></tr> </thead> <tbody data-bind="template: { name: 'rowTmpl', foreach: rows }"></tbody> </script> <script id="rowTmpl" type="text/html"> <tr data-bind="template: { name: 'cellTmpl', foreach: cells }"> <td><button>Delete</button></td> </tr> </script> <script id="headTmpl" type="text/html"> <th data-bind="text: name"></th> </script> <script id="cellTmpl" type="text/html"> <td> <a href="javascript: void(0);" data-bind="text: name"></a> <div data-bind="visible: showDetails, text: $data"></div> </td> </script>
div { height: 20px; } table { border: black solid 1px; } td, th { padding: 1px; text-align: center; }