I want to create a tree like structure where the user can drag and drop leaves. I have a starting point as follows:
HTML
<div ng:controller="controller">
<ul ui-sortable ng-model="items" ui-options="{connectWith: '.item'}" class="item">
<li ng-repeat="item in items" class="item">
{{ item.name }}
<ul ui-sortable ng-model="item.children" ui-options="{connectWith: '.item'}" class="item">
<li ng-repeat="item in item.children" class="item">{{ item.name }}</li>
</ul>
</li>
</ul>
<pre>{{ items | json }}</pre>
</div>
<script src="http://code.angularjs.org/1.0.2/angular.min.js"></script>
<script src="https://raw.github.com/angular-ui/angular-ui/master/build/angular-ui.min.js"></script>
CoffeeScript
myapp = angular.module 'myapp', ['ui']
myapp.controller 'controller', ($scope) ->
$scope.items = [
{id: 1, name: 'Item 1', children: [
{id: 5, name: 'SubItem 1.1', children: [
{id: 11, name: 'SubItem 1.1.1', children: []},
{id: 12, name: 'SubItem 1.1.2', children: []}
]},
{id: 6, name: 'SubItem 1.2', children: []}
]},
{id: 2, name: 'Item 2', children: [
{id: 7, name: 'SubItem 2.1', children: []},
{id: 8, name: 'SubItem 2.2', children: []}
{id: 9, name: 'SubItem 2.3', children: []}
]},
{id: 3, name: 'Item 3', children: [
{id: 10, name: 'SubItem 3.1', children: []}
]}
]
angular.bootstrap document, ['myapp']
The code is in this JSFiddle as well: http://jsfiddle.net/bESrf/1/
On my "real" code, instead of only having one level for children, I extracted the second <ul>
into a template and rendered it recursively, which works fine, but I couldn't find a way to do it in JSFiddle.
What would be the best way to render it recursively and still allow dragging and dropping that would change the array of objects and sub-objects represented by ng-model?
Take a look at this example: http://jsfiddle.net/furf/EJGHX/
I just completed this solution so it is not yet properly documented, but you should be able to mine it for your solution.
You will need to use a few things:
ezTree
directive - to render the treeuiNestedSortable
directive - to enable nestedSortable from your template. $scope.update
Using the ezTree
directive
Given a recursive data structure:
$scope.data = {
children: [{
text: 'I want to create a tree like structure...',
children: [{
text: 'Take a look at this example...',
children: []
}]
}]
};
This template will build the tree:
<ol>
<li ez-tree="child in data.children at ol">
<div>{{item.text}}</div>
<ol></ol>
</li>
</ol>
The ez-tree
expression should be written as item in collection at selector
where item
is the iterated child (ala ng-repeat
), collection
is the root-level collection, and selector
is the CSS selector for the node inside the template where the directive should recurse. The name of the terminal property of the collection, in this case children
will be used to recurse the tree, in this case child.children
. This could be rewritten to be configurable but I'll leave that as an exercise for the reader.
Using uiNestedSortable
directive
<ol ui-nested-sortable="{ listType: 'ol', items: 'li', doNotClear: true }"
ui-nested-sortable-stop="update($event, $ui)">
</ol>
The ui-nested-sortable
attribute should contain a JSON configuration for the nestedSortable plugin. The plugin requires that you specify listType
and items
. My solution requires that doNotClear
be true
. Assign callbacks to events using ui-nested-sortable-*eventName*
. My directive supplies optional $event and $ui arguments to callbacks. Refer to nestedSortable's documentation for other options.
Updating your model
There is more than one way to skin this cat. Here's mine. On the stop event, it extracts the child property of the element's scope to determine which object was moved, the child property of the element's parent's scope to determine the destination of the object, and the position of the element to determine the position of the object at its destination. It then walks the data structure and removes the object from its original position and inserts it into its new position.
$scope.update = function (event, ui) {
var root = event.target,
item = ui.item,
parent = item.parent(),
target = (parent[0] === root) ? $scope.data : parent.scope().child,
child = item.scope().child,
index = item.index();
target.children || (target.children = []);
function walk(target, child) {
var children = target.children,
i;
if (children) {
i = children.length;
while (i--) {
if (children[i] === child) {
return children.splice(i, 1);
} else {
walk(children[i], child)
}
}
}
}
walk($scope.data, child);
target.children.splice(index, 0, child);
};
Slight edit of the fiddle by furf to make it work in IE.
IE gives an error on insertNode when the second argument is null, so when this is the case appendNode is used instead.
http://jsfiddle.net/michieljoris/VmtfR/
if (!cursor) parentNode.appendChild(cached.element);
else parentNode.insertBefore(cached.element, cursor);
The Nested Sortable plugin is inlined in the js, because IE gives a MIME type mismatch when included from github.
Try Angular-NestedSortable, it's an Angularjs plugin that can sort nested lists and bind data, and doesn't need to depend on jQuery. https://github.com/jimliu/Angular-NestedSortable
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With