Skip to content

The knockout code is not using the right paradigm #465

Closed
@EtienneT

Description

@EtienneT

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions