diff --git a/angularFiles.js b/angularFiles.js
index 0233722adfc4..01d9dfd3f0f3 100644
--- a/angularFiles.js
+++ b/angularFiles.js
@@ -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',
diff --git a/docs/content/error/ngRef/noctrl.ngdoc b/docs/content/error/ngRef/noctrl.ngdoc
new file mode 100644
index 000000000000..29d19a9ae134
--- /dev/null
+++ b/docs/content/error/ngRef/noctrl.ngdoc
@@ -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 `
`.
diff --git a/docs/content/error/ngRef/nonassign.ngdoc b/docs/content/error/ngRef/nonassign.ngdoc
new file mode 100644
index 000000000000..9c1c52ee35b7
--- /dev/null
+++ b/docs/content/error/ngRef/nonassign.ngdoc
@@ -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:
+
+```
+
+
+
+
+
+
+
+```
+
+To resolve this error, use a path expression that is assignable:
+
+```
+
+
+```
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index c18889911a50..dca14bdd6ffd 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -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,
diff --git a/src/minErr.js b/src/minErr.js
index a2f0ddc2d544..234d244f544f 100644
--- a/src/minErr.js
+++ b/src/minErr.js
@@ -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);
diff --git a/src/ng/directive/ngRef.js b/src/ng/directive/ngRef.js
new file mode 100644
index 000000000000..4b3c7a746ba4
--- /dev/null
+++ b/src/ng/directive/ngRef.js
@@ -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.
+ *
+ *
+ *
+ *
+ *
+ * You are using a component in the same template to show it.
+ *
+ *
+ *
+ * angular.module('myApp', [])
+ * .component('myToggle', {
+ * controller: function ToggleController() {
+ * var opened = false;
+ * this.isOpen = function() { return opened; };
+ * this.toggle = function() { opened = !opened; };
+ * }
+ * });
+ *
+ *
+ * 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);
+ * });
+ *
+ *
+ *
+ * @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.
+ *
+ *
+ *
+ *
+ *
+ *
+ * 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: 'myToggle',
+ * transclude: true,
+ * controller: function ToggleController() {
+ * var opened = false;
+ * this.isOpen = function() { return opened; };
+ * this.toggle = function() { opened = !opened; };
+ * }
+ * })
+ * .component('myWrapper', {
+ * transclude: true,
+ * template: 'myWrapper' +
+ * '
+ *
+ *
+ * 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;
+ * }
+ *
+ *
+ * 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');
+ * });
+ * });
+ *
+ *
+ *
+ */
+
+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 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);
+ }
+ });
+ };
+ }
+ };
+}];
diff --git a/src/ngAnimate/module.js b/src/ngAnimate/module.js
index 0f929f3b0018..d7ef5d873c50 100644
--- a/src/ngAnimate/module.js
+++ b/src/ngAnimate/module.js
@@ -275,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 ["animation aware" directive](#directive-support), 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
diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js
index 445122b67102..bc0026a14f7c 100644
--- a/src/ngMessages/messages.js
+++ b/src/ngMessages/messages.js
@@ -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
+ *
+ *
This field is required
+ *
This field is too short
+ *
This field has an input error
+ *
+ * ```
+ *
+
*/
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() {
* ...
* ...
* ...
+ * ...
*
*
*
@@ -305,6 +328,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* ...
* ...
* ...
+ * ...
*
* ```
*
@@ -333,6 +357,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
*
You did not enter a field
*
Your field is too short
*
Your field is too long
+ *
This field has an input error
*
*
*
@@ -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++;
+
+ 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();
+ 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())
+
+ /**
+ * @ngdoc directive
+ * @name ngMessageDefault
+ * @restrict AE
+ * @scope
+ *
+ * @description
+ * `ngMessageDefault` is a directive with the purpose to show and hide a default message for
+ * {@link 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
+ *
+ *
+ * ...
+ * ...
+ * ...
+ *
+ *
+ *
+ *
+ * ...
+ * ...
+ * ...
+ *
+ *
+ */
+ .directive('ngMessageDefault', ngMessageDirectiveFactory(true));
-function ngMessageDirectiveFactory() {
+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 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();
- };
+ var commentNode, records, staticExp, dynamicExp;
+
+ if (!isDefault) {
+ commentNode = element[0];
+ staticExp = attrs.ngMessage || attrs.when;
+ dynamicExp = attrs.ngMessageExp || attrs.whenExp;
+
+ 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);
+ 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);
});
}
};
diff --git a/test/helpers/matchers.js b/test/helpers/matchers.js
index ac297609e579..5010b212f6d2 100644
--- a/test/helpers/matchers.js
+++ b/test/helpers/matchers.js
@@ -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',
diff --git a/test/minErrSpec.js b/test/minErrSpec.js
index 4319fd88d569..66e018b077c8 100644
--- a/test/minErrSpec.js
+++ b/test/minErrSpec.js
@@ -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 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 not change objectMaxDepth when undefined is supplied', function() {
- errorHandlingConfig({objectMaxDepth: undefined});
- expect(errorHandlingConfig().objectMaxDepth).toBe(originalObjectMaxDepthInErrorMessage);
+
+ 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);
+ });
});
- 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');
+ });
+
});
});
diff --git a/test/ng/directive/ngRefSpec.js b/test/ng/directive/ngRefSpec.js
new file mode 100644
index 000000000000..ef62fae99cad
--- /dev/null
+++ b/test/ng/directive/ngRefSpec.js
@@ -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('')($rootScope);
+ expect($rootScope.$ctrl).toBe('undamaged');
+ expect($rootScope.myComponentRef).toBe(myComponentController);
+ });
+
+ it('should throw if the expression is not assignable', function() {
+ expect(function() {
+ $compile('')($rootScope);
+ }).toThrowMinErr('ngRef', 'nonassign', 'Expression in ngRef="\'hello\'" is non-assignable!');
+ });
+
+ it('should work with non:normalized entity name', function() {
+ $compile('')($rootScope);
+ expect($rootScope.myComponent1).toBe(myComponentController);
+ });
+
+ it('should work with data-non-normalized entity name', function() {
+ $compile('')($rootScope);
+ expect($rootScope.myComponent2).toBe(myComponentController);
+ });
+
+ it('should work with x-non-normalized entity name', function() {
+ $compile('')($rootScope);
+ expect($rootScope.myComponent3).toBe(myComponentController);
+ });
+
+ it('should work with data-non-normalized attribute name', function() {
+ $compile('')($rootScope);
+ expect($rootScope.myComponent1).toBe(myComponentController);
+ });
+
+ it('should work with x-non-normalized attribute name', function() {
+ $compile('')($rootScope);
+ expect($rootScope.myComponent2).toBe(myComponentController);
+ });
+
+ it('should not bind the controller of an attribute directive', function() {
+ $compile('')($rootScope);
+ expect($rootScope.myComponentRef).toBe(myComponentController);
+ });
+
+ it('should not leak to parent scopes', function() {
+ var template =
+ '
' +
+ '' +
+ '
';
+ $compile(template)($rootScope);
+ expect($rootScope.myComponent).toBe(undefined);
+ });
+
+ it('should nullify the variable once the component is destroyed', function() {
+ var template = '
';
+
+ 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 = '';
+ $rootScope.$ctrl = {};
+ var parent = $compile('')($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('')($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('my text')($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('
my text
')($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('')($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('')($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('')($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('')($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('')($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('')($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('')($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: '',
+ controller: function() {
+ this.text = 'SUCCESS';
+ }
+ });
+ });
+
+ inject(function($compile, $rootScope) {
+ var template = '{{myComponent.text}}';
+ 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 =
+ '
' +
+ '' +
+ '{{myComponent.text}}' +
+ '' +
+ '
';
+ 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: '',
+ transclude: true,
+ controller: function($scope) {
+ this.text = 'SUCCESS';
+ }
+ });
+ });
+
+ inject(function($compile, $rootScope) {
+ var template =
+ '