순수하게 AngularJS만을 사용해서 트리메뉴를 구현해 보았다.
앵귤러의 특징을 활용해 지시어(directive)로 <eu-tree> 라는 태그를 넣으면 자동으로 해당 위치에 트리 메뉴가 생성되도록 하였고, 트리 데이터의 id값, name값 등에 해당하는 내용을 속성으로 입력받을 수 있도록 하여 약간의 유동성을 주었다.
컨트롤러에서 트리 모델의 값을 바꾸면 특별한 처리를 하지 않아도 모델의 값을 watch하고 있다가 바로 실제 트리도 바뀐다.
단순히 컨트롤러에서는 트리 모델을 $scope 상에 할당하는 것으로 트리를 사용할 수 있다.
아직 완전하게 모듈화가 되진 않았지만, 어느정도 실전에서 사용 가능한 형태라고 생각되어 공개한다.
서비스 형태로 eu-tree에 트리 모델을 할당하고 모델에 값을 추가하거나, 삭제하거나, 수정하는 메서드를 추가하는 방안을 고려해보고 있다.
메서드 자체는 이미 완성해두었지만, 아직 모듈화를 하지 않았다.
크롬, 파이어폭스, 오페라, 사파리에서의 동작을 확인했다.
jsFiddle
http://jsfiddle.net/eu81273/qCEjm/
tree.js 파일
/*
* Copyright (c) <2012> <AHN JAE-HA>
*
* version 0.0.1
* angularJS를 사용해서 Tree Menu 구현
*
*/
//지시어 선언
//지시어는 카멜 케이스로 네이밍한다.
angular.module('euTree.directive', [])
.directive('treeElement', function($compile)
{
return {
restrict: 'E', //Element(태그)
link: function (scope, element, attrs)
{
//작업 노드를 현재 노드로 갱신
scope.tree = scope.node;
//하부 요소 숨기기|보이기 설정
var visibility = ( attrs.nodeState != "collapse" ) || 'style="display: none;"';
//하위 요소가 존재할 경우
if( scope.tree.children.length )
{
console.log('[아이콘 클래스 설정]');
console.log(scope.tree.children)
for(var i in scope.tree.children)
{
//하위 요소가 존재하면,
if( scope.tree.children[i].children.length )
{
scope.tree.children[i].className = "eu_" + attrs.nodeState + " eu_deselected";
}
else
{
scope.tree.children[i].className = "eu_child" + " eu_deselected";
}
}
//하위 요소 등록
//1단계 : 임의의 HTML 내용을 적용시키기 위해 먼저 HTML을 DOM 요소로 파싱한다.
var template = angular.element('<ul ' + visibility + '><li ng-repeat="node in tree.children" node-id={{node.' + attrs.nodeId + '}} ng-class="node.className">{{node.' + attrs.nodeName + '}}<tree-element tree="node" node-id=' + attrs.nodeId + ' node-name=' + attrs.nodeName + ' node-state=' + attrs.nodeState + '></tree-element></li></ul>');
//2단계: 템플릿을 컴파일한다.
var linkFunction = $compile(template);
//3단계: 스코프를 컴파일한 템플릿과 연결한다.
linkFunction(scope);
//4단계: HTML 요소를 반영한다.
element.replaceWith( template );
}
else
{
//하위 요소가 없으면 제거
element.remove();
}
}
};
})
.directive('euTree', function($compile)
{
return {
restrict: 'E', //Element(태그)
link: function (scope, element, attrs)
{
//선택된 노드
scope.selectedNode = null;
//TREE를 위한 CSS 적용
var sheet = document.createElement('style')
sheet.innerHTML =
"eu-tree ul{margin:0;padding:0;list-style:none;border:none;overflow:hidden;text-decoration:none;color:#555}" +
"eu-tree li{position:relative;padding:0 0 0 20px;font-size:13px;font-weight:initial;line-height:18px;cursor:pointer}" +
"eu-tree .eu_expand{background:url(" + attrs.expandIcon + ") no-repeat}" +
"eu-tree .eu_collapse{background:url(" + attrs.collapseIcon + ") no-repeat}" +
"eu-tree .eu_child{background:url(" + attrs.childIcon + ") no-repeat}" +
"eu-tree .eu_selected{font-weight:bold;}" +
"eu-tree .hide{display:none;}" +
"eu-tree .eu_deselected{font-weight:normal;}";
document.body.appendChild(sheet);
scope.$watch( attrs.treeData, function(val)
{
console.log('[트리 데이터 변함]');
console.log(scope[attrs.treeData]);
console.log('[아이콘 클래스 설정]');
for(var i in scope[attrs.treeData])
{
//하위 요소가 존재하면,
if( scope[attrs.treeData][i].children.length )
{
scope[attrs.treeData][i].className = "eu_" + attrs.nodeState + " eu_deselected";
}
else
{
scope[attrs.treeData][i].className = "eu_child" + " eu_deselected";
}
}
//1차 요소 설정
console.log('[1차 요소 설정]');
//1단계 : 임의의 HTML 내용을 적용시키기 위해 먼저 HTML을 DOM 요소로 파싱한다.
var template = angular.element('<ul id="euTreeBrowser" class="filetree treeview-famfamfam treeview"><li ng-repeat="node in ' + attrs.treeData + '" node-id={{node.' + attrs.nodeId + '}} ng-class="node.className">{{node.' + attrs.nodeName + '}}<tree-element tree="node" node-id=' + attrs.nodeId + ' node-name=' + attrs.nodeName + ' node-state=' + attrs.nodeState + '></tree-element></li></ul>');
//2단계: 템플릿을 컴파일한다.
var linkFunction = $compile(template);
//3단계: 스코프를 컴파일한 템플릿과 연결한다.
linkFunction(scope);
//4단계: HTML 요소를 반영한다.
element.html(null).append( template );
//노드 클릭 이벤트 설정
console.log('[노드 클릭 이벤트 설정]');
angular.element(document.getElementById('euTreeBrowser')).unbind().bind('click', function(e)
{
console.log(e.target);
//서브 메뉴가 정확히 선택된 경우에만!
if(angular.element(e.target).length)
{
//이전 요소
scope.previousElement = scope.currentElement;
//현재 요소
scope.currentElement = angular.element(e.target);
console.log('[선택한 노드]');
console.log(scope.currentElement);
//선택한 노드 브로드캐스팅
scope.$broadcast('nodeSelected', { selectedNode: scope.currentElement.attr('node-id') });
//이전 선택 노드 선택 제거
if( scope.previousElement )
{
scope.previousElement.addClass("eu_deselected").removeClass("eu_selected");
}
//현재 노드 선택
scope.currentElement.addClass("eu_selected").removeClass("eu_deselected");
//자식 노드가 있으면,
if( scope.currentElement.children().length )
{
//자식 노드 토글
scope.currentElement.children().toggleClass("hide");
//$(e.target).children().slideToggle("fast");
//아이콘 토글
scope.currentElement.toggleClass("eu_collapse");
scope.currentElement.toggleClass("eu_expand");
}
}
});
}, true ); //true - 실제 값의 변화를 추적 | false - 주소값의 변화를 추적
}
};
}).factory('euTreeService', function()
{
return {
};
});
eu-tree를 적용한 treeExam.html 파일
<html ng-app="treeExam" ng-controller="treeController" >
<head>
<meta charset="utf-8">
<title>Directive Example</title>
<script src="http://code.angularjs.org/1.1.0/angular.min.js" type="text/javascript" charset="utf-8"></script>
<script src="js/eu-tree.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
//어플리케이션 모듈을 선언하고, Directive를 인젝션한다.
var directiveExam = angular.module('treeExam', ['euTree.directive']);
directiveExam.controller('treeController', ['$scope', function($scope)
{
//roleList에 tree 데이터 할당
$scope.roleList =
[
{
"roleName" : "사용자",
"roleId" : "role1",
"children" :
[
{
"roleName" : "하부사용자1",
"roleId" : "role11",
"children" : []
},
{
"roleName" : "하부사용자2",
"roleId" : "role12",
"children" :
[
{
"roleName" : "하부사용자2의 하부",
"roleId" : "role121",
"children" :
[
{
"roleName" : "하부사용자2의 하부의 하부1",
"roleId" : "role1211",
"children" : []
},
{
"roleName" : "하부사용자2의 하부의 하부2",
"roleId" : "role1212",
"children" : []
}
]
}
]
}
]
},
{
"roleName" : "관리자",
"roleId" : "role2",
"children" : []
}
];
}]);
</script>
</head>
<body>
<!--
[TREE attribute]
tree-data : the tree model on $scope (ex: "$scope.roleList" in this html file)
node-id : each node's id
node-name : each node's name
node-state : expand/collapse - node's default state
expand-icon, collapse-icon, child-icon : icon image file URL
** 트리 요소를 선택할 때마다 'nodeSelected' 라는 이름으로 브로드캐스트하므로,
** 'nodeSelected' 라는 이름으로 $on으로 브로드캐스트를 받아서 args의
** selectedNode의 'node-id' 값을 통해 필요한 처리를 해줄 수 있다.
**
-->
<eu-tree
tree-data="roleList"
node-id="roleId"
node-name="roleName"
node-state="expand"
expand-icon="img/folder.png"
collapse-icon="img/folder-closed.png"
child-icon="img/file.png" ></eu-tree>
</body>
</html>