diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index 04b468fe5a4a..c6b2228f3c7a 100644 --- a/src/ngMessages/messages.js +++ b/src/ngMessages/messages.js @@ -1,5 +1,13 @@ 'use strict'; +/* jshint ignore:start */ +// this code is in the core, but not in angular-messages.js +var isArray = angular.isArray; +var forEach = angular.forEach; +var isString = angular.isString; +var jqLite = angular.element; +/* jshint ignore:end */ + /** * @ngdoc module * @name ngMessages @@ -12,8 +20,8 @@ * `ngMessage` directives are designed to handle the complexity, inheritance and priority * 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` - * and `ngMessage` directives. + * Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude` + * `ngMessage` and `ngMessageExp` directives. * * # Usage * The `ngMessages` directive listens on a key/value collection which is set on the ngMessages attribute. @@ -26,7 +34,9 @@ * *
*
You did not enter a field
- *
The value entered is too short
+ *
+ * Your email must be between 5 and 100 characters long + *
*
* * ``` @@ -52,7 +62,11 @@ * ngMessages directive to make this happen. * * ```html + * *
...
+ * + * + * ... * ``` * * ## Reusing and Overriding Messages @@ -65,7 +79,10 @@ *
This field is required
*
This field is too short
* - *
+ * + *
+ *
+ *
* ``` * * However, including generic messages may not be useful enough to match all input fields, therefore, @@ -87,12 +104,17 @@ * minlength="5" * required /> * - *
+ * + *
* *
You did not enter your email address
* * *
Your email address is invalid
+ * + * + *
*
* * ``` @@ -102,10 +124,65 @@ * email addresses, date fields, autocomplete inputs, etc...), specialized error messages can be applied * while more generic messages can be used to handle other, more general input errors. * + * ## Dynamic Messaging + * ngMessages also supports using expressions to dynamically change key values. Using arrays and + * repeaters to list messages is also supported. This means that the code below will be able to + * fully adapt itself and display the appropriate message when any of the expression data changes: + * + * ```html + *
+ * + * + *
+ *
You did not enter your email address
+ *
+ * + *
{{ errorMessage.text }}
+ *
+ *
+ *
+ * ``` + * + * The `errorMessage.type` expression can be a string value or it can be an array so + * that multiple errors can be associated with a single error message: + * + * ```html + * + *
+ *
You did not enter your email address
+ *
+ * Your email must be between 5 and 100 characters long + *
+ *
+ * ``` + * + * Feel free to use other structural directives such as ng-if and ng-switch to further control + * what messages are active and when. Be careful, if you place ng-message on the same element + * as these structural directives, Angular may not be able to determine if a message is active + * or not. Therefore it is best to place the ng-message on a child element of the structural + * directive. + * + * ```html + *
+ *
+ *
Please enter something
+ *
+ *
+ * ``` + * * ## Animations - * If the `ngAnimate` module is active within the application then both the `ngMessages` and - * `ngMessage` directives will trigger animations whenever any messages are added and removed - * from the DOM by the `ngMessages` directive. + * If the `ngAnimate` module is active within the application then the `ngMessages`, `ngMessage` and + * `ngMessageExp` directives will trigger animations whenever any messages are added and removed from + * the DOM by the `ngMessages` directive. * * Whenever the `ngMessages` directive contains one or more visible messages then the `.ng-active` CSS * class will be added to the element. The `.ng-inactive` CSS class will be applied when there are no @@ -171,7 +248,7 @@ angular.module('ngMessages', []) * messages use the `ngMessage` directive and will be inserted/removed from the page depending * on if they're present within the key/value object. By default, only one message will be displayed * 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 on the directive container.) + * 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. @@ -182,23 +259,22 @@ angular.module('ngMessages', []) * ```html * * - * ... - * ... - * ... + * ... + * ... + * ... * * * * - * ... - * ... - * ... + * ... + * ... + * ... * * ``` * * @param {string} ngMessages an angular expression evaluating to a key/value object * (this is typically the $error object on an ngModel instance). * @param {string=} ngMessagesMultiple|multiple when set, all messages will be displayed with true - * @param {string=} ngMessagesInclude|include when set, the specified template will be included into the ng-messages container * * @example * * */ - .directive('ngMessages', ['$compile', '$animate', '$templateRequest', - function($compile, $animate, $templateRequest) { - var ACTIVE_CLASS = 'ng-active'; - var INACTIVE_CLASS = 'ng-inactive'; + .directive('ngMessages', ['$animate', function($animate) { + var ACTIVE_CLASS = 'ng-active'; + var INACTIVE_CLASS = 'ng-inactive'; - return { - restrict: 'AE', - controller: function() { - this.$renderNgMessageClasses = angular.noop; - - var messages = []; - this.registerMessage = function(index, message) { - for (var i = 0; i < messages.length; i++) { - if (messages[i].type == message.type) { - if (index != i) { - var temp = messages[index]; - messages[index] = messages[i]; - if (index < messages.length) { - messages[i] = temp; - } else { - messages.splice(0, i); //remove the old one (and shift left) - } - } - return; - } - } - messages.splice(index, 0, message); //add the new one (and shift right) - }; + return { + require: 'ngMessages', + restrict: 'AE', + controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { + var ctrl = this; + var latestKey = 0; - this.renderMessages = function(values, multiple) { - values = values || {}; + var messages = this.messages = {}; + var renderLater, cachedCollection; - var found; - angular.forEach(messages, function(message) { - if ((!found || multiple) && truthyVal(values[message.type])) { - message.attach(); - found = true; - } else { - message.detach(); - } - }); + this.render = function(collection) { + collection = collection || {}; - this.renderElementClasses(found); + renderLater = false; + cachedCollection = collection; - function truthyVal(value) { - return value !== null && value !== false && value; - } - }; - }, - require: 'ngMessages', - link: function($scope, element, $attrs, ctrl) { - ctrl.renderElementClasses = function(bool) { - bool ? $animate.setClass(element, ACTIVE_CLASS, INACTIVE_CLASS) - : $animate.setClass(element, INACTIVE_CLASS, ACTIVE_CLASS); - }; + // this is true if the attribute is empty or if the attribute value is truthy + var multiple = isAttrTruthy($scope, $attrs.ngMessagesMultiple) || + isAttrTruthy($scope, $attrs.multiple); - //JavaScript treats empty strings as false, but ng-message-multiple by itself is an empty string - var multiple = angular.isString($attrs.ngMessagesMultiple) || - angular.isString($attrs.multiple); + var unmatchedMessages = []; + var matchedKeys = {}; + var messageItem = ctrl.head; + var messageFound = false; + var totalMessages = 0; - var cachedValues, watchAttr = $attrs.ngMessages || $attrs['for']; //for is a reserved keyword - $scope.$watchCollection(watchAttr, function(values) { - cachedValues = values; - ctrl.renderMessages(values, multiple); - }); + // we use != instead of !== to allow for both undefined and null values + while (messageItem != null) { + totalMessages++; + var messageCtrl = messageItem.message; - var tpl = $attrs.ngMessagesInclude || $attrs.include; - if (tpl) { - $templateRequest(tpl) - .then(function processTemplate(html) { - var after, container = angular.element('
').html(html); - angular.forEach(container.children(), function(elm) { - elm = angular.element(elm); - after ? after.after(elm) - : element.prepend(elm); //start of the container - after = elm; - $compile(elm)($scope); - }); - ctrl.renderMessages(cachedValues, multiple); - }); - } - } - }; - }]) + 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; + + messageUsed = true; + messageCtrl.attach(); + } + }); + } + + if (messageUsed) { + // unless we want to display multiple messages then we should + // set a flag here to avoid displaying the next message in the list + messageFound = !multiple; + } else { + unmatchedMessages.push(messageCtrl); + } + + messageItem = messageItem.next; + } + + forEach(unmatchedMessages, function(messageCtrl) { + messageCtrl.detach(); + }); + unmatchedMessages.length !== totalMessages + ? $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS) + : $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); + }; + + $scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render); + + this.reRender = function() { + if (!renderLater) { + renderLater = true; + $scope.$evalAsync(function() { + if (renderLater) { + cachedCollection && ctrl.render(cachedCollection); + } + }); + } + }; + + this.register = function(comment, messageCtrl) { + 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]; + ctrl.reRender(); + }; + + function findPreviousMessage(parent, comment) { + var prevNode = comment; + var parentLookup = []; + while (prevNode && prevNode !== parent) { + var prevKey = prevNode.$$ngMessageNode; + if (prevKey && prevKey.length) { + return messages[prevKey]; + } + + // dive deeper into the DOM and examine its children for any ngMessage + // comments that may be in an element that appears deeper in the list + if (prevNode.childNodes.length && parentLookup.indexOf(prevNode) == -1) { + parentLookup.push(prevNode); + prevNode = prevNode.childNodes[prevNode.childNodes.length - 1]; + } else { + prevNode = prevNode.previousSibling || prevNode.parentNode; + } + } + } + + function insertMessageNode(parent, comment, key) { + var messageNode = messages[key]; + if (!ctrl.head) { + ctrl.head = messageNode; + } else { + var match = findPreviousMessage(parent, comment); + if (match) { + messageNode.next = match.next; + match.next = messageNode; + } else { + messageNode.next = ctrl.head; + ctrl.head = messageNode; + } + } + } + + function removeMessageNode(parent, comment, key) { + var messageNode = messages[key]; + + var match = findPreviousMessage(parent, comment); + if (match) { + match.next = messageNode.next; + } else { + ctrl.head = messageNode.next; + } + } + }] + }; + + function isAttrTruthy(scope, attr) { + return (isString(attr) && attr.length === 0) || //empty attribute + truthy(scope.$eval(attr)); + } + + function truthy(val) { + return isString(val) ? val.length : !!val; + } + }]) + + /** + * @ngdoc directive + * @name ngMessagesInclude + * @restrict AE + * @scope + * + * @description + * `ngMessagesInclude` is a directive with the purpose to import existing ngMessage template + * code from a remote template and place the downloaded template code into the exact spot + * that the ngMessagesInclude directive is placed within the ngMessages container. This allows + * for a series of pre-defined messages to be reused and also allows for the developer to + * determine what messages are overridden due to the placement of the ngMessagesInclude directive. + * + * @usage + * ```html + * + * + * ... + * + * + * + * + * ... + * + * ``` + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {string} ngMessagesInclude|src a string value corresponding to the remote template. + */ + .directive('ngMessagesInclude', + ['$templateRequest', '$document', '$compile', function($templateRequest, $document, $compile) { + + return { + restrict: 'AE', + require: '^^ngMessages', + compile: function(element, attrs) { + var comment = jqLite($document[0].createComment(' ngMessagesInclude: ')); + element.after(comment); + + return function($scope, $element, attrs, ngMessagesCtrl) { + // we're removing this since we only need access to the newly + // created comment node as an anchor. + element.remove(); + + $templateRequest(attrs.ngMessagesInclude || attrs.src).then(function(html) { + var elements = jqLite('
').html(html).contents(); + var cursor = comment; + forEach(elements, function(elm) { + elm = jqLite(elm); + cursor.after(elm); + cursor = elm; + }); + $compile(elements)($scope); + }); + }; + } + }; + }]) /** * @ngdoc directive @@ -330,63 +541,97 @@ angular.module('ngMessages', []) * ```html * * - * ... - * ... - * ... + * ... + * ... + * ... * * * * - * ... - * ... - * ... + * ... + * ... + * ... * * ``` * * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. * - * @param {string} ngMessage a string value corresponding to the message key. + * @param {expression} ngMessage|when a string value corresponding to the message key. + * @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key. */ - .directive('ngMessage', ['$animate', function($animate) { - var COMMENT_NODE = 8; + .directive('ngMessage', ngMessageDirectiveFactory('AE')) + .directive('ngMessageExp', ngMessageDirectiveFactory('A')); + +function ngMessageDirectiveFactory(restrict) { + return ['$animate', function($animate) { return { - require: '^ngMessages', + restrict: 'AE', transclude: 'element', terminal: true, - restrict: 'AE', - link: function($scope, $element, $attrs, ngMessages, $transclude) { - var index, element; - - var commentNode = $element[0]; - var parentNode = commentNode.parentNode; - for (var i = 0, j = 0; i < parentNode.childNodes.length; i++) { - var node = parentNode.childNodes[i]; - if (node.nodeType == COMMENT_NODE && node.nodeValue.indexOf('ngMessage') >= 0) { - if (node === commentNode) { - index = j; - break; - } - j++; - } + 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(); + }; + + if (dynamicExp) { + assignRecords(scope.$eval(dynamicExp)); + scope.$watchCollection(dynamicExp, assignRecords); + } else { + assignRecords(staticExp); } - ngMessages.registerMessage(index, { - type: $attrs.ngMessage || $attrs.when, + var currentElement, messageCtrl; + ngMessagesCtrl.register(commentNode, messageCtrl = { + test: function(name) { + return contains(records, name); + }, + exp: staticExp || dynamicExp, attach: function() { - if (!element) { - $transclude($scope, function(clone) { - $animate.enter(clone, null, $element); - element = clone; + if (!currentElement) { + $transclude(scope, function(elm) { + $animate.enter(elm, null, element); + currentElement = elm; + + // in the event that the parent element is destroyed + // by any other structural directive then it's time + // to deregister the message from the controller + currentElement.on('$destroy', function() { + if (currentElement) { + ngMessagesCtrl.deregister(commentNode); + messageCtrl.detach(); + } + }); }); } }, detach: function() { - if (element) { - $animate.leave(element); - element = null; + if (currentElement) { + var elm = currentElement; + currentElement = null; + $animate.leave(elm); } } }); } }; - }]); + }]; + + function contains(collection, key) { + if (collection) { + return isArray(collection) + ? collection.indexOf(key) >= 0 + : collection.hasOwnProperty(key); + } + } +} diff --git a/test/ngMessages/messagesSpec.js b/test/ngMessages/messagesSpec.js index a0b39ddf8570..26215741c6bf 100644 --- a/test/ngMessages/messagesSpec.js +++ b/test/ngMessages/messagesSpec.js @@ -41,7 +41,40 @@ describe('ngMessages', function() { expect(element.text()).toContain('Message is set'); })); - it('should use the data attribute when an element directive is used', + it('should render the same message if multiple message keys match', inject(function($rootScope, $compile) { + element = $compile('
' + + '
Message is set
' + + '
')($rootScope); + $rootScope.$digest(); + + expect(element.text()).not.toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { one: true }; + }); + + expect(element.text()).toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { two: true, one: false }; + }); + + expect(element.text()).toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { three: true, two: false }; + }); + + expect(element.text()).toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { three: false }; + }); + + expect(element.text()).not.toContain('Message is set'); + })); + + it('should use the when attribute when an element directive is used', inject(function($rootScope, $compile) { element = $compile('' + @@ -58,6 +91,111 @@ describe('ngMessages', function() { expect(element.text()).toContain('Message is set'); })); + it('should render the same message if multiple message keys match based on the when attribute', inject(function($rootScope, $compile) { + element = $compile('' + + ' Message is set
' + + '')($rootScope); + $rootScope.$digest(); + + expect(element.text()).not.toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { one: true }; + }); + + expect(element.text()).toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { two: true, one: false }; + }); + + expect(element.text()).toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { three: true, two: false }; + }); + + expect(element.text()).toContain('Message is set'); + + $rootScope.$apply(function() { + $rootScope.col = { three: false }; + }); + + expect(element.text()).not.toContain('Message is set'); + })); + + it('should allow a dynamic expression to be set when ng-message-exp is used', + inject(function($rootScope, $compile) { + + element = $compile('
' + + '
Message is crazy
' + + '
')($rootScope); + $rootScope.$digest(); + + expect(element.text()).not.toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.variable = 'error'; + $rootScope.col = { error: true }; + }); + + expect(element.text()).toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.col = { error: false, failure: true }; + }); + + expect(element.text()).not.toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.variable = ['failure']; + }); + + expect(element.text()).toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.variable = null; + }); + + expect(element.text()).not.toContain('Message is crazy'); + })); + + it('should allow a dynamic expression to be set when the when-exp attribute is used', + inject(function($rootScope, $compile) { + + element = $compile('' + + ' Message is crazy' + + '')($rootScope); + $rootScope.$digest(); + + expect(element.text()).not.toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.variable = 'error, failure'; + $rootScope.col = { error: true }; + }); + + expect(element.text()).toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.col = { error: false, failure: true }; + }); + + expect(element.text()).toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.variable = []; + }); + + expect(element.text()).not.toContain('Message is crazy'); + + $rootScope.$apply(function() { + $rootScope.variable = null; + }); + + expect(element.text()).not.toContain('Message is crazy'); + })); + they('should render empty when $prop is used as a collection value', { 'null': null, 'false': false, @@ -189,6 +327,65 @@ describe('ngMessages', function() { expect(element.hasClass('ng-inactive')).toBe(false); })); + it('should automatically re-render the messages when other directives dynmically change them', + inject(function($rootScope, $compile) { + + element = $compile('
' + + '
Enter something
' + + '
' + + '
{{ item.text }}
' + + '
' + + '
')($rootScope); + + $rootScope.$apply(function() { + $rootScope.col = {}; + $rootScope.items = [ + { text: 'Your age is incorrect', name: 'age' }, + { text: 'You\'re too tall man!', name: 'height' }, + { text: 'Your hair is too long', name: 'hair' } + ]; + }); + + expect(messageChildren().length).toBe(0); + expect(trim(element.text())).toEqual(""); + + $rootScope.$apply(function() { + $rootScope.col = { hair: true }; + }); + + expect(messageChildren().length).toBe(1); + expect(trim(element.text())).toEqual("Your hair is too long"); + + $rootScope.$apply(function() { + $rootScope.col = { age: true, hair: true}; + }); + + expect(messageChildren().length).toBe(1); + expect(trim(element.text())).toEqual("Your age is incorrect"); + + $rootScope.$apply(function() { + // remove the age! + $rootScope.items.shift(); + }); + + expect(messageChildren().length).toBe(1); + expect(trim(element.text())).toEqual("Your hair is too long"); + + $rootScope.$apply(function() { + // remove the hair! + $rootScope.items.length = 0; + $rootScope.col.primary = true; + }); + + expect(messageChildren().length).toBe(1); + expect(trim(element.text())).toEqual("Enter something"); + + function messageChildren() { + return element[0].querySelectorAll('[ng-message], [ng-message-exp]'); + } + })); + + it('should render animations when the active/inactive classes are added/removed', function() { module('ngAnimate'); module('ngAnimateMock'); @@ -219,10 +416,12 @@ describe('ngMessages', function() { describe('when including templates', function() { they('should load a remote template using $prop', - {'
': - '
', - '': - ''}, + {'
': '
' + + '
' + + '
', + '': '' + + '' + + ''}, function(html) { inject(function($compile, $rootScope, $templateCache) { $templateCache.put('abc.html', '
A
' + @@ -259,7 +458,7 @@ describe('ngMessages', function() { expect($templateCache.get('tpl')).toBeUndefined(); - element = $compile('
')($rootScope); + element = $compile('
')($rootScope); $rootScope.$digest(); $httpBackend.flush(); @@ -270,9 +469,11 @@ describe('ngMessages', function() { it('should re-render the messages after download without an extra digest', inject(function($rootScope, $compile, $httpBackend) { - $httpBackend.expect('GET', 'my-messages').respond(201,'
You did not enter a value
'); + $httpBackend.expect('GET', 'my-messages').respond(201, + '
You did not enter a value
'); - element = $compile('
' + + element = $compile('
' + + '
' + '
Your value is that of failure
' + '
')($rootScope); @@ -287,20 +488,22 @@ describe('ngMessages', function() { expect(trim(element.text())).toEqual("Your value is that of failure"); $httpBackend.flush(); + $rootScope.$digest(); expect(element.children().length).toBe(1); expect(trim(element.text())).toEqual("You did not enter a value"); })); - it('should allow for overriding the remote template messages within the element', + it('should allow for overriding the remote template messages within the element depending on where the remote template is placed', inject(function($compile, $rootScope, $templateCache) { $templateCache.put('abc.html', '
A
' + '
B
' + '
C
'); - element = $compile('
' + + element = $compile('
' + '
AAA
' + + '
' + '
CCC
' + '
')($rootScope); @@ -332,50 +535,7 @@ describe('ngMessages', function() { }); expect(element.children().length).toBe(1); - expect(trim(element.text())).toEqual("CCC"); - })); - - it('should retain the order of the remote template\'s messages when overriding within the element', - inject(function($compile, $rootScope, $templateCache) { - - $templateCache.put('abc.html', '
C
' + - '
A
' + - '
B
'); - - element = $compile('
' + - '
AAA
' + - '
CCC
' + - '
')($rootScope); - - $rootScope.$apply(function() { - $rootScope.data = { - 'a': 1, - 'b': 2, - 'c': 3 - }; - }); - - expect(element.children().length).toBe(1); - expect(trim(element.text())).toEqual("CCC"); - - $rootScope.$apply(function() { - $rootScope.data = { - 'a': 1, - 'b': 2 - }; - }); - - expect(element.children().length).toBe(1); - expect(trim(element.text())).toEqual("AAA"); - - $rootScope.$apply(function() { - $rootScope.data = { - 'b': 3 - }; - }); - - expect(element.children().length).toBe(1); - expect(trim(element.text())).toEqual("B"); + expect(trim(element.text())).toEqual("C"); })); }); @@ -412,9 +572,9 @@ describe('ngMessages', function() { '
Y
' + '
Z
'); - element = $compile('
')($rootScope); + element = $compile('
' + + '
' + + '
')($rootScope); $rootScope.$apply(function() { $rootScope.data = { @@ -442,11 +602,10 @@ describe('ngMessages', function() { '
Y
' + '
Z
'); - element = $compile('
' + - '
YYY
' + - '
ZZZ
' + + element = $compile('
' + + '
YYY
' + + '
ZZZ
' + + '
' + '
')($rootScope); $rootScope.$apply(function() { @@ -458,14 +617,14 @@ describe('ngMessages', function() { }); expect(element.children().length).toBe(2); - expect(s(element.text())).toEqual("XZZZ"); + expect(s(element.text())).toEqual("ZZZX"); $rootScope.$apply(function() { $rootScope.data.y = {}; }); expect(element.children().length).toBe(3); - expect(s(element.text())).toEqual("XYYYZZZ"); + expect(s(element.text())).toEqual("YYYZZZX"); })); }); });