Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The knockout code is not using the right paradigm #465

Closed
EtienneT opened this issue Jun 10, 2016 · 10 comments
Closed

The knockout code is not using the right paradigm #465

EtienneT opened this issue Jun 10, 2016 · 10 comments

Comments

@EtienneT
Copy link

EtienneT commented Jun 10, 2016

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.

@nanatiris
Copy link

nanatiris commented Aug 1, 2016

Can anyone confirm that this is working? For some reason, when i tried it, the grid stack wasn't getting a height - I see height: 0px in the element, therefore the elements inside are not visible at all.

Thanks!

Edit: Sorry, there's no issue with the height getting 0px initially. The issue was the addWidget wasn't getting called when the view model is updated. Adding the afterAdd: afterAdd in the foreach binding in the html view will solve the problem.

@troolee
Copy link
Member

troolee commented Aug 10, 2016

Good point. Thank you.

I will take a look on it

@cordasfilip
Copy link

cordasfilip commented Oct 4, 2016

100% agree
here is something for twoWay binding

ko.bindingHandlers.gridStack = {
    helpers: {
        cloneNodes: function (nodesArray, shouldCleanNodes) {
            for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
                var clonedNode = nodesArray[i].cloneNode(true);
                newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode);
            }
            return newNodesArray;
        }
    },
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        var $element = $(element);
        var gridItems = [];
        var fromObs = false;
        var template = ko.bindingHandlers.gridStack.helpers.cloneNodes(element.getElementsByClassName('grid-stack-item'), true);
        ko.virtualElements.emptyNode(element);

        var timeout;
        var grid = $element.gridstack(ko.utils.extend(ko.unwrap(valueAccessor().settings) || {}, {
            auto: true
        })).data('gridstack');

        $element.on('change', function (eve, items) {
            if (!fromObs) {
                if (timeout) {
                    clearTimeout(timeout);
                }
                timeout = setTimeout(function () {
                    for (var i = 0; i < gridItems.length; i++) {
                        var item = gridItems[i];
                        var from = {
                            x: ko.unwrap(item.item.x),
                            y: ko.unwrap(item.item.y),
                            width: ko.unwrap(item.item.width),
                            height: ko.unwrap(item.item.height)
                        };
                        var to = {
                            x: parseInt(item.element.getAttribute("data-gs-x")),
                            y: parseInt(item.element.getAttribute("data-gs-y")),
                            width: parseInt(item.element.getAttribute("data-gs-width")),
                            height: parseInt(item.element.getAttribute("data-gs-height"))
                        };

                        if (from.x != to.x )
                         {   if(ko.isWritableObservable(item.item.x)) {
                                item.item.x(to.x);
                            }else if(!ko.isObservable()){
                                item.item.x = to.x;
                            }
                        }

                        if (from.y != to.y) {
                            if (ko.isWritableObservable(item.item.y)) {
                                item.item.y(to.y);
                            } else if (!ko.isObservable()) {
                                item.item.y = to.y;
                            }
                        }

                        if (from.width != to.width) {
                            if (ko.isWritableObservable(item.item.width)) {
                                item.item.width(to.width);
                            } else if (!ko.isObservable()) {
                                item.item.width = to.width;
                            }
                        }

                        if (from.height != to.height) {
                            if (ko.isWritableObservable(item.item.height)) {
                                item.item.height(to.height);
                            } else if (!ko.isObservable()) {
                                item.item.height = to.height;
                            }
                        }
                    }
                }, 10);

            }
        });

        ko.computed({
            read: function () {
                fromObs = true;
                var widgets = ko.unwrap(valueAccessor().widgets);
                var newGridItems = [];

                for (var i = 0; i < gridItems.length; i++) {
                    var item = ko.utils.arrayFirst(widgets, function (w) { return w == gridItems[i].item; });
                    if (item == null) {
                        grid.removeWidget(gridItems[i].element);
                        ko.cleanNode(gridItems[i].element);
                    } else {
                        newGridItems.push(gridItems[i]);
                    }
                }

                for (var i = 0; i < widgets.length; i++) {
                    var item = ko.utils.arrayFirst(gridItems, function (w) { return w.item == widgets[i]; });
                    if (item == null) {
                        var innerBindingContext = bindingContext['createChildContext'](widgets[i]);
                        var itemElement = ko.bindingHandlers.gridStack.helpers.cloneNodes(template)[0];
                        grid.addWidget(itemElement, ko.unwrap(widgets[i].x), ko.unwrap(widgets[i].y), ko.unwrap(widgets[i].width), ko.unwrap(widgets[i].height), true);
                        ko.applyBindings(innerBindingContext, itemElement)
                        newGridItems.push({ item: widgets[i], element: itemElement });
                    } else {
                        var to = {
                            x: ko.unwrap(widgets[i].x),
                            y: ko.unwrap(widgets[i].y),
                            width: ko.unwrap(widgets[i].width),
                            height: ko.unwrap(widgets[i].height)
                        };
                        var from = {
                            x: parseInt(item.element.getAttribute("data-gs-x")),
                            y: parseInt(item.element.getAttribute("data-gs-y")),
                            width: parseInt(item.element.getAttribute("data-gs-width")),
                            height: parseInt(item.element.getAttribute("data-gs-height"))
                        };

                        if (from.x != to.x || from.y != to.y) {
                            grid.move(item.element, to.x, to.y);
                        }

                        if (from.width != to.width || from.height != to.height) {
                            grid.resize(item.element, to.width, to.height);
                        }
                    }
                }
                gridItems = newGridItems;

                fromObs = false;
            },
            disposeWhenNodeIsRemoved: element
        }).extend({ deferred:true });


        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            gridStack.destroy();
        });

        return { 'controlsDescendantBindings': true };
    }
};

//VM
var vm = {
    widgets:ko.observableArray([
    {x:ko.observable(0), y: ko.observable(0), width: ko.observable(2), height: ko.observable(2)},
    {x: ko.observable(2), y: ko.observable(0), width: ko.observable(4), height: ko.observable(2)},
    {x: ko.observable(6), y: ko.observable(0), width: ko.observable(2), height: ko.observable(4)},
    {x: ko.observable(1), y: ko.observable(2), width: ko.observable(4), height: ko.observable(2)}
    ]),
    add: function ()
    {
        vm.widgets.push({ x: 1, y: 2, width: 4, height: 2 });
    },
    twoWay: function () {
        if (vm.widgets()[0].x() == 10) {
            vm.widgets()[0].x(0);
        } else {
            vm.widgets()[0].x(vm.widgets()[0].x() + 1)
        }
    },
    delete: function (item) {
        vm.widgets.remove(item);
    }
};

ko.applyBindings(vm);

HTML

<button data-bind="click:add">Add</button>
    <button data-bind="click:twoWay">twoWay</button>
    <div class="grid-stack" data-bind="gridStack:{ widgets:widgets }">
        <div class="grid-stack-item">
            <div class="grid-stack-item-content">
                <button data-bind="click: $parent.delete">Delete me</button>
                <div data-bind="text:'x:'+x()"></div>
                <div data-bind="text:'y:'+y()"></div>
                <div data-bind="text:'width:'+width()"></div>
                <div data-bind="text:'height:'+height()"></div>
            </div>
        </div>
    </div>
    <script src="test.js"></script>

PS .size() jquery was braking on jQuery 3.0 >

@adumesny
Copy link
Member

adumesny commented Nov 18, 2019

I don't think radiolips nor I are familiar with knockout, so if you can look at latest demo sample and make a PR with the changes for others that would be great, so we could take it. thank you.

@jneilliii
Copy link

I ran across this in developing a plugin for another framework that uses knockout and this is definitely the better approach. However, when I try to use this example I think it is broken now due to the removal of the jQuery dependencies. When trying to apply the binding I'm getting the error below. I assume this was done in an older jQuery dependant version?

Could not bind view model ConsolidatedtabsViewModel to target #consolidatedtabs_settings_form : TypeError: Unable to process binding "gridStack: function(){return { widgets:widgets} }"
Message: $element.gridstack is not a function

I know your post was real old @cordasfilip, but was wondering if my assumptions are correct in regard to not being a jQuery plugin now?

@cordasfilip
Copy link

I ran across this in developing a plugin for another framework that uses knockout and this is definitely the better approach. However, when I try to use this example I think it is broken now due to the removal of the jQuery dependencies. When trying to apply the binding I'm getting the error below. I assume this was done in an older jQuery dependant version?

Could not bind view model ConsolidatedtabsViewModel to target #consolidatedtabs_settings_form : TypeError: Unable to process binding "gridStack: function(){return { widgets:widgets} }"
Message: $element.gridstack is not a function

I know your post was real old @cordasfilip, but was wondering if my assumptions are correct in regard to not being a jQuery plugin now?

It's really old and I haven't used ko.js or grid stack in years, but from what I can see my solution is no longer valid because of JQuery. But I am sure you could make it work since the dependence on jQuery is minimal.

@jneilliii
Copy link

Thanks, I was looking at the migration guide from 0.6.4 to 1.0.0 ad think the changes will be minimal.

@adumesny
Copy link
Member

adumesny commented Aug 16, 2020

$element.gridstack is not a function was removed in 1.x (API no longer has jquery even though it is still used internally for now).

Might want to take a look at 2.0.0-rc (typescript branch) - where do expose $ from gridstack ES6 module for now, but API calls are different.

@jneilliii
Copy link

Thanks Alain, will definitely take a look.

adumesny added a commit to adumesny/gridstack.js that referenced this issue Dec 3, 2020
* link to issue gridstack#465 so it can be found
@adumesny
Copy link
Member

adumesny commented Dec 3, 2020

closing due to old age and no help updating our demos... added link from doc back here instead.

@adumesny adumesny closed this as completed Dec 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants