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

Commit

Permalink
feat(ngMessages): add support for default message
Browse files Browse the repository at this point in the history
add support for showing default message when a truthy value is not matched by an ng-message directive.

Closes #12008
Closes #12213
Closes #16587
  • Loading branch information
Narretz committed Jun 6, 2018
1 parent 3d6c45d commit a8c263c
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 47 deletions.
176 changes: 129 additions & 47 deletions src/ngMessages/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var jqLite;
* 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 allows keys in a key/value collection to be associated with a child element
Expand Down Expand Up @@ -257,7 +257,26 @@ var jqLite;
* .some-message.ng-leave.ng-leave-active {}
* ```
*
* {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate.
* {@link ngAnimate See the ngAnimate docs} to learn how to use JavaScript animations or to learn
* more about ngAnimate.
*
* ## Displaying a default message
* If the ngMessages renders no inner ngMessage directive (i.e. when none of the truthy
* keys are matched by a defined message), then it will render a default message
* using the {@link ngMessageDefault} directive.
* Note that matched messages will always take precedence over unmatched messages. That means
* the default message will not be displayed when another message is matched. This is also
* true for `ng-messages-multiple`.
*
* ```html
* <div ng-messages="myForm.myField.$error" role="alert">
* <div ng-message="required">This field is required</div>
* <div ng-message="minlength">This field is too short</div>
* <div ng-message-default>This field has an input error</div>
* </div>
* ```
*
*/
angular.module('ngMessages', [], function initAngularHelpers() {
// Access helpers from AngularJS core.
Expand Down Expand Up @@ -286,8 +305,11 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* 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 remote template can also be used to promote message reusability and messages can also be
* overridden.
* A remote template can also be used (With {@link ngMessagesInclude}) to promote message
* reusability and messages can also be overridden.
*
* A default message can also be displayed when no `ngMessage` directive is inserted, using the
* {@link ngMessageDefault} directive.
*
* {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`.
*
Expand All @@ -298,13 +320,15 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <ANY ng-message="stringValue">...</ANY>
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
* <ANY ng-message-exp="expressionValue">...</ANY>
* <ANY ng-message-default>...</ANY>
* </ANY>
*
* <!-- or by using element directives -->
* <ng-messages for="expression" role="alert">
* <ng-message when="stringValue">...</ng-message>
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
* <ng-message when-exp="expressionValue">...</ng-message>
* <ng-message-default>...</ng-message-default>
* </ng-messages>
* ```
*
Expand Down Expand Up @@ -333,6 +357,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <div ng-message="required">You did not enter a field</div>
* <div ng-message="minlength">Your field is too short</div>
* <div ng-message="maxlength">Your field is too long</div>
* <div ng-message-default>This field has an input error</div>
* </div>
* </form>
* </file>
Expand Down Expand Up @@ -370,6 +395,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {

var unmatchedMessages = [];
var matchedKeys = {};
var truthyKeys = 0;
var messageItem = ctrl.head;
var messageFound = false;
var totalMessages = 0;
Expand All @@ -382,13 +408,17 @@ angular.module('ngMessages', [], function initAngularHelpers() {
var messageUsed = false;
if (!messageFound) {
forEach(collection, function(value, key) {
if (!messageUsed && truthy(value) && messageCtrl.test(key)) {
// this is to prevent the same error name from showing up twice
if (matchedKeys[key]) return;
matchedKeys[key] = true;
if (truthy(value) && !messageUsed) {
truthyKeys++;

if (messageCtrl.test(key)) {
// this is to prevent the same error name from showing up twice
if (matchedKeys[key]) return;
matchedKeys[key] = true;

messageUsed = true;
messageCtrl.attach();
messageUsed = true;
messageCtrl.attach();
}
}
});
}
Expand All @@ -408,7 +438,16 @@ angular.module('ngMessages', [], function initAngularHelpers() {
messageCtrl.detach();
});

if (unmatchedMessages.length !== totalMessages) {
var messageMatched = unmatchedMessages.length !== totalMessages;
var attachDefault = ctrl.default && !messageMatched && truthyKeys > 0;

if (attachDefault) {
ctrl.default.attach();
} else if (ctrl.default) {
ctrl.default.detach();
}

if (messageMatched || attachDefault) {
$animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS);
} else {
$animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS);
Expand All @@ -428,23 +467,31 @@ angular.module('ngMessages', [], function initAngularHelpers() {
}
};

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();
};

Expand Down Expand Up @@ -647,9 +694,41 @@ angular.module('ngMessages', [], function initAngularHelpers() {
*
* @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key.
*/
.directive('ngMessageExp', ngMessageDirectiveFactory());
.directive('ngMessageExp', ngMessageDirectiveFactory())

/**
* @ngdoc directive
* @name ngMessageDefault
* @restrict AE
* @scope
*
* @description
* `ngMessageDefault` is a directive with the purpose to show and hide a default message for
* {@link ngMessages}, when none of provided messages matches.
*
* More information about using `ngMessageDefault` can be found in the
* {@link module:ngMessages `ngMessages` module documentation}.
*
* @usage
* ```html
* <!-- using attribute directives -->
* <ANY ng-messages="expression" role="alert">
* <ANY ng-message="stringValue">...</ANY>
* <ANY ng-message="stringValue1, stringValue2, ...">...</ANY>
* <ANY ng-message-default>...</ANY>
* </ANY>
*
* <!-- or by using element directives -->
* <ng-messages for="expression" role="alert">
* <ng-message when="stringValue">...</ng-message>
* <ng-message when="stringValue1, stringValue2, ...">...</ng-message>
* <ng-message-default>...</ng-message-default>
* </ng-messages>
*
*/
.directive('ngMessageDefault', ngMessageDirectiveFactory(true));

function ngMessageDirectiveFactory() {
function ngMessageDirectiveFactory(isDefault) {
return ['$animate', function($animate) {
return {
restrict: 'AE',
Expand All @@ -658,25 +737,28 @@ function ngMessageDirectiveFactory() {
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();
};
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);
if (dynamicExp) {
assignRecords(scope.$eval(dynamicExp));
scope.$watchCollection(dynamicExp, assignRecords);
} else {
assignRecords(staticExp);
}
}

var currentElement, messageCtrl;
Expand All @@ -701,7 +783,7 @@ function ngMessageDirectiveFactory() {
// If the message element was removed via a call to `detach` then `currentElement` will be null
// So this handler only handles cases where something else removed the message element.
if (currentElement && currentElement.$$attachId === $$attachId) {
ngMessagesCtrl.deregister(commentNode);
ngMessagesCtrl.deregister(commentNode, isDefault);
messageCtrl.detach();
}
newScope.$destroy();
Expand All @@ -716,14 +798,14 @@ function ngMessageDirectiveFactory() {
$animate.leave(elm);
}
}
});
}, isDefault);

// We need to ensure that this directive deregisters itself when it no longer exists
// Normally this is done when the attached element is destroyed; but if this directive
// gets removed before we attach the message to the DOM there is nothing to watch
// in which case we must deregister when the containing scope is destroyed.
scope.$on('$destroy', function() {
ngMessagesCtrl.deregister(commentNode);
ngMessagesCtrl.deregister(commentNode, isDefault);
});
}
};
Expand Down
94 changes: 94 additions & 0 deletions test/ngMessages/messagesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,100 @@ describe('ngMessages', function() {
);


describe('default message', function() {
it('should render a default message when no message matches', inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col">' +
' <div ng-message="val">Message is set</div>' +
' <div ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
$rootScope.$apply(function() {
$rootScope.col = { unexpected: false };
});

$rootScope.$digest();

expect(element.text().trim()).toBe('');
expect(element).not.toHaveClass('ng-active');

$rootScope.$apply(function() {
$rootScope.col = { unexpected: true };
});

expect(element.text().trim()).toBe('Default message is set');
expect(element).toHaveClass('ng-active');

$rootScope.$apply(function() {
$rootScope.col = { unexpected: false };
});

expect(element.text().trim()).toBe('');
expect(element).not.toHaveClass('ng-active');

$rootScope.$apply(function() {
$rootScope.col = { val: true, unexpected: true };
});

expect(element.text().trim()).toBe('Message is set');
expect(element).toHaveClass('ng-active');
}));

it('should not render a default message with ng-messages-multiple if another error matches',
inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col" ng-messages-multiple>' +
' <div ng-message="val">Message is set</div>' +
' <div ng-message="other">Other message is set</div>' +
' <div ng-message-default>Default message is set</div>' +
'</div>')($rootScope);

expect(element.text().trim()).toBe('');

$rootScope.$apply(function() {
$rootScope.col = { val: true, other: false, unexpected: false };
});

expect(element.text().trim()).toBe('Message is set');

$rootScope.$apply(function() {
$rootScope.col = { val: true, other: true, unexpected: true };
});

expect(element.text().trim()).toBe('Message is set Other message is set');

$rootScope.$apply(function() {
$rootScope.col = { val: false, other: false, unexpected: true };
});

expect(element.text().trim()).toBe('Default message is set');
})
);

it('should handle a default message with ngIf', inject(function($rootScope, $compile) {
element = $compile('<div ng-messages="col">' +
' <div ng-message="val">Message is set</div>' +
' <div ng-if="default" ng-message-default>Default message is set</div>' +
'</div>')($rootScope);
$rootScope.default = true;
$rootScope.col = {unexpected: true};
$rootScope.$digest();

expect(element.text().trim()).toBe('Default message is set');

$rootScope.$apply('default = false');

expect(element.text().trim()).toBe('');

$rootScope.$apply('default = true');

expect(element.text().trim()).toBe('Default message is set');

$rootScope.$apply(function() {
$rootScope.col = { val: true };
});

expect(element.text().trim()).toBe('Message is set');
}));
});

describe('when including templates', function() {
they('should work with a dynamic collection model which is managed by ngRepeat',
{'<div ng-messages-include="...">': '<div ng-messages="item">' +
Expand Down

0 comments on commit a8c263c

Please sign in to comment.