Peter G.
Peter G.

Reputation: 8034

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>

Upvotes: 0

Views: 437

Answers (2)

Peter G.
Peter G.

Reputation: 8034

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'
    });
  }
};

Upvotes: 0

Nikolay Ermakov
Nikolay Ermakov

Reputation: 5061

The arguments for ko.bindingHandlers.yourBindingName are: element, valueAccessor, allBindings, viewModel, bindingContext (from here - knockoutjs manual).

In your code you probably have it as context argument.

So you can change self.dragMode(true); to context.dragMode(true) and it should work.

Upvotes: 1

Related Questions