From 5d55976f2ee30109d79d02ec39e357eeeaaae357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 7 Jan 2015 18:16:15 -0500 Subject: [PATCH] feat(ngMessages): provide support for dynamic message resolution Prior to this fix it was impossible to apply a binding to a the ngMessage directive to represent the name of the error. It was also not possible to use ngRepeat or any other structural directive to dynamically update the list of messages. This feature patch ensures that both ngMessages can render expressions and automatically update when any dynamic message data changes. BREAKING CHANGE: The `ngMessagesInclude` attribute is now its own directive and that must be placed as a **child** element within the element with the ngMessages directive. (Keep in mind that the former behaviour of the ngMessageInclude attribute was that all **included** ngMessage template code was placed at the **bottom** of the element containing the ngMessages directive; therefore to make this behave in the same way, place the element containing the ngMessagesInclude directive at the end of the container containing the ngMessages directive). ```html
Your message is required
Your message is required
``` Closes #10036 Closes #9338 --- src/ngMessages/messages.js | 499 ++++++++++++++++++++++++-------- test/ngMessages/messagesSpec.js | 287 ++++++++++++++---- 2 files changed, 595 insertions(+), 191 deletions(-) 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"); })); }); });