Skip to content

Commit 74298b6

Browse files
gdangeloNarretz
authored andcommitted
feat(ngMessages): add support for default message
Added support for showing default message when no values are mapped with ng-message. Closes angular#12008
1 parent 459997b commit 74298b6

File tree

2 files changed

+152
-37
lines changed

2 files changed

+152
-37
lines changed

src/ngMessages/messages.js

+109-37
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,6 +257,19 @@ var jqLite;
257257
* .some-message.ng-leave.ng-leave-active {}
258258
* ```
259259
*
260+
* ## Displaying a default message
261+
* If the ngMessages renders no inner ngMessage directive (that is to say when the key values does not
262+
* match the attribute value present on each ngMessage directive), then it will render a default message
263+
* using the ngMessageDefault directive.
264+
*
265+
* ```html
266+
* <div ng-messages="myForm.myField.$error" role="alert">
267+
* <div ng-message="required">This field is required</div>
268+
* <div ng-message="minlength">This field is too short</div>
269+
* <div ng-message-default>This is a default message</div>
270+
* </div>
271+
* ```
272+
*
260273
* {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
261274
*/
262275
angular.module('ngMessages', [], function initAngularHelpers() {
@@ -289,6 +302,9 @@ angular.module('ngMessages', [], function initAngularHelpers() {
289302
* A remote template can also be used to promote message reusability and messages can also be
290303
* overridden.
291304
*
305+
* A default message can also be displayed when no `ngMessage` directive is inserted, using the
306+
* `ngMessageDefault` directive.
307+
*
292308
* {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`.
293309
*
294310
* @usage
@@ -298,13 +314,15 @@ angular.module('ngMessages', [], function initAngularHelpers() {
298314
* <ANY ng-message="stringValue">...</ANY>
299315
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
300316
* <ANY ng-message-exp="expressionValue">...</ANY>
317+
* <ANY ng-message-default>...</ANY>
301318
* </ANY>
302319
*
303320
* <!-- or by using element directives -->
304321
* <ng-messages for="expression" role="alert">
305322
* <ng-message when="stringValue">...</ng-message>
306323
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
307324
* <ng-message when-exp="expressionValue">...</ng-message>
325+
* <ng-message-default>...</ng-message-default>
308326
* </ng-messages>
309327
* ```
310328
*
@@ -333,6 +351,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
333351
* <div ng-message="required">You did not enter a field</div>
334352
* <div ng-message="minlength">Your field is too short</div>
335353
* <div ng-message="maxlength">Your field is too long</div>
354+
* <div ng-message-default>This field has an input error</div>
336355
* </div>
337356
* </form>
338357
* </file>
@@ -409,8 +428,15 @@ angular.module('ngMessages', [], function initAngularHelpers() {
409428
});
410429

411430
if (unmatchedMessages.length !== totalMessages) {
431+
// Unset default message if set
432+
if (ctrl.default) ctrl.default.detach();
433+
412434
$animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
413435
} else {
436+
437+
// Set default message if no other matched
438+
if (ctrl.default) ctrl.default.attach();
439+
414440
$animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
415441
}
416442
};
@@ -428,23 +454,31 @@ angular.module('ngMessages', [], function initAngularHelpers() {
428454
}
429455
};
430456

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++;
457+
this.register = function(comment, messageCtrl, isDefault) {
458+
if (isDefault) {
459+
ctrl.default = messageCtrl;
460+
} else {
461+
var nextKey = latestKey.toString();
462+
messages[nextKey] = {
463+
message: messageCtrl
464+
};
465+
insertMessageNode($element[0], comment, nextKey);
466+
comment.$$ngMessageNode = nextKey;
467+
latestKey++;
468+
}
439469

440470
ctrl.reRender();
441471
};
442472

443-
this.deregister = function(comment) {
444-
var key = comment.$$ngMessageNode;
445-
delete comment.$$ngMessageNode;
446-
removeMessageNode($element[0], comment, key);
447-
delete messages[key];
473+
this.deregister = function(comment, isDefault) {
474+
if (isDefault) {
475+
delete ctrl.default;
476+
} else {
477+
var key = comment.$$ngMessageNode;
478+
delete comment.$$ngMessageNode;
479+
removeMessageNode($element[0], comment, key);
480+
delete messages[key];
481+
}
448482
ctrl.reRender();
449483
};
450484

@@ -647,9 +681,44 @@ angular.module('ngMessages', [], function initAngularHelpers() {
647681
*
648682
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
649683
*/
650-
.directive('ngMessageExp', ngMessageDirectiveFactory());
684+
.directive('ngMessageExp', ngMessageDirectiveFactory())
651685

652-
function ngMessageDirectiveFactory() {
686+
/**
687+
* @ngdoc directive
688+
* @name ngMessageDefault
689+
* @restrict AE
690+
* @scope
691+
*
692+
* @description
693+
* `ngMessageDefault` is a directive with the purpose to show and hide a default message.
694+
* For `ngMessageDefault` to operate, no `ngMessage` inner directive should be displayed
695+
* in the parent `ngMessages` directive.
696+
*
697+
* More information about using `ngMessageDefault` can be found in the
698+
* {@link module:ngMessages `ngMessages` module documentation}.
699+
*
700+
* @usage
701+
* ```html
702+
* <!-- using attribute directives -->
703+
* <ANY ng-messages="expression" role="alert">
704+
* <ANY ng-message="stringValue">...</ANY>
705+
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
706+
* <ANY ng-message-default>...</ANY>
707+
* </ANY>
708+
*
709+
* <!-- or by using element directives -->
710+
* <ng-messages for="expression" role="alert">
711+
* <ng-message when="stringValue">...</ng-message>
712+
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
713+
* <ng-message-default>...</ng-message-default>
714+
* </ng-messages>
715+
* ```
716+
*
717+
* @param ngMessageDefault|when no ngMessage matches.
718+
*/
719+
.directive('ngMessageDefault', ngMessageDirectiveFactory(true));
720+
721+
function ngMessageDirectiveFactory(isDefault) {
653722
return ['$animate', function($animate) {
654723
return {
655724
restrict: 'AE',
@@ -658,25 +727,28 @@ function ngMessageDirectiveFactory() {
658727
terminal: true,
659728
require: '^^ngMessages',
660729
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-
};
730+
var commentNode, records, staticExp, dynamicExp;
731+
732+
if (!isDefault) {
733+
commentNode = element[0];
734+
staticExp = attrs.ngMessage || attrs.when;
735+
dynamicExp = attrs.ngMessageExp || attrs.whenExp;
736+
737+
var assignRecords = function(items) {
738+
records = items
739+
? (isArray(items)
740+
? items
741+
: items.split(/[\s,]+/))
742+
: null;
743+
ngMessagesCtrl.reRender();
744+
};
674745

675-
if (dynamicExp) {
676-
assignRecords(scope.$eval(dynamicExp));
677-
scope.$watchCollection(dynamicExp, assignRecords);
678-
} else {
679-
assignRecords(staticExp);
746+
if (dynamicExp) {
747+
assignRecords(scope.$eval(dynamicExp));
748+
scope.$watchCollection(dynamicExp, assignRecords);
749+
} else {
750+
assignRecords(staticExp);
751+
}
680752
}
681753

682754
var currentElement, messageCtrl;
@@ -701,7 +773,7 @@ function ngMessageDirectiveFactory() {
701773
// If the message element was removed via a call to `detach` then `currentElement` will be null
702774
// So this handler only handles cases where something else removed the message element.
703775
if (currentElement && currentElement.$$attachId === $$attachId) {
704-
ngMessagesCtrl.deregister(commentNode);
776+
ngMessagesCtrl.deregister(commentNode, isDefault);
705777
messageCtrl.detach();
706778
}
707779
newScope.$destroy();
@@ -716,14 +788,14 @@ function ngMessageDirectiveFactory() {
716788
$animate.leave(elm);
717789
}
718790
}
719-
});
791+
}, isDefault);
720792

721793
// We need to ensure that this directive deregisters itself when it no longer exists
722794
// Normally this is done when the attached element is destroyed; but if this directive
723795
// gets removed before we attach the message to the DOM there is nothing to watch
724796
// in which case we must deregister when the containing scope is destroyed.
725797
scope.$on('$destroy', function() {
726-
ngMessagesCtrl.deregister(commentNode);
798+
ngMessagesCtrl.deregister(commentNode, isDefault);
727799
});
728800
}
729801
};

test/ngMessages/messagesSpec.js

+43
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,49 @@ 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.$digest();
671+
672+
expect(element.text().trim()).toBe('Default message is set');
673+
674+
$rootScope.$apply(function() {
675+
$rootScope.col = { val: true };
676+
});
677+
678+
expect(element.text().trim()).toBe('Message is set');
679+
}));
680+
681+
it('should handle a default message with ngIf', inject(function($rootScope, $compile) {
682+
element = $compile('<div ng-messages="col">' +
683+
' <div ng-message="val">Message is set</div>' +
684+
' <div ng-if="default" ng-message-default>Default message is set</div>' +
685+
'</div>')($rootScope);
686+
$rootScope.default = true;
687+
$rootScope.$digest();
688+
689+
expect(element.text().trim()).toBe('Default message is set');
690+
691+
$rootScope.$apply('default = false');
692+
693+
expect(element.text().trim()).toBe('');
694+
695+
$rootScope.$apply('default = true');
696+
697+
expect(element.text().trim()).toBe('Default message is set');
698+
699+
$rootScope.$apply(function() {
700+
$rootScope.col = { val: true };
701+
});
702+
703+
expect(element.text().trim()).toBe('Message is set');
704+
}));
705+
});
706+
664707
describe('when including templates', function() {
665708
they('should work with a dynamic collection model which is managed by ngRepeat',
666709
{'<div ng-messages-include="...">': '<div ng-messages="item">' +

0 commit comments

Comments
 (0)