toggle menu

[AngularJS] directive의 재귀호출을 활용해 구현한 트리 메뉴

2012.11.20 19:26 AngularJS

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







AngularJS 관련 포스팅 더보기