Description
The knockout code example is using a knockout component. Knockout components are not used to wrap an external javascript library. Components should only deal with the view model and not anything DOM related to keep a good separation of concerns between the view and the view model (MVVM).
Knockout custom bindings are what is used to wrap external librairies to listen to view model changes and update the state of the external plugin.
I had all kind of problems with the included knockout example from the project page in a real world application. I decided to re-implement it as a custom binding. I am pretty sure this could be useful for other people as well.
I include here a proposed custom binding and how to use it in the view.
ko.bindingHandlers.GridStack = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var value = valueAccessor();
var v = ko.unwrap(value);
var innerBindingContext = bindingContext.extend(valueAccessor);
innerBindingContext.afterRender = function (items) {
var item = _.find(items, function (i) { return i.nodeType == 1 });
ko.utils.domNodeDisposal.addDisposeCallback(item, function () {
grid.removeWidget(item);
});
};
innerBindingContext.afterAdd = function (item) {
if (!$(item).is('.grid-stack-item')) return;
grid.addWidget(item);
};
ko.applyBindingsToDescendants(innerBindingContext, element);
var g = $('.grid-stack', element);
var grid = g.gridstack({
auto: true,
animate: false
}).data('gridstack');
g.on('change', function (event, items) {
if (items) {
for (var i = 0; i < items.length; i++) {
var item = items[i];
var data = ko.dataFor(item.el.get(0));
var node = item.el.data('_gridstack_node');
if (data && node) {
data.x(node.x);
data.y(node.y);
data.width(node.width);
data.height(node.height);
}
}
if (v.changed)
v.changed();
}
});
// Also tell KO *not* to bind the descendants itself, otherwise they will be bound twice
return { controlsDescendantBindings: true };
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
}
};
And the actual html view:
<div data-bind="GridStack: { data: widgets, changed: changed }">
<div class="grid-stack" data-bind="foreach: { data: widgets, afterRender: afterRender }">
<div class="grid-stack-item" data-bind="attr: {'data-gs-x': $data.x, 'data-gs-y': $data.y, 'data-gs-width': $data.width, 'data-gs-height': $data.height, 'data-gs-auto-position': $data.auto_position}">
<div class="grid-stack-item-content">
<!-- Content here -->
<button class="btn btn-default btn-outline btn-xs pull-right" data-bind="click: $root.deleteWidget" style="position: absolute; z-index: 10; top: 5px; right: 5px;">
<i class="fa fa-remove"></i>
</button>
</div>
</div>
</div>
</div>
The "changed" property in the custom binding is called when items changed, this way you can save the widgets to the server etc.