Skip to content

Commit 0af3210

Browse files
committed
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 1ce5d21 commit 0af3210

File tree

2 files changed

+152
-18
lines changed

2 files changed

+152
-18
lines changed

src/ngMessages/messages.js

+134-18
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var jqLite = angular.element;
2121
* sequencing based on the order of how the messages are defined in the template.
2222
*
2323
* Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude`
24-
* `ngMessage` and `ngMessageExp` directives.
24+
* `ngMessage`, `ngMessageExp` and `ngMessageDefault` directives.
2525
*
2626
* # Usage
2727
* The `ngMessages` directive listens on a key/value collection which is set on the ngMessages attribute.
@@ -239,6 +239,19 @@ var jqLite = angular.element;
239239
* .some-message.ng-leave.ng-leave-active {}
240240
* ```
241241
*
242+
* ## Displaying a default message
243+
* If the ngMessages renders no inner ngMessage directive (that is to say when the key values does not
244+
* match the attribute value present on each ngMessage directive), then it will render a default message
245+
* using the ngMessageDefault directive.
246+
*
247+
* ```html
248+
* <div ng-messages="myForm.myField.$error" role="alert">
249+
* <div ng-message="required">This field is required</div>
250+
* <div ng-message="minlength">This field is too short</div>
251+
* <div ng-message-default>This is a default message</div>
252+
* </div>
253+
* ```
254+
*
242255
* {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
243256
*/
244257
angular.module('ngMessages', [])
@@ -260,6 +273,9 @@ angular.module('ngMessages', [])
260273
* at a time and this depends on the prioritization of the messages within the template. (This can
261274
* be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.)
262275
*
276+
* A default message can also be displayed when no `ngMessage` directive is inserted, using the
277+
* `ngMessageDefault` directive.
278+
*
263279
* A remote template can also be used to promote message reusability and messages can also be
264280
* overridden.
265281
*
@@ -272,13 +288,15 @@ angular.module('ngMessages', [])
272288
* <ANY ng-message="stringValue">...</ANY>
273289
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
274290
* <ANY ng-message-exp="expressionValue">...</ANY>
291+
* <ANY ng-message-default>...</ANY>
275292
* </ANY>
276293
*
277294
* <!-- or by using element directives -->
278295
* <ng-messages for="expression" role="alert">
279296
* <ng-message when="stringValue">...</ng-message>
280297
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
281298
* <ng-message when-exp="expressionValue">...</ng-message>
299+
* <ng-message-default>...</ng-message-default>
282300
* </ng-messages>
283301
* ```
284302
*
@@ -307,6 +325,7 @@ angular.module('ngMessages', [])
307325
* <div ng-message="required">You did not enter a field</div>
308326
* <div ng-message="minlength">Your field is too short</div>
309327
* <div ng-message="maxlength">Your field is too long</div>
328+
* <div ng-message-default>This is a default message</div>
310329
* </div>
311330
* </form>
312331
* </file>
@@ -379,9 +398,17 @@ angular.module('ngMessages', [])
379398
messageCtrl.detach();
380399
});
381400

382-
unmatchedMessages.length !== totalMessages
383-
? $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS)
384-
: $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
401+
if (unmatchedMessages.length !== totalMessages) {
402+
// Unset default message if setted
403+
if (ctrl.default) { ctrl.default.detach(); }
404+
405+
$animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
406+
} else {
407+
// Set default message when no other one matched
408+
if (ctrl.default) { ctrl.default.attach(); }
409+
410+
$animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
411+
}
385412
};
386413

387414
$scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render);
@@ -397,23 +424,32 @@ angular.module('ngMessages', [])
397424
}
398425
};
399426

400-
this.register = function(comment, messageCtrl) {
401-
var nextKey = latestKey.toString();
402-
messages[nextKey] = {
403-
message: messageCtrl
404-
};
405-
insertMessageNode($element[0], comment, nextKey);
406-
comment.$$ngMessageNode = nextKey;
407-
latestKey++;
427+
this.register = function(comment, messageCtrl, isDefault) {
428+
if (isDefault) {
429+
ctrl.default = messageCtrl;
430+
} else {
431+
var nextKey = latestKey.toString();
432+
messages[nextKey] = {
433+
message: messageCtrl
434+
};
435+
insertMessageNode($element[0], comment, nextKey);
436+
comment.$$ngMessageNode = nextKey;
437+
latestKey++;
438+
}
408439

409440
ctrl.reRender();
410441
};
411442

412-
this.deregister = function(comment) {
413-
var key = comment.$$ngMessageNode;
414-
delete comment.$$ngMessageNode;
415-
removeMessageNode($element[0], comment, key);
416-
delete messages[key];
443+
this.deregister = function(comment, isDefault) {
444+
if (isDefault) {
445+
delete ctrl.default;
446+
} else {
447+
var key = comment.$$ngMessageNode;
448+
delete comment.$$ngMessageNode;
449+
removeMessageNode($element[0], comment, key);
450+
delete messages[key];
451+
}
452+
417453
ctrl.reRender();
418454
};
419455

@@ -594,7 +630,43 @@ angular.module('ngMessages', [])
594630
*
595631
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
596632
*/
597-
.directive('ngMessageExp', ngMessageDirectiveFactory('A'));
633+
.directive('ngMessageExp', ngMessageDirectiveFactory('A'))
634+
635+
/**
636+
* @ngdoc directive
637+
* @name ngMessageDefault
638+
* @restrict AE
639+
* @scope
640+
*
641+
* @description
642+
* `ngMessageDefault` is a directive with the purpose to show and hide a default message.
643+
* For `ngMessageDefault` to operate, no `ngMessage` inner directive should be displayed
644+
* in the parent `ngMessages` directive.
645+
*
646+
* More information about using `ngMessageDefault` can be found in the
647+
* {@link module:ngMessages `ngMessages` module documentation}.
648+
*
649+
* @usage
650+
* ```html
651+
* <!-- using attribute directives -->
652+
* <ANY ng-messages="expression" role="alert">
653+
* <ANY ng-message="stringValue">...</ANY>
654+
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
655+
* <ANY ng-message-default>...</ANY>
656+
* </ANY>
657+
*
658+
* <!-- or by using element directives -->
659+
* <ng-messages for="expression" role="alert">
660+
* <ng-message when="stringValue">...</ng-message>
661+
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
662+
* <ng-message-default>...</ng-message-default>
663+
* </ng-messages>
664+
* ```
665+
*
666+
* @param {expression} ngMessageDefault|when no ngMessage matches.
667+
*/
668+
.directive('ngMessageDefault', ngMessageDefaultDirectiveFactory('AE'));
669+
598670

599671
function ngMessageDirectiveFactory(restrict) {
600672
return ['$animate', function($animate) {
@@ -668,3 +740,47 @@ function ngMessageDirectiveFactory(restrict) {
668740
}
669741
}
670742
}
743+
744+
function ngMessageDefaultDirectiveFactory(restrict) {
745+
return ['$animate', function($animate) {
746+
return {
747+
restrict: 'AE',
748+
transclude: 'element',
749+
terminal: true,
750+
require: '^^ngMessages',
751+
link: function(scope, element, attrs, ngMessagesCtrl, $transclude) {
752+
var commentNode = element[0];
753+
754+
var currentElement, messageCtrl;
755+
ngMessagesCtrl.register(commentNode, messageCtrl = {
756+
attach: function() {
757+
if (!currentElement) {
758+
$transclude(scope, function(elm) {
759+
$animate.enter(elm, null, element);
760+
currentElement = elm;
761+
762+
// in the event that the parent element is destroyed
763+
// by any other structural directive then it's time
764+
// to deregister the default message (boolean set to true)
765+
// from the controller
766+
currentElement.on('$destroy', function() {
767+
if (currentElement) {
768+
ngMessagesCtrl.deregister(commentNode, true);
769+
messageCtrl.detach();
770+
}
771+
});
772+
});
773+
}
774+
},
775+
detach: function() {
776+
if (currentElement) {
777+
var elm = currentElement;
778+
currentElement = null;
779+
$animate.leave(elm);
780+
}
781+
}
782+
}, true); // boolean set to true to specify default message
783+
}
784+
};
785+
}];
786+
}

test/ngMessages/messagesSpec.js

+18
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,24 @@ describe('ngMessages', function() {
401401
});
402402
});
403403

404+
it('should render a default message when no one matched', inject(function($rootScope, $compile) {
405+
element = $compile('<div ng-messages="col">' +
406+
' <div ng-message="val">Message is set</div>' +
407+
' <div ng-message-default>Default message is set</div>' +
408+
'</div>')($rootScope);
409+
$rootScope.$digest();
410+
411+
//expect(element.text()).not.toContain('Message is set');
412+
expect(element.text()).toContain('Default message is set');
413+
414+
$rootScope.$apply(function() {
415+
$rootScope.col = { val: true };
416+
});
417+
418+
//expect(element.text()).toContain('Message is set');
419+
expect(element.text()).not.toContain('Default message is set');
420+
}));
421+
404422
describe('when including templates', function() {
405423
they('should work with a dynamic collection model which is managed by ngRepeat',
406424
{'<div ng-messages-include="...">': '<div ng-messages="item">' +

0 commit comments

Comments
 (0)