Last active
July 21, 2016 06:36
-
-
Save furf/4331090 to your computer and use it in GitHub Desktop.
A minimal recursive tree directive for Angular.js. Demo: http://jsfiddle.net/furf/EJGHX/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var app = angular.module('app', []); | |
app.directive('yaTree', function () { | |
return { | |
restrict: 'A', | |
transclude: 'element', | |
priority: 1000, | |
terminal: true, | |
compile: function (tElement, tAttrs, transclude) { | |
var repeatExpr, childExpr, rootExpr, childrenExpr; | |
repeatExpr = tAttrs.yaTree.match(/^(.*) in ((?:.*\.)?(.*)) at (.*)$/); | |
childExpr = repeatExpr[1]; | |
rootExpr = repeatExpr[2]; | |
childrenExpr = repeatExpr[3]; | |
branchExpr = repeatExpr[4]; | |
return function link (scope, element, attrs) { | |
var rootElement = element[0].parentNode, | |
cache = []; | |
// Reverse lookup object to avoid re-rendering elements | |
function lookup (child) { | |
var i = cache.length; | |
while (i--) { | |
if (cache[i].scope[childExpr] === child) { | |
return cache.splice(i, 1)[0]; | |
} | |
} | |
} | |
scope.$watch(rootExpr, function (root) { | |
var currentCache = []; | |
// Recurse the data structure | |
(function walk (children, parentNode, parentScope, depth) { | |
var i = 0, | |
n = children.length, | |
last = n - 1, | |
cursor, | |
child, | |
cached, | |
childScope, | |
grandchildren; | |
// Iterate the children at the current level | |
for (; i < n; ++i) { | |
// We will compare the cached element to the element in | |
// at the destination index. If it does not match, then | |
// the cached element is being moved into this position. | |
cursor = parentNode.childNodes[i]; | |
child = children[i]; | |
// See if this child has been previously rendered | |
// using a reverse lookup by object reference | |
cached = lookup(child); | |
// If the parentScope no longer matches, we've moved. | |
// We'll have to transclude again so that scopes | |
// and controllers are properly inherited | |
if (cached && cached.parentScope !== parentScope) { | |
cache.push(cached); | |
cached = null; | |
} | |
// If it has not, render a new element and prepare its scope | |
// We also cache a reference to its branch node which will | |
// be used as the parentNode in the next level of recursion | |
if (!cached) { | |
transclude(parentScope.$new(), function (clone, childScope) { | |
childScope[childExpr] = child; | |
cached = { | |
scope: childScope, | |
parentScope: parentScope, | |
element: clone[0], | |
branch: clone.find(branchExpr)[0] | |
}; | |
// This had to happen during transclusion so inherited | |
// controllers, among other things, work properly | |
parentNode.insertBefore(cached.element, cursor); | |
}); | |
} else if (cached.element !== cursor) { | |
parentNode.insertBefore(cached.element, cursor); | |
} | |
// Lets's set some scope values | |
childScope = cached.scope; | |
// Store the current depth on the scope in case you want | |
// to use it (for good or evil, no judgment). | |
childScope.$depth = depth; | |
// Emulate some ng-repeat values | |
childScope.$index = i; | |
childScope.$first = (i === 0); | |
childScope.$last = (i === last); | |
childScope.$middle = !(childScope.$first || childScope.$last); | |
// Push the object onto the new cache which will replace | |
// the old cache at the end of the walk. | |
currentCache.push(cached); | |
// If the child has children of its own, recurse 'em. | |
grandchildren = child[childrenExpr]; | |
if (grandchildren && grandchildren.length) { | |
walk(grandchildren, cached.branch, childScope, depth + 1); | |
} | |
} | |
})(root, rootElement, scope, 0); | |
// Cleanup objects which have been removed. | |
// Remove DOM elements and destroy scopes to prevent memory leaks. | |
i = cache.length; | |
while (i--) { | |
cached = cache[i]; | |
if (cached.scope) { | |
cached.scope.$destroy(); | |
} | |
if (cached.element) { | |
cached.element.parentNode.removeChild(cached.element); | |
} | |
} | |
// Replace previous cache. | |
cache = currentCache; | |
}, true); | |
}; | |
} | |
}; | |
}); | |
app.controller('TreeController', function ($scope, $timeout) { | |
$scope.json = ''; | |
$scope.data = { | |
children: [{ | |
title: 'hello, world', | |
children: [{ | |
title: 'test 1', | |
children: [] | |
}, | |
{ | |
title: 'test 2', | |
children: [] | |
}] | |
}] | |
}; | |
$scope.getJson = function () { | |
$scope.json = angular.toJson($scope.data); | |
}; | |
$scope.toggleMinimized = function (child) { | |
child.minimized = !child.minimized; | |
}; | |
$scope.addChild = function (child) { | |
child.children.push({ | |
title: '', | |
children: [] | |
}); | |
}; | |
$scope.remove = function (child) { | |
function walk(target) { | |
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]) | |
} | |
} | |
} | |
} | |
walk($scope.data); | |
} | |
$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); | |
}; | |
}); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ng-cloak] { | |
display: none; | |
} | |
* { | |
box-sizing: border-box | |
} | |
body { | |
font: 12px/16px helvetica, arial, sans-serif; | |
background: #eee; | |
} | |
ol { | |
margin:4px 0; | |
border: 0 none transparent; | |
padding: 0; | |
background:#eee; | |
} | |
li { | |
list-style:none; | |
margin: 1px; | |
border: 1px solid #ddd; | |
padding: 8px 8px 4px 8px; | |
background: #fff; | |
} | |
.ui-sortable li { | |
cursor: move; | |
} | |
.live input, .shadow input { | |
margin: 0; | |
border: 1px solid #ddd; | |
padding: 1px; | |
font: inherit; | |
font-weight: bold; | |
height:2em; | |
} | |
.ui-state-highlight { | |
border: 1px solid #fc0; | |
background: #ffe; | |
} | |
.live, .shadow { | |
float: left; | |
width: 45%; | |
margin-right: 5%; | |
} | |
.shadow { | |
opacity: 0.7; | |
} | |
.debug { | |
clear: both; | |
} | |
.debug textarea { | |
width: 100%; | |
height: 40em; | |
font: 10px/13px monospace; | |
} | |
textarea, input { | |
outline: none; | |
} | |
.pregnant { | |
border: 1px solid #ddd; | |
} | |
.bg0 { background:#fcc; } | |
.bg1 { background:#ffc; } | |
.bg2 { background:#cfc; } | |
.bg3 { background:#cff; } | |
.bg4 { background:#ccf; } | |
.bg5 { background:#fcf; } | |
.minimized > ol > li { display:none; } | |
.minimized > ol { border: 0 none transparent; } | |
.toggle { border: 0 none transparent; background:transparent; width:2em; color:#aaa; } | |
button { cursor: pointer } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script type='text/javascript' src='//code.jquery.com/jquery-1.9.1.js'></script> | |
<script type="text/javascript" src="http://code.jquery.com/ui/1.9.2/jquery-ui.js"></script> | |
<link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.9.2/themes/base/jquery-ui.css"> | |
<script type='text/javascript' src="http://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.2/jquery.ui.touch-punch.min.js"></script> | |
<script type='text/javascript' src="https://raw.github.com/mjsarfatti/nestedSortable/master/jquery.mjs.nestedSortable.js"></script> | |
<script type='text/javascript' src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.1/angular.min.js"></script> | |
<div ng-app="app" ng-cloak> | |
<div ng-controller="TreeController"> | |
<div> | |
<button ng-click="addChild(data)">+ New</button> | |
</div> | |
<div class="live"> | |
<ol> | |
<li ya-tree="child in data.children at ol" ng-class="{minimized:child.minimized}"> | |
<div> | |
<button class="toggle" ng-click="toggleMinimized(child)" ng-switch on="child.minimized"><span ng-switch-when="true">▶</span><span ng-switch-default>▼</span></button> | |
<input ng-model="child.title" /> | |
<button ng-click="addChild(child)">+</button> | |
<button ng-click="remove(child)">x</button> | |
</div> | |
<ol ng-class="{pregnant:child.children.length}"></ol> | |
</li> | |
</ol> | |
</div> | |
<div class="shadow"> | |
<ol> | |
<li ya-tree="child in data.children at ol" class="bg{{$depth%6}}" ng-class="{minimized:child.minimized}"> | |
<div> | |
<input disabled value="{{child.title}}" /> <em>({{$depth}})</em> | |
</div> | |
<ol ng-class="{pregnant:child.children.length}"></ol> | |
</li> | |
</ol> | |
</div> | |
<div><button ng-click="getJson()">Json</button></div> | |
<div>{{json}}</div> | |
</div> | |
</div> |
Cool approach, just saw a later version that synchronizes with the model. very cool stuff.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I saw example http://jsfiddle.net/furf/R2eMe/ It is cool! But I can't to do synchronization with model. Help me.