I am trying to create an easy method for defining a list of groups. The groups include an id, a description, an enabled/disabled field, and a sort order. I am trying to use a KendoUI sortable widget. The problem I am having occurs when I:
- Add a new item
- Move the item up in the list
- Try to remove that item from the list
The item is removed from the observable array, but still shows up in the sortable widget.
I've tried to simplify the code as much as I can to demonstrate the issue.
//--------------------------------------------------------------------------------------------------
// MenuGroup View Models
//--------------------------------------------------------------------------------------------------
function MenuGroup() {
var self = this;
self.MenuGroupId = ko.observable(0);
self.Description = ko.observable("");
self.DisplayOrder = ko.observable(0);
self.MenuActive = ko.observable(true);
}
var menuGroupMapping = {
MenuGroup: {
key: function(group) {
return ko.utils.unwrapObservable(group.MenuGroupId);
},
create: function(group) {
var groupObj = ko.mapping.fromJS(group);
return groupObj;
}
}
}
function MenuGroupsViewModel() {
var self = this;
self.baseUri = "/api/menugroups";
self.menugroups = ko.observableArray();
self.newMenuGroup = ko.observable(new MenuGroup());
self.selectedGroup = ko.observable(new MenuGroup());
self.save = function(formElement) {
self.newMenuGroup().DisplayOrder(self.menugroups().length);
self.menugroups.push(self.newMenuGroup());
var g = new MenuGroup();
self.newMenuGroup(g);
};
self.deleteGroup = function(group) {
bootbox.confirm("Are you sure you want to delete the following menu group: " + group.Description(), function(result) {
self.menugroups.remove(group);
});
}
self.selectGroup = function(group) {
self.selectedGroup(group);
};
self.changeOrder = function(oldPosition, newPosition) {
var movedGroup = self.menugroups()[oldPosition];
movedGroup.DisplayOrder(newPosition);
var id = movedGroup.MenuGroupId();
ko.utils.arrayForEach(self.menugroups(), function(menugroup) {
if (oldPosition < newPosition) {
if (oldPosition <= menugroup.DisplayOrder() && newPosition >= menugroup.DisplayOrder() && menugroup.MenuGroupId() != id) {
menugroup.DisplayOrder(menugroup.DisplayOrder() - 1);
}
} else {
if (oldPosition >= menugroup.DisplayOrder() && newPosition <= menugroup.DisplayOrder() && menugroup.MenuGroupId() != id) {
menugroup.DisplayOrder(menugroup.DisplayOrder() + 1);
}
}
});
self.menugroups().splice(oldPosition, 1);
self.menugroups().splice(newPosition, 0, movedGroup);
}
var data = [{
"MenuGroupId": 0,
"Description": "Group A",
"DisplayOrder": 0,
"MenuActive": true
}, {
"MenuGroupId": 1,
"Description": "Group B",
"DisplayOrder": 1,
"MenuActive": false
}, {
"MenuGroupId": 2,
"Description": "Group C",
"DisplayOrder": 2,
"MenuActive": true
}, {
"MenuGroupId": 3,
"Description": "Group D",
"DisplayOrder": 3,
"MenuActive": true
}]
ko.mapping.fromJS(data, menuGroupMapping, self.menugroups);
}
//--------------------------------------------------------------------------------------------------
// Custom Bindings
//--------------------------------------------------------------------------------------------------
ko.bindingHandlers.showModal = {
init: function(element, valueAccessor) {},
update: function(element, valueAccessor) {
var value = valueAccessor();
if (ko.utils.unwrapObservable(value)) {
$(element).modal('show');
$("input", element).focus();
} else {
$(element).modal('hide');
}
}
}
mg = new MenuGroupsViewModel();
ko.applyBindings(mg, document.getElementById('menu-group-panel'));
$(document).ready(function() {
$("#menu-list").kendoSortable({
handler: ".menu-group-handle",
hint: function(element) {
return element.clone().addClass("hint");
},
placeholder: function(element) {
return element.clone().css({
"opacity": 0.3,
"border": "1px dashed #000000"
});
},
axis: "y",
container: "#menu-list",
cursor: "move",
change: onChange
});
});
function onChange(e) {
mg.changeOrder(e.oldIndex, e.newIndex);
}
body {
padding: 20px;
}
.def-panel {
height: 100%;
margin: 0 30px 0 0;
width: 300px;
float: left;
}
#menu-list {
padding: 0;
width: 200px;
}
.menu-group-handle {
width: 30px;
height: 50px;
display: inline-block;
background-color: darkslategray;
}
div.menu-group {
display: inline-block;
width: 200px;
height: 50px;
background-color: aliceblue;
font-family: "Segoe Ui", Helvetica, Arial, 'DejaVu Sans', 'Liberation Sans', Freesans, sans-serif;
margin: 5px 0;
padding: 0;
cursor: pointer;
}
.menu-group-desc {
font-weight: 700;
margin: 10px;
}
.menu-group-selected {
background-color: darkslateblue !important;
color: white;
}
<link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"/>
<script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="//cdn.kendostatic.com/2014.2.903/js/kendo.all.min.js"></script>
<h2>Group/Item Definitions</h2>
<div id="menu-canvas">
<div id="menu-group-panel" class="def-panel">
<h3>Menu Groups</h3>
<div id="menu-list" data-bind="foreach: menugroups">
<div class="menu-group" data-bind="click: function() { mg.selectGroup($data) }, css: {'menu-group-selected': $parent.selectedGroup().MenuGroupId === $data.MenuGroupId}">
<span class=" menu-group-handle">
</span>
<!--<span data-bind="click: function() { mg.selectGroup($data) }, css: {'menu-group-selected': $parent.selectedGroup().MenuGroupId === $data.MenuGroupId}" class="menu-group">
-->
<span class="menu-group">
<button class="close menu-group-delete" data-bind="click: function() { mg.deleteGroup($data) } ">×</button>
<span class="menu-group-desc" data-bind="text: $data.Description"></span>
<span data-bind="text: $data.DisplayOrder"></span>
</span>
</div>
</div>
<div class="menu-list">
<button data-toggle="modal" data-target="#new-menu-group">New menu group...</button>
</div>
<div style="width: 800px;" data-bind="foreach: menugroups">
<span data-bind="text: $data.DisplayOrder"></span>-<span data-bind="text: $data.Description"></span>,
</div>
<div class="modal fade" data-bind="with: newMenuGroup" id="new-menu-group">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button class="close" data-dismiss="modal">×</button>
<h3>New Menu Group</h3>
</div>
<div class="modal-body">
<form class="form-horizontal" id="add-menugroup" data-bind="submit: $parent.save">
<fieldset>
<div class="form-group">
<label class="control-label col-sm-2">Group Name</label>
<div class="col-sm-10">
<input class="form-control" data-bind="value: Description" type="text" id="menu-group-name" />
</div>
</div>
<div class="form-group">
<div class="col-sm-10 col-sm-push-2">
<div class="checkbox">
<input data-bind="checked: MenuActive" type="checkbox" />
<label>Menu group is active</label>
</div>
</div>
</div>
</fieldset>
</form>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-primary" data-bind="click: $parent.save" data-dismiss="modal">Save Changes</a>
<a href="#" class="btn" data-dismiss="modal">Close</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="//ajax.aspnetcdn.com/ajax/knockout/knockout-3.1.0.debug.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.3.0/bootbox.min.js"></script>
<script src="//ajax.aspnetcdn.com/ajax/jquery.ui/1.10.4/jquery-ui.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>