mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 15:40:22 +00:00
The example about transclusion and scopes worked only because the order of `scope` and `element` arguments is wrong, which means that the `name' property of the scope is not really being updated. To really work, the directive has to define its own scope, either a new child scope or, as is more common with transclusion, an isolated scope. Closes #4774
896 lines
31 KiB
Text
896 lines
31 KiB
Text
@ngdoc overview
|
||
@name Directives
|
||
@description
|
||
|
||
# Creating Custom Directives
|
||
|
||
<div class="alert alert-warning">
|
||
**Note:** this guide is targeted towards developers who are already familiar with AngularJS basics.
|
||
If you're just getting started, we recommend the {@link tutorial/ tutorial} first.
|
||
</div>
|
||
|
||
|
||
This document explains when you'd want to create your own directives in your AngularJS app, and
|
||
how to implement them.
|
||
|
||
|
||
## What are Directives?
|
||
|
||
At a high level, directives are markers on a DOM element (such as an attribute, element
|
||
name, or CSS class) that tell AngularJS's **HTML compiler** ({@link api/ng.$compile `$compile`}) to
|
||
attach a specified behavior to that DOM element or even transform the DOM element and its children.
|
||
|
||
Angular comes with a set of these directives built-in, like `ngBind`, `ngModel`, and `ngView`.
|
||
Much like you create controllers and services, you can create your own directives for Angular to use.
|
||
When Angular {@link guide/bootstrap bootstraps} your application, the
|
||
{@link guide/compiler HTML compiler} traverses the DOM matching directives against the DOM elements.
|
||
|
||
<div class="alert alert-info">
|
||
**What does it mean to "compile" an HTML template?**
|
||
|
||
For AngularJS, "compilation" means attaching event listeners to the HTML to make it interactive.
|
||
The reason we use the term "compile" is that the recursive process of attaching directives
|
||
mirrors the process of compiling source code in
|
||
{@link http://en.wikipedia.org/wiki/Compiled_languages compiled programming languages}.
|
||
</div>
|
||
|
||
|
||
## Matching Directives
|
||
|
||
Before we can write a directive, we need to know how Angular's {@link guide/compiler HTML compiler}
|
||
determines when to use a given directive.
|
||
|
||
In the following example, we say that the `<input>` element **matches** the `ngModel` directive.
|
||
|
||
```javascript
|
||
<input ng-model="foo">
|
||
```
|
||
|
||
The following also **matches** `ngModel`:
|
||
|
||
```javascript
|
||
<input data-ng:model="foo">
|
||
```
|
||
|
||
Angular **normalizes** an element's tag and attribute name to determine which elements match which
|
||
directives. We typically refer to directives by their case-sensitive
|
||
{@link http://en.wikipedia.org/wiki/CamelCase camelCase} **normalized** name (e.g. `ngModel`).
|
||
However, in the DOM, we refer to directives by case-insensitive forms, typically using
|
||
{@link http://en.wikipedia.org/wiki/Letter_case#Computers dash-delimited} attributes on DOM elements
|
||
(e.g. `ng-model`).
|
||
|
||
The **normalization** process is as follows:
|
||
|
||
1. Strip `x-` and `data-` from the front of the element/attributes.
|
||
2. Convert the `:`, `-`, or `_`-delimited name to `camelCase`.
|
||
|
||
Here are some equivalent examples of elements that match `ngBind`:
|
||
|
||
<example module="docsBindExample">
|
||
<file name="script.js">
|
||
angular.module('docsBindExample', [])
|
||
.controller('Ctrl1', function Ctrl1($scope) {
|
||
$scope.name = 'Max Karl Ernst Ludwig Planck (April 23, 1858 – October 4, 1947)';
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl1">
|
||
Hello <input ng-model='name'> <hr/>
|
||
<span ng-bind="name"></span> <br/>
|
||
<span ng:bind="name"></span> <br/>
|
||
<span ng_bind="name"></span> <br/>
|
||
<span data-ng-bind="name"></span> <br/>
|
||
<span x-ng-bind="name"></span> <br/>
|
||
</div>
|
||
</file>
|
||
<file name="scenario.js">
|
||
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)');
|
||
});
|
||
</file>
|
||
</example>
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** Prefer using the dash-delimited format (e.g. `ng-bind` for `ngBind`).
|
||
If you want to use an HTML validating tool, you can instead use the `data`-prefixed version (e.g. `data-ng-bind` for `ngBind`).
|
||
The other forms shown above are accepted for legacy reasons but we advise you to avoid them.
|
||
</div>
|
||
|
||
`$compile` can match directives based on element names, attributes, class names, as well as comments.
|
||
|
||
All of the Angular-provided directives match attribute name, tag name, comments, or class name.
|
||
The following demonstrates the various ways a directive (`myDir` in this case) can be referenced from within a template:
|
||
|
||
```html
|
||
<my-dir></my-dir>
|
||
<span my-dir="exp"></span>
|
||
<!-- directive: my-dir exp -->
|
||
<span class="my-dir: exp;"></span>
|
||
```
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** Prefer using directives via tag name and attributes over comment and class names.
|
||
Doing so generally makes it easier to determine what directives a given element matches.
|
||
</div>
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** Comment directives were commonly used in places where the DOM API limits (e.g. inside `<table>` elements)
|
||
to create directives that spanned multiple elements. AngularJS 1.2 introduces
|
||
{@link api/ng.directive:ngRepeat `ng-repeat-start` and `ng-repeat-end`} as a better solution to this problem.
|
||
Developers are encouraged to use this over custom comment directives when possible.
|
||
</div>
|
||
|
||
|
||
|
||
### 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#methods_$watch watches} and will update as part of normal {@link
|
||
api/ng.$rootScope.Scope#methods_$digest digest} cycle. An example of interpolation is shown below:
|
||
|
||
```html
|
||
<a ng-href="img/{{username}}.jpg">Hello {{username}}!</a>
|
||
```
|
||
|
||
|
||
### `ngAttr` attribute bindings
|
||
|
||
Web browsers are sometimes picky about what values they consider valid for attributes.
|
||
|
||
For example, considering this template:
|
||
|
||
```html
|
||
<svg>
|
||
<circle cx="{{cx}}"></circle>
|
||
</svg>
|
||
```
|
||
|
||
We would expect Angular to be able to bind to this, but when we check the console we see
|
||
something like `Error: Invalid value for attribute cx="{{cx}}"`. Because of the SVG DOM API's restrictions,
|
||
you cannot simply write `cx="{{cx}}"`.
|
||
|
||
With `ng-attr-cx` you can work around this problem.
|
||
|
||
If an attribute with a binding is prefixed with the `ngAttr` prefix (denormalized as `ng-attr-`)
|
||
then during the binding will be applied to the corresponding unprefixed attribute. This allows
|
||
you to bind to attributes that would otherwise be eagerly processed by browsers
|
||
(e.g. an SVG element's `circle[cx]` attributes).
|
||
|
||
For example, we could fix the example above by instead writing:
|
||
|
||
```html
|
||
<svg>
|
||
<circle ng-attr-cx="{{cx}}"></circle>
|
||
</svg>
|
||
```
|
||
|
||
|
||
## Creating Directives
|
||
|
||
First let's talk about the API for registering directives. Much like controllers, directives are registered on
|
||
modules. To register a directive, you use the `module.directive` API. `module.directive` takes the
|
||
{@link guide/directive#creating-custom-directives_matching-directives normalized} directive name followed
|
||
by a **factory function.** This factory function should return
|
||
an object with the different options to tell `$compile` how the directive should behave when matched.
|
||
|
||
|
||
The factory function 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 function is invoked using {@link
|
||
api/AUTO.$injector#methods_invoke $injector.invoke} which
|
||
makes it injectable just like a controller.
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** Prefer using the definition object over returning a function.
|
||
</div>
|
||
|
||
|
||
We'll go over a few common examples of directives, then dive deep into the different options
|
||
and compilation process.
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** In order to avoid collisions with some future standard, it's best to prefix your own
|
||
directive names. For instance, if you created a `<carousel>` directive, it would be problematic if HTML7
|
||
introduced the same element. A two or three letter prefix (e.g. `btfCarousel`) works well. Similarly, do
|
||
not prefix your own directives with `ng` or they might conflict with directives included in a future
|
||
version of Angular.
|
||
</div>
|
||
|
||
For the following examples, we'll use the prefix `my` (e.g. `myCustomer`).
|
||
|
||
|
||
### Template-expanding directive
|
||
|
||
Let's say you have a chunk of your template that represents a customer's information. This template is repeated
|
||
many times in your code. When you change it in one place, you have to change it in several others. This is a
|
||
good opportunity to use a directive to simplify your template.
|
||
|
||
Let's create a directive that simply replaces its contents with a static template:
|
||
|
||
<example module="docsSimpleDirective">
|
||
<file name="script.js">
|
||
angular.module('docsSimpleDirective', [])
|
||
.controller('Ctrl', function($scope) {
|
||
$scope.customer = {
|
||
name: 'Naomi',
|
||
address: '1600 Amphitheatre'
|
||
};
|
||
})
|
||
.directive('myCustomer', function() {
|
||
return {
|
||
template: 'Name: {{customer.name}} Address: {{customer.address}}'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<div my-customer></div>
|
||
</div>
|
||
</file>
|
||
</example>
|
||
|
||
Notice that we have bindings in this directive. After `$compile` compiles and links `<div my-customer></div>`,
|
||
it will try to match directives on the element's children. This means you can compose directives of other directives.
|
||
We'll see how to do that in {@link
|
||
guide/directive#creating-custom-directives_demo_creating-directives-that-communicate an example} below.
|
||
|
||
In the example above we in-lined the value of the `template` option, but this will become annoying as the size
|
||
of your template grows.
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** Unless your template is very small, it's typically better to break it apart into its own
|
||
HTML file and load it with the `templateUrl` option.
|
||
</div>
|
||
|
||
If you are familiar with `ngInclude`, `templateUrl` works just like it. Here's the same example using `templateUrl`
|
||
instead:
|
||
|
||
<example module="docsTemplateUrlDirective">
|
||
<file name="script.js">
|
||
angular.module('docsTemplateUrlDirective', [])
|
||
.controller('Ctrl', function($scope) {
|
||
$scope.customer = {
|
||
name: 'Naomi',
|
||
address: '1600 Amphitheatre'
|
||
};
|
||
})
|
||
.directive('myCustomer', function() {
|
||
return {
|
||
templateUrl: 'my-customer.html'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<div my-customer></div>
|
||
</div>
|
||
</file>
|
||
<file name="my-customer.html">
|
||
Name: {{customer.name}} Address: {{customer.address}}
|
||
</file>
|
||
</example>
|
||
|
||
Great! But what if we wanted to have our directive match the tag name `<my-customer>` instead?
|
||
If we simply put a `<my-customer>` element into the HMTL, it doesn't work.
|
||
|
||
<div class="alert alert-waring">
|
||
**Note:** When you create a directive, it is restricted to attribute only by default. In order to create
|
||
directives that are triggered by element name, you need to use the `restrict` option.
|
||
</div>
|
||
|
||
The `restrict` option is typically set to:
|
||
|
||
* `'A'` - only matches attribute name
|
||
* `'E'` - only matches element name
|
||
* `'AE'` - matches either attribute or element name
|
||
|
||
Let's change our directive to use `restrict: 'E'`:
|
||
|
||
<example module="docsRestrictDirective">
|
||
<file name="script.js">
|
||
angular.module('docsRestrictDirective', [])
|
||
.controller('Ctrl', function($scope) {
|
||
$scope.customer = {
|
||
name: 'Naomi',
|
||
address: '1600 Amphitheatre'
|
||
};
|
||
})
|
||
.directive('myCustomer', function() {
|
||
return {
|
||
restrict: 'E',
|
||
templateUrl: 'my-customer.html'
|
||
};
|
||
});
|
||
</file>
|
||
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<my-customer></my-customer>
|
||
</div>
|
||
</file>
|
||
|
||
<file name="my-customer.html">
|
||
Name: {{customer.name}} Address: {{customer.address}}
|
||
</file>
|
||
</example>
|
||
|
||
For more on the {@link api/ng.$compile#description_comprehensive-directive-api_directive-definition-object
|
||
`restrict`, see the API docs}.
|
||
|
||
<div class="alert alert-info">
|
||
**When should I use an attribute versus an element?**
|
||
|
||
Use an element when you are creating a component that is in control of the template. The common case for this
|
||
is when you are creating a Domain-Specific Language for parts of your template.
|
||
|
||
Use an attribute when you are decorating an existing element with new functionality.
|
||
</div>
|
||
|
||
Using an element for the `myCustomer` directive is clearly the right choice because you're not decorating an element
|
||
with some "customer" behavior; you're defining the core behavior of the element as a customer component.
|
||
|
||
|
||
|
||
### Isolating the Scope of a Directive
|
||
|
||
Our `myCustomer` directive above is great, but it has a fatal flaw. We can only use it once within a given scope.
|
||
|
||
In its current implementation, we'd need to create a different controller each time In order to re-use such a directive:
|
||
|
||
<example module="docsScopeProblemExample">
|
||
<file name="script.js">
|
||
angular.module('docsScopeProblemExample', [])
|
||
.controller('NaomiCtrl', function($scope) {
|
||
$scope.customer = {
|
||
name: 'Naomi',
|
||
address: '1600 Amphitheatre'
|
||
};
|
||
})
|
||
.controller('IgorCtrl', function($scope) {
|
||
$scope.customer = {
|
||
name: 'Igor',
|
||
address: '123 Somewhere'
|
||
};
|
||
})
|
||
.directive('myCustomer', function() {
|
||
return {
|
||
restrict: 'E',
|
||
templateUrl: 'my-customer.html'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="NaomiCtrl">
|
||
<my-customer></my-customer>
|
||
</div>
|
||
<hr>
|
||
<div ng-controller="IgorCtrl">
|
||
<my-customer></my-customer>
|
||
</div>
|
||
</file>
|
||
<file name="my-customer.html">
|
||
Name: {{customer.name}} Address: {{customer.address}}
|
||
</file>
|
||
</example>
|
||
|
||
This is clearly not a great solution.
|
||
|
||
What we want to be able to do is separate the scope inside a directive from the scope
|
||
outside, and then map the outer scope to a directive's inner scope. We can do this by creating what we call an
|
||
**isolate scope**. To do this, we can use a directive's `scope` option:
|
||
|
||
<example module="docsIsolateScopeDirective">
|
||
<file name="script.js">
|
||
angular.module('docsIsolateScopeDirective', [])
|
||
.controller('Ctrl', function($scope) {
|
||
$scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
|
||
$scope.igor = { name: 'Igor', address: '123 Somewhere' };
|
||
})
|
||
.directive('myCustomer', function() {
|
||
return {
|
||
restrict: 'E',
|
||
scope: {
|
||
customer: '=customer'
|
||
},
|
||
templateUrl: 'my-customer.html'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<my-customer customer="naomi"></my-customer>
|
||
<hr>
|
||
<my-customer customer="igor"></my-customer>
|
||
</div>
|
||
</file>
|
||
<file name="my-customer.html">
|
||
Name: {{customer.name}} Address: {{customer.address}}
|
||
</file>
|
||
</example>
|
||
|
||
Looking at `index.html`, the first `<my-customer>` element binds the inner scope's `customer` to `naomi`,
|
||
which we have exposed on our controller's scope. The second binds `customer` to `igor`.
|
||
|
||
Let's take a closer look at the scope option:
|
||
|
||
```javascript
|
||
//...
|
||
scope: {
|
||
customer: '=customer'
|
||
},
|
||
//...
|
||
```
|
||
|
||
The property name (`customer`) corresponds to the variable name of the `myCustomer` directive's isolated scope.
|
||
The value of the property (`=customer`) tells `$compile` to bind to the `customer` attribute.
|
||
|
||
<div class="alert alert-warning">
|
||
**Note:** These `=attr` attributes in the `scope` option of directives are normalized just like directive names.
|
||
To bind to the attribute in `<div bind-to-this="thing">`, you'd specify a binding of `=bindToThis`.
|
||
</div>
|
||
|
||
For cases where the attribute name is the same as the value you want to bind to inside
|
||
the directive's scope, you can use this shorthand syntax:
|
||
|
||
```javascript
|
||
//...
|
||
scope: {
|
||
// same as '=customer'
|
||
customer: '='
|
||
},
|
||
//...
|
||
```
|
||
|
||
Besides making it possible to bind different data to the scope inside a directive, using an isolated scope has another
|
||
effect.
|
||
|
||
We can show this by adding another property, `vojta`, to our scope and trying to access it
|
||
from within our directive's template:
|
||
|
||
<example module="docsIsolationExample">
|
||
<file name="script.js">
|
||
angular.module('docsIsolationExample', [])
|
||
.controller('Ctrl', function($scope) {
|
||
$scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
|
||
|
||
$scope.vojta = { name: 'Vojta', address: '3456 Somewhere Else' };
|
||
})
|
||
.directive('myCustomer', function() {
|
||
return {
|
||
restrict: 'E',
|
||
scope: {
|
||
customer: '=customer'
|
||
},
|
||
templateUrl: 'my-customer-plus-vojta.html'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<my-customer customer="naomi"></my-customer>
|
||
</div>
|
||
</file>
|
||
<file name="my-customer-plus-vojta.html">
|
||
Name: {{customer.name}} Address: {{customer.address}}
|
||
<hr>
|
||
Name: {{vojta.name}} Address: {{vojta.address}}
|
||
</file>
|
||
</example>
|
||
|
||
Notice that `{{vojta.name}}` and `{{vojta.address}}` are empty, meaning they are undefined.
|
||
Although we defined `vojta` in the controller, it's not available within the directive.
|
||
|
||
As the name suggests, the **isolate scope** of the directive isolates everything except models that
|
||
you've explicitly added to the `scope: {}` hash object. This is helpful when building reusable
|
||
components because it prevents a component from changing your model state except for the models
|
||
that you explicitly pass in.
|
||
|
||
<div class="alert alert-warning">
|
||
**Note:** Normally, a scope prototypically inherits from its parent. An isolated scope does not.
|
||
</div>
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** Use the `scope` option to create isolate scopes when making components that you
|
||
want to reuse throughout your app.
|
||
</div>
|
||
|
||
|
||
### Creating a Directive that Manipulates the DOM
|
||
|
||
In this example we will build a directive that displays the current time.
|
||
Once a second, it updates the DOM to reflect the current time.
|
||
|
||
Directives that want to modify the DOM typically use the `link` option.
|
||
`link` takes a function with the following signature, `function link(scope, element, attrs) { ... }` where:
|
||
|
||
* `scope` is an Angular scope object.
|
||
* `element` is the jqLite-wrapped element that this directive matches.
|
||
* `attrs` is an object with the normalized attribute names and their corresponding values.
|
||
|
||
In our `link` function, we want to update the displayed time once a second, or whenever a user
|
||
changes the time formatting string that our directive binds to. We also want to remove the timeout
|
||
if the directive is deleted so we don't introduce a memory leak.
|
||
|
||
<example module="docsTimeDirective">
|
||
<file name="script.js">
|
||
angular.module('docsTimeDirective', [])
|
||
.controller('Ctrl2', function($scope) {
|
||
$scope.format = 'M/d/yy h:mm:ss a';
|
||
})
|
||
.directive('myCurrentTime', function($timeout, dateFilter) {
|
||
|
||
function link(scope, element, attrs) {
|
||
var format,
|
||
timeoutId;
|
||
|
||
function updateTime() {
|
||
element.text(dateFilter(new Date(), format));
|
||
}
|
||
|
||
scope.$watch(attrs.myCurrentTime, function(value) {
|
||
format = value;
|
||
updateTime();
|
||
});
|
||
|
||
function scheduleUpdate() {
|
||
// save the timeoutId for canceling
|
||
timeoutId = $timeout(function() {
|
||
updateTime(); // update DOM
|
||
scheduleUpdate(); // schedule the next update
|
||
}, 1000);
|
||
}
|
||
|
||
element.on('$destroy', function() {
|
||
$timeout.cancel(timeoutId);
|
||
});
|
||
|
||
// start the UI update process.
|
||
scheduleUpdate();
|
||
}
|
||
|
||
return {
|
||
link: link
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl2">
|
||
Date format: <input ng-model="format"> <hr/>
|
||
Current time is: <span my-current-time="format"></span>
|
||
</div>
|
||
</file>
|
||
</example>
|
||
|
||
There are a couple of things to note here.
|
||
Just like the `module.controller` API, the function argument in `module.directive` is dependency injected.
|
||
Because of this, we can use `$timeout` and `dateFilter` inside our directive's `link` function.
|
||
|
||
We register an event `element.on('$destroy', ...)`. What fires this `$destroy` event?
|
||
|
||
There are a few special events that AngularJS emits. When a DOM node that has been compiled
|
||
with Angular's compiler is destroyed, it emits a `$destroy` event. Similarly, when an AngularJS
|
||
scope is destroyed, it broadcasts a `$destroy` event to listening scopes.
|
||
|
||
By listening to this event, you can remove event listeners that might cause memory leaks.
|
||
Listeners registered to scopes and elements are automatically cleaned up when they are destroyed,
|
||
but if you registered a listener on a service, or registered a listener on a DOM node that isn't
|
||
being deleted, you'll have to clean it up yourself or you risk introducing a memory leak.
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** Directives should clean up after themselves. You can use `element.on('$destroy', ...)`
|
||
or `scope.$on('$destroy', ...)` to run a clean-up function when the directive is removed.
|
||
</div>
|
||
|
||
|
||
### Creating a Directive that Wraps Other Elements
|
||
|
||
We've seen that you can pass in models to a directive using the isolate scope, but sometimes
|
||
it's desirable to be able to pass in an entire template rather than a string or an object.
|
||
Let's say that we want to create a "dialog box" component. The dialog box should be able to
|
||
wrap any arbitrary content.
|
||
|
||
To do this, we need to use the `transclude` option.
|
||
|
||
<example module="docsTransclusionDirective">
|
||
<file name="script.js">
|
||
angular.module('docsTransclusionDirective', [])
|
||
.controller('Ctrl', function($scope) {
|
||
$scope.name = 'Tobias';
|
||
})
|
||
.directive('myDialog', function() {
|
||
return {
|
||
restrict: 'E',
|
||
transclude: true,
|
||
templateUrl: 'my-dialog.html'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<my-dialog>Check out the contents, {{name}}!</my-dialog>
|
||
</div>
|
||
</file>
|
||
<file name="my-dialog.html">
|
||
<div class="alert" ng-transclude>
|
||
</div>
|
||
</file>
|
||
</example>
|
||
|
||
What does this `transclude` option do, exactly? `transclude` makes the contents of a directive with this
|
||
option have access to the scope **outside** of the directive rather than inside.
|
||
|
||
To illustrate this, see the example below. Notice that we've added a `link` function in `script.js` that
|
||
redefines `name` as `Jeff`. What do you think the `{{name}}` binding will resolve to now?
|
||
|
||
<example module="docsTransclusionExample">
|
||
<file name="script.js">
|
||
angular.module('docsTransclusionExample', [])
|
||
.controller('Ctrl', function($scope) {
|
||
$scope.name = 'Tobias';
|
||
})
|
||
.directive('myDialog', function() {
|
||
return {
|
||
restrict: 'E',
|
||
transclude: true,
|
||
scope: {},
|
||
templateUrl: 'my-dialog.html',
|
||
link: function (scope, element) {
|
||
scope.name = 'Jeff';
|
||
}
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<my-dialog>Check out the contents, {{name}}!</my-dialog>
|
||
</div>
|
||
</file>
|
||
<file name="my-dialog.html">
|
||
<div class="alert" ng-transclude>
|
||
</div>
|
||
</file>
|
||
</example>
|
||
|
||
Ordinarily, we would expect that `{{name}}` would be `Jeff`. However, we see in this example that
|
||
the `{{name}}` binding is still `Tobias`.
|
||
|
||
The `transclude` option changes the way scopes are nested. It makes it so that the **contents** of a
|
||
transcluded directive have whatever scope is outside the directive, rather than whatever scope is on
|
||
the inside. In doing so, it gives the contents access to the outside scope.
|
||
|
||
Note that if the directive did not create its own scope, then `scope` in `scope.name = 'Jeff';` would
|
||
reference the outside scope and we would see `Jeff` in the output.
|
||
|
||
This behavior makes sense for a directive that wraps some content, because otherwise you'd have to
|
||
pass in each model you wanted to use separately. If you have to pass in each model that you want to
|
||
use, then you can't really have arbitrary contents, can you?
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** only use `transclude: true` when you want to create a directive that wraps arbitrary content.
|
||
</div>
|
||
|
||
Next, we want to add buttons to this dialog box, and allow someone using the directive to bind their own
|
||
behavior to it.
|
||
|
||
<example module="docsIsoFnBindExample">
|
||
<file name="script.js">
|
||
angular.module('docsIsoFnBindExample', [])
|
||
.controller('Ctrl', function($scope, $timeout) {
|
||
$scope.name = 'Tobias';
|
||
$scope.hideDialog = function () {
|
||
$scope.dialogIsHidden = true;
|
||
$timeout(function () {
|
||
$scope.dialogIsHidden = false;
|
||
}, 2000);
|
||
};
|
||
})
|
||
.directive('myDialog', function() {
|
||
return {
|
||
restrict: 'E',
|
||
transclude: true,
|
||
scope: {
|
||
'close': '&onClose'
|
||
},
|
||
templateUrl: 'my-dialog-close.html'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<div ng-controller="Ctrl">
|
||
<my-dialog ng-hide="dialogIsHidden" on-close="dialogIsHidden = true">
|
||
Check out the contents, {{name}}!
|
||
</my-dialog>
|
||
</div>
|
||
</file>
|
||
<file name="my-dialog-close.html">
|
||
<div class="alert">
|
||
<a href class="close" ng-click="close()">×</a>
|
||
<div ng-transclude></div>
|
||
</div>
|
||
</file>
|
||
</example>
|
||
|
||
We want to run the function we pass by invoking it from the directive's scope, but have it run
|
||
in the context of the scope where its registered.
|
||
|
||
We saw earlier how to use `=prop` in the `scope` option, but in the above example, we're using
|
||
`&prop` instead. `&` bindings expose a function to an isolated scope allowing the isolated scope
|
||
to invoke it, but maintaining the original scope of the function. So when a user clicks the
|
||
`x` in the dialog, it runs `Ctrl`'s `close` function.
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** use `&prop` in the `scope` option when you want your directive
|
||
to expose an API for binding to behaviors.
|
||
</div>
|
||
|
||
|
||
### Creating a Directive that Adds Event Listeners
|
||
|
||
Previously, we used the `link` function to create a directive that manipulated its
|
||
DOM elements. Building upon that example, let's make a directive that reacts to events on
|
||
its elements.
|
||
|
||
For instance, what if we wanted to create a directive that lets a user drag an
|
||
element?
|
||
|
||
<example module="dragModule">
|
||
<file name="script.js">
|
||
angular.module('dragModule', []).
|
||
directive('myDraggable', function($document) {
|
||
return function(scope, element, attr) {
|
||
var startX = 0, startY = 0, x = 0, y = 0;
|
||
|
||
element.css({
|
||
position: 'relative',
|
||
border: '1px solid red',
|
||
backgroundColor: 'lightgrey',
|
||
cursor: 'pointer'
|
||
});
|
||
|
||
element.on('mousedown', function(event) {
|
||
// Prevent default dragging of selected content
|
||
event.preventDefault();
|
||
startX = event.pageX - x;
|
||
startY = event.pageY - y;
|
||
$document.on('mousemove', mousemove);
|
||
$document.on('mouseup', mouseup);
|
||
});
|
||
|
||
function mousemove(event) {
|
||
y = event.pageY - startY;
|
||
x = event.pageX - startX;
|
||
element.css({
|
||
top: y + 'px',
|
||
left: x + 'px'
|
||
});
|
||
}
|
||
|
||
function mouseup() {
|
||
$document.unbind('mousemove', mousemove);
|
||
$document.unbind('mouseup', mouseup);
|
||
}
|
||
}
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<span my-draggable>Drag ME</span>
|
||
</file>
|
||
</example>
|
||
|
||
|
||
|
||
### Creating Directives that Communicate
|
||
|
||
You can compose any directives by using them within templates.
|
||
|
||
Sometimes, you want a component that's built from a combination of directives.
|
||
|
||
Imagine you want to have a container with tabs in which the contents of the container correspond
|
||
to which tab is active.
|
||
|
||
<example module="docsTabsExample">
|
||
<file name="script.js">
|
||
angular.module('docsTabsExample', [])
|
||
.directive('myTabs', function() {
|
||
return {
|
||
restrict: 'E',
|
||
transclude: true,
|
||
scope: {},
|
||
controller: function($scope) {
|
||
var panes = $scope.panes = [];
|
||
|
||
$scope.select = function(pane) {
|
||
angular.forEach(panes, function(pane) {
|
||
pane.selected = false;
|
||
});
|
||
pane.selected = true;
|
||
};
|
||
|
||
this.addPane = function(pane) {
|
||
if (panes.length == 0) {
|
||
$scope.select(pane);
|
||
}
|
||
panes.push(pane);
|
||
};
|
||
},
|
||
templateUrl: 'my-tabs.html'
|
||
};
|
||
})
|
||
.directive('myPane', function() {
|
||
return {
|
||
require: '^myTabs',
|
||
restrict: 'E',
|
||
transclude: true,
|
||
scope: {
|
||
title: '@'
|
||
},
|
||
link: function(scope, element, attrs, tabsCtrl) {
|
||
tabsCtrl.addPane(scope);
|
||
},
|
||
templateUrl: 'my-pane.html'
|
||
};
|
||
});
|
||
</file>
|
||
<file name="index.html">
|
||
<my-tabs>
|
||
<my-pane title="Hello">
|
||
<h4>Hello</h4>
|
||
<p>Lorem ipsum dolor sit amet</p>
|
||
</my-pane>
|
||
<my-pane title="World">
|
||
<h4>World</h4>
|
||
<em>Mauris elementum elementum enim at suscipit.</em>
|
||
<p><a href ng-click="i = i + 1">counter: {{i || 0}}</a></p>
|
||
</my-pane>
|
||
</my-tabs>
|
||
</file>
|
||
<file name="my-tabs.html">
|
||
<div class="tabbable">
|
||
<ul class="nav nav-tabs">
|
||
<li ng-repeat="pane in panes" ng-class="{active:pane.selected}">
|
||
<a href="" ng-click="select(pane)">{{pane.title}}</a>
|
||
</li>
|
||
</ul>
|
||
<div class="tab-content" ng-transclude></div>
|
||
</div>
|
||
</file>
|
||
<file name="my-pane.html">
|
||
<div class="tab-pane" ng-show="selected" ng-transclude>
|
||
</div>
|
||
</file>
|
||
</example>
|
||
|
||
The `myPane` directive has a `require` option with value `^myTabs`. When a directive uses this
|
||
option, `$compile` will throw an error unless the specified controller is found. The `^` prefix
|
||
means that this directive searches for the controller on its parents (without the `^` prefix, the
|
||
directive would look for the controller on just its own element).
|
||
|
||
So where does this `myTabs` controller come from? Directives can specify controllers using
|
||
the unsurprisingly named `controller` option. As you can see, the `myTabs` directive uses this
|
||
option. Just like `ngController`, this option attaches a controller to the template of the directive.
|
||
|
||
Looking back at `myPane`'s definition, notice the last argument in its `link` function: `tabsCtrl`.
|
||
When a directive requires a controller, it receives that controller as the fourth argument of its
|
||
`link` function. Taking advantage of this, `myPane` can call the `addPane` function of `myTabs`.
|
||
|
||
Savvy readers may be wondering what the difference is between `link` and `controller`.
|
||
The basic difference is that `controller` can expose an API, and `link` functions can interact with
|
||
controllers using `require`.
|
||
|
||
<div class="alert alert-success">
|
||
**Best Practice:** use `controller` when you want to expose an API to other directives.
|
||
Otherwise use `link`.
|
||
</div>
|
||
|
||
### Summary
|
||
|
||
Here we've seen the main use cases for directives. Each of these samples acts as a good starting
|
||
point for creating your own directives.
|
||
|
||
You might also be interested in an in-depth explanation of the compilation process that's
|
||
available in the {@link guide/compiler compiler guide}.
|
||
|
||
The {@link api/ng.$compile `$compile` API} page has a comprehensive list of directive options for reference.
|
||
|