Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 84d80be

Browse files
authored
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
1 parent 03a4782 commit 84d80be

File tree

2 files changed

+223
-47
lines changed

2 files changed

+223
-47
lines changed

src/ngMessages/messages.js

+129-47
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var jqLite;
1818
* sequencing based on the order of how the messages are defined in the template.
1919
*
2020
* Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude`
21-
* `ngMessage` and `ngMessageExp` directives.
21+
* `ngMessage`, `ngMessageExp` and `ngMessageDefault` directives.
2222
*
2323
* ## Usage
2424
* The `ngMessages` directive allows keys in a key/value collection to be associated with a child element
@@ -257,7 +257,26 @@ var jqLite;
257257
* .some-message.ng-leave.ng-leave-active {}
258258
* ```
259259
*
260-
* {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
260+
* {@link ngAnimate See the ngAnimate docs} to learn how to use JavaScript animations or to learn
261+
* more about ngAnimate.
262+
*
263+
* ## Displaying a default message
264+
* If the ngMessages renders no inner ngMessage directive (i.e. when none of the truthy
265+
* keys are matched by a defined message), then it will render a default message
266+
* using the {@link ngMessageDefault} directive.
267+
* Note that matched messages will always take precedence over unmatched messages. That means
268+
* the default message will not be displayed when another message is matched. This is also
269+
* true for `ng-messages-multiple`.
270+
*
271+
* ```html
272+
* <div ng-messages="myForm.myField.$error" role="alert">
273+
* <div ng-message="required">This field is required</div>
274+
* <div ng-message="minlength">This field is too short</div>
275+
* <div ng-message-default>This field has an input error</div>
276+
* </div>
277+
* ```
278+
*
279+
261280
*/
262281
angular.module('ngMessages', [], function initAngularHelpers() {
263282
// Access helpers from AngularJS core.
@@ -286,8 +305,11 @@ angular.module('ngMessages', [], function initAngularHelpers() {
286305
* at a time and this depends on the prioritization of the messages within the template. (This can
287306
* be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.)
288307
*
289-
* A remote template can also be used to promote message reusability and messages can also be
290-
* overridden.
308+
* A remote template can also be used (With {@link ngMessagesInclude}) to promote message
309+
* reusability and messages can also be overridden.
310+
*
311+
* A default message can also be displayed when no `ngMessage` directive is inserted, using the
312+
* {@link ngMessageDefault} directive.
291313
*
292314
* {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`.
293315
*
@@ -298,13 +320,15 @@ angular.module('ngMessages', [], function initAngularHelpers() {
298320
* <ANY ng-message="stringValue">...</ANY>
299321
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
300322
* <ANY ng-message-exp="expressionValue">...</ANY>
323+
* <ANY ng-message-default>...</ANY>
301324
* </ANY>
302325
*
303326
* <!-- or by using element directives -->
304327
* <ng-messages for="expression" role="alert">
305328
* <ng-message when="stringValue">...</ng-message>
306329
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
307330
* <ng-message when-exp="expressionValue">...</ng-message>
331+
* <ng-message-default>...</ng-message-default>
308332
* </ng-messages>
309333
* ```
310334
*
@@ -333,6 +357,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
333357
* <div ng-message="required">You did not enter a field</div>
334358
* <div ng-message="minlength">Your field is too short</div>
335359
* <div ng-message="maxlength">Your field is too long</div>
360+
* <div ng-message-default>This field has an input error</div>
336361
* </div>
337362
* </form>
338363
* </file>
@@ -370,6 +395,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
370395

371396
var unmatchedMessages = [];
372397
var matchedKeys = {};
398+
var truthyKeys = 0;
373399
var messageItem = ctrl.head;
374400
var messageFound = false;
375401
var totalMessages = 0;
@@ -382,13 +408,17 @@ angular.module('ngMessages', [], function initAngularHelpers() {
382408
var messageUsed = false;
383409
if (!messageFound) {
384410
forEach(collection, function(value, key) {
385-
if (!messageUsed && truthy(value) && messageCtrl.test(key)) {
386-
// this is to prevent the same error name from showing up twice
387-
if (matchedKeys[key]) return;
388-
matchedKeys[key] = true;
411+
if (truthy(value) && !messageUsed) {
412+
truthyKeys++;
413+
414+
if (messageCtrl.test(key)) {
415+
// this is to prevent the same error name from showing up twice
416+
if (matchedKeys[key]) return;
417+
matchedKeys[key] = true;
389418

390-
messageUsed = true;
391-
messageCtrl.attach();
419+
messageUsed = true;
420+
messageCtrl.attach();
421+
}
392422
}
393423
});
394424
}
@@ -408,7 +438,16 @@ angular.module('ngMessages', [], function initAngularHelpers() {
408438
messageCtrl.detach();
409439
});
410440

411-
if (unmatchedMessages.length !== totalMessages) {
441+
var messageMatched = unmatchedMessages.length !== totalMessages;
442+
var attachDefault = ctrl.default && !messageMatched && truthyKeys > 0;
443+
444+
if (attachDefault) {
445+
ctrl.default.attach();
446+
} else if (ctrl.default) {
447+
ctrl.default.detach();
448+
}
449+
450+
if (messageMatched || attachDefault) {
412451
$animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
413452
} else {
414453
$animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
@@ -428,23 +467,31 @@ angular.module('ngMessages', [], function initAngularHelpers() {
428467
}
429468
};
430469

431-
this.register = function(comment, messageCtrl) {
432-
var nextKey = latestKey.toString();
433-
messages[nextKey] = {
434-
message: messageCtrl
435-
};
436-
insertMessageNode($element[0], comment, nextKey);
437-
comment.$$ngMessageNode = nextKey;
438-
latestKey++;
470+
this.register = function(comment, messageCtrl, isDefault) {
471+
if (isDefault) {
472+
ctrl.default = messageCtrl;
473+
} else {
474+
var nextKey = latestKey.toString();
475+
messages[nextKey] = {
476+
message: messageCtrl
477+
};
478+
insertMessageNode($element[0], comment, nextKey);
479+
comment.$$ngMessageNode = nextKey;
480+
latestKey++;
481+
}
439482

440483
ctrl.reRender();
441484
};
442485

443-
this.deregister = function(comment) {
444-
var key = comment.$$ngMessageNode;
445-
delete comment.$$ngMessageNode;
446-
removeMessageNode($element[0], comment, key);
447-
delete messages[key];
486+
this.deregister = function(comment, isDefault) {
487+
if (isDefault) {
488+
delete ctrl.default;
489+
} else {
490+
var key = comment.$$ngMessageNode;
491+
delete comment.$$ngMessageNode;
492+
removeMessageNode($element[0], comment, key);
493+
delete messages[key];
494+
}
448495
ctrl.reRender();
449496
};
450497

@@ -647,9 +694,41 @@ angular.module('ngMessages', [], function initAngularHelpers() {
647694
*
648695
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
649696
*/
650-
.directive('ngMessageExp', ngMessageDirectiveFactory());
697+
.directive('ngMessageExp', ngMessageDirectiveFactory())
698+
699+
/**
700+
* @ngdoc directive
701+
* @name ngMessageDefault
702+
* @restrict AE
703+
* @scope
704+
*
705+
* @description
706+
* `ngMessageDefault` is a directive with the purpose to show and hide a default message for
707+
* {@link ngMessages}, when none of provided messages matches.
708+
*
709+
* More information about using `ngMessageDefault` can be found in the
710+
* {@link module:ngMessages `ngMessages` module documentation}.
711+
*
712+
* @usage
713+
* ```html
714+
* <!-- using attribute directives -->
715+
* <ANY ng-messages="expression" role="alert">
716+
* <ANY ng-message="stringValue">...</ANY>
717+
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
718+
* <ANY ng-message-default>...</ANY>
719+
* </ANY>
720+
*
721+
* <!-- or by using element directives -->
722+
* <ng-messages for="expression" role="alert">
723+
* <ng-message when="stringValue">...</ng-message>
724+
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
725+
* <ng-message-default>...</ng-message-default>
726+
* </ng-messages>
727+
*
728+
*/
729+
.directive('ngMessageDefault', ngMessageDirectiveFactory(true));
651730

652-
function ngMessageDirectiveFactory() {
731+
function ngMessageDirectiveFactory(isDefault) {
653732
return ['$animate', function($animate) {
654733
return {
655734
restrict: 'AE',
@@ -658,25 +737,28 @@ function ngMessageDirectiveFactory() {
658737
terminal: true,
659738
require: '^^ngMessages',
660739
link: function(scope, element, attrs, ngMessagesCtrl, $transclude) {
661-
var commentNode = element[0];
662-
663-
var records;
664-
var staticExp = attrs.ngMessage || attrs.when;
665-
var dynamicExp = attrs.ngMessageExp || attrs.whenExp;
666-
var assignRecords = function(items) {
667-
records = items
668-
? (isArray(items)
669-
? items
670-
: items.split(/[\s,]+/))
671-
: null;
672-
ngMessagesCtrl.reRender();
673-
};
740+
var commentNode, records, staticExp, dynamicExp;
741+
742+
if (!isDefault) {
743+
commentNode = element[0];
744+
staticExp = attrs.ngMessage || attrs.when;
745+
dynamicExp = attrs.ngMessageExp || attrs.whenExp;
746+
747+
var assignRecords = function(items) {
748+
records = items
749+
? (isArray(items)
750+
? items
751+
: items.split(/[\s,]+/))
752+
: null;
753+
ngMessagesCtrl.reRender();
754+
};
674755

675-
if (dynamicExp) {
676-
assignRecords(scope.$eval(dynamicExp));
677-
scope.$watchCollection(dynamicExp, assignRecords);
678-
} else {
679-
assignRecords(staticExp);
756+
if (dynamicExp) {
757+
assignRecords(scope.$eval(dynamicExp));
758+
scope.$watchCollection(dynamicExp, assignRecords);
759+
} else {
760+
assignRecords(staticExp);
761+
}
680762
}
681763

682764
var currentElement, messageCtrl;
@@ -701,7 +783,7 @@ function ngMessageDirectiveFactory() {
701783
// If the message element was removed via a call to `detach` then `currentElement` will be null
702784
// So this handler only handles cases where something else removed the message element.
703785
if (currentElement && currentElement.$$attachId === $$attachId) {
704-
ngMessagesCtrl.deregister(commentNode);
786+
ngMessagesCtrl.deregister(commentNode, isDefault);
705787
messageCtrl.detach();
706788
}
707789
newScope.$destroy();
@@ -716,14 +798,14 @@ function ngMessageDirectiveFactory() {
716798
$animate.leave(elm);
717799
}
718800
}
719-
});
801+
}, isDefault);
720802

721803
// We need to ensure that this directive deregisters itself when it no longer exists
722804
// Normally this is done when the attached element is destroyed; but if this directive
723805
// gets removed before we attach the message to the DOM there is nothing to watch
724806
// in which case we must deregister when the containing scope is destroyed.
725807
scope.$on('$destroy', function() {
726-
ngMessagesCtrl.deregister(commentNode);
808+
ngMessagesCtrl.deregister(commentNode, isDefault);
727809
});
728810
}
729811
};

test/ngMessages/messagesSpec.js

+94
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,100 @@ describe('ngMessages', function() {
661661
);
662662

663663

664+
describe('default message', function() {
665+
it('should render a default message when no message matches', inject(function($rootScope, $compile) {
666+
element = $compile('<div ng-messages="col">' +
667+
' <div ng-message="val">Message is set</div>' +
668+
' <div ng-message-default>Default message is set</div>' +
669+
'</div>')($rootScope);
670+
$rootScope.$apply(function() {
671+
$rootScope.col = { unexpected: false };
672+
});
673+
674+
$rootScope.$digest();
675+
676+
expect(element.text().trim()).toBe('');
677+
expect(element).not.toHaveClass('ng-active');
678+
679+
$rootScope.$apply(function() {
680+
$rootScope.col = { unexpected: true };
681+
});
682+
683+
expect(element.text().trim()).toBe('Default message is set');
684+
expect(element).toHaveClass('ng-active');
685+
686+
$rootScope.$apply(function() {
687+
$rootScope.col = { unexpected: false };
688+
});
689+
690+
expect(element.text().trim()).toBe('');
691+
expect(element).not.toHaveClass('ng-active');
692+
693+
$rootScope.$apply(function() {
694+
$rootScope.col = { val: true, unexpected: true };
695+
});
696+
697+
expect(element.text().trim()).toBe('Message is set');
698+
expect(element).toHaveClass('ng-active');
699+
}));
700+
701+
it('should not render a default message with ng-messages-multiple if another error matches',
702+
inject(function($rootScope, $compile) {
703+
element = $compile('<div ng-messages="col" ng-messages-multiple>' +
704+
' <div ng-message="val">Message is set</div>' +
705+
' <div ng-message="other">Other message is set</div>' +
706+
' <div ng-message-default>Default message is set</div>' +
707+
'</div>')($rootScope);
708+
709+
expect(element.text().trim()).toBe('');
710+
711+
$rootScope.$apply(function() {
712+
$rootScope.col = { val: true, other: false, unexpected: false };
713+
});
714+
715+
expect(element.text().trim()).toBe('Message is set');
716+
717+
$rootScope.$apply(function() {
718+
$rootScope.col = { val: true, other: true, unexpected: true };
719+
});
720+
721+
expect(element.text().trim()).toBe('Message is set Other message is set');
722+
723+
$rootScope.$apply(function() {
724+
$rootScope.col = { val: false, other: false, unexpected: true };
725+
});
726+
727+
expect(element.text().trim()).toBe('Default message is set');
728+
})
729+
);
730+
731+
it('should handle a default message with ngIf', inject(function($rootScope, $compile) {
732+
element = $compile('<div ng-messages="col">' +
733+
' <div ng-message="val">Message is set</div>' +
734+
' <div ng-if="default" ng-message-default>Default message is set</div>' +
735+
'</div>')($rootScope);
736+
$rootScope.default = true;
737+
$rootScope.col = {unexpected: true};
738+
$rootScope.$digest();
739+
740+
expect(element.text().trim()).toBe('Default message is set');
741+
742+
$rootScope.$apply('default = false');
743+
744+
expect(element.text().trim()).toBe('');
745+
746+
$rootScope.$apply('default = true');
747+
748+
expect(element.text().trim()).toBe('Default message is set');
749+
750+
$rootScope.$apply(function() {
751+
$rootScope.col = { val: true };
752+
});
753+
754+
expect(element.text().trim()).toBe('Message is set');
755+
}));
756+
});
757+
664758
describe('when including templates', function() {
665759
they('should work with a dynamic collection model which is managed by ngRepeat',
666760
{'<div ng-messages-include="...">': '<div ng-messages="item">' +

0 commit comments

Comments
 (0)