mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
345 lines
12 KiB
Plaintext
345 lines
12 KiB
Plaintext
@ngdoc overview
|
||
@name Expressions
|
||
@sortOrder 270
|
||
@description
|
||
|
||
# Angular Expressions
|
||
|
||
Angular expressions are JavaScript-like code snippets that are usually placed in bindings such as
|
||
`{{ expression }}`.
|
||
|
||
For example, these are valid expressions in Angular:
|
||
|
||
* `1+2`
|
||
* `a+b`
|
||
* `user.name`
|
||
* `items[index]`
|
||
|
||
|
||
## Angular Expressions vs. JavaScript Expressions
|
||
|
||
Angular expressions are like JavaScript expressions with the following differences:
|
||
|
||
* **Context:** JavaScript expressions are evaluated against the global `window`.
|
||
In Angular, expressions are evaluated against a {@link ng.$rootScope.Scope `scope`} object.
|
||
|
||
* **Forgiving:** In JavaScript, trying to evaluate undefined properties generates `ReferenceError`
|
||
or `TypeError`. In Angular, expression evaluation is forgiving to `undefined` and `null`.
|
||
|
||
* **No Control Flow Statements:** You cannot use the following in an Angular expression:
|
||
conditionals, loops, or exceptions.
|
||
|
||
* **No Function Declarations:** You cannot declare functions in an Angular expression,
|
||
even inside `ng-init` directive.
|
||
|
||
* **No RegExp Creation With Literal Notation:** You cannot create regular expressions
|
||
in an Angular expression.
|
||
|
||
* **No Comma And Void Operators:** You cannot use `,` or `void` in an Angular expression.
|
||
|
||
* **Filters:** You can use {@link guide/filter filters} within expressions to format data before
|
||
displaying it.
|
||
|
||
If you want to run more complex JavaScript code, you should make it a controller method and call
|
||
the method from your view. If you want to `eval()` an Angular expression yourself, use the
|
||
{@link ng.$rootScope.Scope#$eval `$eval()`} method.
|
||
|
||
## Example
|
||
<example>
|
||
<file name="index.html">
|
||
<span>
|
||
1+2={{1+2}}
|
||
</span>
|
||
</file>
|
||
|
||
<file name="protractor.js" type="protractor">
|
||
it('should calculate expression in binding', function() {
|
||
expect(element(by.binding('1+2')).getText()).toEqual('1+2=3');
|
||
});
|
||
</file>
|
||
</example>
|
||
|
||
You can try evaluating different expressions here:
|
||
|
||
<example module="expressionExample">
|
||
<file name="index.html">
|
||
<div ng-controller="ExampleController" class="expressions">
|
||
Expression:
|
||
<input type='text' ng-model="expr" size="80"/>
|
||
<button ng-click="addExp(expr)">Evaluate</button>
|
||
<ul>
|
||
<li ng-repeat="expr in exprs track by $index">
|
||
[ <a href="" ng-click="removeExp($index)">X</a> ]
|
||
<tt>{{expr}}</tt> => <span ng-bind="$parent.$eval(expr)"></span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</file>
|
||
|
||
<file name="script.js">
|
||
angular.module('expressionExample', [])
|
||
.controller('ExampleController', ['$scope', function($scope) {
|
||
var exprs = $scope.exprs = [];
|
||
$scope.expr = '3*10|currency';
|
||
$scope.addExp = function(expr) {
|
||
exprs.push(expr);
|
||
};
|
||
|
||
$scope.removeExp = function(index) {
|
||
exprs.splice(index, 1);
|
||
};
|
||
}]);
|
||
</file>
|
||
|
||
<file name="protractor.js" type="protractor">
|
||
it('should allow user expression testing', function() {
|
||
element(by.css('.expressions button')).click();
|
||
var lis = element(by.css('.expressions ul')).all(by.repeater('expr in exprs'));
|
||
expect(lis.count()).toBe(1);
|
||
expect(lis.get(0).getText()).toEqual('[ X ] 3*10|currency => $30.00');
|
||
});
|
||
</file>
|
||
</example>
|
||
|
||
|
||
## Context
|
||
|
||
Angular does not use JavaScript's `eval()` to evaluate expressions. Instead Angular's
|
||
{@link ng.$parse $parse} service processes these expressions.
|
||
|
||
Angular expressions do not have access to global variables like `window`, `document` or `location`.
|
||
This restriction is intentional. It prevents accidental access to the global state – a common source of subtle bugs.
|
||
|
||
Instead use services like `$window` and `$location` in functions called from expressions. Such services
|
||
provide mockable access to globals.
|
||
|
||
<example module="expressionExample">
|
||
<file name="index.html">
|
||
<div class="example2" ng-controller="ExampleController">
|
||
Name: <input ng-model="name" type="text"/>
|
||
<button ng-click="greet()">Greet</button>
|
||
<button ng-click="window.alert('Should not see me')">Won't greet</button>
|
||
</div>
|
||
</file>
|
||
|
||
<file name="script.js">
|
||
angular.module('expressionExample', [])
|
||
.controller('ExampleController', ['$window', '$scope', function($window, $scope) {
|
||
$scope.name = 'World';
|
||
|
||
$scope.greet = function() {
|
||
$window.alert('Hello ' + $scope.name);
|
||
};
|
||
}]);
|
||
</file>
|
||
|
||
<file name="protractor.js" type="protractor">
|
||
it('should calculate expression in binding', function() {
|
||
if (browser.params.browser == 'safari') {
|
||
// Safari can't handle dialogs.
|
||
return;
|
||
}
|
||
element(by.css('[ng-click="greet()"]')).click();
|
||
|
||
var alertDialog = browser.switchTo().alert();
|
||
|
||
expect(alertDialog.getText()).toEqual('Hello World');
|
||
|
||
alertDialog.accept();
|
||
});
|
||
</file>
|
||
</example>
|
||
|
||
## Forgiving
|
||
|
||
Expression evaluation is forgiving to undefined and null. In JavaScript, evaluating `a.b.c` throws
|
||
an exception if `a` is not an object. While this makes sense for a general purpose language, the
|
||
expression evaluations are primarily used for data binding, which often look like this:
|
||
|
||
{{a.b.c}}
|
||
|
||
It makes more sense to show nothing than to throw an exception if `a` is undefined (perhaps we are
|
||
waiting for the server response, and it will become defined soon). If expression evaluation wasn't
|
||
forgiving we'd have to write bindings that clutter the code, for example: `{{((a||{}).b||{}).c}}`
|
||
|
||
Similarly, invoking a function `a.b.c()` on `undefined` or `null` simply returns `undefined`.
|
||
|
||
|
||
## No Control Flow Statements
|
||
|
||
Apart from the ternary operator (`a ? b : c`), you cannot write a control flow statement in an
|
||
expression. The reason behind this is core to the Angular philosophy that application logic should
|
||
be in controllers, not the views. If you need a real conditional, loop, or to throw from a view
|
||
expression, delegate to a JavaScript method instead.
|
||
|
||
## No function declarations or RegExp creation with literal notation
|
||
|
||
You can't declare functions or create regular expressions from within AngularJS expressions. This is
|
||
to avoid complex model transformation logic inside templates. Such logic is better placed in a
|
||
controller or in a dedicated filter where it can be tested properly.
|
||
|
||
## `$event`
|
||
|
||
Directives like {@link ng.directive:ngClick `ngClick`} and {@link ng.directive:ngFocus `ngFocus`}
|
||
expose a `$event` object within the scope of that expression. The object is an instance of a [jQuery
|
||
Event Object](http://api.jquery.com/category/events/event-object/) when jQuery is present or a
|
||
similar jqLite object.
|
||
|
||
<example module="eventExampleApp">
|
||
<file name="index.html">
|
||
<div ng-controller="EventController">
|
||
<button ng-click="clickMe($event)">Event</button>
|
||
<p><code>$event</code>: <pre> {{$event | json}}</pre></p>
|
||
<p><code>clickEvent</code>: <pre>{{clickEvent | json}}</pre></p>
|
||
</div>
|
||
</file>
|
||
|
||
<file name="script.js">
|
||
angular.module('eventExampleApp', []).
|
||
controller('EventController', ['$scope', function($scope) {
|
||
/*
|
||
* expose the event object to the scope
|
||
*/
|
||
$scope.clickMe = function(clickEvent) {
|
||
$scope.clickEvent = simpleKeys(clickEvent);
|
||
console.log(clickEvent);
|
||
};
|
||
|
||
/*
|
||
* return a copy of an object with only non-object keys
|
||
* we need this to avoid circular references
|
||
*/
|
||
function simpleKeys (original) {
|
||
return Object.keys(original).reduce(function (obj, key) {
|
||
obj[key] = typeof original[key] === 'object' ? '{ ... }' : original[key];
|
||
return obj;
|
||
}, {});
|
||
}
|
||
}]);
|
||
</file>
|
||
</example>
|
||
|
||
Note in the example above how we can pass in `$event` to `clickMe`, but how it does not show up
|
||
in `{{$event}}`. This is because `$event` is outside the scope of that binding.
|
||
|
||
|
||
## One-time binding
|
||
|
||
An expression that starts with `::` is considered a one-time expression. One-time expressions
|
||
will stop recalculating once they are stable, which happens after the first digest if the expression
|
||
result is a non-undefined value (see value stabilization algorithm below).
|
||
|
||
<example module="oneTimeBidingExampleApp">
|
||
<file name="index.html">
|
||
<div ng-controller="EventController">
|
||
<button ng-click="clickMe($event)">Click Me</button>
|
||
<p id="one-time-binding-example">One time binding: {{::name}}</p>
|
||
<p id="normal-binding-example">Normal binding: {{name}}</p>
|
||
</div>
|
||
</file>
|
||
<file name="script.js">
|
||
angular.module('oneTimeBidingExampleApp', []).
|
||
controller('EventController', ['$scope', function($scope) {
|
||
var counter = 0;
|
||
var names = ['Igor', 'Misko', 'Chirayu', 'Lucas'];
|
||
/*
|
||
* expose the event object to the scope
|
||
*/
|
||
$scope.clickMe = function(clickEvent) {
|
||
$scope.name = names[counter % names.length];
|
||
counter++;
|
||
};
|
||
}]);
|
||
</file>
|
||
<file name="protractor.js" type="protractor">
|
||
it('should freeze binding after its value has stabilized', function() {
|
||
var oneTimeBiding = element(by.id('one-time-binding-example'));
|
||
var normalBinding = element(by.id('normal-binding-example'));
|
||
|
||
expect(oneTimeBiding.getText()).toEqual('One time binding:');
|
||
expect(normalBinding.getText()).toEqual('Normal binding:');
|
||
element(by.buttonText('Click Me')).click();
|
||
|
||
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
|
||
expect(normalBinding.getText()).toEqual('Normal binding: Igor');
|
||
element(by.buttonText('Click Me')).click();
|
||
|
||
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
|
||
expect(normalBinding.getText()).toEqual('Normal binding: Misko');
|
||
|
||
element(by.buttonText('Click Me')).click();
|
||
element(by.buttonText('Click Me')).click();
|
||
|
||
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
|
||
expect(normalBinding.getText()).toEqual('Normal binding: Lucas');
|
||
});
|
||
</file>
|
||
</example>
|
||
|
||
|
||
### Why this feature
|
||
|
||
The main purpose of one-time binding expression is to provide a way to create a binding
|
||
that gets deregistered and frees up resources once the binding is stabilized.
|
||
Reducing the number of expressions being watched makes the digest loop faster and allows more
|
||
information to be displayed at the same time.
|
||
|
||
|
||
### Value stabilization algorithm
|
||
|
||
One-time binding expressions will retain the value of the expression at the end of the
|
||
digest cycle as long as that value is not undefined. If the value of the expression is set
|
||
within the digest loop and later, within the same digest loop, it is set to undefined,
|
||
then the expression is not fulfilled and will remain watched.
|
||
|
||
1. Given an expression that starts with `::`, when a digest loop is entered and expression
|
||
is dirty-checked, store the value as V
|
||
2. If V is not undefined, mark the result of the expression as stable and schedule a task
|
||
to deregister the watch for this expression when we exit the digest loop
|
||
3. Process the digest loop as normal
|
||
4. When digest loop is done and all the values have settled process the queue of watch
|
||
deregistration tasks. For each watch to be deregistered check if it still evaluates
|
||
to value that is not `undefined`. If that's the case, deregister the watch. Otherwise
|
||
keep dirty-checking the watch in the future digest loops by following the same
|
||
algorithm starting from step 1
|
||
|
||
|
||
### How to benefit from one-time binding
|
||
|
||
If the expression will not change once set, it is a candidate for one-time binding.
|
||
Here are three example cases.
|
||
|
||
When interpolating text or attributes:
|
||
|
||
```html
|
||
<div name="attr: {{::color}}">text: {{::name}}</div>
|
||
```
|
||
|
||
When using a directive with bidirectional binding and the parameters will not change:
|
||
|
||
```js
|
||
someModule.directive('someDirective', function() {
|
||
return {
|
||
scope: {
|
||
name: '=',
|
||
color: '@'
|
||
},
|
||
template: '{{name}}: {{color}}'
|
||
};
|
||
});
|
||
```
|
||
|
||
```html
|
||
<div some-directive name="::myName" color="My color is {{::myColor}}"></div>
|
||
```
|
||
|
||
|
||
When using a directive that takes an expression:
|
||
|
||
```html
|
||
<ul>
|
||
<li ng-repeat="item in ::items">{{item.name}};</li>
|
||
</ul>
|
||
```
|
||
|