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/ngMessages/messagesSpec.js b/test/ngMessages/messagesSpec.js
index b86f764f37d0..527a577b1f18 100644
--- a/test/ngMessages/messagesSpec.js
+++ b/test/ngMessages/messagesSpec.js
@@ -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('' +
+ '
Message is set
' +
+ '
Default message is set
' +
+ '
')($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('' +
+ '
Message is set
' +
+ '
Other message is set
' +
+ '
Default message is set
' +
+ '
')($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('' +
+ '
Message is set
' +
+ '
Default message is set
' +
+ '
')($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',
{'