순수하게 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>