Compare commits

...

24 Commits

Author SHA1 Message Date
George Kalpakas 223cf2b5bb docs(CHANGELOG): add release notes for 1.7.2 2018-06-12 16:34:38 +03:00
George Kalpakas c387e0d79d chore(doc-gen): error on unmatched links
Closes #16597
2018-06-12 14:45:22 +03:00
George Kalpakas cbf74d5d64 docs(*): fix dangling or ambiguous links 2018-06-12 14:45:06 +03:00
George Kalpakas a812327acd revert: refactor($compile): remove preAssignBindingsEnabled leftovers
This reverts commit 8e104ee508.

This internal clean-up turned out to break popular UI libraries (e.g.
`ngMaterial`, `ui-bootstrap`) and cause pain to developers.

Fixes #16594

Closes #16595
2018-06-11 17:09:48 +03:00
George Kalpakas 35fce310e9 docs(CHANGELOG): fix links to issues/PRs 2018-06-08 17:10:08 +03:00
George Kalpakas 93a754a490 docs(CHANGELOG): add release notes for 1.7.1 2018-06-08 16:26:22 +03:00
Georgios Kalpakas 7d9d387195 feat(ngAria): add support for ignoring a specific element
Fixes #14602
Fixes #14672

Closes #14833
2018-06-08 11:23:37 +03:00
Georgios Kalpakas feac52d840 refactor(ngAria): move test helpers inside of closure 2018-06-08 11:23:26 +03:00
Georgios Kalpakas f4f571efdf feat($route): add support for the reloadOnUrl configuration option
Enables users to specify that a particular route should not be reloaded after a
URL change (including a change in `$location.path()`), if the new URL maps to
the same route.
The default behavior is still to always load the matched route when any part of
the URL changes.

Related to #1699, #5860, #14999 (potentially closing the first two).

Fixes #7925

Closes #15002
2018-06-08 11:01:35 +03:00
Martin Staffa a8c263c194 feat(ngMessages): add support for default message
add support for showing default message when a truthy value is not matched by an ng-message directive.

Closes #12008
Closes #12213
Closes #16587
2018-06-06 17:54:59 +02:00
Martin Staffa 3d6c45d76e feat(errorHandlingConfig): add option to exclude error params from url
Specific errors, such as those during nested module loading, can create very long
error urls because the error message includes the error stack. These urls create visual
clutter in the browser console, are often not clickable, and may be rejected
by the docs page because they are simply too long.

We've already made improvements to the error display in #16283, which excludes
the error url from error parameters, which results in cleaner error messages.

Further, modern browsers restrict console message length intelligently.

This option can still be useful for older browsers like Internet Explorer, or
in general to reduce visual clutter in the console.

Closes #14744
Closes #15707
Closes #16283
Closes #16299 
Closes #16591
2018-06-06 17:54:57 +02:00
Martin Staffa bf841d3512 feat(ngRef): add directive to publish controller, or element into scope
Thanks to @drpicox for the original implementation: PR #14080 

Closes #16511
2018-06-06 17:54:52 +02:00
Christian Oliff a1d88457de docs(.editorconfig): change link to use https 2018-06-06 17:54:40 +02:00
Jakub Freisler b011ae9544 docs(ngAnimate): add "animating between value changes" section
Add a section which covers use case when users need to animate upon
a variable's value changes (not between two states).

Refers #16561

Closes #16582
2018-06-05 23:24:52 +03:00
Georgii Dolzhykov 63fdee6b9c docs($httpBackend): headers param of expect* can be function
Closes #16588
2018-06-04 15:18:34 +03:00
Jakub Freisler 257ebbb514 docs(ngAnimate): refactor of ngAnimate docs
- Synced "animation aware" directives tables in API docs and "Animation"
  guide.
- Sorted tables alphabetically.
- Added info about `ngAnimateSwap` directive.

References #16561

Closes #16581
2018-06-02 11:49:37 +03:00
George Kalpakas 2b6c986736 fix(ngModel): do not throw if view value changes on destroyed scope
This could for example happen if updating the value is debounced (either
by asynchronously calling `$setViewValue()` or via `ngModelOptions`).

Fixes #16583

Closes #16585
2018-05-31 12:31:22 +03:00
Georgii Dolzhykov 2da4950406 refactor($compile): remove preAssignBindingsEnabled leftovers
Now that we don't need to support `preAssignBindingsEnabled` (removed in #15782),
complexity introduced in `$controller` by #7645 can be removed.

One difference with the previous implementation is that all non-ES2015-class controller instances
were available on the element before calling their constructors. Now it depends on the relative
order of controllers. Controller constructors shouldn't be used to access other controllers
(e.g. via `$element.controller(directiveName)`). The recommended way is to use the `require`
property of the directive definition object and the life cycle hooks `$onChanges` or `$onInit`.

See
https://docs.angularjs.org/api/ng/service/$compile#-require-
https://docs.angularjs.org/api/ng/service/$compile#life-cycle-hooks

Closes #16580
2018-05-30 23:05:29 +03:00
George Kalpakas 789db83a8a fix($compile): support transcluding multi-element directives
Previously, transcluding multi-element directives (e.g. `foo-start`/`foo-end`)
was not supported on elements with multi-slot transclusion (a `uterdir` error
was thrown).
This commit fixes it by putting the transcluded nodes into a DocumentFragment,
where they can be traversed via `.nextSibling`.

Fixes #15554
Closes #15555
2018-05-26 17:29:46 +02:00
Martin Staffa ce443792c4 docs(ngMockE2E): add docs for $httpBackend.matchLatestDefinitionEnabled()
Closes #16577
2018-05-26 17:29:43 +02:00
Martin Staffa dd47867bfb chore: try ios 11.2 2018-05-26 11:05:27 +02:00
George Kalpakas f2ebb82ba5 refactor(ngModelOptions): fix ng-closure-runner warning
Without this fix `grunt minall` emits the following warning:
> WARNING - Parse error. Non-JSDoc comment has annotations.
> Did you mean to start it with '/**'?

Closes #16575
2018-05-24 13:02:28 +03:00
Martin Staffa 12698755be chore: set ios 11 in saucelabs to 11.3 2018-05-24 00:02:17 +02:00
Martin Staffa f7d7954904 chore: add platform to ios saucelabs 2018-05-24 00:02:13 +02:00
30 changed files with 2874 additions and 246 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# http://editorconfig.org
# https://editorconfig.org
root = true
+76
View File
@@ -1,3 +1,79 @@
<a name="1.7.2"></a>
# 1.7.2 extreme-compatiplication (2018-06-12)
In the previous release, we removed a private, undocumented API that was no longer used by
AngularJS. It turned out that several popular UI libraries (such as
[AngularJS Material](https://material.angularjs.org/),
[UI Bootstrap](https://angular-ui.github.io/bootstrap/),
[ngDialog](http://likeastore.github.io/ngDialog/) and probably others) relied on that API.
In order to avoid unnecessary pain for developers, this release reverts the removal of the private
API and restores compatibility of the aforementioned libraries with the latest AngularJS.
## Reverts
- **$compile:** remove `preAssignBindingsEnabled` leftovers
([2da495](https://github.com/angular/angular.js/commit/2da49504065e9e2b71a7a5622e45118d8abbe87e),
[#16580](https://github.com/angular/angular.js/pull/16580),
[a81232](https://github.com/angular/angular.js/commit/a812327acda8bc890a4c4e809f0debb761c29625),
[#16595](https://github.com/angular/angular.js/pull/16595))
<a name="1.7.1"></a>
# 1.7.1 momentum-defiance (2018-06-08)
## Bug Fixes
- **$compile:** support transcluding multi-element directives
([789db8](https://github.com/angular/angular.js/commit/789db83a8ae0e2db5db13289b2c29e56093d967a),
[#15554](https://github.com/angular/angular.js/issues/15554),
[#15555](https://github.com/angular/angular.js/issues/15555))
- **ngModel:** do not throw if view value changes on destroyed scope
([2b6c98](https://github.com/angular/angular.js/commit/2b6c9867369fd3ef1ddb687af1153478ab62ee1b),
[#16583](https://github.com/angular/angular.js/issues/16583),
[#16585](https://github.com/angular/angular.js/issues/16585))
## New Features
- **$compile:** add one-way collection bindings
([f9d1ca](https://github.com/angular/angular.js/commit/f9d1ca20c38f065f15769fbe23aee5314cb58bd4),
[#14039](https://github.com/angular/angular.js/issues/14039),
[#16553](https://github.com/angular/angular.js/issues/16553),
[#15874](https://github.com/angular/angular.js/issues/15874))
- **ngRef:** add directive to publish controller, or element into scope
([bf841d](https://github.com/angular/angular.js/commit/bf841d35120bf3c4655fde46af4105c85a0f1cdc),
[#16511](https://github.com/angular/angular.js/issues/16511))
- **errorHandlingConfig:** add option to exclude error params from url
([3d6c45](https://github.com/angular/angular.js/commit/3d6c45d76e30b1b3c4eb9672cf4a93e5251c06b3),
[#14744](https://github.com/angular/angular.js/issues/14744),
[#15707](https://github.com/angular/angular.js/issues/15707),
[#16283](https://github.com/angular/angular.js/issues/16283),
[#16299](https://github.com/angular/angular.js/issues/16299),
[#16591](https://github.com/angular/angular.js/issues/16591))
- **ngAria:** add support for ignoring a specific element
([7d9d38](https://github.com/angular/angular.js/commit/7d9d387195292cb5e04984602b752d31853cfea6),
[#14602](https://github.com/angular/angular.js/issues/14602),
[#14672](https://github.com/angular/angular.js/issues/14672),
[#14833](https://github.com/angular/angular.js/issues/14833))
- **ngCookies:** support samesite option
([10a229](https://github.com/angular/angular.js/commit/10a229ce1befdeaf6295d1635dc11391c252a91a),
[#16543](https://github.com/angular/angular.js/issues/16543),
[#16544](https://github.com/angular/angular.js/issues/16544))
- **ngMessages:** add support for default message
([a8c263](https://github.com/angular/angular.js/commit/a8c263c1947cc85ee60b4732f7e4bcdc7ba463e8),
[#12008](https://github.com/angular/angular.js/issues/12008),
[#12213](https://github.com/angular/angular.js/issues/12213),
[#16587](https://github.com/angular/angular.js/issues/16587))
- **ngMock, ngMockE2E:** add option to match latest definition for `$httpBackend` request
([773f39](https://github.com/angular/angular.js/commit/773f39c9345479f5f8b6321236ce6ad96f77aa92),
[#16251](https://github.com/angular/angular.js/issues/16251),
[#11637](https://github.com/angular/angular.js/issues/11637),
[#16560](https://github.com/angular/angular.js/issues/16560))
- **$route:** add support for the `reloadOnUrl` configuration option
([f4f571](https://github.com/angular/angular.js/commit/f4f571efdf86d6acbcd5c6b1de66b4b33a259125),
[#7925](https://github.com/angular/angular.js/issues/7925),
[#15002](https://github.com/angular/angular.js/issues/15002))
<a name="1.7.0"></a>
# 1.7.0 nonexistent-physiology (2018-05-11)
+1
View File
@@ -74,6 +74,7 @@ var angularFiles = {
'src/ng/directive/ngNonBindable.js',
'src/ng/directive/ngOptions.js',
'src/ng/directive/ngPluralize.js',
'src/ng/directive/ngRef.js',
'src/ng/directive/ngRepeat.js',
'src/ng/directive/ngShowHide.js',
'src/ng/directive/ngStyle.js',
+1
View File
@@ -148,6 +148,7 @@ module.exports = new Package('angularjs', [
.config(function(checkAnchorLinksProcessor) {
checkAnchorLinksProcessor.base = '/';
checkAnchorLinksProcessor.errorOnUnmatchedLinks = true;
// We are only interested in docs that have an area (i.e. they are pages)
checkAnchorLinksProcessor.checkDoc = function(doc) { return doc.area; };
})
+17
View File
@@ -0,0 +1,17 @@
@ngdoc error
@name ngRef:noctrl
@fullName A controller for the value of `ngRefRead` could not be found on the element.
@description
This error occurs when the {@link ng.ngRef ngRef directive} specifies
a value in `ngRefRead` that cannot be resolved to a directive / component controller.
Causes for this error can be:
1. Your `ngRefRead` value has a typo.
2. You have a typo in the *registered* directive / component name.
3. The directive / component does not have a controller.
Note that `ngRefRead` takes the name of the component / directive, not the name of controller, and
also not the combination of directive and 'Controller'. For example, for a directive called 'myDirective',
the correct declaration is `<div ng-ref="$ctrl.ref" ng-ref-read="myDirective">`.
+27
View File
@@ -0,0 +1,27 @@
@ngdoc error
@name ngRef:nonassign
@fullName Non-Assignable Expression
@description
This error occurs when ngRef defines an expression that is not-assignable.
In order for ngRef to work, it must be possible to write the reference into the path defined with the expression.
For example, the following expressions are non-assignable:
```
<my-directive ng-ref="{}"></my-directive>
<my-directive ng-ref="myFn()"></my-directive>
<!-- missing attribute value is also invalid -->
<my-directive ng-ref></my-directive>
```
To resolve this error, use a path expression that is assignable:
```
<my-directive ng-ref="$ctrl.reference"></my-directive>
```
+13 -10
View File
@@ -222,23 +222,26 @@ triggered:
| Directive | Supported Animations |
|-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
| {@link ng.directive:ngIf#animations ngIf} | enter and leave |
| {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
| {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
| {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
| {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave |
| {@link ng.directive:ngClass#animations ngClass / {{class&#125;&#8203;&#125;} | add and remove |
| {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
| {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
| {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
| {@link ng.directive:ngIf#animations ngIf} | enter and leave |
| {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
| {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
| {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
| {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
| {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
(More information can be found by visiting the documentation associated with each directive.)
For a full breakdown of the steps involved during each animation event, refer to the
{@link ng.$animate API docs}.
{@link ng.$animate `$animate` API docs}.
## How do I use animations in my own directives?
+16 -8
View File
@@ -30,13 +30,21 @@ Any version branch not shown in the following table (e.g. 1.5.x) is no longer be
Any version branch not shown in the following table (e.g. 1.6.x) is no longer being developed.
<table class="dev-status table table-bordered">
<thead>
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
</thead>
<tbody>
<tr class="security"><td><span>1.2.x</span></td><td>Long Term Support</td><td>Last version to provide IE 8 support</td></tr>
<tr class="stable"><td><span>1.7.x</span></td><td>Long Term Support</td><td>See [Long Term Support](#long-term-support) section below.</td></tr>
</tbody>
<thead>
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
</thead>
<tbody>
<tr class="security">
<td><span>1.2.x</span></td>
<td>Long Term Support</td>
<td>Last version to provide IE 8 support</td>
</tr>
<tr class="stable">
<td><span>1.7.x</span></td>
<td>Long Term Support</td>
<td>See {@link version-support-status#long-term-support Long Term Support} section below.</td>
</tr>
</tbody>
</table>
### Long Term Support
@@ -51,4 +59,4 @@ At this time we will focus exclusively on providing fixes to bugs that satisfy a
### Blog Post
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
+3 -1
View File
@@ -107,12 +107,14 @@ module.exports = function(config, specificOptions) {
'SL_iOS_10': {
base: 'SauceLabs',
browserName: 'iphone',
platform: 'OS X 10.12',
version: '10.3'
},
'SL_iOS_11': {
base: 'SauceLabs',
browserName: 'iphone',
version: '11'
platform: 'OS X 10.12',
version: '11.2'
},
'BS_Chrome': {
+2 -2
View File
@@ -32,8 +32,8 @@
"commitplease": "^2.7.10",
"cross-spawn": "^4.0.0",
"cz-conventional-changelog": "1.1.4",
"dgeni": "^0.4.0",
"dgeni-packages": "^0.16.4",
"dgeni": "^0.4.9",
"dgeni-packages": "^0.26.2",
"eslint-plugin-promise": "^3.6.0",
"event-stream": "~3.1.0",
"glob": "^6.0.1",
+2
View File
@@ -28,6 +28,7 @@
ngInitDirective,
ngNonBindableDirective,
ngPluralizeDirective,
ngRefDirective,
ngRepeatDirective,
ngShowDirective,
ngStyleDirective,
@@ -194,6 +195,7 @@ function publishExternalAPI(angular) {
ngInit: ngInitDirective,
ngNonBindable: ngNonBindableDirective,
ngPluralize: ngPluralizeDirective,
ngRef: ngRefDirective,
ngRepeat: ngRepeatDirective,
ngShow: ngShowDirective,
ngStyle: ngStyleDirective,
+16 -3
View File
@@ -7,7 +7,8 @@
*/
var minErrConfig = {
objectMaxDepth: 5
objectMaxDepth: 5,
urlErrorParamsEnabled: true
};
/**
@@ -30,12 +31,21 @@ var minErrConfig = {
* * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
* non-positive or non-numeric value, removes the max depth limit.
* Default: 5
*
* * `urlErrorParamsEnabled` **{Boolean}** - Specifies wether the generated error url will
* contain the parameters of the thrown error. Disabling the parameters can be useful if the
* generated error url is very long.
*
* Default: true. When used without argument, it returns the current value.
*/
function errorHandlingConfig(config) {
if (isObject(config)) {
if (isDefined(config.objectMaxDepth)) {
minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
}
if (isDefined(config.urlErrorParamsEnabled) && isBoolean(config.urlErrorParamsEnabled)) {
minErrConfig.urlErrorParamsEnabled = config.urlErrorParamsEnabled;
}
} else {
return minErrConfig;
}
@@ -50,6 +60,7 @@ function isValidObjectMaxDepth(maxDepth) {
return isNumber(maxDepth) && maxDepth > 0;
}
/**
* @description
*
@@ -113,8 +124,10 @@ function minErr(module, ErrorConstructor) {
message += '\n' + url + (module ? module + '/' : '') + code;
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
if (minErrConfig.urlErrorParamsEnabled) {
for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
}
}
return new ErrorConstructor(message);
+7 -5
View File
@@ -2575,7 +2575,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// We have transclusion slots,
// collect them up, compile them and store their transclusion functions
$template = [];
$template = window.document.createDocumentFragment();
var slotMap = createMap();
var filledSlots = createMap();
@@ -2603,10 +2603,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var slotName = slotMap[directiveNormalize(nodeName_(node))];
if (slotName) {
filledSlots[slotName] = true;
slots[slotName] = slots[slotName] || [];
slots[slotName].push(node);
slots[slotName] = slots[slotName] || window.document.createDocumentFragment();
slots[slotName].appendChild(node);
} else {
$template.push(node);
$template.appendChild(node);
}
});
@@ -2620,9 +2620,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
for (var slotName in slots) {
if (slots[slotName]) {
// Only define a transclusion function if the slot was filled
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName].childNodes, transcludeFn);
}
}
$template = $template.childNodes;
}
$compileNode.empty(); // clear contents
+2 -1
View File
@@ -287,6 +287,7 @@ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $
this.$$currentValidationRunId = 0;
this.$$scope = $scope;
this.$$rootScope = $scope.$root;
this.$$attr = $attr;
this.$$element = $element;
this.$$animate = $animate;
@@ -864,7 +865,7 @@ NgModelController.prototype = {
this.$$pendingDebounce = this.$$timeout(function() {
that.$commitViewValue();
}, debounceDelay);
} else if (this.$$scope.$root.$$phase) {
} else if (this.$$rootScope.$$phase) {
this.$commitViewValue();
} else {
this.$$scope.$apply(function() {
+1 -1
View File
@@ -41,7 +41,7 @@ ModelOptions.prototype = {
options = extend({}, options);
// Inherit options from the parent if specified by the value `"$inherit"`
forEach(options, /* @this */ function(option, key) {
forEach(options, /** @this */ function(option, key) {
if (option === '$inherit') {
if (key === '*') {
inheritAll = true;
+296
View File
@@ -0,0 +1,296 @@
'use strict';
/**
* @ngdoc directive
* @name ngRef
* @restrict A
*
* @description
* The `ngRef` attribute tells AngularJS to assign the controller of a component (or a directive)
* to the given property in the current scope. It is also possible to add the jqlite-wrapped DOM
* element to the scope.
*
* If the element with `ngRef` is destroyed `null` is assigned to the property.
*
* Note that if you want to assign from a child into the parent scope, you must initialize the
* target property on the parent scope, otherwise `ngRef` will assign on the child scope.
* This commonly happens when assigning elements or components wrapped in {@link ngIf} or
* {@link ngRepeat}. See the second example below.
*
*
* @element ANY
* @param {string} ngRef property name - A valid AngularJS expression identifier to which the
* controller or jqlite-wrapped DOM element will be bound.
* @param {string=} ngRefRead read value - The name of a directive (or component) on this element,
* or the special string `$element`. If a name is provided, `ngRef` will
* assign the matching controller. If `$element` is provided, the element
* itself is assigned (even if a controller is available).
*
*
* @example
* ### Simple toggle
* This example shows how the controller of the component toggle
* is reused in the template through the scope to use its logic.
* <example name="ng-ref-component" module="myApp">
* <file name="index.html">
* <my-toggle ng-ref="myToggle"></my-toggle>
* <button ng-click="myToggle.toggle()">Toggle</button>
* <div ng-show="myToggle.isOpen()">
* You are using a component in the same template to show it.
* </div>
* </file>
* <file name="index.js">
* angular.module('myApp', [])
* .component('myToggle', {
* controller: function ToggleController() {
* var opened = false;
* this.isOpen = function() { return opened; };
* this.toggle = function() { opened = !opened; };
* }
* });
* </file>
* <file name="protractor.js" type="protractor">
* it('should publish the toggle into the scope', function() {
* var toggle = element(by.buttonText('Toggle'));
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(false);
* toggle.click();
* expect(toggle.evaluate('myToggle.isOpen()')).toEqual(true);
* });
* </file>
* </example>
*
* @example
* ### ngRef inside scopes
* This example shows how `ngRef` works with child scopes. The `ngRepeat`-ed `myWrapper` components
* are assigned to the scope of `myRoot`, because the `toggles` property has been initialized.
* The repeated `myToggle` components are published to the child scopes created by `ngRepeat`.
* `ngIf` behaves similarly - the assignment of `myToggle` happens in the `ngIf` child scope,
* because the target property has not been initialized on the `myRoot` component controller.
*
* <example name="ng-ref-scopes" module="myApp">
* <file name="index.html">
* <my-root></my-root>
* </file>
* <file name="index.js">
* angular.module('myApp', [])
* .component('myRoot', {
* templateUrl: 'root.html',
* controller: function() {
* this.wrappers = []; // initialize the array so that the wrappers are assigned into the parent scope
* }
* })
* .component('myToggle', {
* template: '<strong>myToggle</strong><button ng-click="$ctrl.toggle()" ng-transclude></button>',
* transclude: true,
* controller: function ToggleController() {
* var opened = false;
* this.isOpen = function() { return opened; };
* this.toggle = function() { opened = !opened; };
* }
* })
* .component('myWrapper', {
* transclude: true,
* template: '<strong>myWrapper</strong>' +
* '<div>ngRepeatToggle.isOpen(): {{$ctrl.ngRepeatToggle.isOpen() | json}}</div>' +
* '<my-toggle ng-ref="$ctrl.ngRepeatToggle"><ng-transclude></ng-transclude></my-toggle>'
* });
* </file>
* <file name="root.html">
* <strong>myRoot</strong>
* <my-toggle ng-ref="$ctrl.outerToggle">Outer Toggle</my-toggle>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* <div><em>wrappers assigned to root</em><br>
* <div ng-repeat="wrapper in $ctrl.wrappers">
* wrapper.ngRepeatToggle.isOpen(): {{wrapper.ngRepeatToggle.isOpen() | json}}
* </div>
*
* <ul>
* <li ng-repeat="(index, value) in [1,2,3]">
* <strong>ngRepeat</strong>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* <my-wrapper ng-ref="$ctrl.wrappers[index]">ngRepeat Toggle {{$index + 1}}</my-wrapper>
* </li>
* </ul>
*
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen()}} // This is always undefined because it's
* assigned to the child scope created by ngIf.
* </div>
* <div ng-if="true">
<strong>ngIf</strong>
* <my-toggle ng-ref="ngIfToggle">ngIf Toggle</my-toggle>
* <div>ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}</div>
* <div>outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}</div>
* </div>
* </file>
* <file name="styles.css">
* ul {
* list-style: none;
* padding-left: 0;
* }
*
* li[ng-repeat] {
* background: lightgreen;
* padding: 8px;
* margin: 8px;
* }
*
* [ng-if] {
* background: lightgrey;
* padding: 8px;
* }
*
* my-root {
* background: lightgoldenrodyellow;
* padding: 8px;
* display: block;
* }
*
* my-wrapper {
* background: lightsalmon;
* padding: 8px;
* display: block;
* }
*
* my-toggle {
* background: lightblue;
* padding: 8px;
* display: block;
* }
* </file>
* <file name="protractor.js" type="protractor">
* var OuterToggle = function() {
* this.toggle = function() {
* element(by.buttonText('Outer Toggle')).click();
* };
* this.isOpen = function() {
* return element.all(by.binding('outerToggle.isOpen()')).first().getText();
* };
* };
* var NgRepeatToggle = function(i) {
* var parent = element.all(by.repeater('(index, value) in [1,2,3]')).get(i - 1);
* this.toggle = function() {
* element(by.buttonText('ngRepeat Toggle ' + i)).click();
* };
* this.isOpen = function() {
* return parent.element(by.binding('ngRepeatToggle.isOpen() | json')).getText();
* };
* this.isOuterOpen = function() {
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
* };
* };
* var NgRepeatToggles = function() {
* var toggles = [1,2,3].map(function(i) { return new NgRepeatToggle(i); });
* this.forEach = function(fn) {
* toggles.forEach(fn);
* };
* this.isOuterOpen = function(i) {
* return toggles[i - 1].isOuterOpen();
* };
* };
* var NgIfToggle = function() {
* var parent = element(by.css('[ng-if]'));
* this.toggle = function() {
* element(by.buttonText('ngIf Toggle')).click();
* };
* this.isOpen = function() {
* return by.binding('ngIfToggle.isOpen() | json').getText();
* };
* this.isOuterOpen = function() {
* return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
* };
* };
*
* it('should toggle the outer toggle', function() {
* var outerToggle = new OuterToggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
* outerToggle.toggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
* });
*
* it('should toggle all outer toggles', function() {
* var outerToggle = new OuterToggle();
* var repeatToggles = new NgRepeatToggles();
* var ifToggle = new NgIfToggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): false');
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): false');
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): false');
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
* outerToggle.toggle();
* expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
* expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): true');
* expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): true');
* expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): true');
* expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): true');
* });
*
* it('should toggle each repeat iteration separately', function() {
* var repeatToggles = new NgRepeatToggles();
*
* repeatToggles.forEach(function(repeatToggle) {
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): false');
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
* repeatToggle.toggle();
* expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): true');
* expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
* });
* });
* </file>
* </example>
*
*/
var ngRefMinErr = minErr('ngRef');
var ngRefDirective = ['$parse', function($parse) {
return {
priority: -1, // Needed for compatibility with element transclusion on the same element
restrict: 'A',
compile: function(tElement, tAttrs) {
// Get the expected controller name, converts <data-some-thing> into "someThing"
var controllerName = directiveNormalize(nodeName_(tElement));
// Get the expression for value binding
var getter = $parse(tAttrs.ngRef);
var setter = getter.assign || function() {
throw ngRefMinErr('nonassign', 'Expression in ngRef="{0}" is non-assignable!', tAttrs.ngRef);
};
return function(scope, element, attrs) {
var refValue;
if (attrs.hasOwnProperty('ngRefRead')) {
if (attrs.ngRefRead === '$element') {
refValue = element;
} else {
refValue = element.data('$' + attrs.ngRefRead + 'Controller');
if (!refValue) {
throw ngRefMinErr(
'noctrl',
'The controller for ngRefRead="{0}" could not be found on ngRef="{1}"',
attrs.ngRefRead,
tAttrs.ngRef
);
}
}
} else {
refValue = element.data('$' + controllerName + 'Controller');
}
refValue = refValue || element;
setter(scope, refValue);
// when the element is removed, remove it (nullify it)
element.on('$destroy', function() {
// only remove it if value has not changed,
// because animations (and other procedures) may duplicate elements
if (getter(scope) === refValue) {
setter(scope, null);
}
});
};
}
};
}];
+35 -14
View File
@@ -17,20 +17,28 @@
* ## Directive Support
* The following directives are "animation aware":
*
* | Directive | Supported Animations |
* |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
* | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
* | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
* | {@link ng.directive:form#animations form} & {@link ng.directive:ngModel#animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
* | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
* | {@link module:ngMessages#animations ngMessage} | enter and leave |
* | Directive | Supported Animations |
* |-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
* | {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
* | {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave |
* | {@link ng.directive:ngClass#animations ngClass / {{class&#125;&#8203;&#125;} | add and remove |
* | {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
* | {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
* | {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
* | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
* | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
* | {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
* | {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
* | {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
* | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move |
* | {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
* | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
* | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
*
* (More information can be found by visiting each the documentation associated with each directive.)
* (More information can be found by visiting the documentation associated with each directive.)
*
* For a full breakdown of the steps involved during each animation event, refer to the
* {@link ng.$animate `$animate` API docs}.
*
* ## CSS-based Animations
*
@@ -267,9 +275,22 @@
* .message.ng-enter-prepare {
* opacity: 0;
* }
*
* ```
*
* ### Animating between value changes
*
* Sometimes you need to animate between different expression states, whose values
* don't necessary need to be known or referenced in CSS styles.
* Unless possible with another {@link ngAnimate#directive-support "animation aware" directive},
* that specific use case can always be covered with {@link ngAnimate.directive:ngAnimateSwap} as
* can be seen in {@link ngAnimate.directive:ngAnimateSwap#examples this example}.
*
* Note that {@link ngAnimate.directive:ngAnimateSwap} is a *structural directive*, which means it
* creates a new instance of the element (including any other/child directives it may have) and
* links it to a new scope every time *swap* happens. In some cases this might not be desirable
* (e.g. for performance reasons, or when you wish to retain internal state on the original
* element instance).
*
* ## JavaScript-based Animations
*
* ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
+20 -4
View File
@@ -14,8 +14,8 @@
*
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
* directives are supported:
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
* `ngDblClick`, and `ngMessages`.
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`,
* `ngClick`, `ngDblClick`, and `ngMessages`.
*
* Below is a more detailed breakdown of the attributes handled by ngAria:
*
@@ -46,11 +46,17 @@
* <md-checkbox ng-disabled="disabled" aria-disabled="true">
* ```
*
* ## Disabling Attributes
* It's possible to disable individual attributes added by ngAria with the
* ## Disabling Specific Attributes
* It is possible to disable individual attributes added by ngAria with the
* {@link ngAria.$ariaProvider#config config} method. For more details, see the
* {@link guide/accessibility Developer Guide}.
*
* ## Disabling `ngAria` on Specific Elements
* It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable`
* attribute on it. Note that only the element itself (and not its child elements) will be ignored.
*/
var ARIA_DISABLE_ATTR = 'ngAriaDisable';
var ngAriaModule = angular.module('ngAria', ['ng']).
info({ angularVersion: '"NG_VERSION_FULL"' }).
provider('$aria', $AriaProvider);
@@ -132,6 +138,8 @@ function $AriaProvider() {
function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
return function(scope, elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
var ariaCamelName = attr.$normalize(ariaAttr);
if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
scope.$watch(attr[attrName], function(boolVal) {
@@ -251,6 +259,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
require: 'ngModel',
priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
compile: function(elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
var shape = getShape(attr, elem);
return {
@@ -347,6 +357,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
restrict: 'A',
require: '?ngMessages',
link: function(scope, elem, attr, ngMessages) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
if (!elem.attr('aria-live')) {
elem.attr('aria-live', 'assertive');
}
@@ -357,6 +369,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
return {
restrict: 'A',
compile: function(elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
var fn = $parse(attr.ngClick);
return function(scope, elem, attr) {
@@ -389,6 +403,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
}])
.directive('ngDblclick', ['$aria', function($aria) {
return function(scope, elem, attr) {
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) {
elem.attr('tabindex', 0);
}
+128 -46
View File
@@ -18,7 +18,7 @@ var jqLite;
* sequencing based on the order of how the messages are defined in the template.
*
* Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude`
* `ngMessage` and `ngMessageExp` directives.
* `ngMessage`, `ngMessageExp` and `ngMessageDefault` directives.
*
* ## Usage
* The `ngMessages` directive allows keys in a key/value collection to be associated with a child element
@@ -257,7 +257,26 @@ var jqLite;
* .some-message.ng-leave.ng-leave-active {}
* ```
*
* {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
* {@link ngAnimate See the ngAnimate docs} to learn how to use JavaScript animations or to learn
* more about ngAnimate.
*
* ## Displaying a default message
* If the ngMessages renders no inner ngMessage directive (i.e. when none of the truthy
* keys are matched by a defined message), then it will render a default message
* using the {@link ngMessageDefault} directive.
* Note that matched messages will always take precedence over unmatched messages. That means
* the default message will not be displayed when another message is matched. This is also
* true for `ng-messages-multiple`.
*
* ```html
* <div ng-messages="myForm.myField.$error" role="alert">
* <div ng-message="required">This field is required</div>
* <div ng-message="minlength">This field is too short</div>
* <div ng-message-default>This field has an input error</div>
* </div>
* ```
*
*/
angular.module('ngMessages', [], function initAngularHelpers() {
// Access helpers from AngularJS core.
@@ -286,8 +305,11 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* at a time and this depends on the prioritization of the messages within the template. (This can
* be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.)
*
* A remote template can also be used to promote message reusability and messages can also be
* overridden.
* A remote template can also be used (With {@link ngMessagesInclude}) to promote message
* reusability and messages can also be overridden.
*
* A default message can also be displayed when no `ngMessage` directive is inserted, using the
* {@link ngMessageDefault} directive.
*
* {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`.
*
@@ -298,6 +320,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <ANY ng-message="stringValue">...</ANY>
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
* <ANY ng-message-exp="expressionValue">...</ANY>
* <ANY ng-message-default>...</ANY>
* </ANY>
*
* <!-- or by using element directives -->
@@ -305,6 +328,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <ng-message when="stringValue">...</ng-message>
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
* <ng-message when-exp="expressionValue">...</ng-message>
* <ng-message-default>...</ng-message-default>
* </ng-messages>
* ```
*
@@ -333,6 +357,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <div ng-message="required">You did not enter a field</div>
* <div ng-message="minlength">Your field is too short</div>
* <div ng-message="maxlength">Your field is too long</div>
* <div ng-message-default>This field has an input error</div>
* </div>
* </form>
* </file>
@@ -370,6 +395,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
var unmatchedMessages = [];
var matchedKeys = {};
var truthyKeys = 0;
var messageItem = ctrl.head;
var messageFound = false;
var totalMessages = 0;
@@ -382,13 +408,17 @@ angular.module('ngMessages', [], function initAngularHelpers() {
var messageUsed = false;
if (!messageFound) {
forEach(collection, function(value, key) {
if (!messageUsed && truthy(value) && messageCtrl.test(key)) {
// this is to prevent the same error name from showing up twice
if (matchedKeys[key]) return;
matchedKeys[key] = true;
if (truthy(value) && !messageUsed) {
truthyKeys++;
messageUsed = true;
messageCtrl.attach();
if (messageCtrl.test(key)) {
// this is to prevent the same error name from showing up twice
if (matchedKeys[key]) return;
matchedKeys[key] = true;
messageUsed = true;
messageCtrl.attach();
}
}
});
}
@@ -408,7 +438,16 @@ angular.module('ngMessages', [], function initAngularHelpers() {
messageCtrl.detach();
});
if (unmatchedMessages.length !== totalMessages) {
var messageMatched = unmatchedMessages.length !== totalMessages;
var attachDefault = ctrl.default && !messageMatched && truthyKeys > 0;
if (attachDefault) {
ctrl.default.attach();
} else if (ctrl.default) {
ctrl.default.detach();
}
if (messageMatched || attachDefault) {
$animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
} else {
$animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
@@ -428,23 +467,31 @@ angular.module('ngMessages', [], function initAngularHelpers() {
}
};
this.register = function(comment, messageCtrl) {
var nextKey = latestKey.toString();
messages[nextKey] = {
message: messageCtrl
};
insertMessageNode($element[0], comment, nextKey);
comment.$$ngMessageNode = nextKey;
latestKey++;
this.register = function(comment, messageCtrl, isDefault) {
if (isDefault) {
ctrl.default = messageCtrl;
} else {
var nextKey = latestKey.toString();
messages[nextKey] = {
message: messageCtrl
};
insertMessageNode($element[0], comment, nextKey);
comment.$$ngMessageNode = nextKey;
latestKey++;
}
ctrl.reRender();
};
this.deregister = function(comment) {
var key = comment.$$ngMessageNode;
delete comment.$$ngMessageNode;
removeMessageNode($element[0], comment, key);
delete messages[key];
this.deregister = function(comment, isDefault) {
if (isDefault) {
delete ctrl.default;
} else {
var key = comment.$$ngMessageNode;
delete comment.$$ngMessageNode;
removeMessageNode($element[0], comment, key);
delete messages[key];
}
ctrl.reRender();
};
@@ -647,9 +694,41 @@ angular.module('ngMessages', [], function initAngularHelpers() {
*
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
*/
.directive('ngMessageExp', ngMessageDirectiveFactory());
.directive('ngMessageExp', ngMessageDirectiveFactory())
function ngMessageDirectiveFactory() {
/**
* @ngdoc directive
* @name ngMessageDefault
* @restrict AE
* @scope
*
* @description
* `ngMessageDefault` is a directive with the purpose to show and hide a default message for
* {@link directive:ngMessages}, when none of provided messages matches.
*
* More information about using `ngMessageDefault` can be found in the
* {@link module:ngMessages `ngMessages` module documentation}.
*
* @usage
* ```html
* <!-- using attribute directives -->
* <ANY ng-messages="expression" role="alert">
* <ANY ng-message="stringValue">...</ANY>
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
* <ANY ng-message-default>...</ANY>
* </ANY>
*
* <!-- or by using element directives -->
* <ng-messages for="expression" role="alert">
* <ng-message when="stringValue">...</ng-message>
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
* <ng-message-default>...</ng-message-default>
* </ng-messages>
*
*/
.directive('ngMessageDefault', ngMessageDirectiveFactory(true));
function ngMessageDirectiveFactory(isDefault) {
return ['$animate', function($animate) {
return {
restrict: 'AE',
@@ -658,25 +737,28 @@ function ngMessageDirectiveFactory() {
terminal: true,
require: '^^ngMessages',
link: function(scope, element, attrs, ngMessagesCtrl, $transclude) {
var commentNode = element[0];
var commentNode, records, staticExp, dynamicExp;
var records;
var staticExp = attrs.ngMessage || attrs.when;
var dynamicExp = attrs.ngMessageExp || attrs.whenExp;
var assignRecords = function(items) {
records = items
? (isArray(items)
? items
: items.split(/[\s,]+/))
: null;
ngMessagesCtrl.reRender();
};
if (!isDefault) {
commentNode = element[0];
staticExp = attrs.ngMessage || attrs.when;
dynamicExp = attrs.ngMessageExp || attrs.whenExp;
if (dynamicExp) {
assignRecords(scope.$eval(dynamicExp));
scope.$watchCollection(dynamicExp, assignRecords);
} else {
assignRecords(staticExp);
var assignRecords = function(items) {
records = items
? (isArray(items)
? items
: items.split(/[\s,]+/))
: null;
ngMessagesCtrl.reRender();
};
if (dynamicExp) {
assignRecords(scope.$eval(dynamicExp));
scope.$watchCollection(dynamicExp, assignRecords);
} else {
assignRecords(staticExp);
}
}
var currentElement, messageCtrl;
@@ -701,7 +783,7 @@ function ngMessageDirectiveFactory() {
// If the message element was removed via a call to `detach` then `currentElement` will be null
// So this handler only handles cases where something else removed the message element.
if (currentElement && currentElement.$$attachId === $$attachId) {
ngMessagesCtrl.deregister(commentNode);
ngMessagesCtrl.deregister(commentNode, isDefault);
messageCtrl.detach();
}
newScope.$destroy();
@@ -716,14 +798,14 @@ function ngMessageDirectiveFactory() {
$animate.leave(elm);
}
}
});
}, isDefault);
// We need to ensure that this directive deregisters itself when it no longer exists
// Normally this is done when the attached element is destroyed; but if this directive
// gets removed before we attach the message to the DOM there is nothing to watch
// in which case we must deregister when the containing scope is destroyed.
scope.$on('$destroy', function() {
ngMessagesCtrl.deregister(commentNode);
ngMessagesCtrl.deregister(commentNode, isDefault);
});
}
};
+66 -20
View File
@@ -1558,7 +1558,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1573,7 +1574,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1588,7 +1590,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1605,7 +1608,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1622,7 +1626,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1654,7 +1659,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string} url HTTP url string that supports colon param matching.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled. See #when for more info.
* order to change how a matched request is handled.
* See {@link ngMock.$httpBackend#when `when`} for more info.
*/
$httpBackend.whenRoute = function(method, url) {
var pathObj = parseRoute(url);
@@ -1743,8 +1749,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation for GET requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
* and returns true if the url matches the current expectation.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1758,8 +1765,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation for HEAD requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
* and returns true if the url matches the current expectation.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1773,8 +1781,9 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation for DELETE requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
* and returns true if the url matches the current expectation.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1788,11 +1797,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation for POST requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* and returns true if the url matches the current expectation.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1806,11 +1816,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation for PUT requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* and returns true if the url matches the current expectation.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1824,11 +1835,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation for PATCH requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* and returns true if the url matches the current expectation.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1842,7 +1854,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* Creates a new request expectation for JSONP requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)=} url HTTP url or function that receives an url
* and returns true if the url matches the current definition.
* and returns true if the url matches the current expectation.
* @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
@@ -1860,7 +1872,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string} url HTTP url string that supports colon param matching.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled. See #expect for more info.
* order to change how a matched request is handled.
* See {@link ngMock.$httpBackend#expect `expect`} for more info.
*/
$httpBackend.expectRoute = function(method, url) {
var pathObj = parseRoute(url);
@@ -2756,6 +2769,39 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#matchLatestDefinition
* @module ngMockE2E
* @description
* This method can be used to change which mocked responses `$httpBackend` returns, when defining
* them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
* By default, `$httpBackend` returns the first definition that matches. When setting
* `$http.matchLatestDefinition(true)`, it will use the last response that matches, i.e. the
* one that was added last.
*
* ```js
* hb.when('GET', '/url1').respond(200, 'content', {});
* hb.when('GET', '/url1').respond(201, 'another', {});
* hb('GET', '/url1'); // receives "content"
*
* $http.matchLatestDefinition(true)
* hb('GET', '/url1'); // receives "another"
*
* hb.when('GET', '/url1').respond(201, 'onemore', {});
* hb('GET', '/url1'); // receives "onemore"
* ```
*
* This is useful if a you have a default response that is overriden inside specific tests.
*
* Note that different from config methods on providers, `matchLatestDefinition()` can be changed
* even when the application is already running.
*
* @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
* If omitted, it will return the current value.
* @return {$httpBackend|Boolean} self when used as a setter, and the current value when used
* as a getter
*/
angular.mock.e2e = {};
angular.mock.e2e.$httpBackendDecorator =
['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock];
+43 -7
View File
@@ -183,11 +183,22 @@ function $RouteProvider() {
* `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same
* route definition, will cause the latter to be ignored.
*
* - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes
* (inluding the path) even if the new URL maps to the same route.
*
* If the option is set to `false` and the URL in the browser changes, but the new URL maps
* to the same route, then a `$routeUpdate` event is broadcasted on the root scope (without
* reloading the route).
*
* - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()`
* or `$location.hash()` changes.
*
* If the option is set to `false` and url in the browser changes, then
* `$routeUpdate` event is broadcasted on the root scope.
* If the option is set to `false` and the URL in the browser changes, then a `$routeUpdate`
* event is broadcasted on the root scope (without reloading the route).
*
* <div class="alert alert-warning">
* **Note:** This option has no effect if `reloadOnUrl` is set to `false`.
* </div>
*
* - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive
*
@@ -202,6 +213,9 @@ function $RouteProvider() {
this.when = function(path, route) {
//copy original route object to preserve params inherited from proto chain
var routeCopy = shallowCopy(route);
if (angular.isUndefined(routeCopy.reloadOnUrl)) {
routeCopy.reloadOnUrl = true;
}
if (angular.isUndefined(routeCopy.reloadOnSearch)) {
routeCopy.reloadOnSearch = true;
}
@@ -544,8 +558,9 @@ function $RouteProvider() {
* @name $route#$routeUpdate
* @eventType broadcast on root scope
* @description
* The `reloadOnSearch` property has been set to false, and we are reusing the same
* instance of the Controller.
* Broadcasted if the same instance of a route (including template, controller instance,
* resolved dependencies, etc.) is being reused. This can happen if either `reloadOnSearch` or
* `reloadOnUrl` has been set to `false`.
*
* @param {Object} angularEvent Synthetic event object
* @param {Route} current Current/previous route information.
@@ -653,9 +668,7 @@ function $RouteProvider() {
var lastRoute = $route.current;
preparedRoute = parseRoute();
preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
&& angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
&& !preparedRoute.reloadOnSearch && !forceReload;
preparedRouteIsUpdateOnly = isNavigationUpdateOnly(preparedRoute, lastRoute);
if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
@@ -835,6 +848,29 @@ function $RouteProvider() {
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
}
/**
* @param {Object} newRoute - The new route configuration (as returned by `parseRoute()`).
* @param {Object} oldRoute - The previous route configuration (as returned by `parseRoute()`).
* @returns {boolean} Whether this is an "update-only" navigation, i.e. the URL maps to the same
* route and it can be reused (based on the config and the type of change).
*/
function isNavigationUpdateOnly(newRoute, oldRoute) {
// IF this is not a forced reload
return !forceReload
// AND both `newRoute`/`oldRoute` are defined
&& newRoute && oldRoute
// AND they map to the same Route Definition Object
&& (newRoute.$$route === oldRoute.$$route)
// AND `reloadOnUrl` is disabled
&& (!newRoute.reloadOnUrl
// OR `reloadOnSearch` is disabled
|| (!newRoute.reloadOnSearch
// AND both routes have the same path params
&& angular.equals(newRoute.pathParams, oldRoute.pathParams)
)
);
}
/**
* @returns {string} interpolation of the redirect path with the parameters
*/
+1
View File
@@ -313,6 +313,7 @@ beforeEach(function() {
function generateCompare(isNot) {
return function(actual, namespace, code, content) {
var matcher = new MinErrMatcher(isNot, namespace, code, content, {
inputType: 'error',
expectedAction: 'equal',
+49 -17
View File
@@ -2,32 +2,57 @@
describe('errors', function() {
var originalObjectMaxDepthInErrorMessage = minErrConfig.objectMaxDepth;
var originalUrlErrorParamsEnabled = minErrConfig.urlErrorParamsEnabled;
afterEach(function() {
minErrConfig.objectMaxDepth = originalObjectMaxDepthInErrorMessage;
minErrConfig.urlErrorParamsEnabled = originalUrlErrorParamsEnabled;
});
describe('errorHandlingConfig', function() {
it('should get default objectMaxDepth', function() {
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
describe('objectMaxDepth',function() {
it('should get default objectMaxDepth', function() {
expect(errorHandlingConfig().objectMaxDepth).toBe(5);
});
it('should set objectMaxDepth', function() {
errorHandlingConfig({objectMaxDepth: 3});
expect(errorHandlingConfig().objectMaxDepth).toBe(3);
});
it('should not change objectMaxDepth when undefined is supplied', function() {
errorHandlingConfig({objectMaxDepth: undefined});
expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage);
});
they('should set objectMaxDepth to NaN when $prop is supplied',
[NaN, null, true, false, -1, 0], function(maxDepth) {
errorHandlingConfig({objectMaxDepth: maxDepth});
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
}
);
});
it('should set objectMaxDepth', function() {
errorHandlingConfig({objectMaxDepth: 3});
expect(errorHandlingConfig().objectMaxDepth).toBe(3);
describe('urlErrorParamsEnabled',function() {
it('should get default urlErrorParamsEnabled', function() {
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(true);
});
it('should set urlErrorParamsEnabled', function() {
errorHandlingConfig({urlErrorParamsEnabled: false});
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(false);
errorHandlingConfig({urlErrorParamsEnabled: true});
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(true);
});
it('should not change its value when non-boolean is supplied', function() {
errorHandlingConfig({urlErrorParamsEnabled: 123});
expect(errorHandlingConfig().urlErrorParamsEnabled).toBe(originalUrlErrorParamsEnabled);
});
});
it('should not change objectMaxDepth when undefined is supplied', function() {
errorHandlingConfig({objectMaxDepth: undefined});
expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage);
});
they('should set objectMaxDepth to NaN when $prop is supplied',
[NaN, null, true, false, -1, 0], function(maxDepth) {
errorHandlingConfig({objectMaxDepth: maxDepth});
expect(errorHandlingConfig().objectMaxDepth).toBeNaN();
}
);
});
describe('minErr', function() {
@@ -165,7 +190,6 @@ describe('errors', function() {
.toMatch(/^[\s\S]*\?p0=a&p1=b&p2=value%20with%20space$/);
});
it('should strip error reference urls from the error message parameters', function() {
var firstError = testError('firstcode', 'longer string and so on');
@@ -177,5 +201,13 @@ describe('errors', function() {
'%3A%2F%2Ferrors.angularjs.org%2F%22NG_VERSION_FULL%22%2Ftest%2Ffirstcode');
});
it('should not generate URL query parameters when urlErrorParamsEnabled is false', function() {
errorHandlingConfig({urlErrorParamsEnabled: false});
expect(testError('acode', 'aproblem', 'a', 'b', 'c').message).toBe('[test:acode] aproblem\n' +
'https://errors.angularjs.org/"NG_VERSION_FULL"/test/acode');
});
});
});
+44
View File
@@ -8843,6 +8843,50 @@ describe('$compile', function() {
});
});
it('should correctly handle multi-element directives', function() {
module(function() {
directive('foo', valueFn({
template: '[<div ng-transclude></div>]',
transclude: true
}));
directive('bar', valueFn({
template: '[<div ng-transclude="header"></div>|<div ng-transclude="footer"></div>]',
transclude: {
header: 'header',
footer: 'footer'
}
}));
});
inject(function($compile, $rootScope) {
var tmplWithFoo =
'<foo>' +
'<div ng-if-start="true">Hello, </div>' +
'<div ng-if-end>world!</div>' +
'</foo>';
var tmplWithBar =
'<bar>' +
'<header ng-if-start="true">This is a </header>' +
'<header ng-if-end>header!</header>' +
'<footer ng-if-start="true">This is a </footer>' +
'<footer ng-if-end>footer!</footer>' +
'</bar>';
var elem1 = $compile(tmplWithFoo)($rootScope);
var elem2 = $compile(tmplWithBar)($rootScope);
$rootScope.$digest();
expect(elem1.text()).toBe('[Hello, world!]');
expect(elem2.text()).toBe('[This is a header!|This is a footer!]');
dealoc(elem1);
dealoc(elem2);
});
});
//see issue https://github.com/angular/angular.js/issues/12936
it('should use the proper scope when it is on the root element of a replaced directive template', function() {
module(function() {
+8 -2
View File
@@ -6,7 +6,7 @@ describe('ngModel', function() {
describe('NgModelController', function() {
/* global NgModelController: false */
var ctrl, scope, ngModelAccessor, element, parentFormCtrl;
var ctrl, scope, element, parentFormCtrl;
beforeEach(inject(function($rootScope, $controller) {
var attrs = {name: 'testAlias', ngModel: 'value'};
@@ -21,7 +21,6 @@ describe('ngModel', function() {
element = jqLite('<form><input></form>');
scope = $rootScope;
ngModelAccessor = jasmine.createSpy('ngModel accessor');
ctrl = $controller(NgModelController, {
$scope: scope,
$element: element.find('input'),
@@ -438,6 +437,13 @@ describe('ngModel', function() {
expect(ctrl.$modelValue).toBe('c');
expect(scope.value).toBe('c');
}));
it('should not throw an error if the scope has been destroyed', function() {
scope.$destroy();
ctrl.$setViewValue('some-val');
expect(ctrl.$viewValue).toBe('some-val');
});
});
+561
View File
@@ -0,0 +1,561 @@
'use strict';
describe('ngRef', function() {
beforeEach(function() {
jasmine.addMatchers({
toEqualJq: function(util) {
return {
compare: function(actual, expected) {
// Jquery <= 2.2 objects add a context property that is irrelevant for equality
if (actual && actual.hasOwnProperty('context')) {
delete actual.context;
}
if (expected && expected.hasOwnProperty('context')) {
delete expected.context;
}
return {
pass: util.equals(actual, expected)
};
}
};
}
});
});
describe('on a component', function() {
var myComponentController, attributeDirectiveController, $rootScope, $compile;
beforeEach(module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: 'foo',
controller: function() {
myComponentController = this;
}
});
$compileProvider.directive('attributeDirective', function() {
return {
restrict: 'A',
controller: function() {
attributeDirectiveController = this;
}
};
});
}));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
}));
it('should bind in the current scope the controller of a component', function() {
$rootScope.$ctrl = 'undamaged';
$compile('<my-component ng-ref="myComponentRef"></my-component>')($rootScope);
expect($rootScope.$ctrl).toBe('undamaged');
expect($rootScope.myComponentRef).toBe(myComponentController);
});
it('should throw if the expression is not assignable', function() {
expect(function() {
$compile('<my-component ng-ref="\'hello\'"></my-component>')($rootScope);
}).toThrowMinErr('ngRef', 'nonassign', 'Expression in ngRef="\'hello\'" is non-assignable!');
});
it('should work with non:normalized entity name', function() {
$compile('<my:component ng-ref="myComponent1"></my:component>')($rootScope);
expect($rootScope.myComponent1).toBe(myComponentController);
});
it('should work with data-non-normalized entity name', function() {
$compile('<data-my-component ng-ref="myComponent2"></data-my-component>')($rootScope);
expect($rootScope.myComponent2).toBe(myComponentController);
});
it('should work with x-non-normalized entity name', function() {
$compile('<x-my-component ng-ref="myComponent3"></x-my-component>')($rootScope);
expect($rootScope.myComponent3).toBe(myComponentController);
});
it('should work with data-non-normalized attribute name', function() {
$compile('<my-component data-ng-ref="myComponent1"></my-component>')($rootScope);
expect($rootScope.myComponent1).toBe(myComponentController);
});
it('should work with x-non-normalized attribute name', function() {
$compile('<my-component x-ng-ref="myComponent2"></my-component>')($rootScope);
expect($rootScope.myComponent2).toBe(myComponentController);
});
it('should not bind the controller of an attribute directive', function() {
$compile('<my-component attribute-directive-1 ng-ref="myComponentRef"></my-component>')($rootScope);
expect($rootScope.myComponentRef).toBe(myComponentController);
});
it('should not leak to parent scopes', function() {
var template =
'<div ng-if="true">' +
'<my-component ng-ref="myComponent"></my-component>' +
'</div>';
$compile(template)($rootScope);
expect($rootScope.myComponent).toBe(undefined);
});
it('should nullify the variable once the component is destroyed', function() {
var template = '<div><my-component ng-ref="myComponent"></my-component></div>';
var element = $compile(template)($rootScope);
expect($rootScope.myComponent).toBe(myComponentController);
var componentElement = element.children();
var isolateScope = componentElement.isolateScope();
componentElement.remove();
isolateScope.$destroy();
expect($rootScope.myComponent).toBe(null);
});
it('should be compatible with entering/leaving components', inject(function($animate) {
var template = '<my-component ng-ref="myComponent"></my-component>';
$rootScope.$ctrl = {};
var parent = $compile('<div></div>')($rootScope);
var leaving = $compile(template)($rootScope);
var leavingController = myComponentController;
$animate.enter(leaving, parent);
expect($rootScope.myComponent).toBe(leavingController);
var entering = $compile(template)($rootScope);
var enteringController = myComponentController;
$animate.enter(entering, parent);
$animate.leave(leaving, parent);
expect($rootScope.myComponent).toBe(enteringController);
}));
it('should allow binding to a nested property', function() {
$rootScope.obj = {};
$compile('<my-component ng-ref="obj.myComponent"></my-component>')($rootScope);
expect($rootScope.obj.myComponent).toBe(myComponentController);
});
});
it('should bind the jqlite wrapped DOM element if there is no component', inject(function($compile, $rootScope) {
var el = $compile('<span ng-ref="mySpan">my text</span>')($rootScope);
expect($rootScope.mySpan).toEqualJq(el);
expect($rootScope.mySpan[0].textContent).toBe('my text');
}));
it('should nullify the expression value if the DOM element is destroyed', inject(function($compile, $rootScope) {
var element = $compile('<div><span ng-ref="mySpan">my text</span></div>')($rootScope);
element.children().remove();
expect($rootScope.mySpan).toBe(null);
}));
it('should bind the controller of an element directive', function() {
var myDirectiveController;
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
controller: function() {
myDirectiveController = this;
}
};
});
});
inject(function($compile, $rootScope) {
$compile('<my-directive ng-ref="myDirective"></my-directive>')($rootScope);
expect($rootScope.myDirective).toBe(myDirectiveController);
});
});
describe('ngRefRead', function() {
it('should bind the element instead of the controller of a component if ngRefRead="$element" is set', function() {
module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: 'my text',
controller: function() {}
});
});
inject(function($compile, $rootScope) {
var el = $compile('<my-component ng-ref="myEl" ng-ref-read="$element"></my-component>')($rootScope);
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
it('should bind the element instead an element-directive controller if ngRefRead="$element" is set', function() {
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
restrict: 'E',
template: 'my text',
controller: function() {}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<my-directive ng-ref="myEl" ng-ref-read="$element"></my-directive>')($rootScope);
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
it('should bind an attribute-directive controller if ngRefRead="controllerName" is set', function() {
var attrDirective1Controller;
module(function($compileProvider) {
$compileProvider.directive('elementDirective', function() {
return {
restrict: 'E',
template: 'my text',
controller: function() {}
};
});
$compileProvider.directive('attributeDirective1', function() {
return {
restrict: 'A',
controller: function() {
attrDirective1Controller = this;
}
};
});
$compileProvider.directive('attributeDirective2', function() {
return {
restrict: 'A',
controller: function() {}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<element-directive' +
'attribute-directive-1' +
'attribute-directive-2' +
'ng-ref="myController"' +
'ng-ref-read="$element"></element-directive>')($rootScope);
expect($rootScope.myController).toBe(attrDirective1Controller);
});
});
it('should throw if no controller is found for the ngRefRead value', function() {
module(function($compileProvider) {
$compileProvider.directive('elementDirective', function() {
return {
restrict: 'E',
template: 'my text',
controller: function() {}
};
});
});
inject(function($compile, $rootScope) {
expect(function() {
$compile('<element-directive ' +
'ng-ref="myController"' +
'ng-ref-read="attribute"></element-directive>')($rootScope);
}).toThrowMinErr('ngRef', 'noctrl', 'The controller for ngRefRead="attribute" could not be found on ngRef="myController"');
});
});
});
it('should bind the jqlite element if the controller is on an attribute-directive', function() {
var myDirectiveController;
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
restrict: 'A',
template: 'my text',
controller: function() {
myDirectiveController = this;
}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<div my-directive ng-ref="myEl"></div>')($rootScope);
expect(myDirectiveController).toBeDefined();
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
it('should bind the jqlite element if the controller is on an class-directive', function() {
var myDirectiveController;
module(function($compileProvider) {
$compileProvider.directive('myDirective', function() {
return {
restrict: 'C',
template: 'my text',
controller: function() {
myDirectiveController = this;
}
};
});
});
inject(function($compile, $rootScope) {
var el = $compile('<div class="my-directive" ng-ref="myEl"></div>')($rootScope);
expect(myDirectiveController).toBeDefined();
expect($rootScope.myEl).toEqualJq(el);
expect($rootScope.myEl[0].textContent).toBe('my text');
});
});
describe('transclusion', function() {
it('should work with simple transclusion', function() {
module(function($compileProvider) {
$compileProvider
.component('myComponent', {
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: function() {
this.text = 'SUCCESS';
}
});
});
inject(function($compile, $rootScope) {
var template = '<my-component ng-ref="myComponent">{{myComponent.text}}</my-component>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
it('should be compatible with element transclude components', function() {
module(function($compileProvider) {
$compileProvider
.component('myComponent', {
transclude: 'element',
controller: function($animate, $element, $transclude) {
this.text = 'SUCCESS';
this.$postLink = function() {
$transclude(function(clone, newScope) {
$animate.enter(clone, $element.parent(), $element);
});
};
}
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-component ng-ref="myComponent">' +
'{{myComponent.text}}' +
'</my-component>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
it('should be compatible with ngIf and transclusion on same element', function() {
module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: '<ng-transclude></ng-transclude>',
transclude: true,
controller: function($scope) {
this.text = 'SUCCESS';
}
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-component ng-if="present" ng-ref="myComponent" >' +
'{{myComponent.text}}' +
'</my-component>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply('present = false');
expect(element.text()).toBe('');
$rootScope.$apply('present = true');
expect(element.text()).toBe('SUCCESS');
$rootScope.$apply('present = false');
expect(element.text()).toBe('');
$rootScope.$apply('present = true');
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
it('should be compatible with element transclude & destroy components', function() {
var myComponentController;
module(function($compileProvider) {
$compileProvider
.component('myTranscludingComponent', {
transclude: 'element',
controller: function($animate, $element, $transclude) {
myComponentController = this;
var currentClone, currentScope;
this.transclude = function(text) {
this.text = text;
$transclude(function(clone, newScope) {
currentClone = clone;
currentScope = newScope;
$animate.enter(clone, $element.parent(), $element);
});
};
this.destroy = function() {
currentClone.remove();
currentScope.$destroy();
};
}
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-transcluding-component ng-ref="myComponent">' +
'{{myComponent.text}}' +
'</my-transcluding-component>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('');
myComponentController.transclude('transcludedOk');
$rootScope.$apply();
expect(element.text()).toBe('transcludedOk');
myComponentController.destroy();
$rootScope.$apply();
expect(element.text()).toBe('');
});
});
it('should be compatible with element transclude directives', function() {
module(function($compileProvider) {
$compileProvider
.directive('myDirective', function($animate) {
return {
transclude: 'element',
controller: function() {
this.text = 'SUCCESS';
},
link: function(scope, element, attrs, ctrl, $transclude) {
$transclude(function(clone, newScope) {
$animate.enter(clone, element.parent(), element);
});
}
};
});
});
inject(function($compile, $rootScope) {
var template =
'<div>' +
'<my-directive ng-ref="myDirective">' +
'{{myDirective.text}}' +
'</my-directive>' +
'</div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect(element.text()).toBe('SUCCESS');
dealoc(element);
});
});
});
it('should work with components with templates via $http', function() {
module(function($compileProvider) {
$compileProvider.component('httpComponent', {
templateUrl: 'template.html',
controller: function() {
this.me = true;
}
});
});
inject(function($compile, $httpBackend, $rootScope) {
var template = '<div><http-component ng-ref="controller"></http-component></div>';
var element = $compile(template)($rootScope);
$httpBackend.expect('GET', 'template.html').respond('ok');
$rootScope.$apply();
expect($rootScope.controller).toBeUndefined();
$httpBackend.flush();
expect($rootScope.controller.me).toBe(true);
dealoc(element);
});
});
it('should work with ngRepeat-ed components', function() {
var controllers = [];
module(function($compileProvider) {
$compileProvider.component('myComponent', {
template: 'foo',
controller: function() {
controllers.push(this);
}
});
});
inject(function($compile, $rootScope) {
$rootScope.elements = [0,1,2,3,4];
$rootScope.controllers = []; // Initialize the array because ngRepeat creates a child scope
var template = '<div><my-component ng-repeat="(key, el) in elements" ng-ref="controllers[key]"></my-component></div>';
var element = $compile(template)($rootScope);
$rootScope.$apply();
expect($rootScope.controllers).toEqual(controllers);
$rootScope.$apply('elements = []');
expect($rootScope.controllers).toEqual([null, null, null, null, null]);
});
});
});
+254 -23
View File
@@ -9,17 +9,236 @@ describe('$aria', function() {
dealoc(element);
});
function injectScopeAndCompiler() {
return inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
scope = _$rootScope_;
});
}
describe('with `ngAriaDisable`', function() {
beforeEach(injectScopeAndCompiler);
beforeEach(function() {
jasmine.addMatchers({
toHaveAttribute: function toHaveAttributeMatcher() {
return {
compare: function toHaveAttributeCompare(element, attr) {
var node = element[0];
var pass = node.hasAttribute(attr);
var message = 'Expected `' + node.outerHTML + '` ' + (pass ? 'not ' : '') +
'to have attribute `' + attr + '`.';
function compileElement(inputHtml) {
element = $compile(inputHtml)(scope);
scope.$digest();
}
return {
pass: pass,
message: message
};
}
};
}
});
});
// ariaChecked
it('should not attach aria-checked to custom checkbox', function() {
compileElement('<div role="checkbox" ng-model="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-checked');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-checked');
});
it('should not attach aria-checked to custom radio controls', function() {
compileElement(
'<div role="radio" ng-model="val" value="one" ng-aria-disable></div>' +
'<div role="radio" ng-model="val" value="two" ng-aria-disable></div>');
var radio1 = element.eq(0);
var radio2 = element.eq(1);
scope.$apply('val = "one"');
expect(radio1).not.toHaveAttribute('aria-checked');
expect(radio2).not.toHaveAttribute('aria-checked');
scope.$apply('val = "two"');
expect(radio1).not.toHaveAttribute('aria-checked');
expect(radio2).not.toHaveAttribute('aria-checked');
});
// ariaDisabled
it('should not attach aria-disabled to custom controls', function() {
compileElement('<div ng-disabled="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-disabled');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-disabled');
});
// ariaHidden
it('should not attach aria-hidden to `ngShow`', function() {
compileElement('<div ng-show="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-hidden');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-hidden');
});
it('should not attach aria-hidden to `ngHide`', function() {
compileElement('<div ng-hide="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-hidden');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-hidden');
});
// ariaInvalid
it('should not attach aria-invalid to input', function() {
compileElement('<input ng-model="val" ng-minlength="10" ng-aria-disable />');
scope.$apply('val = "lt 10"');
expect(element).not.toHaveAttribute('aria-invalid');
scope.$apply('val = "gt 10 characters"');
expect(element).not.toHaveAttribute('aria-invalid');
});
it('should not attach aria-invalid to custom controls', function() {
compileElement('<div role="textbox" ng-model="val" ng-minlength="10" ng-aria-disable></div>');
scope.$apply('val = "lt 10"');
expect(element).not.toHaveAttribute('aria-invalid');
scope.$apply('val = "gt 10 characters"');
expect(element).not.toHaveAttribute('aria-invalid');
});
// ariaLive
it('should not attach aria-live to `ngMessages`', function() {
compileElement('<div ng-messages="val" ng-aria-disable>');
expect(element).not.toHaveAttribute('aria-live');
});
// ariaReadonly
it('should not attach aria-readonly to custom controls', function() {
compileElement('<div ng-readonly="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-readonly');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-readonly');
});
// ariaRequired
it('should not attach aria-required to custom controls with `required`', function() {
compileElement('<div ng-model="val" required ng-aria-disable></div>');
expect(element).not.toHaveAttribute('aria-required');
});
it('should not attach aria-required to custom controls with `ngRequired`', function() {
compileElement('<div ng-model="val" ng-required="val" ng-aria-disable></div>');
scope.$apply('val = false');
expect(element).not.toHaveAttribute('aria-required');
scope.$apply('val = true');
expect(element).not.toHaveAttribute('aria-required');
});
// ariaValue
it('should not attach aria-value* to input[range]', function() {
compileElement('<input type="range" ng-model="val" min="0" max="100" ng-aria-disable />');
expect(element).not.toHaveAttribute('aria-valuemax');
expect(element).not.toHaveAttribute('aria-valuemin');
expect(element).not.toHaveAttribute('aria-valuenow');
scope.$apply('val = 50');
expect(element).not.toHaveAttribute('aria-valuemax');
expect(element).not.toHaveAttribute('aria-valuemin');
expect(element).not.toHaveAttribute('aria-valuenow');
scope.$apply('val = 150');
expect(element).not.toHaveAttribute('aria-valuemax');
expect(element).not.toHaveAttribute('aria-valuemin');
expect(element).not.toHaveAttribute('aria-valuenow');
});
it('should not attach aria-value* to custom controls', function() {
compileElement(
'<div role="progressbar" ng-model="val" min="0" max="100" ng-aria-disable></div>' +
'<div role="slider" ng-model="val" min="0" max="100" ng-aria-disable></div>');
var progressbar = element.eq(0);
var slider = element.eq(1);
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
expect(progressbar).not.toHaveAttribute(attr);
expect(slider).not.toHaveAttribute(attr);
});
scope.$apply('val = 50');
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
expect(progressbar).not.toHaveAttribute(attr);
expect(slider).not.toHaveAttribute(attr);
});
scope.$apply('val = 150');
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
expect(progressbar).not.toHaveAttribute(attr);
expect(slider).not.toHaveAttribute(attr);
});
});
// bindKeypress
it('should not bind keypress to `ngClick`', function() {
scope.onClick = jasmine.createSpy('onClick');
compileElement(
'<div ng-click="onClick()" tabindex="0" ng-aria-disable></div>' +
'<ul><li ng-click="onClick()" tabindex="0" ng-aria-disable></li></ul>');
var div = element.find('div');
var li = element.find('li');
div.triggerHandler({type: 'keypress', keyCode: 32});
li.triggerHandler({type: 'keypress', keyCode: 32});
expect(scope.onClick).not.toHaveBeenCalled();
});
// bindRoleForClick
it('should not attach role to custom controls', function() {
compileElement(
'<div ng-click="onClick()" ng-aria-disable></div>' +
'<div type="checkbox" ng-model="val" ng-aria-disable></div>' +
'<div type="radio" ng-model="val" ng-aria-disable></div>' +
'<div type="range" ng-model="val" ng-aria-disable></div>');
expect(element.eq(0)).not.toHaveAttribute('role');
expect(element.eq(1)).not.toHaveAttribute('role');
expect(element.eq(2)).not.toHaveAttribute('role');
expect(element.eq(3)).not.toHaveAttribute('role');
});
// tabindex
it('should not attach tabindex to custom controls', function() {
compileElement(
'<div role="checkbox" ng-model="val" ng-aria-disable></div>' +
'<div role="slider" ng-model="val" ng-aria-disable></div>');
expect(element.eq(0)).not.toHaveAttribute('tabindex');
expect(element.eq(1)).not.toHaveAttribute('tabindex');
});
it('should not attach tabindex to `ngClick` or `ngDblclick`', function() {
compileElement(
'<div ng-click="onClick()" ng-aria-disable></div>' +
'<div ng-dblclick="onDblclick()" ng-aria-disable></div>');
expect(element.eq(0)).not.toHaveAttribute('tabindex');
expect(element.eq(1)).not.toHaveAttribute('tabindex');
});
});
describe('aria-hidden', function() {
beforeEach(injectScopeAndCompiler);
@@ -895,19 +1114,31 @@ describe('$aria', function() {
expect(element.attr('tabindex')).toBe('0');
});
});
});
function expectAriaAttrOnEachElement(elem, ariaAttr, expected) {
angular.forEach(elem, function(val) {
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
});
}
// Helpers
function compileElement(inputHtml) {
element = $compile(inputHtml)(scope);
scope.$digest();
}
function configAriaProvider(config) {
return function() {
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider) {
$ariaProvider.config(config);
function configAriaProvider(config) {
return function() {
module(function($ariaProvider) {
$ariaProvider.config(config);
});
};
}
function expectAriaAttrOnEachElement(elem, ariaAttr, expected) {
angular.forEach(elem, function(val) {
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
});
module('ariaTest');
};
}
}
function injectScopeAndCompiler() {
return inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
scope = _$rootScope_;
});
}
});
+94
View File
@@ -661,6 +661,100 @@ describe('ngMessages', function() {
);
describe('default message', function() {
it('should render a default message when no message matches', inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col">' +
' <div ng-message="val">Message is set</div>' +
' <div ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
$rootScope.$apply(function() {
$rootScope.col = { unexpected: false };
});
$rootScope.$digest();
expect(element.text().trim()).toBe('');
expect(element).not.toHaveClass('ng-active');
$rootScope.$apply(function() {
$rootScope.col = { unexpected: true };
});
expect(element.text().trim()).toBe('Default message is set');
expect(element).toHaveClass('ng-active');
$rootScope.$apply(function() {
$rootScope.col = { unexpected: false };
});
expect(element.text().trim()).toBe('');
expect(element).not.toHaveClass('ng-active');
$rootScope.$apply(function() {
$rootScope.col = { val: true, unexpected: true };
});
expect(element.text().trim()).toBe('Message is set');
expect(element).toHaveClass('ng-active');
}));
it('should not render a default message with ng-messages-multiple if another error matches',
inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col" ng-messages-multiple>' +
' <div ng-message="val">Message is set</div>' +
' <div ng-message="other">Other message is set</div>' +
' <div ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
expect(element.text().trim()).toBe('');
$rootScope.$apply(function() {
$rootScope.col = { val: true, other: false, unexpected: false };
});
expect(element.text().trim()).toBe('Message is set');
$rootScope.$apply(function() {
$rootScope.col = { val: true, other: true, unexpected: true };
});
expect(element.text().trim()).toBe('Message is set Other message is set');
$rootScope.$apply(function() {
$rootScope.col = { val: false, other: false, unexpected: true };
});
expect(element.text().trim()).toBe('Default message is set');
})
);
it('should handle a default message with ngIf', inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col">' +
' <div ng-message="val">Message is set</div>' +
' <div ng-if="default" ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
$rootScope.default = true;
$rootScope.col = {unexpected: true};
$rootScope.$digest();
expect(element.text().trim()).toBe('Default message is set');
$rootScope.$apply('default = false');
expect(element.text().trim()).toBe('');
$rootScope.$apply('default = true');
expect(element.text().trim()).toBe('Default message is set');
$rootScope.$apply(function() {
$rootScope.col = { val: true };
});
expect(element.text().trim()).toBe('Message is set');
}));
});
describe('when including templates', function() {
they('should work with a dynamic collection model which is managed by ngRepeat',
{'<div ng-messages-include="...">': '<div ng-messages="item">' +
+389 -57
View File
@@ -65,8 +65,8 @@ describe('$route', function() {
$httpBackend.when('GET', 'Chapter.html').respond('chapter');
$httpBackend.when('GET', 'test.html').respond('test');
$httpBackend.when('GET', 'foo.html').respond('foo');
$httpBackend.when('GET', 'baz.html').respond('baz');
$httpBackend.when('GET', 'bar.html').respond('bar');
$httpBackend.when('GET', 'baz.html').respond('baz');
$httpBackend.when('GET', 'http://example.com/trusted-template.html').respond('cross domain trusted template');
$httpBackend.when('GET', '404.html').respond('not found');
};
@@ -76,6 +76,7 @@ describe('$route', function() {
dealoc(element);
});
it('should allow cancellation via $locationChangeStart via $routeChangeStart', function() {
module(function($routeProvider) {
$routeProvider.when('/Edit', {
@@ -1677,95 +1678,413 @@ describe('$route', function() {
});
describe('reloadOnUrl', function() {
it('should reload when `reloadOnUrl` is true and `.url()` changes', function() {
var routeChange = jasmine.createSpy('routeChange');
module(function($routeProvider) {
$routeProvider.when('/path/:param', {});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Reload on `path` change
$location.path('/path/bar');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar'});
routeChange.calls.reset();
// Reload on `search` change
$location.search('foo', 'bar');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
routeChange.calls.reset();
// Reload on `hash` change
$location.hash('baz');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
});
});
it('should reload when `reloadOnUrl` is false and URL maps to different route',
function() {
var routeChange = jasmine.createSpy('routeChange');
var routeUpdate = jasmine.createSpy('routeUpdate');
module(function($routeProvider) {
$routeProvider.
when('/path/:param', {reloadOnUrl: false}).
otherwise({});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
expect(routeChange).not.toHaveBeenCalled();
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Route change
$location.path('/other/path/bar');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
expect($routeParams).toEqual({});
});
}
);
it('should not reload when `reloadOnUrl` is false and URL maps to the same route',
function() {
var routeChange = jasmine.createSpy('routeChange');
var routeUpdate = jasmine.createSpy('routeUpdate');
module(function($routeProvider) {
$routeProvider.when('/path/:param', {reloadOnUrl: false});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
expect(routeChange).not.toHaveBeenCalled();
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Route update (no reload)
$location.path('/path/bar').search('foo', 'bar').hash('baz');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalledOnce();
expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
});
}
);
it('should update `$routeParams` even when not reloading a route', function() {
var routeChange = jasmine.createSpy('routeChange');
module(function($routeProvider) {
$routeProvider.when('/path/:param', {reloadOnUrl: false});
});
inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
expect(routeChange).not.toHaveBeenCalled();
// Initial load
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect($routeParams).toEqual({param: 'foo'});
routeChange.calls.reset();
// Route update (no reload)
$location.path('/path/bar');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect($routeParams).toEqual({param: 'bar'});
});
});
describe('with `$route.reload()`', function() {
var $location;
var $log;
var $rootScope;
var $route;
var routeChangeStart;
var routeChangeSuccess;
beforeEach(module(function($routeProvider) {
$routeProvider.when('/path/:param', {
template: '',
reloadOnUrl: false,
controller: function Controller($log) {
$log.debug('initialized');
}
});
}));
beforeEach(inject(function($compile, _$location_, _$log_, _$rootScope_, _$route_) {
$location = _$location_;
$log = _$log_;
$rootScope = _$rootScope_;
$route = _$route_;
routeChangeStart = jasmine.createSpy('routeChangeStart');
routeChangeSuccess = jasmine.createSpy('routeChangeSuccess');
$rootScope.$on('$routeChangeStart', routeChangeStart);
$rootScope.$on('$routeChangeSuccess', routeChangeSuccess);
element = $compile('<div><ng-view></ng-view></div>')($rootScope);
}));
it('should reload the current route', function() {
$location.path('/path/foo');
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
routeChangeStart.calls.reset();
routeChangeSuccess.calls.reset();
$log.reset();
$route.reload();
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
$log.reset();
});
it('should support preventing a route reload', function() {
$location.path('/path/foo');
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
routeChangeStart.calls.reset();
routeChangeSuccess.calls.reset();
$log.reset();
routeChangeStart.and.callFake(function(evt) { evt.preventDefault(); });
$route.reload();
$rootScope.$digest();
expect($location.path()).toBe('/path/foo');
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
});
it('should reload the current route even if `reloadOnUrl` is disabled',
inject(function($routeParams) {
$location.path('/path/foo');
$rootScope.$digest();
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
expect($routeParams).toEqual({param: 'foo'});
routeChangeStart.calls.reset();
routeChangeSuccess.calls.reset();
$log.reset();
$location.path('/path/bar');
$rootScope.$digest();
expect(routeChangeStart).not.toHaveBeenCalled();
expect(routeChangeSuccess).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
expect($routeParams).toEqual({param: 'bar'});
$route.reload();
$rootScope.$digest();
expect(routeChangeStart).toHaveBeenCalledOnce();
expect(routeChangeSuccess).toHaveBeenCalledOnce();
expect($log.debug.logs).toEqual([['initialized']]);
expect($routeParams).toEqual({param: 'bar'});
$log.reset();
})
);
});
});
describe('reloadOnSearch', function() {
it('should reload a route when reloadOnSearch is enabled and .search() changes', function() {
it('should not have any effect if `reloadOnUrl` is false', function() {
var reloaded = jasmine.createSpy('route reload');
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop});
$routeProvider.when('/foo', {
reloadOnUrl: false,
reloadOnSearch: true
});
});
inject(function($route, $location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', reloaded);
$location.path('/foo');
$rootScope.$digest();
expect(reloaded).toHaveBeenCalled();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({});
reloaded.calls.reset();
// trigger reload
// trigger reload (via .search())
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(reloaded).toHaveBeenCalled();
expect($routeParams).toEqual({foo:'bar'});
expect(reloaded).not.toHaveBeenCalled();
expect($routeParams).toEqual({foo: 'bar'});
// trigger reload (via .hash())
$location.hash('baz');
$rootScope.$digest();
expect(reloaded).not.toHaveBeenCalled();
expect($routeParams).toEqual({foo: 'bar'});
});
});
it('should not reload a route when reloadOnSearch is disabled and only .search() changes', function() {
var routeChange = jasmine.createSpy('route change'),
routeUpdate = jasmine.createSpy('route update');
it('should reload when `reloadOnSearch` is true and `.search()`/`.hash()` changes',
function() {
var reloaded = jasmine.createSpy('route reload');
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
});
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop});
});
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
inject(function($route, $location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', reloaded);
expect(routeChange).not.toHaveBeenCalled();
$location.path('/foo');
$rootScope.$digest();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({});
$location.path('/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalled();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
routeChange.calls.reset();
reloaded.calls.reset();
// don't trigger reload
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalled();
});
});
// trigger reload (via .search())
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({foo: 'bar'});
reloaded.calls.reset();
// trigger reload (via .hash())
$location.hash('baz');
$rootScope.$digest();
expect(reloaded).toHaveBeenCalledOnce();
expect($routeParams).toEqual({foo: 'bar'});
});
}
);
it('should reload reloadOnSearch route when url differs only in route path param', function() {
var routeChange = jasmine.createSpy('route change');
it('should not reload when `reloadOnSearch` is false and `.search()`/`.hash()` changes',
function() {
var routeChange = jasmine.createSpy('route change'),
routeUpdate = jasmine.createSpy('route update');
module(function($routeProvider) {
$routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
});
module(function($routeProvider) {
$routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
});
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
$rootScope.$on('$routeUpdate', routeUpdate);
expect(routeChange).not.toHaveBeenCalled();
expect(routeChange).not.toHaveBeenCalled();
$location.path('/foo/aaa');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalled();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
$location.path('/foo');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
expect(routeUpdate).not.toHaveBeenCalled();
$location.path('/foo/bbb');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalled();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
routeChange.calls.reset();
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
});
});
// don't trigger reload (via .search())
$location.search({foo: 'bar'});
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalledOnce();
routeUpdate.calls.reset();
// don't trigger reload (via .hash())
$location.hash('baz');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
expect(routeUpdate).toHaveBeenCalled();
});
}
);
it('should update params when reloadOnSearch is disabled and .search() changes', function() {
it('should reload when `reloadOnSearch` is false and url differs only in route path param',
function() {
var routeChange = jasmine.createSpy('route change');
module(function($routeProvider) {
$routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
});
inject(function($route, $location, $rootScope) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
expect(routeChange).not.toHaveBeenCalled();
$location.path('/foo/aaa');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
$location.path('/foo/bbb');
$rootScope.$digest();
expect(routeChange).toHaveBeenCalledTimes(2);
routeChange.calls.reset();
$location.search({foo: 'bar'}).hash('baz');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
});
}
);
it('should update params when `reloadOnSearch` is false and `.search()` changes', function() {
var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher');
module(function($routeProvider) {
@@ -1852,7 +2171,8 @@ describe('$route', function() {
});
});
describe('reload', function() {
describe('with `$route.reload()`', function() {
var $location;
var $log;
var $rootScope;
@@ -1886,6 +2206,7 @@ describe('$route', function() {
element = $compile('<div><div ng-view></div></div>')($rootScope);
}));
it('should reload the current route', function() {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1908,6 +2229,7 @@ describe('$route', function() {
$log.reset();
});
it('should support preventing a route reload', function() {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1930,6 +2252,7 @@ describe('$route', function() {
expect($log.debug.logs).toEqual([]);
});
it('should reload even if reloadOnSearch is false', inject(function($routeParams) {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1946,6 +2269,15 @@ describe('$route', function() {
expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
routeChangeSuccessSpy.calls.reset();
$log.reset();
$location.hash('c');
$rootScope.$digest();
expect($routeParams).toEqual({barId: '123', a: 'b'});
expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
$route.reload();
$rootScope.$digest();
expect($routeParams).toEqual({barId: '123', a: 'b'});
+701 -24
View File
File diff suppressed because it is too large Load Diff