From 0af3210e2e5037a9d3ca764a412f2a484bd929ec Mon Sep 17 00:00:00 2001 From: gdangelo Date: Thu, 25 Jun 2015 23:43:18 +0200 Subject: [PATCH 1/2] 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 | 152 ++++++++++++++++++++++++++++---- test/ngMessages/messagesSpec.js | 18 ++++ 2 files changed, 152 insertions(+), 18 deletions(-) diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index 14ffe00506b0..ecd4e43bbc8e 100644 --- a/src/ngMessages/messages.js +++ b/src/ngMessages/messages.js @@ -21,7 +21,7 @@ var jqLite = angular.element; * 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 listens on a key/value collection which is set on the ngMessages attribute. @@ -239,6 +239,19 @@ var jqLite = angular.element; * .some-message.ng-leave.ng-leave-active {} * ``` * + * ## 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 ngMessageDefault directive. + * + * ```html + *
+ *
This field is required
+ *
This field is too short
+ *
This is a default message
+ *
+ * ``` + * * {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate. */ angular.module('ngMessages', []) @@ -260,6 +273,9 @@ angular.module('ngMessages', []) * 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 default message can also be displayed when no `ngMessage` directive is inserted, using the + * `ngMessageDefault` directive. + * * A remote template can also be used to promote message reusability and messages can also be * overridden. * @@ -272,6 +288,7 @@ angular.module('ngMessages', []) * ... * ... * ... + * ... * * * @@ -279,6 +296,7 @@ angular.module('ngMessages', []) * ... * ... * ... + * ... * * ``` * @@ -307,6 +325,7 @@ angular.module('ngMessages', []) *
You did not enter a field
*
Your field is too short
*
Your field is too long
+ *
This is a default message
* * * @@ -379,9 +398,17 @@ angular.module('ngMessages', []) messageCtrl.detach(); }); - unmatchedMessages.length !== totalMessages - ? $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS) - : $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); + if (unmatchedMessages.length !== totalMessages) { + // Unset default message if setted + if (ctrl.default) { ctrl.default.detach(); } + + $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS); + } else { + // Set default message when no other one matched + if (ctrl.default) { ctrl.default.attach(); } + + $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); + } }; $scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render); @@ -397,23 +424,32 @@ angular.module('ngMessages', []) } }; - 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(); }; @@ -594,7 +630,43 @@ angular.module('ngMessages', []) * * @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key. */ - .directive('ngMessageExp', ngMessageDirectiveFactory('A')); + .directive('ngMessageExp', ngMessageDirectiveFactory('A')) + + /** + * @ngdoc directive + * @name ngMessageDefault + * @restrict AE + * @scope + * + * @description + * `ngMessageDefault` is a directive with the purpose to show and hide a default message. + * For `ngMessageDefault` to operate, no `ngMessage` inner directive should be displayed + * in the parent `ngMessages` directive. + * + * More information about using `ngMessageDefault` can be found in the + * {@link module:ngMessages `ngMessages` module documentation}. + * + * @usage + * ```html + * + * + * ... + * ... + * ... + * + * + * + * + * ... + * ... + * ... + * + * ``` + * + * @param {expression} ngMessageDefault|when no ngMessage matches. + */ + .directive('ngMessageDefault', ngMessageDefaultDirectiveFactory('AE')); + function ngMessageDirectiveFactory(restrict) { return ['$animate', function($animate) { @@ -668,3 +740,47 @@ function ngMessageDirectiveFactory(restrict) { } } } + +function ngMessageDefaultDirectiveFactory(restrict) { + return ['$animate', function($animate) { + return { + restrict: 'AE', + transclude: 'element', + terminal: true, + require: '^^ngMessages', + link: function(scope, element, attrs, ngMessagesCtrl, $transclude) { + var commentNode = element[0]; + + var currentElement, messageCtrl; + ngMessagesCtrl.register(commentNode, messageCtrl = { + attach: function() { + if (!currentElement) { + $transclude(scope, function(elm) { + $animate.enter(elm, null, element); + currentElement = elm; + + // in the event that the parent element is destroyed + // by any other structural directive then it's time + // to deregister the default message (boolean set to true) + // from the controller + currentElement.on('$destroy', function() { + if (currentElement) { + ngMessagesCtrl.deregister(commentNode, true); + messageCtrl.detach(); + } + }); + }); + } + }, + detach: function() { + if (currentElement) { + var elm = currentElement; + currentElement = null; + $animate.leave(elm); + } + } + }, true); // boolean set to true to specify default message + } + }; + }]; +} diff --git a/test/ngMessages/messagesSpec.js b/test/ngMessages/messagesSpec.js index 037175031ded..f81cd4baadf0 100644 --- a/test/ngMessages/messagesSpec.js +++ b/test/ngMessages/messagesSpec.js @@ -401,6 +401,24 @@ describe('ngMessages', function() { }); }); + it('should render a default message when no one matched', inject(function($rootScope, $compile) { + element = $compile('
' + + '
Message is set
' + + '
Default message is set
' + + '
')($rootScope); + $rootScope.$digest(); + + //expect(element.text()).not.toContain('Message is set'); + expect(element.text()).toContain('Default message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { val: true }; + }); + + //expect(element.text()).toContain('Message is set'); + expect(element.text()).not.toContain('Default message is set'); + })); + describe('when including templates', function() { they('should work with a dynamic collection model which is managed by ngRepeat', {'
': '
' + From 4c09386e2f49f5871c3d327444b1a6c24cd3542f Mon Sep 17 00:00:00 2001 From: gdangelo Date: Sun, 5 Jul 2015 13:46:48 +0200 Subject: [PATCH 2/2] refactor(ngMessages): change code to make it DRY Added a new param (boolean, isDefault) to the factory ngMessageDirectiveFactory, in order to reuse code in ngMessageDefault directive. Closes #12008 --- src/ngMessages/messages.js | 93 +++++++++++--------------------------- 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index ecd4e43bbc8e..6edb638dfb82 100644 --- a/src/ngMessages/messages.js +++ b/src/ngMessages/messages.js @@ -665,10 +665,10 @@ angular.module('ngMessages', []) * * @param {expression} ngMessageDefault|when no ngMessage matches. */ - .directive('ngMessageDefault', ngMessageDefaultDirectiveFactory('AE')); + .directive('ngMessageDefault', ngMessageDirectiveFactory('AE', true)); -function ngMessageDirectiveFactory(restrict) { +function ngMessageDirectiveFactory(restrict, isDefault) { return ['$animate', function($animate) { return { restrict: 'AE', @@ -676,25 +676,28 @@ function ngMessageDirectiveFactory(restrict) { 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(); - }; - - if (dynamicExp) { - assignRecords(scope.$eval(dynamicExp)); - scope.$watchCollection(dynamicExp, assignRecords); - } else { - assignRecords(staticExp); + 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); + } } var currentElement, messageCtrl; @@ -713,7 +716,7 @@ function ngMessageDirectiveFactory(restrict) { // to deregister the message from the controller currentElement.on('$destroy', function() { if (currentElement) { - ngMessagesCtrl.deregister(commentNode); + ngMessagesCtrl.deregister(commentNode, isDefault); messageCtrl.detach(); } }); @@ -727,7 +730,7 @@ function ngMessageDirectiveFactory(restrict) { $animate.leave(elm); } } - }); + }, isDefault); } }; }]; @@ -740,47 +743,3 @@ function ngMessageDirectiveFactory(restrict) { } } } - -function ngMessageDefaultDirectiveFactory(restrict) { - return ['$animate', function($animate) { - return { - restrict: 'AE', - transclude: 'element', - terminal: true, - require: '^^ngMessages', - link: function(scope, element, attrs, ngMessagesCtrl, $transclude) { - var commentNode = element[0]; - - var currentElement, messageCtrl; - ngMessagesCtrl.register(commentNode, messageCtrl = { - attach: function() { - if (!currentElement) { - $transclude(scope, function(elm) { - $animate.enter(elm, null, element); - currentElement = elm; - - // in the event that the parent element is destroyed - // by any other structural directive then it's time - // to deregister the default message (boolean set to true) - // from the controller - currentElement.on('$destroy', function() { - if (currentElement) { - ngMessagesCtrl.deregister(commentNode, true); - messageCtrl.detach(); - } - }); - }); - } - }, - detach: function() { - if (currentElement) { - var elm = currentElement; - currentElement = null; - $animate.leave(elm); - } - } - }, true); // boolean set to true to specify default message - } - }; - }]; -}