Angular는 지시자(Directive)를 통하여 DOM을 확장한다. 지시자에 대해 자세히 알아보자
특징
- Camel cased name 을 갖는다. 예) ngBind
- 변환될 때는 Snake case - 으로 변경된다 예) ng-bind
- 지시자는 element name, attribute, class name, comment 에 올 수 있다
- <span my-dir="exp"></span>
- <span class="my-dir: exp;"></span>
- <my-dir></my-dir>
- <!-- directive:my-dir exp -->
- compiler는 $interpolate 서비스를 이용해서 text와 attribute를 매칭시킨다. 표현이 watch에 등록되어 있고, digest 싸이클의 일부로서 업데이트 되는 것이다
<a href="img/{{username}}.jpg">Hello {{username}}!</a>
- HTML에서 이루어지는 3 스텝
+ Step 1: 최초에 Angular의 template은 HTML이므로 표준 브라우져 API를 사용하여 DOM 으로 파싱된다. 다른 템플릿 시스템과 차별점이기도 하다 (참조)
+ Step 2: $compile 메소드를 통해서 DOM을 해석하면서 지시자(directive)를 찾는다. 지시자는 $compile() 펑션이 수행되면서 DOM 구조를 변경하고 응답을 위하여 link() 펑션으로 return 한다
+ Step 3: 각 지시자별로 link() 를 호출하여 지시자별 listener등록과 scope에 대한 watch를 설정한다. 이에따라 scope와 DOM 사이에 Live Binding이 되는 것이다
- var $compile = ...; // injected into your code
- var scope = ...;
- var html = '<div ng-bind="exp"></div>';
- // Step 1: parse HTML into DOM element
- var template = angular.element(html);
- // Step 2: compile the template
- var linkFn = $compile(template);
- // Step 3: link the compiled template with the scope.
- linkFn(scope);
- 왜 compile과 link를 나눈거지?
+ model이 변경되면 DOM이 변경되어야 한다. 예) ng-repeat의 경우 model 목록이 늘면 DOM도 변경됨
+ {{user}}는 interpolation(값을 채우는) 지시자 이지만 ng-repeat은 ngRepeat 지시자이다. 여기서의 딜레마는 모델 데이터가 늘어나면 li 앨리먼트를 복제하여 compile과정을 또 거쳐야 하는데 그렇게 하면 성능이 느려진다. 이에 대한 해결책은 과정을 두개로 나누는 것이다. 그래서 ngRepeat 지시자를 해석하고 linking function 을 얻는다. ngRepeat 은 표현이 변경되면 li 엘리먼트를 복제하여 배열에 추가하고 새로운 scope 생성하여 복제한 li 엘러먼트에 할당하고 복제한 li 의 linking function 을 호출한다
- Hello {{user}}, you have these actions:
- <ul>
- <li ng-repeat="action in user.actions">
- {{action.description}}
- </li>
- </ul>
- 정리
+ compile function : 지시자에 따라 template DOM 엘리먼트를 변환하여 특별한 DOM 엘리먼트를 만들어 준다
+ link function : 지시자가 복제된 DOM 엘리먼트에 listener 등록하도록 해준다
지시자 만들기
- 전체 내역
- var myModule = angular.module(...);
- myModule.directive('directiveName', function factory(injectables) {
- var directiveDefinitionObject = {
- priority: 0,
- template: '<div></div>',
- templateUrl: 'directive.html',
- replace: false,
- transclude: false,
- restrict: 'A',
- scope: false,
- compile: function compile(tElement, tAttrs, transclude) {
- return {
- pre: function preLink(scope, iElement, iAttrs, controller) { ... },
- post: function postLink(scope, iElement, iAttrs, controller) { ... }
- }
- },
- link: function postLink(scope, iElement, iAttrs) { ... }
- };
- return directiveDefinitionObject;
- });
- 기본 내역 : 없은 것은 기본값을 사용한다
- var myModule = angular.module(...);
- myModule.directive('directiveName', function factory(injectables) {
- var directiveDefinitionObject = {
- compile: function compile(tElement, tAttrs) {
- return function postLink(scope, iElement, iAttrs) { ... }
- }
- };
- return directiveDefinitionObject;
- });
- 간단 내역 : 지시자가 템플릿 변환이 없고 자신의 인스턴스에 대한 것만 관련 있을 때 사용함
- var myModule = angular.module(...);
- myModule.directive('directiveName', function factory(injectables) {
- return function postLink(scope, iElement, iAttrs) { ... }
- });
- Factory method : 지시자를 생성하는 역할이고, compiler가 지시자를 찾을 때 최초 호출하여 초기화 한다. $injector.invoke 를 사용한다
- 지시자 정의하기
+ 지시자 정의는 compiler 에게 이렇게 이렇게 해달라는 설명을 제공하는 과정이다
+ name : 현재 scope의 명칭 (optional)
+ priority : compile 펑션의 호출 우선 순위를 지정한다
+ terminal : true ?
+ scope : true 이면 새로운 scope 생성, {} 새로운 격리된 scope 생성으로 부모 scope 상속이 없다. 새로운 컴포넌트 만들 때 유용한다
{} 로 만들때 안의 내용들
> @ : local scope property
> = : bi-directional binding between local property and the parent scope property
> & : parent scope 에서 execute expression 방법을 제공
///////////////
// @ 사용하기
/*local scope property html<div ng-app="drinkApp"><div ng-controller="AppCtrl"><div drink flavor="strawberry"></div></div></div>*/var app = angular.module("drinkApp", []);app.controller("AppCtrl", function($scope) {$scope.ctrlFlavor = "blackberry";})app.directive("drink", function() {return {scope: {flavor: "@"},template: '<div>{{flavor}}</div>'/*scope: {flavor: "@"} 표현은 link: function() {...} 과 완전히 동일하다link: function(scope, element, attrs) {scope.flavor = attrs.flavor;}*/}})///////////////
// = 사용하기 (소스)
/*flavor="{{ctrlFlavor}}" 를 사용하지 않음에 주의한다<div ng-app="drinkApp"><div ng-controller="AppCtrl">Ctrl<input type="text" ng-model="ctrlFlavor">Dir<div drink flavor="ctrlFlavor"></div></div></div>*/var app = angular.module("drinkApp", []);app.controller("AppCtrl", function($scope) {// parent property$scope.ctrlFlavor = "blackberry";})app.directive("drink", function() {return {scope: {flavor: "="},// local propertytemplate: '<input type="text" ng-model="flavor"> '}})///////////////
// & 사용하기 (소스)
/*message를 controller parent scope로 전달 이는 scope가 isolated되어 있어도 상위로 값을 전달 할 수 있게 한다<div ng-app="phoneApp"><div ng-controller="AppCtrl"><div phone dial="callHome(message)"></div><div phone dial="callHome(message)"></div></div></div>*/var app = angular.module("phoneApp", []);app.controller("AppCtrl", function($scope) {$scope.callHome = function(message) {alert(message);}})app.directive("phone", function() {return {scope: {dial: "&"},// {message:value} 객체를 통하여 contrller scope의 callHome에 전달template: '<input type="text" ng-model="value">' +'<div class="button" ng-click="dial({message:value})">Call Home</div> '}})/////////////////////////
// @, =, & 종합 사용하기 (소스)
/*@, &, = integration scope<div ng-app="phoneApp"><div ng-controller="AppCtrl"><phone number="1111111" network="network" make-call="leaveVoicemail(number, message)"></phone><phone number="2222222" network="network" make-call="leaveVoicemail(number, message)"></phone><phone number="3333333" network="network" make-call="leaveVoicemail(number, message)"></phone></div></div>*/var app = angular.module("phoneApp", []);app.controller("AppCtrl", function($scope) {$scope.leaveVoicemail = function(number, message) {alert("Number: " + number + " said: " + message);}})app.directive("phone", function() {return {restrict: "E",scope: {number: "@", // local scope propertynetwork: "=", // bi-directional binding between local property and parent propertymakeCall: "&" // send expressions to parent scope},template: '<div class="panel">Number: {{number}} Network:<select ng-model="network"ng-options="network for network in networks">' +'</select></div><input type="text" ng-model="value">' +'<div class="button" ng-click="makeCall({number: number, message:value})">Call Home</div> ',link: function(scope) {scope.networks = ["KT", "SKT", "LGU+"];scope.network = scope.networks[0];}}})
+ controller : pre-linking 전에 호출되어서 지시자들끼리 공유를 할려고 할때 사용
+ require : 다른 controller에 현재 지시자의 linking function을 전달 할때 사용용
+ restrict : 지시자 선언 종류
E
- Element name:<my-directive></my-directive>
A
- Attribute:<div my-directive="exp"> </div>
C
- Class:<div class="my-directive: exp;"></div>
M
- Comment:<!-- directive: my-directive exp -->
+ template : HTML file
+ templateUrl : 로딩할 URL 지정
+ replace : true 이면 현재 엘리먼트를 append가 아닌 replace 한다
+ transclude : ngTransclude 엘러먼트로 컴파일 및 지시자로 쓰임
+ compile : compile function 정의
+ link : link function 정의
- Compile function
+ template DOM을 변환하는 역할이다
+ 사용자 정의할 때만 사용하면 되고 기존 지시자에는 사용할 일이 없겠다
function compile(tElement, tAttrs, transclude) { ... }
+ tElement : template element
+ tAttrs : template attribute
+ transclude : transclude linking function: function(scope, cloneLinkingFn)
+ return 값을 두가지로 줄 수 있다
> function 형태로 리턴 : linking function 으로 등록하는 것과 동일함
> object 형태로 리턴 : pre-linking, post-linking을 제어하고 싶을 때 사용한다
- Linking function
+ DOM 업데이트를 위하여 DOM listener를 등록하는 역할이다
+ template 이 복제된 후에 수행되고 지시자의 대부분 로직이 놓인다
function link(scope, iElement, iAttrs, controller) { ... }
+ scope : watch 등록할 때 지시자에 의해서 사용 됨
+ iElement : instance element.
+ iAttrs : instance attribute
+ controller : controller instance
+ pre-linking function : child element가 link 되기전에 호출
post-linking function : child element link 된 후에 호출
- Attribute Object
+ compile() 과 link() 에 파라미터로로 전달되는 객체
+ 값의 변화를 $observe 할 수도 있고 $set하거나 .XXX 로 읽을 수도 있다
- function linkingFn(scope, elm, attrs, ctrl) {
- // get the attribute value
- console.log(attrs.ngModel);
- // change the attribute
- attrs.$set('ngModel', 'new value');
- // observe changes to interpolated attribute
- attrs.$observe('ngModel', function(value) {
- console.log('ngModel has changed value to ' + value);
- });
- }
Transclusion 과 Scope 이해하기
- 재사용 컴포넌트를 만들고자 하는 다음의 경우를 생각해 보자
+ show 버튼 클릭하면 다이얼로그 박스가 열린다
+ 다이얼로그 박스는 제목이 있고,
- <div>
- <button ng-click="show=true">show</button>
- <dialog title="Hello {{username}}."
- visible="show"
- on-cancel="show = false"
- on-ok="show = false; doSomething()">
- Body goes here: {{username}} is {{title}}.
- </dialog>
- </div>
+ dialog 위젯이 변환되면 아마 하기와 같이 나올것이다
> username이 들어간 title을 넣어 줘야 한다
> onOk, onCancel 버튼이 동작해야 한다
> scope 에서 local 변수에 대한 맵핑작업이 필요하다
- <div ng-show="visible">
- <h3>{{title}}</h3>
- <div class="body" ng-transclude></div>
- <div class="footer">
- <button ng-click="onOk()">Save changes</button>
- <button ng-click="onCancel()">Close</button>
- </div>
- </div>
+ scope 정의
> transclude DOM 은 다이얼로그 위젯의 격리된 scope의 child 가 되어서 어느 것과도 binding되지 않고 영향을 미지치치 않고 title에 값을 맵핑할 수 있다. 즉, transclude scope는 original scope의 자식이 되기 때문이다 (아직 이해 실패^^;)
- transclude: true,
- scope: {
- title: '@', // the title uses the data-binding from the parent scope
- onOk: '&', // create a delegate onOk function
- onCancel: '&', // create a delegate onCancel function
- visible: '=' // set up visible to accept data-binding
- },
- restrict: 'E',
- replace: true
- Transclude 옵션을 설정하는 간단한 예제 (소스)
/*Simple transclude sample dom<div ng-app="phoneApp"><div ng-controller="AppCtrl"><panel><div class="button">Click me!</div></panel></div></div>*/var app = angular.module("phoneApp", []);app.controller("AppCtrl", function($scope) {})// transclude: true와 <div ng-transclude를 설정하지 않으면 DOM의 <div class="button"이 안보이고// template의 내용만 보이게 된다. template 내용안에 DOM 의 button class가 같이 보이게 하기 위한 방법> 버튼이 안보임
> transclude 설정으로 버튼이 보임
app.directive("panel", function() {return {restrict: "E",transclude: true,template: '<div class="panel" ng-transclude>This is a panel component</div>'}})
전반적으로 처음 접하는 개념이어서 정리가 서툴다. 다시 반복하여 리팩토링 예정임... ^^
미스코가 직접 이야기하는 Directive에 대해서 들어보자
- 유튜브
+ Directive의 compile & link 개념의 C의 컴파일과 링크에서 영감을 받았다
+ compile은 scope를 핸들링하지 않는다. template자체를 변경할 수 있다. 즉 compile은 template에 대한 해석만을 담당한다.
+ link는 element instance에 대해서 조작을 한다. 예로 element.addClass()와 같은 호출.
- 디렉티브 만들기 : 간단 총정리하기
<참조>
- 원문 : Developer Guide - Directives
- AngularJS Directive로 컴포넌트 만들기 (필독)
- 동영상강좌 : http://egghead.io
+ isolated @, =, & 소스 : https://gist.github.com/ysyun/5385927
+ 테스트를 위한 index.html 파일 내역 : CSS로 foundation을 사용한다
- Directives 이해 높이기-소스포함 (필독)
'AngularJS > Concept' 카테고리의 다른 글
[AngularJS] 웹 애플리케이션 견고하게 만드는 방법 (0) | 2013.04.27 |
---|---|
[AngularJS] 개발자 가이드 - 06. 모듈 (0) | 2013.04.11 |
[AngularJS] 개발자 가이드 - 04. 개념 이해하기 (0) | 2013.04.10 |
[AngularJS] 개발자 가이드 - 03. HTML 컴파일러 (0) | 2013.04.10 |
[AngularJS] 개발자 가이드 - 02. 시작하기 (0) | 2013.04.10 |