Problem:
I'm trying to build a dashboard of widgets. Each widget will have a delete button on its header. When clicked on this button, corresponding widget have to disappear.
How I designed:
I have two knockout components.
my-widget-list:
VO will have an observableArray of widget objects.
my-widget:
VO will have details to display within the widget.
Note: For simplicity, I'm replacing the widget object with just numbers.
ko.components.register('my-widget-list', {
viewModel : function(params) {
var self = this;
self.values = ko.observableArray([10,20,30,40,50]);
self.deleteWidget = function(obj)
{
self.values.remove(obj);
}
},
template: {element: 'my-widget-list-template'}
});
ko.components.register('my-widget', {
viewModel : function(params) {
var self = this;
self.value = params.value;
},
template: {element: 'my-widget-template'}
});
ko.applyBindings({});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<my-widget-list></my-widget-list>
<script id="my-widget-list-template" type="text/html">
<div data-bind="foreach:values">
<my-widget params="value: $data"></my-widget><br>
</div>
</script>
<script id="my-widget-template" type="text/html">
<span data-bind="text: value"></span>
<button data-bind="click: $parent.deleteWidget">Delete</button>
</script>
Now, I want to invoke my-widget-list's deleteWidget function when the button is clicked.
I have thought about
Passing the parent view model reference into the child
Passing the parent function in the params attribute of the child component as a callback
But I wish to know from experts what's the best way to achieve this.
JsFiddle Link
Thanks in advance
You can pass in the parent as a param to the child:
ko.components.register('my-widget-list', {
viewModel : function(params) {
var self = this;
self.values = ko.observableArray([10,20,30,40,50]);
self.deleteWidget = function(obj) {
self.values.remove(obj);
}
},
template: {element: 'my-widget-list-template'}
});
ko.components.register('my-widget', {
viewModel : function(params) {
var self = this;
self.value = params.value;
self.remove = function () {
params.parent.deleteWidget(self.value);
};
},
template: {element: 'my-widget-template'}
});
ko.applyBindings({});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<my-widget-list></my-widget-list>
<script id="my-widget-list-template" type="text/html">
<div data-bind="foreach:values">
<my-widget params="value: $data, parent: $parent"></my-widget><br>
</div>
</script>
<script id="my-widget-template" type="text/html">
<span data-bind="text: value"></span>
<button data-bind="click: remove">Delete</button>
</script>
But I'm not sure if that is a good idea, as it needlessly couples the child to the parent.
I'd recommend implementing the "remove" button in the parent, i.e. in <my-widget-list>, this way the widget can exist without a widget-list (or in a differently structured one) while the widget-list is in control of its children.
Compare window managers: They work the same way. The window manager draws the frame and the minimize/maximize/close buttons, while the window contents is drawn by the respective child process. That logic makes sense in your scenario as well.
Alternative implementation with removeWidget control in the parent:
ko.components.register('my-widget-list', {
viewModel : function(params) {
var self = this;
self.values = ko.observableArray([10,20,30,40,50]);
self.deleteWidget = function(obj) {
self.values.remove(obj);
}
},
template: {element: 'my-widget-list-template'}
});
ko.components.register('my-widget', {
viewModel : function(params) {
var self = this;
self.value = params.value;
},
template: {element: 'my-widget-template'}
});
ko.applyBindings({});
.widget-container {
position: relative;
display: inline-block;
padding: 10px 5px 5px 5px;
margin: 0 5px 5px 0;
border: 1px solid silver;
border-radius: 2px;
min-width: 40px;
}
.widget-buttons {
position: absolute;
top: 2px;
right: 2px;
}
.widget-buttons > button {
font-size: 2px;
padding: 0;
height: 15px;
width: 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<my-widget-list></my-widget-list>
<script id="my-widget-list-template" type="text/html">
<div class="widget-list" data-bind="foreach:values">
<div class="widget-container">
<div class="widget-buttons">
<button data-bind="click: $parent.deleteWidget">X</button>
</div>
<my-widget params="value: $data"></my-widget>
</div>
</div>
</script>
<script id="my-widget-template" type="text/html">
<div class="widget">
<span data-bind="text: value"></span>
</div>
</script>
Related
I wan't to have a conditional css class and a dynamic css class added via the css binding.
Like so:
data-bind="css: {$data.something() : true, open : showOpen() }"
Clearest is probably to combine them in one computed:
function ViewModel() {
var self = this;
self.something = ko.observable("danger");
self.showOpen = ko.observable(true);
self.cssClass = ko.computed(function() {
return self.something() + (self.showOpen() ? " open" : "");
});
}
ko.applyBindings(new ViewModel());
div { padding: 10px; }
.danger { background-color: orange; }
.open { border: 5px solid gray; border-width: 5px 5px 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>
<div data-bind="css: cssClass"> my div with class: <code data-bind="text: cssClass"></code> </div>
<hr>
<label><input type="checkbox" data-bind="checked: showOpen"> showOpen</label>
<br>
<input type="text" data-bind="value: something, valueUpdate: 'afterkeydown'">
Allows you to unit test the entire thing, and keeps your view concise.
I prefer a custom binding like this:
ko.bindingHandlers.klass = {
init: function (el, val) {
var prevClass = null
ko.computed(function () {
if (prevClass)
$(el).removeClass(prevClass);
var newClass = ko.unwrap(val());
$(el).addClass(newClass);
prevClass = newClass;
}, null, {disposeWhenNodeIsRemoved: el})
}
}
var vmo = {
cssClass: ko.observable('a'),
toggle: function () { vmo.cssClass(vmo.cssClass() == 'a' ? 'b' : 'a') }
}
ko.applyBindings(vmo);
.a {
color: red;
}
.b {
color: blue;
}
.another {
text-decoration: underline;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<p data-bind='klass: cssClass, css: {another: true}'>Hello</p>
<button data-bind='click: toggle'>Toggle</button>
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>
The problem was actually caused by an extra space character between </div> closing tags. The example warns about that. In my case it was inserted automatically by the code formatter, so it went unnoticed. The line in the HTML template should be </div></div><!-- <---- NO SPACE BETWEEN THESE CLOSING TAGS --> with NO space between the </div></div>
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><!-- <---- NO SPACE BETWEEN THESE CLOSING TAGS -->
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 -->
https://jsfiddle.net/Ldv10oz5/2/
How would I set div to scroll dynamically if list bigger than div?
<div style="border: 1px solid black; width: 150px; height: 75px;">
<div data-bind="foreach: {data:teams}">
<div>
<span data-bind="text: name"></span>
</div>
</div>
</div>
function Team(name) {
var self = this;
self.name = name;
}
function AppViewModel() {
var self = this;
self.teams = ko.observableArray([
new Team('red'),
new Team('blue'),
new Team('yellow'),
new Team('green'),
new Team('orange'),
]);
}
var vm = new AppViewModel();
ko.applyBindings(vm);
Try to add to parent div (this with border) css:
overflow: auto;
I'm using knockoutjs to bind data and this is the first time I use knockoutjs, I have a list data show to table, when user scroll down to bottom, it's will be load more new data. Here is my code:
HTML:
<div data-bind="template: { name: 'product-template', foreach: listProduct }" id="data-list"></div>
<script type="text/html" id="product-template">
<table>
<tr>
<td data-bind="text:name"></td>
<td data-bind="text:description"></td>
</tr>
</table>
</script>
and JS here:
var product = {
get : function(pageNumber){
var self = this;
self.listProduct = ko.observableArray([]);
request.product(pageNumber, function(resp){
//response list data of product
//example: {"data":[{"name":"sony","desciption":"this is sony"},{"name": "toshiba","description": "this is toshiba"}]};
if(pageNumber>1){
self.listProduct.push(resp.data);
}else{
self.listProduct(resp.data);
}
})
}
}
and then I call function like this:
ko.applyBindings(new product.get(1), document.getElementById("data-list"));// it's success to bind data
and bind more data when I call scroll down to bottom event:
ko.applyBindings(new product.get(2), document.getElementById("data-list"));// I got error: Error You cannot apply bindings multiple times to the same element
There is any something wrong? thanks.
I think what you want to do is something like infinite scroll, so see the example, i think this will simplify what you doing.
var viewModel = {
items: ko.observableArray([]),
//this function always will be called when scroll event was trigered
scrolled: function(data, event) {
var elem = event.target;
if (elem.scrollTop > (elem.scrollHeight - elem.offsetHeight - 200)) {
getItems(20);
}
},
//you can use this like your page
maxId: 0
};
function getItems(cnt) {
//here you do the requst for the data
//create fake data to pass to echo service
for (var i = 0; i < cnt; i++) {
var id = viewModel.maxId++;
viewModel.items.push({
id: id,
name: "Name" + id
});
}
}
ko.applyBindings(viewModel);
getItems(20);
#main { height: 500px; width: 500px; overflow: scroll; }
#main div { background-color: #eee; margin: 5px; height: 100px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div>
<span data-bind="text: items().length"></span>
</div>
<div id="main" data-bind="foreach: items, event: { scroll: scrolled }">
<div data-bind="text: name"></div>
</div>
In the attached example I have a nested sortable that is capable of displaying tree structures.
The goal is to make the structure expand when new child is added to make the change visible.
A function automatically expands the structure when a new item is being added, but it only expands after adding 2nd child, it should expand immediately after adding 1st child.
Something is probably wrong with the template, or a simple jQuery+CSS trick could solve the problem, but I can't find the right one.
function Node(data) {
var self = this;
typeof data != 'undefined' ? self.id = data.id : self.id = '1';
self.parent = ko.observable();
self.children = ko.observableArray();
self.addNode = function() {
var child = new Node({
'id': self.id + '.' + (self.children().length + 1)
});
child.parent(self);
self.children.push(child);
return child;
}
};
var tree = new Node();
var child1 = tree.addNode();
var child2 = tree.addNode();
var viewModel = function() {
this.tree = ko.observable(tree);
this.addChild = function(node, event) {
var self = this;
node.addNode()
var $parent = $(event.target).parent().parent();
if ($parent.prop('tagName') == 'LI') {
if (!$parent
.hasClass('mjs-nestedSortable-expanded')) {
$parent
.addClass('mjs-nestedSortable-expanded');
}
if ($parent
.hasClass('mjs-nestedSortable-collapsed')) {
$parent
.removeClass('mjs-nestedSortable-collapsed');
}
}
}
};
ko.applyBindings(new viewModel());
$('.sortable')
.nestedSortable({
startCollapsed: true
});
ol.sortable,
ol.sortable ol {
margin: 0 0 0 25px;
padding: 0;
list-style-type: none;
}
ol.sortable {
margin: 4em 0;
}
.sortable li {
margin: 5px 0 0 0;
padding: 0;
}
.sortable li div {
border: 1px solid #d4d4d4;
cursor: move;
}
.sortable .disclose {
cursor: pointer;
width: 10px;
display: none;
}
.sortable li.mjs-nestedSortable-collapsed>ol {
display: none;
}
.sortable li.mjs-nestedSortable-branch>div>.disclose {
display: inline-block;
}
.sortable li.mjs-nestedSortable-collapsed>div>.disclose>span:before {
content: '+ ';
}
.sortable li.mjs-nestedSortable-expanded>div>.disclose>span:before {
content: '- ';
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.js"></script>
<script src="https://cdn.rawgit.com/furf/jquery-ui-touch-punch/master/jquery.ui.touch-punch.min.js"></script>
<script src="https://cdn.rawgit.com/mjsarfatti/nestedSortable/master/jquery.mjs.nestedSortable.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="dd" data-bind="template: {name:'nodeTemplate', data: tree}"></div>
<script id='nodeTemplate' type='text/html'>
<div>
<span class="disclose"><span></span></span>
<span data-bind="text: id"></span>
Add child
</div>
<ol class="sortable ui-sortable" data-bind="foreach: { data: children, as: 'child' }">
<!-- ko if: child.children().length > 0 -->
<li class="mjs-nestedSortable-collapsed mjs-nestedSortable-branch" data-bind="template: {name:'nodeTemplate', data: child}, attr: { 'data-id': child.id }">
</li>
<!-- /ko -->
<!-- ko if: child.children().length == 0 -->
<li class="mjs-nestedSortable-leaf" data-bind="template: {name:'nodeTemplate', data: child}, attr: { 'data-id': child.id}">
</li>
<!-- /ko -->
</ol>
</script>
I don't know why but in the first call of AddChild you lose reference to the the parent element. You can replace knockout code:
var $parent = $(event.target).parent().parent();
to jQuery workaround:
var $parent = $('.dd').find('*').filter(function() {
return $(this).text() === node.id;
}).parent().parent();
modified snippet:
function Node(data) {
var self = this;
typeof data != 'undefined' ? self.id = data.id : self.id = '1';
self.parent = ko.observable();
self.children = ko.observableArray();
self.addNode = function() {
var child = new Node({
'id': self.id + '.' + (self.children().length + 1)
});
child.parent(self);
self.children.push(child);
return child;
}
};
var tree = new Node();
var child1 = tree.addNode();
var child2 = tree.addNode();
var viewModel = function() {
this.tree = ko.observable(tree);
this.addChild = function(node, event) {
var self = this;
node.addNode()
var $parent = $('.dd').find('*').filter(function() {
return $(this).text() === node.id;
}).parent().parent();
if ($parent.prop('tagName') == 'LI') {
if (!$parent
.hasClass('mjs-nestedSortable-expanded')) {
$parent
.addClass('mjs-nestedSortable-expanded');
}
if ($parent
.hasClass('mjs-nestedSortable-collapsed')) {
$parent
.removeClass('mjs-nestedSortable-collapsed');
}
}
}
};
ko.applyBindings(new viewModel());
$('.sortable')
.nestedSortable({
startCollapsed: true
});
ol.sortable,
ol.sortable ol {
margin: 0 0 0 25px;
padding: 0;
list-style-type: none;
}
ol.sortable {
margin: 4em 0;
}
.sortable li {
margin: 5px 0 0 0;
padding: 0;
}
.sortable li div {
border: 1px solid #d4d4d4;
cursor: move;
}
.sortable .disclose {
cursor: pointer;
width: 10px;
display: none;
}
.sortable li.mjs-nestedSortable-collapsed>ol {
display: none;
}
.sortable li.mjs-nestedSortable-branch>div>.disclose {
display: inline-block;
}
.sortable li.mjs-nestedSortable-collapsed>div>.disclose>span:before {
content: '+ ';
}
.sortable li.mjs-nestedSortable-expanded>div>.disclose>span:before {
content: '- ';
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.js"></script>
<script src="https://cdn.rawgit.com/furf/jquery-ui-touch-punch/master/jquery.ui.touch-punch.min.js"></script>
<script src="https://cdn.rawgit.com/mjsarfatti/nestedSortable/master/jquery.mjs.nestedSortable.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="dd" data-bind="template: {name:'nodeTemplate', data: tree}"></div>
<script id='nodeTemplate' type='text/html'>
<div>
<span class="disclose"><span></span></span>
<span data-bind="text: id"></span>
Add child
</div>
<ol class="sortable ui-sortable" data-bind="foreach: { data: children, as: 'child' }">
<!-- ko if: child.children().length > 0 -->
<li class="mjs-nestedSortable-collapsed mjs-nestedSortable-branch" data-bind="template: {name:'nodeTemplate', data: child}, attr: { 'data-id': child.id }">
</li>
<!-- /ko -->
<!-- ko if: child.children().length == 0 -->
<li class="mjs-nestedSortable-leaf" data-bind="template: {name:'nodeTemplate', data: child}, attr: { 'data-id': child.id}">
</li>
<!-- /ko -->
</ol>
</script>