Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS - How to make a draggable tree?

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?

like image 270
kolrie Avatar asked Jan 12 '13 22:01

kolrie


3 Answers

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);
};
like image 104
furf Avatar answered Nov 15 '22 22:11

furf


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.

like image 33
michieljoris Avatar answered Nov 15 '22 22:11

michieljoris


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

like image 31
Jim Liu Avatar answered Nov 15 '22 20:11

Jim Liu