Search code examples
javascriptknockout.jsforeachgridstack

Gridstack not updating on delete


Here is my example of Gridstack layout that uses Knockout bindings. The problem is that my view doesn't get updated based on the model, when it should.

After pressing Delete me the console output shows that the widgets observable array gets updated correctly, while the view doesn't. The cause seems to be on this line (which is not being called):

ko.utils.domNodeDisposal.addDisposeCallback(item, function () { 
    self.grid.removeWidget(item); 
});

As far as I know, the foreach binding should update automatically, why it doesn't?

var ViewModel = function() {
  var self = this;
  self.grid = null;
  self.widgets = ko.observableArray([{
    x: 0,
    y: 0,
    width: 1,
    height: 1
  }, {
    x: 0,
    y: 1,
    width: 1,
    height: 1
  }]);
  self.deleteWidget = function(item) {
    console.log("widgets before", self.widgets());
    self.widgets.remove(item);
    console.log("widgets after", self.widgets());
    return false;
  };
  self.afterAddWidget = function(items) {
    if (self.grid == null) {
      self.grid = $('.grid-stack').gridstack({
        auto: false
      }).data('gridstack');
    }
    var item = _.find(items, function(i) {
      return i.nodeType == 1
    });
    self.grid.addWidget(item);
    ko.utils.domNodeDisposal.addDisposeCallback(item, function() {
      self.grid.removeWidget(item);
    });
  };
};
ko.applyBindings(new ViewModel());
.grid-stack {
  background: lightgoldenrodyellow;
}
.grid-stack-item-content {
  color: #2c3e50;
  text-align: center;
  background-color: #18bc9c;
}
<link rel="stylesheet" href="https://raw.githubusercontent.com/troolee/gridstack.js/master/dist/gridstack.css" />
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.0/jquery-ui.js" type="text/javascript"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js" type="text/javascript"></script>
<script type="text/javascript" src="https://rawgit.com/troolee/gridstack.js/master/dist/gridstack.js"></script>

<div class="grid-stack" data-bind="foreach: {data: widgets, afterRender: afterAddWidget}">
  <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">
      <button data-bind="click: $root.deleteWidget">Delete me</button>
    </div>
  </div>
</div>


Solution

  • Gridstack is a DOM-controlling widget. You need some kind of binding handler to make Knockout play nicely with DOM-controlling widgets.

    It looks like maybe you're working from this example. It uses components in a way that is sort of a built-in binding handler. It seems to work, but I recommend putting DOM-manipulation where it goes: in the binding handlers.

    Update: Here is your example with the gridstack code put into a binding handler. It simply wraps a foreach binding handler and adds the afterRender option to it in the update. Now the viewmodel looks like a viewmodel, and you would be able to handle multiple gridstacks on a page without $(.grid-stack) picking the wrong one.

    ko.bindingHandlers.gridstack = {
      init: function(element, valueAccessor, allBindingsAccessor, data, context) {
        ko.bindingHandlers.foreach.init(element, valueAccessor, allBindingsAccessor, data, context);
    
        return {
          controlsDescendantBindings: true
        };
      },
      update: function(element, valueAccessor, allBindingsAccessor, data, context) {
        var widgets = valueAccessor(),
          grid = $(element).gridstack().data('gridstack'),
          afterAddWidget = function(items) {
            var item = _.find(items, function(i) {
              return i.nodeType === 1;
            });
            grid.addWidget(item);
            ko.utils.domNodeDisposal.addDisposeCallback(item, function() {
              grid.removeWidget(item);
            });
          },
          newVA = function() {
            return {
              data: widgets,
              afterRender: afterAddWidget
            };
          };
        ko.bindingHandlers.foreach.update(element, newVA, allBindingsAccessor, data, context);
      }
    };
    
    var ViewModel = function() {
      var self = this;
      self.grid = null;
      self.widgets = ko.observableArray([{
        x: 0,
        y: 0,
        width: 1,
        height: 1
      }, {
        x: 0,
        y: 1,
        width: 1,
        height: 1
      }]);
      self.deleteWidget = function(item) {
        self.widgets.remove(item);
      };
    };
    ko.applyBindings(new ViewModel());
    .grid-stack {
      background: lightgoldenrodyellow;
    }
    .grid-stack-item-content {
      color: #2c3e50;
      text-align: center;
      background-color: #18bc9c;
    }
    <link rel="stylesheet" href="https://raw.githubusercontent.com/troolee/gridstack.js/master/dist/gridstack.css" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js" type="text/javascript"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.0/jquery-ui.js" type="text/javascript"></script>
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js" type="text/javascript"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js" type="text/javascript"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-debug.js" type="text/javascript"></script>
    <script type="text/javascript" src="https://rawgit.com/troolee/gridstack.js/master/dist/gridstack.js"></script>
    
    <div class="grid-stack" data-bind="gridstack: widgets">
      <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">
          <button data-bind="click: $parent.deleteWidget">Delete me</button>
        </div>
      </div></div><!-- <---- NO SPACE BETWEEN THESE CLOSING TAGS -->