9

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?

kolrie
  • 12,562
  • 14
  • 64
  • 98

3 Answers3

20

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:

  1. the ezTree directive - to render the tree
  2. Manuele J Sarfatti's nestedSortable plugin for jQuery UI
  3. (optional) the uiNestedSortable directive - to enable nestedSortable from your template.
  4. controller code for updating your model - refer to $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);
};
furf
  • 2,689
  • 1
  • 18
  • 13
  • Wow, thanks for this! I will give it a shot and let you know. – kolrie Jan 31 '13 at 07:03
  • **FYI** - This doesn't work in IE as is... To make it work in IE10 change: `cursor = parentNode.childNodes[i];` **to be** `cursor = parentNode.childNodes[i]? parentNode.childNodes[i] : null;` Figured this out thanks to [this response](http://stackoverflow.com/questions/9377887/ie-doesnt-support-insertbefore) – JustMaier Sep 09 '13 at 21:44
  • 1
    Can you update this answer so it fits for the linked fiddle? Also please comment on the changes regarding nestedSortable which is missing from currently linked fiddle but is mentioned in this answer. – minder Oct 20 '13 at 20:54
6

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.

michieljoris
  • 621
  • 6
  • 4
  • raw.github.com/ETC files can be linked in via rawgithub.com/ETC (notice the removal of the dot) – Justin Obney Aug 07 '13 at 21:04
  • 1
    I updated the fiddle and linked the Nested Sortable plugin directly. Hopefully it works in IE still. I also added the use of isAllowed to implement conditional drag and drop. See also [fiddle](http://jsfiddle.net/michieljoris/PdKc7/2/) for another way to set isAllowed – michieljoris Aug 08 '13 at 15:14
  • 1
    I thought it might be nice to give this a home at Github, since it's pretty general-purpose and improvements continue: https://github.com/nelsonblaha/draggable-tree – blaha Jan 16 '14 at 18:06
6

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

Peter Butkovic
  • 11,143
  • 10
  • 57
  • 81
Jim Liu
  • 372
  • 3
  • 6