From 62b285d8edbe8154e67bcd5c3bc44fb8d806baa8 Mon Sep 17 00:00:00 2001 From: gdangelo Date: Thu, 25 Jun 2015 23:43:18 +0200 Subject: [PATCH 1/4] feat(ngMessages): add support for default message Added support for showing default message when no values are mapped with ng-message. Closes #12008 --- src/ngMessages/messages.js | 150 +++++++++++++++++++++++--------- test/ngMessages/messagesSpec.js | 43 +++++++++ 2 files changed, 153 insertions(+), 40 deletions(-) diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index 445122b67102..c185b50eb786 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,21 @@ 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 (that is to say when the key values does not + * match the attribute value present on each ngMessage directive), then it will render a default message + * using the {@link ngMessageDefault} directive. + * + * ```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 +300,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 +315,7 @@ angular.module('ngMessages', [], function initAngularHelpers() { * ... * ... * ... + * ... * * * @@ -305,6 +323,7 @@ angular.module('ngMessages', [], function initAngularHelpers() { * ... * ... * ... + * ... * * ``` * @@ -333,6 +352,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
* * * @@ -409,8 +429,15 @@ angular.module('ngMessages', [], function initAngularHelpers() { }); if (unmatchedMessages.length !== totalMessages) { + // Unset default message if set + if (ctrl.default) ctrl.default.detach(); + $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS); } else { + + // Set default message if no other matched + if (ctrl.default) ctrl.default.attach(); + $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); } }; @@ -428,23 +455,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 +682,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 +725,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 +771,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 +786,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..15132ecffb90 100644 --- a/test/ngMessages/messagesSpec.js +++ b/test/ngMessages/messagesSpec.js @@ -661,6 +661,49 @@ 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.$digest(); + + expect(element.text().trim()).toBe('Default message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { val: true }; + }); + + expect(element.text().trim()).toBe('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.$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', {'
': '
' + From e80516f465831bdaedfa94e583eba1505bcbe554 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 6 Jun 2018 12:30:02 +0200 Subject: [PATCH 2/4] fixup! feat(ngMessages): add support for default message --- src/ngMessages/messages.js | 28 +++++++++++++-------- test/ngMessages/messagesSpec.js | 43 ++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index c185b50eb786..aeecbd09aa66 100644 --- a/src/ngMessages/messages.js +++ b/src/ngMessages/messages.js @@ -261,9 +261,12 @@ var jqLite; * more about ngAnimate. * * ## Displaying a default message - * If the ngMessages renders no inner ngMessage directive (that is to say when the key values does not - * match the attribute value present on each ngMessage directive), then it will render 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 *
@@ -390,6 +393,7 @@ angular.module('ngMessages', [], function initAngularHelpers() { var unmatchedMessages = []; var matchedKeys = {}; + var truthyKeys = 0; var messageItem = ctrl.head; var messageFound = false; var totalMessages = 0; @@ -402,13 +406,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(); + } } }); } @@ -435,8 +443,8 @@ angular.module('ngMessages', [], function initAngularHelpers() { $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS); } else { - // Set default message if no other matched - if (ctrl.default) ctrl.default.attach(); + // Set default message if keys in collection do not match any message + if (ctrl.default && truthyKeys > 0) ctrl.default.attach(); $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); } diff --git a/test/ngMessages/messagesSpec.js b/test/ngMessages/messagesSpec.js index 15132ecffb90..81ddf4cd7b32 100644 --- a/test/ngMessages/messagesSpec.js +++ b/test/ngMessages/messagesSpec.js @@ -667,23 +667,64 @@ describe('ngMessages', function() { '
Message is set
' + '
Default message is set
' + '
')($rootScope); + $rootScope.$apply(function() { + $rootScope.col = { unexpected: false }; + }); + $rootScope.$digest(); + expect(element.text().trim()).toBe(''); + + $rootScope.$apply(function() { + $rootScope.col = { unexpected: true }; + }); + expect(element.text().trim()).toBe('Default message is set'); $rootScope.$apply(function() { - $rootScope.col = { val: true }; + $rootScope.col = { val: true, unexpected: true }; }); expect(element.text().trim()).toBe('Message is set'); })); + 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'); From 2ebc62fee8b7634f6086d911c036cc5b48264cbc Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 6 Jun 2018 14:20:51 +0200 Subject: [PATCH 3/4] fixup! feat(ngMessages): add support for default message --- src/ngMessages/messages.js | 18 +++++++++++------- test/ngMessages/messagesSpec.js | 12 +++++++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index aeecbd09aa66..bc0026a14f7c 100644 --- a/src/ngMessages/messages.js +++ b/src/ngMessages/messages.js @@ -275,6 +275,8 @@ var jqLite; *
This field has an input error
*
* ``` + * + */ angular.module('ngMessages', [], function initAngularHelpers() { // Access helpers from AngularJS core. @@ -436,16 +438,18 @@ angular.module('ngMessages', [], function initAngularHelpers() { messageCtrl.detach(); }); - if (unmatchedMessages.length !== totalMessages) { - // Unset default message if set - if (ctrl.default) ctrl.default.detach(); + 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 { - - // Set default message if keys in collection do not match any message - if (ctrl.default && truthyKeys > 0) ctrl.default.attach(); - $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); } }; diff --git a/test/ngMessages/messagesSpec.js b/test/ngMessages/messagesSpec.js index 81ddf4cd7b32..0def854811d1 100644 --- a/test/ngMessages/messagesSpec.js +++ b/test/ngMessages/messagesSpec.js @@ -1,6 +1,6 @@ 'use strict'; -describe('ngMessages', function() { +fdescribe('ngMessages', function() { beforeEach(inject.strictDi()); beforeEach(module('ngMessages')); @@ -674,18 +674,28 @@ describe('ngMessages', function() { $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', From 84de725349f73362de6dda2e327da630358de8c8 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 6 Jun 2018 14:33:10 +0200 Subject: [PATCH 4/4] remove fdescribe --- test/ngMessages/messagesSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ngMessages/messagesSpec.js b/test/ngMessages/messagesSpec.js index 0def854811d1..527a577b1f18 100644 --- a/test/ngMessages/messagesSpec.js +++ b/test/ngMessages/messagesSpec.js @@ -1,6 +1,6 @@ 'use strict'; -fdescribe('ngMessages', function() { +describe('ngMessages', function() { beforeEach(inject.strictDi()); beforeEach(module('ngMessages'));