@@ -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 */
262281angular . 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 } ;
0 commit comments