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
This commit is contained in:
Martin Staffa
2018-06-06 17:40:55 +02:00
committed by GitHub
parent 03a4782a35
commit 84d80be2b4
2 changed files with 222 additions and 46 deletions
+128 -46
View File
@@ -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
@@ -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.
@@ -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`.
*
@@ -298,6 +320,7 @@ 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 -->
@@ -305,6 +328,7 @@ angular.module('ngMessages', [], function initAngularHelpers() {
* <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>
* ```
*
@@ -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>
@@ -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;
@@ -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++;
messageUsed = true;
messageCtrl.attach();
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();
}
}
});
}
@@ -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);
@@ -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();
};
@@ -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())
function 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(isDefault) {
return ['$animate', function($animate) {
return {
restrict: 'AE',
@@ -658,25 +737,28 @@ function ngMessageDirectiveFactory() {
terminal: true,
require: '^^ngMessages',
link: function(scope, element, attrs, ngMessagesCtrl, $transclude) {
var commentNode = element[0];
var commentNode, records, staticExp, dynamicExp;
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();
};
if (!isDefault) {
commentNode = element[0];
staticExp = attrs.ngMessage || attrs.when;
dynamicExp = attrs.ngMessageExp || attrs.whenExp;
if (dynamicExp) {
assignRecords(scope.$eval(dynamicExp));
scope.$watchCollection(dynamicExp, assignRecords);
} else {
assignRecords(staticExp);
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);
}
}
var currentElement, messageCtrl;
@@ -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();
@@ -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);
});
}
};
+94
View File
@@ -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">' +