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

feat(ngMessages): add support for default message #16587

Merged
merged 4 commits into from
Jun 6, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line should only be called if there is no default... Or perhaps this whole block should look like:

var messageMatched = unmatchedMessages.length !== totalMessages;

if (ctrl.default) {
  if (messageMatched) {
    ctrl.default.detach();
  } else if (truthyKeys > 0) {
    ctrl.default.attach();
  }
}
if (messageMatched || ctrl.default) {
  $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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the og PR you wondered if we need to detach the default controller first here @gkalpak - I've tested this and in defautl circumstances it definitely gets detached.

} 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
96 changes: 95 additions & 1 deletion test/ngMessages/messagesSpec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

describe('ngMessages', function() {
fdescribe('ngMessages', function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops!

beforeEach(inject.strictDi());
beforeEach(module('ngMessages'));

Expand Down 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