This repository was archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27.4k
/
Copy pathdirective.ngdoc
745 lines (591 loc) · 32.6 KB
/
directive.ngdoc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
@ngdoc overview
@name Directives
@description
Directives are a way to teach HTML new tricks. During DOM compilation directives are matched
against the HTML and executed. This allows directives to register behavior, or transform the DOM.
Angular comes with a built in set of directives which are useful for building web applications but
can be extended such that HTML can be turned into a declarative domain specific language (DSL).
# Invoking directives from HTML
Directives have camel cased names such as `ngBind`. The directive can be invoked by translating
the camel case name into snake case with these special characters `:`, `-`, or `_`. Optionally the
directive can be prefixed with `x-`, or `data-` to make it HTML validator compliant. Here is a
list of some of the possible directive names: `ng:bind`, `ng-bind`, `ng_bind`, `x-ng-bind` and
`data-ng-bind`.
The directives can be placed in element names, attributes, class names, as well as comments. Here
are some equivalent examples of invoking `myDir`. (However, most directives are restricted to
attribute only.)
<pre>
<span my-dir="exp"></span>
<span class="my-dir: exp;"></span>
<my-dir></my-dir>
<!-- directive: my-dir exp -->
</pre>
The following demonstrates the various ways a Directive (ngBind in this case) can be referenced from within a template.
<doc:example>
<doc:source >
<script>
function Ctrl1($scope) {
$scope.name = 'Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)';
}
</script>
<div ng-controller="Ctrl1">
Hello <input ng-model='name'> <hr/>
<span ng-bind="name"> <span ng-bind="name"></span> <br/>
<span ng:bind="name"> <span ng:bind="name"></span> <br/>
<span ng_bind="name"> <span ng_bind="name"></span> <br/>
<span data-ng-bind="name"> <span data-ng-bind="name"></span> <br/>
<span x-ng-bind="name"> <span x-ng-bind="name"></span> <br/>
</div>
</doc:source>
<doc:scenario>
it('should show off bindings', function() {
expect(element('div[ng-controller="Ctrl1"] span[ng-bind]').text())
.toBe('Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)');
});
</doc:scenario>
</doc:example>
# Text and attribute bindings
During the compilation process the {@link api/ng.$compile compiler} matches text and
attributes using the {@link api/ng.$interpolate $interpolate} service to see if they
contain embedded expressions. These expressions are registered as {@link
api/ng.$rootScope.Scope#$watch watches} and will update as part of normal {@link
api/ng.$rootScope.Scope#$digest digest} cycle. An example of interpolation is shown
here:
<pre>
<a ng-href="img/{{username}}.jpg">Hello {{username}}!</a>
</pre>
# ngAttr attribute bindings
If an attribute with a binding is prefixed with `ngAttr` prefix (denormalized prefix: 'ng-attr-',
'ng:attr-') then during the compilation the prefix will be removed and the binding will be applied
to an unprefixed attribute. This allows binding to attributes that would otherwise be eagerly
processed by browsers in their uncompiled form (e.g. `img[src]` or svg's `circle[cx]` attributes).
For example, considering template:
<svg>
<circle ng-attr-cx="{{cx}}"></circle>
</svg>
and model cx set to 5, will result in rendering this dom:
<svg>
<circle cx="5"></circle>
</svg>
If you were to bind `{{cx}}` directly to the `cx` attribute, you'd get the following error:
`Error: Invalid value for attribute cx="{{cx}}"`. With `ng-attr-cx` you can work around this
problem.
# Compilation process, and directive matching
Compilation of HTML happens in three phases:
1. First the HTML is parsed into DOM using the standard browser API. This is important to
realize because the templates must be parsable HTML. This is in contrast to most templating
systems that operate on strings, rather than on DOM elements.
2. The compilation of the DOM is performed by the call to the {@link api/ng.$compile
$compile()} method. The method traverses the DOM and matches the directives. If a match is found
it is added to the list of directives associated with the given DOM element. Once all directives
for a given DOM element have been identified they are sorted by priority and their `compile()`
functions are executed. The directive compile function has a chance to modify the DOM structure
and is responsible for producing a `link()` function explained next. The {@link
api/ng.$compile $compile()} method returns a combined linking function, which is a
collection of all of the linking functions returned from the individual directive compile
functions.
3. Link the template with scope by calling the linking function returned from the previous step.
This in turn will call the linking function of the individual directives allowing them to
register any listeners on the elements and set up any {@link
api/ng.$rootScope.Scope#$watch watches} with the {@link
api/ng.$rootScope.Scope scope}. The result of this is a live binding between the
scope and the DOM. A change in the scope is reflected in the DOM.
<pre>
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);
</pre>
## Reasons behind the compile/link separation
At this point you may wonder why the compile process is broken down to a compile and link phase.
To understand this, let's look at a real world example with a repeater:
<pre>
Hello {{user}}, you have these actions:
<ul>
<li ng-repeat="action in user.actions">
{{action.description}}
</li>
</ul>
</pre>
The short answer is that compile and link separation is needed any time a change in model causes
a change in DOM structure such as in repeaters.
When the above example is compiled, the compiler visits every node and looks for directives. The
`{{user}}` is an example of an {@link api/ng.$interpolate interpolation} directive. {@link
api/ng.directive:ngRepeat ngRepeat} is another directive. But {@link
api/ng.directive:ngRepeat ngRepeat} has a dilemma. It needs to be
able to quickly stamp out new `li`s for every `action` in `user.actions`. This means that it needs
to save a clean copy of the `li` element for cloning purposes and as new `action`s are inserted,
the template `li` element needs to be cloned and inserted into `ul`. But cloning the `li` element
is not enough. It also needs to compile the `li` so that its directives such as
`{{action.description}}` evaluate against the right {@link api/ng.$rootScope.Scope
scope}. A naive method would be to simply insert a copy of the `li` element and then compile it.
But compiling on every `li` element clone would be slow, since the compilation requires that we
traverse the DOM tree and look for directives and execute them. If we put the compilation inside a
repeater which needs to unroll 100 items we would quickly run into performance problems.
The solution is to break the compilation process into two phases; the compile phase where all of
the directives are identified and sorted by priority, and a linking phase where any work which
links a specific instance of the {@link api/ng.$rootScope.Scope scope} and the specific
instance of an `li` is performed.
{@link api/ng.directive:ngRepeat ngRepeat} works by preventing the
compilation process from descending into the `li` element. Instead the {@link
api/ng.directive:ngRepeat ngRepeat} directive compiles `li`
separately. The result of the `li` element compilation is a linking function which contains all
of the directives contained in the `li` element, ready to be attached to a specific clone of the `li`
element. At runtime the {@link api/ng.directive:ngRepeat ngRepeat}
watches the expression and as items are added to the array it clones the `li` element, creates a
new {@link api/ng.$rootScope.Scope scope} for the cloned `li` element and calls the
link function on the cloned `li`.
Summary:
* *compile function* - The compile function is relatively rare in directives, since most
directives are concerned with working with a specific DOM element instance rather than
transforming the template DOM element. Any operation which can be shared among the instance of
directives should be moved to the compile function for performance reasons.
* *link function* - It is rare for the directive not to have a link function. A link function
allows the directive to register listeners to the specific cloned DOM element instance as well
as to copy content into the DOM from the scope.
# Writing directives (short version)
In this example we will build a directive that displays the current time.
<doc:example module="time">
<doc:source>
<script>
function Ctrl2($scope) {
$scope.format = 'M/d/yy h:mm:ss a';
}
angular.module('time', [])
// Register the 'myCurrentTime' directive factory method.
// We inject $timeout and dateFilter service since the factory method is DI.
.directive('myCurrentTime', function($timeout, dateFilter) {
// return the directive link function. (compile function not needed)
return function(scope, element, attrs) {
var format, // date format
timeoutId; // timeoutId, so that we can cancel the time updates
// used to update the UI
function updateTime() {
element.text(dateFilter(new Date(), format));
}
// watch the expression, and update the UI on change.
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
// schedule update in one second
function updateLater() {
// save the timeoutId for canceling
timeoutId = $timeout(function() {
updateTime(); // update DOM
updateLater(); // schedule another update
}, 1000);
}
// listen on DOM destroy (removal) event, and cancel the next UI update
// to prevent updating time after the DOM element was removed.
element.on('$destroy', function() {
$timeout.cancel(timeoutId);
});
updateLater(); // kick off the UI update process.
}
});
</script>
<div ng-controller="Ctrl2">
Date format: <input ng-model="format"> <hr/>
Current time is: <span my-current-time="format"></span>
</div>
</doc:source>
</doc:example>
# Writing directives (long version)
There are different ways to declare a directive. The difference resides in the return
value of the factory function. You can either return a Directive Definition Object
(see below) that defines the directive properties, or just the postLink function
of such an object (all other properties will have the default values).
Here's an example directive declared with a Directive Definition Object:
<pre>
var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
priority: 0,
template: '<div></div>', // or // function(tElement, tAttrs) { ... },
// or
// templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
replace: false,
transclude: false,
restrict: 'A',
scope: false,
controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
compile: function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
// or
// return function postLink( ... ) { ... }
},
// or
// link: {
// pre: function preLink(scope, iElement, iAttrs, controller) { ... },
// post: function postLink(scope, iElement, iAttrs, controller) { ... }
// }
// or
// link: function postLink( ... ) { ... }
};
return directiveDefinitionObject;
});
</pre>
In most cases you will not need such fine control and so the above can be simplified. You can still
return a Directive Definition Object, but only setting the 'link' function property of the Object,
and rely on the default values for other properties.
Therefore the above can be simplified as:
<pre>
var myModule = angular.module(...);
myModule.directive('directiveName', function factory(injectables) {
var directiveDefinitionObject = {
link: function postLink(scope, iElement, iAttrs) { ... }
};
return directiveDefinitionObject;
// or
// return function postLink(scope, iElement, iAttrs) { ... }
});
</pre>
## Factory method
The factory method is responsible for creating the directive. It is invoked only once, when the
{@link api/ng.$compile compiler} matches the directive for the first time. You can
perform any initialization work here. The method is invoked using the {@link
api/AUTO.$injector#invoke $injector.invoke} which
makes it injectable following all of the rules of injection annotation.
## Directive Definition Object
The directive definition object provides instructions to the {@link api/ng.$compile
compiler}. The attributes are:
* `name` - Name of the current scope. Optional and defaults to the name at registration.
* `priority` - When there are multiple directives defined on a single DOM element, sometimes it
is necessary to specify the order in which the directives are applied. The `priority` is used
to sort the directives before their `compile` functions get called. Priority is defined as a
number. Directives with greater numerical `priority` are compiled first. The order of directives with
the same priority is undefined. The default priority is `0`.
* `terminal` - If set to true then the current `priority` will be the last set of directives
which will execute (any directives at the current priority will still execute
as the order of execution on same `priority` is undefined).
* `scope` - If set to:
* `true` - then a new scope will be created for this directive. If multiple directives on the
same element request a new scope, only one new scope is created. The new scope rule does not
apply for the root of the template since the root of the template always gets a new scope.
* `{}` (object hash) - then a new 'isolate' scope is created. The 'isolate' scope differs from
normal scope in that it does not prototypically inherit from the parent scope. This is useful
when creating reusable components, which should not accidentally read or modify data in the
parent scope. <br/>
The 'isolate' scope takes an object hash which defines a set of local scope properties
derived from the parent scope. These local properties are useful for aliasing values for
templates. Locals definition is a hash of local scope property to its source:
* `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is
always a string since DOM attributes are strings. If no `attr` name is specified then the
attribute name is assumed to be the same as the local name.
Given `<widget my-attr="hello {{name}}">` and widget definition
of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect
the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the
`localName` property on the widget scope. The `name` is read from the parent scope (not
component scope).
* `=` or `=attr` - set up bi-directional binding between a local scope property and the
parent scope property of name defined via the value of the `attr` attribute. If no `attr`
name is specified then the attribute name is assumed to be the same as the local name.
Given `<widget my-attr="parentModel">` and widget definition of
`scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the
value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent
scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You
can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional.
* `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
If no `attr` name is specified then the attribute name is assumed to be the same as the
local name. Given `<widget my-attr="count = count + value">` and widget definition of
`scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to
a function wrapper for the `count = count + value` expression. Often it's desirable to
pass data from the isolated scope via an expression and to the parent scope, this can be
done by passing a map of local variable names and values into the expression wrapper fn.
For example, if the expression is `increment(amount)` then we can specify the amount value
by calling the `localFn` as `localFn({amount: 22})`.
* `controller` - Controller constructor function. The controller is instantiated before the
pre-linking phase and it is shared with other directives (see
`require` attribute). This allows the directives to communicate with each other and augment
each other's behavior. The controller is injectable (and supports bracket notation) with the following locals:
* `$scope` - Current scope associated with the element
* `$element` - Current element
* `$attrs` - Current attributes object for the element
* `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
`function(cloneLinkingFn)`.
* `require` - Require another directive and inject its controller as the fourth argument to the linking function. The
`require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the injected
argument will be an array in corresponding order. If no such directive can be
found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with:
* (no prefix) - Locate the required controller on the current element.
* `?` - Attempt to locate the required controller, or return `null` if not found.
* `^` - Locate the required controller by searching the element's parents.
* `?^` - Attempt to locate the required controller by searching the element's parents, or return `null` if not found.
* `controllerAs` - Controller alias at the directive scope. An alias for the controller so it
can be referenced at the directive template. The directive needs to define a scope for this
configuration to be used. Useful in the case when directive is used as component.
* `restrict` - String of subset of `EACM` which restricts the directive to a specific directive
declaration style. If omitted, the default (attributes only) is used.
* `E` - Element name: `<my-directive></my-directive>`
* `A` - Attribute (default): `<div my-directive="exp"></div>`
* `C` - Class: `<div class="my-directive: exp;"></div>`
* `M` - Comment: `<!-- directive: my-directive exp -->`
* `template` - replace the current element with the contents of the HTML. The replacement process
migrates all of the attributes / classes from the old element to the new one. See the
{@link guide/directive#Components Creating Components} section below for more information.
You can specify `template` as a string representing the template or as a function which takes
two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and
returns a string value representing the template.
* `templateUrl` - Same as `template` but the template is loaded from the specified URL. Because
the template loading is asynchronous the compilation/linking is suspended until the template
is loaded.
You can specify `templateUrl` as a string representing the URL or as a function which takes two
arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns
a string value representing the url. In either case, the template URL is passed through {@link
api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}.
* `replace` - specify where the template should be inserted. Defaults to `false`.
* `true` - the template will replace the current element.
* `false` - the template will replace the contents of the current element.
* `transclude` - compile the content of the element and make it available to the directive.
Typically used with {@link api/ng.directive:ngTransclude
ngTransclude}. The advantage of transclusion is that the linking function receives a
transclusion function which is pre-bound to the correct scope. In a typical setup the widget
creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate`
scope. This makes it possible for the widget to have private state, and the transclusion to
be bound to the parent (pre-`isolate`) scope.
* `true` - transclude the content of the directive.
* `'element'` - transclude the whole element including any directives defined at lower priority.
* `compile`: This is the compile function described in the section below.
* `link`: This is the link function described in the section below. This property is used only
if the `compile` property is not defined.
## Compile function
<pre>
function compile(tElement, tAttrs, transclude) { ... }
</pre>
The compile function deals with transforming the template DOM. Since most directives do not do
template transformation, it is not used often. Examples that require compile functions are
directives that transform template DOM, such as {@link
api/ng.directive:ngRepeat ngRepeat}, or load the contents
asynchronously, such as {@link api/ngRoute.directive:ngView ngView}. The
compile function takes the following arguments.
* `tElement` - template element - The element where the directive has been declared. It is
safe to do template transformation on the element and child elements only.
* `tAttrs` - template attributes - Normalized list of attributes declared on this element shared
between all directive compile functions. See {@link
guide/directive#Attributes Attributes}.
* `transclude` - A transclude linking function: `function(scope, cloneLinkingFn)`.
NOTE: The template instance and the link instance may not be the same objects if the template has
been cloned. For this reason it is not safe in the compile function to do anything other than DOM
transformation that applies to all DOM clones. Specifically, DOM listener registration should be
done in a linking function rather than in a compile function.
A compile function can have a return value which can be either a function or an object.
* returning a (post-link) function - is equivalent to registering the linking function via the
`link` property of the config object when the compile function is empty.
* returning an object with function(s) registered via `pre` and `post` properties - allows you to
control when a linking function should be called during the linking phase. See info about
pre-linking and post-linking functions below.
## Linking function
<pre>
function link(scope, iElement, iAttrs, controller) { ... }
</pre>
The link function is responsible for registering DOM listeners as well as updating the DOM. It is
executed after the template has been cloned. This is where most of the directive logic will be
put.
* `scope` - {@link api/ng.$rootScope.Scope Scope} - The scope to be used by the
directive for registering {@link api/ng.$rootScope.Scope#$watch watches}.
* `iElement` - instance element - The element where the directive is to be used. It is safe to
manipulate the children of the element only in `postLink` function since the children have
already been linked.
* `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared
between all directive linking functions. See {@link
guide/directive#Attributes Attributes}.
* `controller` - a controller instance - A controller instance if at least one directive on the
element defines a controller. The controller is shared among all the directives, which allows
the directives to use the controllers as a communication channel.
### Pre-linking function
Executed before the child elements are linked. Not safe to do DOM transformation since the
compiler linking function will fail to locate the correct elements for linking.
### Post-linking function
Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function.
<a name="Attributes"></a>
## Attributes
The {@link api/ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the
link() or compile() functions - is a way of accessing:
* *normalized attribute names:* Since a directive such as 'ngBind' can be expressed in many ways
such as 'ng:bind', or 'x-ng-bind', the attributes object allows for normalized access to
the attributes.
* *directive inter-communication:* All directives share the same instance of the attributes
object which allows the directives to use the attributes object as inter directive
communication.
* *supports interpolation:* Interpolation attributes are assigned to the attribute object
allowing other directives to read the interpolated value.
* *observing interpolated attributes:* Use `$observe` to observe the value changes of attributes
that contain interpolation (e.g. `src="{{bar}}"`). Not only is this very efficient but it's also
the only way to easily get the actual value because during the linking phase the interpolation
hasn't been evaluated yet and so the value is at this time set to `undefined`.
<pre>
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);
});
}
</pre>
# Understanding Transclusion and Scopes
It is often desirable to have reusable components. Below is a pseudo code showing how a simplified
dialog component may work.
<pre>
<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>
</pre>
Clicking on the "show" button will open the dialog. The dialog will have a title, which is
data bound to `username`, and it will also have a body which we would like to transclude
into the dialog.
Here is an example of what the template definition for the `dialog` widget may look like.
<pre>
<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>
</pre>
This will not render properly, unless we do some scope magic.
The first issue we have to solve is that the dialog box template expects `title` to be defined, but
the place of instantiation would like to bind to `username`. Furthermore the buttons expect the
`onOk` and `onCancel` functions to be present in the scope. This limits the usefulness of the
widget. To solve the mapping issue we use the `locals` to create local variables which the template
expects as follows:
<pre>
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
}
</pre>
Creating local properties on widget scope creates two problems:
1. isolation - if the user forgets to set `title` attribute of the dialog widget the dialog
template will bind to parent scope property. This is unpredictable and undesirable.
2. transclusion - the transcluded DOM can see the widget locals, which may overwrite the
properties which the transclusion needs for data-binding. In our example the `title`
property of the widget clobbers the `title` property of the transclusion.
To solve the issue of lack of isolation, the directive declares a new `isolated` scope. An
isolated scope does not prototypically inherit from the child scope, and therefore we don't have
to worry about accidentally clobbering any properties.
However `isolated` scope creates a new problem: if a transcluded DOM is a child of the widget
isolated scope then it will not be able to bind to anything. For this reason the transcluded scope
is a child of the original scope, before the widget created an isolated scope for its local
variables. This makes the transcluded and widget isolated scope siblings.
This may seem to be unexpected complexity, but it gives the widget user and developer the least
surprise.
Therefore the final directive definition looks something like this:
<pre>
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
</pre>
<a name="Components"></a>
# Creating Components
It is often desirable to replace a single directive with a more complex DOM structure. This
allows the directives to become a short hand for reusable components from which applications
can be built.
Following is an example of building a reusable widget.
<doc:example module="zippyModule">
<doc:source>
<script>
function Ctrl3($scope) {
$scope.title = 'Lorem Ipsum';
$scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
}
angular.module('zippyModule', [])
.directive('zippy', function(){
return {
restrict: 'C',
// This HTML will replace the zippy directive.
replace: true,
transclude: true,
scope: { title:'@zippyTitle' },
template: '<div>' +
'<div class="title">{{title}}</div>' +
'<div class="body" ng-transclude></div>' +
'</div>',
// The linking function will add behavior to the template
link: function(scope, element, attrs) {
// Title element
var title = angular.element(element.children()[0]),
// Opened / closed state
opened = true;
// Clicking on title should open/close the zippy
title.on('click', toggle);
// Toggle the closed/opened state
function toggle() {
opened = !opened;
element.removeClass(opened ? 'closed' : 'opened');
element.addClass(opened ? 'opened' : 'closed');
}
// initialize the zippy
toggle();
}
}
});
</script>
<style>
.zippy {
border: 1px solid black;
display: inline-block;
width: 250px;
}
.zippy.opened > .title:before { content: '▼ '; }
.zippy.opened > .body { display: block; }
.zippy.closed > .title:before { content: '► '; }
.zippy.closed > .body { display: none; }
.zippy > .title {
background-color: black;
color: white;
padding: .1em .3em;
cursor: pointer;
}
.zippy > .body {
padding: .1em .3em;
}
</style>
<div ng-controller="Ctrl3">
Title: <input ng-model="title"> <br>
Text: <textarea ng-model="text"></textarea>
<hr>
<div class="zippy" zippy-title="Details: {{title}}...">{{text}}</div>
</div>
</doc:source>
<doc:scenario>
it('should bind and open / close', function() {
input('title').enter('TITLE');
input('text').enter('TEXT');
expect(element('.title').text()).toEqual('Details: TITLE...');
expect(binding('text')).toEqual('TEXT');
expect(element('.zippy').prop('className')).toMatch(/closed/);
element('.zippy > .title').click();
expect(element('.zippy').prop('className')).toMatch(/opened/);
});
</doc:scenario>
</doc:example>