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:
+128
-46
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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">' +
|
||||
|
||||
Reference in New Issue
Block a user