knockout.jsjquery-ui-sortableknockout-sortable

Passing Knockout observable to jQueryUI sortable


Here I'm attempting to pass reference to Knockout observable to a sortable object, which is part of jQueryUI and doesn't have access to the variable by default.

In the code example I'm trying to make a reference to viewModel through self on the line self.dragMode(true);. The line is highlighted in the code example.

The attempted calls have failed and whenever I call the referenced object I get undefined. What is the right way of doing this?

var viewModel = function() {
  var self = this;
  self.gridItems = ko.observableArray(
    [{
      "rowItems": [{
        "name": "Item 1"
      }, {
        "name": "Item 2"
      }, {
        "name": "Item 3"
      }]
    }, {
      "rowItems": [{
        "name": "Item 4"
      }]
    }, {
      "rowItems": [{
        "name": "Item 5"
      }, {
        "name": "Item 6"
      }]
    }]
  );
  self.selectedRowItem = ko.observable();
  self.dragMode = ko.observable(false);

  console.log(self.gridItems());

  self.gridItems().splice(0, 0, {
    "rowItems": [{
      "placeholder": true,
      "name": ""
    }]
  });

  for (var i = 1; i < self.gridItems().length; i++) {
    console.log(self.gridItems()[i]);
    self.gridItems()[i].rowItems.splice(0, 0, {
      "placeholder": true,
      "name": ""
    });
    for (var j = 1; j < self.gridItems()[i].rowItems.length; j++) {
      self.gridItems()[i].rowItems.splice(j + 1, 0, {
        "placeholder": true,
        "name": ""
      });
      j++;
    }
    self.gridItems().splice(i + 1, 0, {
      "rowItems": [{
        "placeholder": true,
        "name": ""
      }]
    });
    i++;
  }

  console.log(self.gridItems());
};

//connect items with observableArrays
ko.bindingHandlers.sortableList = {
  self: this,
  init: function(element, valueAccessor, allBindingsAccessor, context) {
    $(element).data("sortList", valueAccessor()); //attach meta-data
    $(element).sortable({
      start: function(event, ui) {
      	self.dragMode(true); // HERE NEED TO ACCESS VIEWMODEL
      },
      update: function(event, ui) {
        var item = ui.item.data("sortItem");
        if (item) {
          //identify parents
          var originalParent = ui.item.data("parentList");
          var newParent = ui.item.parent().data("sortList");
          //figure out its new position
          var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
          if (position >= 0) {
            if (newParent[position].placeholder) {
            	originalParent.remove(item);
            	newParent.splice(position, 0, item);
            } else {
            	return;
            }
          }

          ui.item.remove();
        }
      },
      cancel: ':input,button,.contenteditable,.sortable-placeholder-horizontal,.sortable-placeholder-vertical',
      connectWith: '.sortable-container'
    });
  }
};

//attach meta-data
ko.bindingHandlers.sortableItem = {
  init: function(element, valueAccessor) {
    var options = valueAccessor();
    $(element).data("sortItem", options.item);
    $(element).data("parentList", options.parentList);
  }
};

ko.applyBindings(new viewModel());
.sortable-grid .sortable {
  list-style-type: none;
  margin: 0;
  padding: 0;
  width: 100% !important;
  display: table !important;
  table-layout: auto;
}

.sortable-grid .sortable .sortable-item {
  margin: 0 3px 3px 3px;
  padding: 0.4em;
  font-size: 1.4em;
  cursor: move;
}

.sortable-grid .sortable div.fixed {
  cursor: default;
  color: #959595;
  opacity: 0.5;
}

.sortable-grid {
  
}

.sortable-grid .sortable .sortable-row {
  height: 100% !important;
  padding: 0 !important;
  margin: 0 !important;
  display: table-row !important;
}

.sortable-grid .sortable .sortable-item {
  border: 1px solid green;
  //width:initial;
  display: table-cell;
  margin: 0 !important;
}

.sortable-grid .sortable .sortable-item > p {
  //width:100%;
  display: inline;
  margin: 0 !important;
  z-index: 9999;
  cursor: text;
}

.sortable-grid .sortable .sortable-placeholder-horizontal {
  background-color: red;
}

.sortable-grid .sortable .sortable-placeholder {
  background-color: red;
  display: table-cell;
  margin: 0 !important;
  width:10px !important;
}

.sortable-grid .sortable .sortable-placeholder:hover {
  background-color: blue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link href="https://code.jquery.com/ui/1.12.0-beta.1/themes/smoothness/jquery-ui.css" rel="stylesheet" />
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" rel="stylesheet" />
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

<div class="" data-bind="template: { name: 'gridTmpl', foreach: gridItems, templateOptions: { parentList: gridItems} }, sortableList: gridItems">
</div>

<script id="gridTmpl" type="text/html">
  <div class="sortable-grid">
    <div class="sortable sortable-container">
      <div class="sortable-row sortable sortable-container" data-bind="template: { name: 'rowTmpl', foreach: rowItems, templateOptions: { parentList: rowItems} }, sortableList: rowItems">
      </div>
    </div>
  </div>
</script>

<script id="rowTmpl" type="text/html">
  <!-- ko if: !$data.placeholder -->
  <div class="sortable-item" data-bind="sortableItem: { item: $data, parentList: $data.parentList }">
    <p class="contenteditable" contenteditable="true" data-bind="text: name"></p>
  </div>
  <!-- /ko -->
  <!-- ko if: $data.placeholder && $root.dragMode -->
  <div class="sortable-placeholder" data-bind="sortableItem: { item: $data, parentList: $data.parentList }">
    </p>
  </div>
  <!-- /ko -->
</script>


Solution

  • In this case it was necessary to use bindingContext, which contained the reference to global viewModel. The viewModel argument couldn't be used because the context changed depending on where the template is used locally.

    Finally, these were all the changes needed to get the viewModel and pass it to jQuery sortable:

    1. $(element).data("viewModel", bindingContext.$root);
    2. var viewModel = ui.item.parent().data("viewModel");

    Updated binding handler:

    //connect items with observableArrays
    ko.bindingHandlers.sortableList = {
      init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        $(element).data("sortList", valueAccessor()); //attach meta-data
        $(element).data("viewModel", bindingContext.$root);// ? bindingContext.$root : bindingContext); //attach meta-data
        $(element).sortable({
          start: function(event, ui) {
            //identify viewModel
            var viewModel = ui.item.parent().data("viewModel");
            viewModel.dragMode(true);
          },
          change: function(event, ui) {
            //identify viewModel
            var viewModel = ui.item.parent().data("viewModel");
            viewModel.dragMode(true);
          },
          update: function(event, ui) {
            var item = ui.item.data("sortItem");
            if (item) {
    
              //identify parents
              var originalParent = ui.item.data("parentList");
              var newParent = ui.item.parent().data("sortList");
              //identify viewModel
              var viewModel = ui.item.parent().data("viewModel");
              //figure out its new position
              var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
              if (position >= 0) {
                console.log(newParent[position]);
                if (newParent[position].placeholder) {
                  console.log(originalParent);
                    originalParent.remove(item);
                    newParent.splice(position, 0, item);
                    viewModel.dragMode(false);
                } else {
                    return;
                }
              }
    
              ui.item.remove();
            }
          },
          cancel: ':input,button,.contenteditable,.sortable-placeholder-horizontal,.sortable-placeholder-vertical',
          connectWith: '.sortable-container'
        });
      }
    };