From 8af4fde18246ac1587b471a549e70d5d858bf0ee Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 29 Nov 2011 12:11:32 -0800 Subject: add($compile): add compiler v2.0 - not connected --- src/Angular.js | 38 ++ src/angular-mocks.js | 12 +- src/jqLite.js | 22 +- src/service/compiler.js | 1033 ++++++++++++++++++++++---------- src/service/formFactory.js | 72 +-- test/AngularSpec.js | 23 + test/directivesSpec.js | 8 +- test/jqLiteSpec.js | 2 +- test/service/compilerSpec.js | 1340 ++++++++++++++++++++++++++++++++++++------ test/testabilityPatch.js | 5 +- 10 files changed, 2001 insertions(+), 554 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 17ede3aa..4a0589c3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -626,6 +626,22 @@ function copy(source, destination){ return destination; } +/** + * Create a shallow copy of an object + * @param src + */ +function shallowCopy(src) { + var dst = {}, + key; + for(key in src) { + if (src.hasOwnProperty(key)) { + dst[key] = src[key]; + } + } + return dst; +} + + /** * @ngdoc function * @name angular.equals @@ -750,6 +766,19 @@ function toBoolean(value) { return value; } +/** + * @returns {string} Returns the string representation of the element. + */ +function startingTag(element) { + element = jqLite(element).clone(); + try { + // turns out IE does not let you set .html() on elements which + // are not allowed to have children. So we just ignore it. + element.html(''); + } catch(e) {}; + return jqLite('
// controller
- function MyController($http) {
- var scope = this;
-
+ function MyController($scope, $http) {
$http.get('/auth.py').success(function(data) {
- scope.user = data;
+ $scope.user = data;
});
this.saveMessage = function(message) {
- scope.status = 'Saving...';
+ $scope.status = 'Saving...';
$http.post('/add-msg.py', message).success(function(response) {
- scope.status = '';
+ $scope.status = '';
}).error(function() {
- scope.status = 'ERROR!';
+ $scope.status = 'ERROR!';
});
};
}
diff --git a/src/jqLite.js b/src/jqLite.js
index 9e16f8ec..e48d250b 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -100,13 +100,27 @@ function getStyle(element) {
}
+var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
+var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
+var MOZ_HACK_REGEXP = /^moz([A-Z])/;
/**
- * Converts dash-separated names to camelCase. Useful for dealing with css properties.
+ * Converts all accepted directives format into proper directive name.
+ * All of these will become 'myDirective':
+ * my:DiRective
+ * my-directive
+ * x-my-directive
+ * data-my:directive
+ *
+ * Also there is special case for Moz prefix starting with upper case letter.
+ * @param name Name to normalize
*/
function camelCase(name) {
- return name.replace(/\-(\w)/g, function(all, letter, offset){
- return (offset == 0 && letter == 'w') ? 'w' : letter.toUpperCase();
- });
+ return name.
+ replace(PREFIX_REGEXP, '').
+ replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
+ return offset ? letter.toUpperCase() : letter;
+ }).
+ replace(MOZ_HACK_REGEXP, 'Moz$1');
}
/////////////////////////////////////////////
diff --git a/src/service/compiler.js b/src/service/compiler.js
index adf1ffa9..6185c909 100644
--- a/src/service/compiler.js
+++ b/src/service/compiler.js
@@ -1,356 +1,753 @@
'use strict';
+/**
+ * @ngdoc function
+ * @name angular.module.ng.$compile
+ * @function
+ *
+ * @description
+ * Compiles a piece of HTML string or DOM into a template and produces a template function, which
+ * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together.
+ *
+ * The compilation is a process of walking the DOM tree and trying to match DOM elements to
+ * {@link angular.module.ng.$compileProvider.directive directives}. For each match it
+ * executes corresponding template function and collects the
+ * instance functions into a single template function which is then returned.
+ *
+ * The template function can then be used once to produce the view or as it is the case with
+ * {@link angular.module.ng.$compileProvider.directive.ng:repeat repeater} many-times, in which
+ * case each call results in a view that is a DOM clone of the original template.
+ *
+
+
+
+
+
+
+
+
+
+
+ it('should auto compile', function() {
+ expect(element('div[compile]').text()).toBe('Hello Angular');
+ input('html').enter('{{name}}!');
+ expect(element('div[compile]').text()).toBe('Angular!');
+ });
+
+
- Template.prototype = {
- link: function(element, scope) {
- var childScope = scope,
- locals = {$element: element};
- if (this.newScope) {
- childScope = scope.$new();
- element.data($$scope, childScope);
- }
- forEach(this.linkFns, function(fn) {
- try {
- if (isArray(fn) || fn.$inject) {
- $injector.invoke(fn, childScope, locals);
- } else {
- fn.call(childScope, element);
+ *
+ *
+ * @param {string|DOMElement} element Element or HTML string to compile into a template function.
+ * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template
+ * (a DOM element/tree) to a scope. Where:
+ *
+ * * `scope` - A {@link angular.module.ng.$rootScope.Scope Scope} to bind to.
+ * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
+ * `template` and call the `cloneAttachFn` function allowing the caller to attach the
+ * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
+ * called as:
`cloneAttachFn(clonedElement, scope)` where:
+ *
+ * * `clonedElement` - is a clone of the original `element` passed into the compiler.
+ * * `scope` - is the current scope with which the linking function is working with.
+ *
+ * Calling the linking function returns the element of the template. It is either the original element
+ * passed in, or the clone of the element if the `cloneAttachFn` is provided.
+ *
+ * After linking the view is not updateh until after a call to $digest which typically is done by
+ * Angular automatically.
+ *
+ * If you need access to the bound view, there are two ways to do it:
+ *
+ * - If you are not asking the linking function to clone the template, create the DOM element(s)
+ * before you send them to the compiler and keep this reference around.
+ *
+ * var element = $compile('{{total}}
')(scope);
+ *
+ *
+ * - if on the other hand, you need the element to be cloned, the view reference from the original
+ * example would not point to the clone, but rather to the original template that was cloned. In
+ * this case, you can access the clone via the cloneAttachFn:
+ *
+ * var templateHTML = angular.element('{{total}}
'),
+ * scope = ....;
+ *
+ * var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) {
+ * //attach the clone to DOM document at the right place
+ * });
+ *
+ * //now we have reference to the cloned DOM via `clone`
+ *
+ *
+ *
+ * Compiler Methods For Widgets and Directives:
+ *
+ * The following methods are available for use when you write your own widgets, directives,
+ * and markup.
+ *
+ *
+ * For information on how the compiler works, see the
+ * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide.
+ */
+
+
+$CompileProvider.$inject = ['$provide'];
+function $CompileProvider($provide) {
+ var hasDirectives = {},
+ Suffix = 'Directive',
+ COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
+ CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
+ CONTENT_REGEXP = /\<\\>/i,
+ HAS_ROOT_ELEMENT = /^\<[\s\S]*\>$/,
+ SIDE_EFFECT_ATTRS = {};
+
+ forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) {
+ SIDE_EFFECT_ATTRS[name] = name;
+ SIDE_EFFECT_ATTRS[directiveNormalize('ng_' + name)] = name;
+ });
+
+
+ this.directive = function registerDirective(name, directiveFactory) {
+ if (isString(name)) {
+ assertArg(directiveFactory, 'directive');
+ if (!hasDirectives.hasOwnProperty(name)) {
+ hasDirectives[name] = [];
+ $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
+ function($injector, $exceptionHandler) {
+ var directives = [];
+ forEach(hasDirectives[name], function(directiveFactory) {
+ try {
+ var directive = $injector.invoke(directiveFactory);
+ if (isFunction(directive)) {
+ directive = { compile: valueFn(directive) };
+ } else if (!directive.compile && directive.link) {
+ directive.compile = valueFn(directive.link);
+ }
+ directive.priority = directive.priority || 0;
+ directive.name = name;
+ directive.restrict = directive.restrict || 'EACM';
+ directives.push(directive);
+ } catch (e) {
+ $exceptionHandler(e);
}
- } catch (e) {
- $exceptionHandler(e);
+ });
+ return directives;
+ }]);
+ }
+ hasDirectives[name].push(directiveFactory);
+ } else {
+ forEach(name, reverseParams(registerDirective));
+ }
+ return this;
+ };
+
+
+ this.$get = ['$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache',
+ function($injector, $interpolate, $exceptionHandler, $http, $templateCache) {
+
+ return function(templateElement) {
+ templateElement = jqLite(templateElement);
+ var linkingFn = compileNodes(templateElement, templateElement);
+ return function(scope, cloneConnectFn){
+ assertArg(scope, 'scope');
+ // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
+ // and sometimes changes the structure of the DOM.
+ var element = cloneConnectFn
+ ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!!
+ : templateElement;
+ element.data('$scope', scope);
+ if (cloneConnectFn) cloneConnectFn(element, scope);
+ if (linkingFn) linkingFn(scope, element, element);
+ return element;
+ };
+ };
+
+ //================================
+
+ /**
+ * Compile function matches each node in nodeList against the directives. Once all directives
+ * for a particular node are collected their compile functions are executed. The compile
+ * functions return values - the linking functions - are combined into a composite linking
+ * function, which is the a linking function for the node.
+ *
+ * @param {NodeList} nodeList an array of nodes to compile
+ * @param {DOMElement=} rootElement If the nodeList is the root of the compilation tree then the
+ * rootElement must be set the jqLite collection of the compile root. This is
+ * needed so that the jqLite collection items can be replaced with widgets.
+ * @returns {?function} A composite linking function of all of the matched directives or null.
+ */
+ function compileNodes(nodeList, rootElement) {
+ var linkingFns = [],
+ directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound;
+
+ for(var i = 0, ii = nodeList.length; i < ii; i++) {
+ attrs = {
+ $attr: {},
+ $normalize: directiveNormalize,
+ $set: attrSetter
+ };
+ // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
+ directives = collectDirectives(nodeList[i], [], attrs);
+
+ directiveLinkingFn = (directives.length)
+ ? applyDirectivesToNode(directives, nodeList[i], attrs, rootElement)
+ : null;
+
+ childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal)
+ ? null
+ : compileNodes(nodeList[i].childNodes);
+
+ linkingFns.push(directiveLinkingFn);
+ linkingFns.push(childLinkingFn);
+ linkingFnFound = (linkingFnFound || directiveLinkingFn || childLinkingFn);
+ }
+
+ // return a linking function if we have found anything, null otherwise
+ return linkingFnFound ? linkingFn : null;
+
+ function linkingFn(scope, nodeList, rootElement) {
+ if (linkingFns.length != nodeList.length * 2) {
+ throw Error('Template changed structure!');
+ }
+
+ var childLinkingFn, directiveLinkingFn, node, childScope;
+
+ for(var i=0, n=0, ii=linkingFns.length; i
+ addDirective(directives, directiveNormalize(nodeName_(node).toLowerCase()), 'E');
+
+ // iterate over the attributes
+ for (var attr, name, nName, value, nAttrs = node.attributes,
+ j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
+ attr = nAttrs[j];
+ name = attr.name;
+ nName = directiveNormalize(name.toLowerCase());
+ attrsMap[nName] = name;
+ attrs[nName] = value = trim((msie && name == 'href')
+ ? decodeURIComponent(node.getAttribute(name, 2))
+ : attr.value);
+ if (BOOLEAN_ATTR[nName]) {
+ attrs[nName] = true; // presence means true
}
- });
- var i,
- childNodes = element[0].childNodes,
- children = this.children,
- paths = this.paths,
- length = paths.length;
- for (i = 0; i < length; i++) {
- // sometimes `element` can be modified by one of the linker functions in `this.linkFns`
- // and childNodes may be added or removed
- // TODO: element structure needs to be re-evaluated if new children added
- // if the childNode still exists
- if (childNodes[paths[i]])
- children[i].link(jqLite(childNodes[paths[i]]), childScope);
- else
- delete paths[i]; // if child no longer available, delete path
+ addAttrInterpolateDirective(directives, value, nName);
+ addDirective(directives, nName, 'A');
}
- },
-
- addLinkFn:function(linkingFn) {
- if (linkingFn) {
- this.linkFns.push(linkingFn);
+ // use class as directive
+ className = node.className;
+ while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
+ nName = directiveNormalize(match[2]);
+ if (addDirective(directives, nName, 'C')) {
+ attrs[nName] = trim(match[3]);
+ }
+ className = className.substr(match.index + match[0].length);
}
- },
+ break;
+ case 3: /* Text Node */
+ addTextInterpolateDirective(directives, node.nodeValue);
+ break;
+ case 8: /* Comment */
+ match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
+ if (match) {
+ nName = directiveNormalize(match[1]);
+ if (addDirective(directives, nName, 'M')) {
+ attrs[nName] = trim(match[2]);
+ }
+ }
+ break;
+ }
+
+ directives.sort(byPriority);
+ return directives;
+ }
- addChild: function(index, template) {
- if (template) {
- this.paths.push(index);
- this.children.push(template);
+ /**
+ * Once the directives have been collected their compile functions is executed. This method
+ * is responsible for inlining widget templates as well as terminating the application
+ * of the directives if the terminal directive has been reached..
+ *
+ * @param {Array} directives Array of collected directives to execute their compile function.
+ * this needs to be pre-sorted by priority order.
+ * @param {Node} templateNode The raw DOM node to apply the compile functions to
+ * @param {Object} templateAttrs The shared attribute function
+ * @param {DOMElement} rootElement If we are working on the root of the compile tree then this
+ * argument has the root jqLite array so that we can replace widgets on it.
+ * @returns linkingFn
+ */
+ function applyDirectivesToNode(directives, templateNode, templateAttrs, rootElement) {
+ var terminalPriority = -Number.MAX_VALUE,
+ preLinkingFns = [],
+ postLinkingFns = [],
+ newScopeDirective = null,
+ templateDirective = null,
+ delayedLinkingFn = null,
+ element = templateAttrs.$element = jqLite(templateNode),
+ directive, linkingFn;
+
+ // executes all directives on the current element
+ for(var i = 0, ii = directives.length; i < ii; i++) {
+ directive = directives[i];
+
+ if (terminalPriority > directive.priority) {
+ break; // prevent further processing of directives
+ }
+
+ if (directive.scope) {
+ assertNoDuplicate('new scope', newScopeDirective, directive, element);
+ newScopeDirective = directive;
+ }
+
+ if (directive.template) {
+ assertNoDuplicate('template', templateDirective, directive, element);
+ templateDirective = directive;
+
+ // include the contents of the original element into the template and replace the element
+ var content = directive.template.replace(CONTENT_REGEXP, element.html());
+ templateNode = jqLite(content)[0];
+ if (directive.replace) {
+ replaceWith(rootElement, element, templateNode);
+
+ var newTemplateAttrs = {$attr: {}};
+
+ // combine directives from the original node and from the template:
+ // - take the array of directives for this element
+ // - split it into two parts, those that were already applied and those that weren't
+ // - collect directives from the template, add them to the second group and sort them
+ // - append the second group with new directives to the first group
+ directives = directives.concat(
+ collectDirectives(
+ templateNode,
+ directives.splice(i + 1, directives.length - (i + 1)),
+ newTemplateAttrs
+ )
+ );
+ mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
+
+ ii = directives.length;
+ } else {
+ element.html(content);
+ }
+ }
+
+ if (directive.templateUrl) {
+ assertNoDuplicate('template', templateDirective, directive, element);
+ templateDirective = directive;
+ delayedLinkingFn = compileTemplateUrl(directives.splice(i, directives.length - i),
+ compositeLinkFn, element, templateAttrs, rootElement, directive.replace);
+ ii = directives.length;
+ } else if (directive.compile) {
+ try {
+ linkingFn = directive.compile(element, templateAttrs);
+ if (isFunction(linkingFn)) {
+ postLinkingFns.push(linkingFn);
+ } else if (linkingFn) {
+ if (linkingFn.pre) preLinkingFns.push(linkingFn.pre);
+ if (linkingFn.post) postLinkingFns.push(linkingFn.post);
+ }
+ } catch (e) {
+ $exceptionHandler(e, startingTag(element));
}
- },
+ }
- empty: function() {
- return this.linkFns.length === 0 && this.paths.length === 0;
+ if (directive.terminal) {
+ compositeLinkFn.terminal = true;
+ terminalPriority = Math.max(terminalPriority, directive.priority);
}
- };
- ///////////////////////////////////
- //Compiler
- //////////////////////////////////
-
- /**
- * @ngdoc function
- * @name angular.module.ng.$compile
- * @function
- *
- * @description
- * Compiles a piece of HTML string or DOM into a template and produces a template function, which
- * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together.
- *
- * The compilation is a process of walking the DOM tree and trying to match DOM elements to
- * {@link angular.markup markup}, {@link angular.attrMarkup attrMarkup},
- * {@link angular.widget widgets}, and {@link angular.directive directives}. For each match it
- * executes corresponding markup, attrMarkup, widget or directive template function and collects the
- * instance functions into a single template function which is then returned.
- *
- * The template function can then be used once to produce the view or as it is the case with
- * {@link angular.widget.@ng:repeat repeater} many-times, in which case each call results in a view
- * that is a DOM clone of the original template.
- *
-
- angular.injector(['ng']).invoke(function($rootScope, $compile) {
- // Chose one:
-
- // A: compile the entire window.document.
- var element = $compile(window.document)($rootScope);
-
- // B: compile a piece of html
- var element = $compile('click me')($rootScope);
-
- // C: compile a piece of html and retain reference to both the dom and scope
- var element = $compile('click me')(scope);
- // at this point template was transformed into a view
- });
-
- *
- *
- * @param {string|DOMElement} element Element or HTML to compile into a template function.
- * @returns {function(scope[, cloneAttachFn])} a template function which is used to bind template
- * (a DOM element/tree) to a scope. Where:
- *
- * * `scope` - A {@link angular.module.ng.$rootScope.Scope Scope} to bind to.
- * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
- * `template` and call the `cloneAttachFn` function allowing the caller to attach the
- * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
- * called as:
`cloneAttachFn(clonedElement, scope)` where:
- *
- * * `clonedElement` - is a clone of the original `element` passed into the compiler.
- * * `scope` - is the current scope with which the linking function is working with.
- *
- * Calling the template function returns the element of the template. It is either the original element
- * passed in, or the clone of the element if the `cloneAttachFn` is provided.
- *
- * It is important to understand that the returned scope is "linked" to the view DOM, but no linking
- * (instance) functions registered by {@link angular.directive directives} or
- * {@link angular.widget widgets} found in the template have been executed yet. This means that the
- * view is likely empty and doesn't contain any values that result from evaluation on the scope. To
- * bring the view to life, the scope needs to run through a $digest phase which typically is done by
- * Angular automatically, except for the case when an application is being
- * {@link guide/dev_guide.bootstrap.manual_bootstrap} manually bootstrapped, in which case the
- * $digest phase must be invoked by calling {@link angular.module.ng.$rootScope.Scope#$apply}.
- *
- * If you need access to the bound view, there are two ways to do it:
- *
- * - If you are not asking the linking function to clone the template, create the DOM element(s)
- * before you send them to the compiler and keep this reference around.
- *
- * var $injector = angular.injector(['ng']);
- * var scope = $injector.invoke(function($rootScope, $compile){
- * var element = $compile('{{total}}
')($rootScope);
- * });
- *
- *
- * - if on the other hand, you need the element to be cloned, the view reference from the original
- * example would not point to the clone, but rather to the original template that was cloned. In
- * this case, you can access the clone via the cloneAttachFn:
- *
- * var original = angular.element('{{total}}
'),
- * scope = someParentScope.$new(),
- * clone;
- *
- * $compile(original)(scope, function(clonedElement, scope) {
- * clone = clonedElement;
- * //attach the clone to DOM document at the right place
- * });
- *
- * //now we have reference to the cloned DOM via `clone`
- *
- *
- *
- * Compiler Methods For Widgets and Directives:
- *
- * The following methods are available for use when you write your own widgets, directives,
- * and markup. (Recall that the compile function's this is a reference to the compiler.)
- *
- * `compile(element)` - returns linker -
- * Invoke a new instance of the compiler to compile a DOM element and return a linker function.
- * You can apply the linker function to the original element or a clone of the original element.
- * The linker function returns a scope.
- *
- * * `comment(commentText)` - returns element - Create a comment element.
- *
- * * `element(elementName)` - returns element - Create an element by name.
- *
- * * `text(text)` - returns element - Create a text element.
- *
- * * `descend([set])` - returns descend state (true or false). Get or set the current descend
- * state. If true the compiler will descend to children elements.
- *
- * * `directives([set])` - returns directive state (true or false). Get or set the current
- * directives processing state. The compiler will process directives only when directives set to
- * true.
- *
- * For information on how the compiler works, see the
- * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide.
- */
- function Compiler(markup, attrMarkup, directives, widgets){
- this.markup = markup;
- this.attrMarkup = attrMarkup;
- this.directives = directives;
- this.widgets = widgets;
}
+ compositeLinkFn.scope = !!newScopeDirective;
+
+ // if we have templateUrl, then we have to delay linking
+ return delayedLinkingFn || compositeLinkFn;
- Compiler.prototype = {
- compile: function(templateElement) {
- templateElement = jqLite(templateElement);
- var index = 0,
- template,
- parent = templateElement.parent();
- if (templateElement.length > 1) {
- // https://github.com/angular/angular.js/issues/338
- throw Error("Cannot compile multiple element roots: " +
- jqLite('').append(templateElement.clone()).html());
+ ////////////////////
+
+
+ function compositeLinkFn(childLinkingFn, scope, linkNode) {
+ var attrs, element, i, ii;
+
+ if (templateNode === linkNode) {
+ attrs = templateAttrs;
+ } else {
+ attrs = shallowCopy(templateAttrs);
+ attrs.$element = jqLite(linkNode);
+ }
+ element = attrs.$element;
+
+ // PRELINKING
+ for(i = 0, ii = preLinkingFns.length; i < ii; i++) {
+ try {
+ preLinkingFns[i](scope, element, attrs);
+ } catch (e) {
+ $exceptionHandler(e, startingTag(element));
}
- if (parent && parent[0]) {
- parent = parent[0];
- for(var i = 0; i < parent.childNodes.length; i++) {
- if (parent.childNodes[i] == templateElement[0]) {
- index = i;
- }
- }
+ }
+
+ // RECURSION
+ childLinkingFn && childLinkingFn(scope, linkNode.childNodes);
+
+ // POSTLINKING
+ for(i = 0, ii = postLinkingFns.length; i < ii; i++) {
+ try {
+ postLinkingFns[i](scope, element, attrs);
+ } catch (e) {
+ $exceptionHandler(e, startingTag(element));
}
- template = this.templatize(templateElement, index) || new Template();
- return function(scope, cloneConnectFn){
- assertArg(scope, 'scope');
- // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
- // and sometimes changes the structure of the DOM.
- var element = cloneConnectFn
- ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!!
- : templateElement;
- element.data($$scope, scope);
- scope.$element = element;
- (cloneConnectFn||noop)(element, scope);
- template.link(element, scope);
- return element;
- };
- },
-
- templatize: function(element, elementIndex){
- var self = this,
- widget,
- fn,
- directiveFns = self.directives,
- descend = true,
- directives = true,
- elementName = nodeName_(element),
- elementNamespace = elementName.indexOf(':') > 0 ? lowercase(elementName).replace(':', '-') : '',
- template,
- locals = {$element: element},
- selfApi = {
- compile: bind(self, self.compile),
- descend: function(value){ if(isDefined(value)) descend = value; return descend;},
- directives: function(value){ if(isDefined(value)) directives = value; return directives;},
- scope: function(value){ if(isDefined(value)) template.newScope = template.newScope || value; return template.newScope;}
- };
- element.addClass(elementNamespace);
- template = new Template();
- eachAttribute(element, function(value, name){
- if (!widget) {
- if ((widget = self.widgets('@' + name))) {
- element.addClass('ng-attr-widget');
- if (isFunction(widget) && !widget.$inject) {
- widget.$inject = ['$value', '$element'];
- }
- locals.$value = value;
- }
- }
- });
- if (!widget) {
- if ((widget = self.widgets(elementName))) {
- if (elementNamespace)
- element.addClass('ng-widget');
- if (isFunction(widget) && !widget.$inject) {
- widget.$inject = ['$element'];
- }
+ }
+ }
+ }
+
+
+ /**
+ * looks up the directive and decorates it with exception handling and proper parameters. We
+ * call this the boundDirective.
+ *
+ * @param {string} name name of the directive to look up.
+ * @param {string} location The directive must be found in specific format.
+ * String containing any of theses characters:
+ *
+ * * `E`: element name
+ * * `A': attribute
+ * * `C`: class
+ * * `M`: comment
+ * @returns true if directive was added.
+ */
+ function addDirective(tDirectives, name, location) {
+ var match = false;
+ if (hasDirectives.hasOwnProperty(name)) {
+ for(var directive, directives = $injector.get(name + Suffix),
+ i=0, ii = directives.length; i ignore
+ return;
+ }
+ directives.push({
+ priority: 100,
+ compile: function(element, attr) {
+ if (interpolateFn) {
+ return function(scope, element, attr) {
+ scope.$watch(interpolateFn, function(scope, value){
+ attr.$set(name, value);
+ });
+ };
+ } else {
+ attr.$set(name, value);
}
- // Process non text child nodes
- if (descend) {
- eachNode(element, function(child, i){
- template.addChild(i, self.templatize(child, i));
- });
+ }
+ });
+ }
+
+
+ /**
+ * This is a special jqLite.replaceWith, which can replace items which
+ * have no parents, provided that the containing jqLite collection is provided.
+ *
+ * @param {JqLite=} rootElement The root of the compile tree. Used so that we can replace nodes
+ * in the root of the tree.
+ * @param {JqLite} element The jqLite element which we are going to replace. We keep the shell,
+ * but replace its DOM node reference.
+ * @param {Node} newNode The new DOM node.
+ */
+ function replaceWith(rootElement, element, newNode) {
+ var oldNode = element[0],
+ parent = oldNode.parentNode,
+ i, ii;
+
+ if (rootElement) {
+ for(i = 0, ii = rootElement.length; i
-
-
-
- it('should enter invalid HTML', function() {
- expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
- input('html').enter('<');
- expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
- });
-
-
+ angular.module('formModule', [], function($compileProvider){
+ $compileProvider.directive('ngHtmlEditorModel', function ($formFactory) {
+ return function(scope, element, attr) {
+ var form = $formFactory.forElement(element),
+ widget;
+ element.attr('contentEditable', true);
+ widget = form.$createWidget({
+ scope: scope,
+ model: attr.ngHtmlEditorModel,
+ controller: HTMLEditorWidget,
+ controllerArgs: {$element: element}});
+ // if the element is destroyed, then we need to
+ // notify the form.
+ element.bind('$destroy', function() {
+ widget.$destroy();
+ });
+ };
+ });
+ });
+
+
+
+
+ it('should enter invalid HTML', function() {
+ expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
+ input('htmlContent').enter('<');
+ expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
+ });
+
+
*/
/**
diff --git a/test/AngularSpec.js b/test/AngularSpec.js
index 0ff0508f..2d469698 100644
--- a/test/AngularSpec.js
+++ b/test/AngularSpec.js
@@ -75,6 +75,22 @@ describe('angular', function() {
});
});
+ describe('shallow copy', function() {
+ it('should make a copy', function() {
+ var original = {key:{}};
+ var copy = shallowCopy(original);
+ expect(copy).toEqual(original);
+ expect(copy.key).toBe(original.key);
+ });
+ });
+
+ describe('elementHTML', function() {
+ it('should dump element', function() {
+ expect(lowercase(startingTag('something'))).
+ toEqual('');
+ });
+ });
+
describe('equals', function() {
it('should return true if same object', function() {
var o = {};
@@ -501,4 +517,11 @@ describe('angular', function() {
dealoc(element);
});
});
+
+
+ describe('startingElementHtml', function(){
+ it('should show starting element tag only', function(){
+ expect(startingTag('text ')).toEqual('');
+ });
+ });
});
diff --git a/test/directivesSpec.js b/test/directivesSpec.js
index e52d9fcb..5bd5f5bd 100644
--- a/test/directivesSpec.js
+++ b/test/directivesSpec.js
@@ -2,14 +2,18 @@
describe("directive", function() {
- var $filterProvider;
+ var $filterProvider, element;
beforeEach(module(['$filterProvider', function(provider){
$filterProvider = provider;
}]));
+ afterEach(function() {
+ dealoc(element);
+ });
+
it("should ng:init", inject(function($rootScope, $compile) {
- var element = $compile('')($rootScope);
+ element = $compile('')($rootScope);
expect($rootScope.a).toEqual(123);
}));
diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js
index 3abe4549..8b49502f 100644
--- a/test/jqLiteSpec.js
+++ b/test/jqLiteSpec.js
@@ -1,4 +1,3 @@
-'use strict';
describe('jqLite', function() {
var scope, a, b, c;
@@ -880,6 +879,7 @@ describe('jqLite', function() {
it('should covert dash-separated strings to camelCase', function() {
expect(camelCase('foo-bar')).toBe('fooBar');
expect(camelCase('foo-bar-baz')).toBe('fooBarBaz');
+ expect(camelCase('foo:bar_baz')).toBe('fooBarBaz');
});
diff --git a/test/service/compilerSpec.js b/test/service/compilerSpec.js
index 2df15c51..8d91ed5d 100644
--- a/test/service/compilerSpec.js
+++ b/test/service/compilerSpec.js
@@ -1,216 +1,1184 @@
'use strict';
-describe('compiler', function() {
- var textMarkup, attrMarkup, directives, widgets, compile, log;
-
- beforeEach(module(function($provide){
- textMarkup = [];
- attrMarkup = [];
- widgets = extensionMap({}, 'widget');
- directives = {
- hello: function(expression, element){
- log += "hello ";
- return function() {
- log += expression;
- };
- },
-
- observe: function(expression, element){
- return function() {
- this.$watch(expression, function(val) {
- if (val)
- log += ":" + val;
- });
- };
- }
-
- };
- log = "";
- $provide.value('$textMarkup', textMarkup);
- $provide.value('$attrMarkup', attrMarkup);
- $provide.value('$directive', directives);
- $provide.value('$widget', widgets);
- }));
+describe('$compile', function() {
+ var element;
+ beforeEach(module(provideLog, function($provide, $compileProvider){
+ element = null;
- it('should not allow compilation of multiple roots', inject(function($rootScope, $compile) {
- expect(function() {
- $compile('A');
- }).toThrow("Cannot compile multiple element roots: " + ie("A"));
- function ie(text) {
- return msie < 9 ? uppercase(text) : text;
- }
- }));
+ $compileProvider.directive('log', function(log) {
+ return {
+ priority:0,
+ compile: valueFn(function(scope, element, attrs) {
+ log(attrs.log || 'LOG');
+ })
+ };
+ });
+
+ $compileProvider.directive('highLog', function(log) {
+ return { priority:3, compile: valueFn(function(scope, element, attrs) {
+ log(attrs.highLog || 'HIGH');
+ })};
+ });
+
+ $compileProvider.directive('mediumLog', function(log) {
+ return { priority:2, compile: valueFn(function(scope, element, attrs) {
+ log(attrs.mediumLog || 'MEDIUM');
+ })};
+ });
+ $compileProvider.directive('greet', function() {
+ return { priority:10, compile: valueFn(function(scope, element, attrs) {
+ element.text("Hello " + attrs.greet);
+ })};
+ });
- it('should recognize a directive', inject(function($rootScope, $compile) {
- var e = jqLite('');
- directives.directive = function(expression, element){
- log += "found";
- expect(expression).toEqual("expr");
- expect(element).toEqual(e);
- return function initFn() {
- log += ":init";
+ $compileProvider.directive('set', function() {
+ return function(scope, element, attrs) {
+ element.text(attrs.set);
};
- };
- var linkFn = $compile(e);
- expect(log).toEqual("found");
- linkFn($rootScope);
- expect(e.hasClass('ng-directive')).toEqual(true);
- expect(log).toEqual("found:init");
- }));
+ });
+ $compileProvider.directive('mediumStop', valueFn({
+ priority: 2,
+ terminal: true
+ }));
- it('should recurse to children', inject(function($rootScope, $compile) {
- $compile('')($rootScope);
- expect(log).toEqual("hello misko");
+ $compileProvider.directive('stop', valueFn({
+ terminal: true
+ }));
+
+ $compileProvider.directive('negativeStop', valueFn({
+ priority: -100, // even with negative priority we still should be able to stop descend
+ terminal: true
+ }));
}));
- it('should observe scope', inject(function($rootScope, $compile) {
- $compile('')($rootScope);
- expect(log).toEqual("");
- $rootScope.$digest();
- $rootScope.name = 'misko';
- $rootScope.$digest();
- $rootScope.$digest();
- $rootScope.name = 'adam';
- $rootScope.$digest();
- $rootScope.$digest();
- expect(log).toEqual(":misko:adam");
- }));
+ afterEach(function(){
+ dealoc(element);
+ });
- it('should prevent descend', inject(function($rootScope, $compile) {
- directives.stop = function() { this.descend(false); };
- $compile('')($rootScope);
- expect(log).toEqual("hello misko");
- }));
+ describe('configuration', function() {
+ it('should register a directive', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('div', function(log) {
+ return function(scope, element) {
+ log('OK');
+ element.text('SUCCESS');
+ };
+ })
+ });
+ inject(function($compile, $rootScope, log) {
+ element = $compile('')($rootScope);
+ expect(element.text()).toEqual('SUCCESS');
+ expect(log).toEqual('OK');
+ })
+ });
+ it('should allow registration of multiple directives with same name', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('div', function(log) {
+ return log.fn('1');
+ });
+ $compileProvider.directive('div', function(log) {
+ return log.fn('2');
+ });
+ });
+ inject(function($compile, $rootScope, log) {
+ element = $compile('')($rootScope);
+ expect(log).toEqual('1; 2');
+ });
+ });
+ });
+
+
+ describe('compile phase', function() {
+
+ describe('multiple directives per element', function() {
+ it('should allow multiple directives per element', inject(function($compile, $rootScope, log){
+ element = $compile(
+ '')
+ ($rootScope);
+ expect(element.text()).toEqual('Hello angular');
+ expect(log).toEqual('H; M; L');
+ }));
+
+
+ it('should recurse to children', inject(function($compile, $rootScope){
+ element = $compile('01234')($rootScope);
+ expect(element.text()).toEqual('0hello2angular4');
+ }));
- it('should allow creation of templates', inject(function($rootScope, $compile) {
- directives.duplicate = function(expr, element){
- element.replaceWith(document.createComment("marker"));
- element.removeAttr("duplicate");
- var linker = this.compile(element);
- return function(marker) {
- this.$watch('value', function() {
- var scope = $rootScope.$new;
- linker(scope, noop);
- marker.after(scope.$element);
+
+ it('should allow directives in classes', inject(function($compile, $rootScope, log) {
+ element = $compile('')($rootScope);
+ expect(element.html()).toEqual('Hello angular');
+ expect(log).toEqual('123');
+ }));
+
+
+ it('should allow directives in comments', inject(
+ function($compile, $rootScope, log) {
+ element = $compile('01')($rootScope);
+ expect(log).toEqual('angular');
+ }
+ ));
+
+
+ it('should receive scope, element, and attributes', function() {
+ var injector;
+ module(function($compileProvider) {
+ $compileProvider.directive('log', function($injector, $rootScope) {
+ injector = $injector;
+ return {
+ compile: function(element, templateAttr) {
+ expect(typeof templateAttr.$normalize).toBe('function');
+ expect(typeof templateAttr.$set).toBe('function');
+ expect(isElement(templateAttr.$element)).toBeTruthy();
+ expect(element.text()).toEqual('unlinked');
+ expect(templateAttr.exp).toEqual('abc');
+ expect(templateAttr.aa).toEqual('A');
+ expect(templateAttr.bb).toEqual('B');
+ expect(templateAttr.cc).toEqual('C');
+ return function(scope, element, attr) {
+ expect(element.text()).toEqual('unlinked');
+ expect(attr).toBe(templateAttr);
+ expect(scope).toEqual($rootScope);
+ element.text('worked');
+ }
+ }
+ };
+ });
});
- };
- };
- $compile('beforexafter')($rootScope);
- expect(sortedHtml($rootScope.$element)).
- toEqual('' +
- 'before<#comment>#comment>' +
- 'after' +
+ inject(function($rootScope, $compile, $injector) {
+ element = $compile(
+ 'unlinked')($rootScope);
+ expect(element.text()).toEqual('worked');
+ expect(injector).toBe($injector); // verify that directive is injectable
+ });
+ });
+ });
+
+ describe('error handling', function() {
+
+ it('should handle exceptions', function() {
+ module(function($compileProvider, $exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ $compileProvider.directive('factoryError', function() { throw 'FactoryError'; });
+ $compileProvider.directive('templateError',
+ valueFn({ compile: function() { throw 'TemplateError'; } }));
+ $compileProvider.directive('linkingError',
+ valueFn(function() { throw 'LinkingError'; }));
+ });
+ inject(function($rootScope, $compile, $exceptionHandler) {
+ element = $compile('')($rootScope);
+ expect($exceptionHandler.errors[0]).toEqual('FactoryError');
+ expect($exceptionHandler.errors[1][0]).toEqual('TemplateError');
+ expect(ie($exceptionHandler.errors[1][1])).
+ toEqual('');
+ expect($exceptionHandler.errors[2][0]).toEqual('LinkingError');
+ expect(ie($exceptionHandler.errors[2][1])).
+ toEqual('');
+
+
+ // crazy stuff to make IE happy
+ function ie(text) {
+ var list = [],
+ parts;
+
+ parts = lowercase(text).
+ replace('<', '').
+ replace('>', '').
+ split(' ');
+ parts.sort();
+ forEach(parts, function(value, key){
+ if (value.substring(0,3) == 'ng-') {
+ } else {
+ list.push(value.replace('=""', ''));
+ }
+ });
+ return '<' + list.join(' ') + '>';
+ }
+ });
+ });
+
+
+ it('should prevent changing of structure', inject(
+ function($compile, $rootScope){
+ element = jqLite("");
+ var linkFn = $compile(element);
+ element.append("");
+ expect(function() {
+ linkFn($rootScope);
+ }).toThrow('Template changed structure!');
+ }
+ ));
+ });
+
+ describe('compiler control', function() {
+ describe('priority', function() {
+ it('should honor priority', inject(function($compile, $rootScope, log){
+ element = $compile(
+ '')
+ ($rootScope);
+ expect(log).toEqual('H; M; L');
+ }));
+ });
+
+
+ describe('terminal', function() {
+
+ it('should prevent further directives from running', inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ expect(element.text()).toEqual('OK');
+ }
+ ));
+
+
+ it('should prevent further directives from running, but finish current priority level',
+ inject(function($rootScope, $compile, log) {
+ // class is processed after attrs, so putting log in class will put it after
+ // the stop in the current level. This proves that the log runs after stop
+ element = $compile(
+ '')($rootScope);
+ expect(element.text()).toEqual('OK');
+ expect(log.toArray().sort()).toEqual(['HIGH', 'MEDIUM']);
+ })
+ );
+ });
+
+
+ describe('restrict', function() {
+
+ it('should allow restriction of attributes', function() {
+ module(function($compileProvider, $provide) {
+ forEach({div:'E', attr:'A', clazz:'C', all:'EAC'}, function(restrict, name) {
+ $compileProvider.directive(name, function(log) {
+ return {
+ restrict: restrict,
+ compile: valueFn(function(scope, element, attr) {
+ log(name);
+ })
+ };
+ });
+ });
+ });
+ inject(function($rootScope, $compile, log) {
+ dealoc($compile('')($rootScope));
+ expect(log).toEqual('');
+ log.reset();
+
+ dealoc($compile('')($rootScope));
+ expect(log).toEqual('div');
+ log.reset();
+
+ dealoc($compile(' ')($rootScope));
+ expect(log).toEqual('');
+ log.reset();
+
+ dealoc($compile('')($rootScope));
+ expect(log).toEqual('attr');
+ log.reset();
+
+ dealoc($compile(' ')($rootScope));
+ expect(log).toEqual('');
+ log.reset();
+
+ dealoc($compile('')($rootScope));
+ expect(log).toEqual('clazz');
+ log.reset();
+
+ dealoc($compile(' ')($rootScope));
+ expect(log).toEqual('all; all; all');
+ });
+ });
+ });
+
+
+ describe('template', function() {
+
+
+ beforeEach(module(function($compileProvider) {
+ $compileProvider.directive('replace', valueFn({
+ replace: true,
+ template: 'Hello: <> ',
+ compile: function(element, attr) {
+ attr.$set('compiled', 'COMPILED');
+ expect(element).toBe(attr.$element);
+ }
+ }));
+ $compileProvider.directive('append', valueFn({
+ template: 'Hello: <> ',
+ compile: function(element, attr) {
+ attr.$set('compiled', 'COMPILED');
+ expect(element).toBe(attr.$element);
+ }
+ }));
+ }));
+
+
+ it('should replace element with template', inject(function($compile, $rootScope) {
+ element = $compile('content')($rootScope);
+ expect(element.text()).toEqual('Hello: content');
+ expect(element.find('div').attr('compiled')).toEqual('COMPILED');
+ }));
+
+
+ it('should append element with template', inject(function($compile, $rootScope) {
+ element = $compile('content')($rootScope);
+ expect(element.text()).toEqual('Hello: content');
+ expect(element.find('div').attr('compiled')).toEqual('COMPILED');
+ }));
+
+
+ it('should compile replace template', inject(function($compile, $rootScope, log) {
+ element = $compile('{{ "angular" }}')
+ ($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: angular');
+ // HIGH goes after MEDIUM since it executes as part of replaced template
+ expect(log).toEqual('MEDIUM; HIGH; LOG');
+ }));
+
+
+ it('should compile append template', inject(function($compile, $rootScope, log) {
+ element = $compile('{{ "angular" }}')
+ ($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: angular');
+ expect(log).toEqual('HIGH; LOG; MEDIUM');
+ }));
+
+
+ it('should merge attributes', inject(function($compile, $rootScope) {
+ element = $compile(
+ '')
+ ($rootScope);
+ var div = element.find('div');
+ expect(div.hasClass('medium-log')).toBe(true);
+ expect(div.hasClass('log')).toBe(true);
+ expect(div.css('width')).toBe('10px');
+ expect(div.css('height')).toBe('20px');
+ expect(div.attr('replace')).toEqual('');
+ expect(div.attr('high-log')).toEqual('');
+ }));
+
+ it('should prevent multiple templates per element', inject(function($compile) {
+ try {
+ $compile('')
+ fail();
+ } catch(e) {
+ expect(e.message).toMatch(/Multiple directives .* asking for template/);
+ }
+ }));
+
+ it('should play nice with repeater when inline', inject(function($compile, $rootScope) {
+ element = $compile(
+ '' +
+ '{{i}}; ' +
+ '')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: 1; Hello: 2; ');
+ }));
+
+
+ it('should play nice with repeater when append', inject(function($compile, $rootScope) {
+ element = $compile(
+ '' +
+ '{{i}}; ' +
+ '')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('Hello: 1; Hello: 2; ');
+ }));
+ });
+
+
+ describe('async templates', function() {
+
+ beforeEach(module(
+ function($compileProvider) {
+ $compileProvider.directive('hello', valueFn({ templateUrl: 'hello.html' }));
+ $compileProvider.directive('cau', valueFn({ templateUrl:'cau.html' }));
+
+ $compileProvider.directive('cError', valueFn({
+ templateUrl:'error.html',
+ compile: function() {
+ throw Error('cError');
+ }
+ }));
+ $compileProvider.directive('lError', valueFn({
+ templateUrl: 'error.html',
+ compile: function() {
+ throw Error('lError');
+ }
+ }));
+
+
+ $compileProvider.directive('iHello', valueFn({
+ replace: true,
+ templateUrl: 'hello.html'
+ }));
+ $compileProvider.directive('iCau', valueFn({
+ replace: true,
+ templateUrl:'cau.html'
+ }));
+
+ $compileProvider.directive('iCError', valueFn({
+ replace: true,
+ templateUrl:'error.html',
+ compile: function() {
+ throw Error('cError');
+ }
+ }));
+ $compileProvider.directive('iLError', valueFn({
+ replace: true,
+ templateUrl: 'error.html',
+ compile: function() {
+ throw Error('lError');
+ }
+ }));
+
+ }
+ ));
+
+
+ it('should append template via $http and cache it in $templateCache', inject(
+ function($compile, $httpBackend, $templateCache, $rootScope, $browser) {
+ $httpBackend.expect('GET', 'hello.html').respond('Hello! World!');
+ $templateCache.put('cau.html', 'Cau!');
+ element = $compile('ignoreignore')($rootScope);
+ expect(sortedHtml(element)).
+ toEqual('');
+
+ $rootScope.$digest();
+
+
+ expect(sortedHtml(element)).
+ toEqual('Cau!');
+
+ $httpBackend.flush();
+ expect(sortedHtml(element)).toEqual(
+ '' +
+ 'Hello! World!' +
+ 'Cau!' +
+ '');
+ }
+ ));
+
+
+ it('should inline template via $http and cache it in $templateCache', inject(
+ function($compile, $httpBackend, $templateCache, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond('Hello!');
+ $templateCache.put('cau.html', 'Cau!');
+ element = $compile('ignoreignore')($rootScope);
+ expect(sortedHtml(element)).
+ toEqual('');
+
+ $rootScope.$digest();
+
+
+ expect(sortedHtml(element)).
+ toEqual('Cau!');
+
+ $httpBackend.flush();
+ expect(sortedHtml(element)).
+ toEqual('Hello!Cau!');
+ }
+ ));
+
+
+ it('should compile, link and flush the template append', inject(
+ function($compile, $templateCache, $rootScope, $browser) {
+ $templateCache.put('hello.html', 'Hello, {{name}}!');
+ $rootScope.name = 'Elvis';
+ element = $compile('')($rootScope);
+
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('Hello, Elvis!');
+ }
+ ));
+
+
+ it('should compile, link and flush the template inline', inject(
+ function($compile, $templateCache, $rootScope) {
+ $templateCache.put('hello.html', 'Hello, {{name}}!');
+ $rootScope.name = 'Elvis';
+ element = $compile('')($rootScope);
+
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('Hello, Elvis!');
+ }
+ ));
+
+
+ it('should compile, flush and link the template append', inject(
+ function($compile, $templateCache, $rootScope) {
+ $templateCache.put('hello.html', 'Hello, {{name}}!');
+ $rootScope.name = 'Elvis';
+ var template = $compile('');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('Hello, Elvis!');
+ }
+ ));
+
+
+ it('should compile, flush and link the template inline', inject(
+ function($compile, $templateCache, $rootScope) {
+ $templateCache.put('hello.html', 'Hello, {{name}}!');
+ $rootScope.name = 'Elvis';
+ var template = $compile('');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+
+ expect(sortedHtml(element)).
+ toEqual('Hello, Elvis!');
+ }
+ ));
+
+
+ it('should resolve widgets after cloning in append mode', function() {
+ module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ });
+ inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
+ $exceptionHandler) {
+ $httpBackend.expect('GET', 'hello.html').respond('{{greeting}} ');
+ $httpBackend.expect('GET', 'error.html').respond('');
+ $templateCache.put('cau.html', '{{name}}');
+ $rootScope.greeting = 'Hello';
+ $rootScope.name = 'Elvis';
+ var template = $compile(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
'');
- $rootScope.value = 1;
- $rootScope.$digest();
- expect(sortedHtml($rootScope.$element)).
- toEqual('' +
- 'before<#comment>#comment>' +
- 'x' +
- 'after' +
- '');
- $rootScope.value = 2;
- $rootScope.$digest();
- expect(sortedHtml($rootScope.$element)).
- toEqual('' +
- 'before<#comment>#comment>' +
- 'x' +
- 'x' +
- 'after' +
- '');
- $rootScope.value = 3;
- $rootScope.$digest();
- expect(sortedHtml($rootScope.$element)).
- toEqual('' +
- 'before<#comment>#comment>' +
- 'x' +
- 'x' +
- 'x' +
- 'after' +
- '');
- }));
+ var e1;
+ var e2;
+
+ e1 = template($rootScope.$new(), noop); // clone
+ expect(e1.text()).toEqual('');
+
+ $httpBackend.flush();
+
+ e2 = template($rootScope.$new(), noop); // clone
+ $rootScope.$digest();
+ expect(e1.text()).toEqual('Hello Elvis');
+ expect(e2.text()).toEqual('Hello Elvis');
+
+ expect($exceptionHandler.errors.length).toEqual(2);
+ expect($exceptionHandler.errors[0][0].message).toEqual('cError');
+ expect($exceptionHandler.errors[1][0].message).toEqual('lError');
+
+ dealoc(e1);
+ dealoc(e2);
+ });
+ });
+
+
+ it('should resolve widgets after cloning in inline mode', function() {
+ module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+ });
+ inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser,
+ $exceptionHandler) {
+ $httpBackend.expect('GET', 'hello.html').respond('{{greeting}} ');
+ $httpBackend.expect('GET', 'error.html').respond('');
+ $templateCache.put('cau.html', '{{name}}');
+ $rootScope.greeting = 'Hello';
+ $rootScope.name = 'Elvis';
+ var template = $compile(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '');
+ var e1;
+ var e2;
+
+ e1 = template($rootScope.$new(), noop); // clone
+ expect(e1.text()).toEqual('');
+
+ $httpBackend.flush();
+
+ e2 = template($rootScope.$new(), noop); // clone
+ $rootScope.$digest();
+ expect(e1.text()).toEqual('Hello Elvis');
+ expect(e2.text()).toEqual('Hello Elvis');
+
+ expect($exceptionHandler.errors.length).toEqual(2);
+ expect($exceptionHandler.errors[0][0].message).toEqual('cError');
+ expect($exceptionHandler.errors[1][0].message).toEqual('lError');
+
+ dealoc(e1);
+ dealoc(e2);
+ });
+ });
+
+
+ it('should be implicitly terminal and not compile placeholder content in append', inject(
+ function($compile, $templateCache, $rootScope, log) {
+ // we can't compile the contents because that would result in a memory leak
+
+ $templateCache.put('hello.html', 'Hello!');
+ element = $compile('')($rootScope);
+
+ expect(log).toEqual('');
+ }
+ ));
+
+
+ it('should be implicitly terminal and not compile placeholder content in inline', inject(
+ function($compile, $templateCache, $rootScope, log) {
+ // we can't compile the contents because that would result in a memory leak
+
+ $templateCache.put('hello.html', 'Hello!');
+ element = $compile('')($rootScope);
+
+ expect(log).toEqual('');
+ }
+ ));
+
+
+ it('should throw an error and clear element content if the template fails to load', inject(
+ function($compile, $httpBackend, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond(404, 'Not Found!');
+ element = $compile('content')($rootScope);
+
+ expect(function() {
+ $httpBackend.flush();
+ }).toThrow('Failed to load template: hello.html');
+ expect(sortedHtml(element)).toBe('');
+ }
+ ));
+
+
+ it('should prevent multiple templates per element', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('sync', valueFn({
+ template: ''
+ }));
+ $compileProvider.directive('async', valueFn({
+ templateUrl: 'template.html'
+ }));
+ });
+ inject(function($compile){
+ expect(function() {
+ $compile('');
+ }).toThrow('Multiple directives [sync, async] asking for template on: <'+
+ (msie <= 8 ? 'DIV' : 'div') + ' class="sync async">');
+ });
+ });
+
+
+ describe('delay compile / linking functions until after template is resolved', function(){
+ var template;
+ beforeEach(module(function($compileProvider) {
+ function directive (name, priority, options) {
+ $compileProvider.directive(name, function(log) {
+ return (extend({
+ priority: priority,
+ compile: function() {
+ log(name + '-C');
+ return function() { log(name + '-L'); }
+ }
+ }, options || {}));
+ });
+ }
+
+ directive('first', 10);
+ directive('second', 5, { templateUrl: 'second.html' });
+ directive('third', 3);
+ directive('last', 0);
+
+ directive('iFirst', 10, {replace: true});
+ directive('iSecond', 5, {replace: true, templateUrl: 'second.html' });
+ directive('iThird', 3, {replace: true});
+ directive('iLast', 0, {replace: true});
+ }));
+
+ it('should flush after link append', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('{{1+2}}');
+ template = $compile('');
+ element = template($rootScope);
+ expect(log).toEqual('first-C');
+
+ log('FLUSH');
+ $httpBackend.flush();
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'first-C; FLUSH; second-C; last-C; third-C; ' +
+ 'third-L; first-L; second-L; last-L');
+
+ var span = element.find('span');
+ expect(span.attr('first')).toEqual('');
+ expect(span.attr('second')).toEqual('');
+ expect(span.find('div').attr('third')).toEqual('');
+ expect(span.attr('last')).toEqual('');
+
+ expect(span.text()).toEqual('3');
+ }));
+
+
+ it('should flush after link inline', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('{{1+2}}');
+ template = $compile('');
+ element = template($rootScope);
+ expect(log).toEqual('iFirst-C');
+
+ log('FLUSH');
+ $httpBackend.flush();
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
+ 'iFirst-L; iSecond-L; iThird-L; iLast-L');
+
+ var div = element.find('div');
+ expect(div.attr('i-first')).toEqual('');
+ expect(div.attr('i-second')).toEqual('');
+ expect(div.attr('i-third')).toEqual('');
+ expect(div.attr('i-last')).toEqual('');
+
+ expect(div.text()).toEqual('3');
+ }));
+
+
+ it('should flush before link append', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('{{1+2}}');
+ template = $compile('');
+ expect(log).toEqual('first-C');
+ log('FLUSH');
+ $httpBackend.flush();
+ expect(log).toEqual('first-C; FLUSH; second-C; last-C; third-C');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'first-C; FLUSH; second-C; last-C; third-C; ' +
+ 'third-L; first-L; second-L; last-L');
+
+ var span = element.find('span');
+ expect(span.attr('first')).toEqual('');
+ expect(span.attr('second')).toEqual('');
+ expect(span.find('div').attr('third')).toEqual('');
+ expect(span.attr('last')).toEqual('');
+
+ expect(span.text()).toEqual('3');
+ }));
+
+
+ it('should flush before link inline', inject(
+ function($compile, $rootScope, $httpBackend, log) {
+ $httpBackend.expect('GET', 'second.html').respond('{{1+2}}');
+ template = $compile('');
+ expect(log).toEqual('iFirst-C');
+ log('FLUSH');
+ $httpBackend.flush();
+ expect(log).toEqual('iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C');
+
+ element = template($rootScope);
+ $rootScope.$digest();
+ expect(log).toEqual(
+ 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
+ 'iFirst-L; iSecond-L; iThird-L; iLast-L');
+
+ var div = element.find('div');
+ expect(div.attr('i-first')).toEqual('');
+ expect(div.attr('i-second')).toEqual('');
+ expect(div.attr('i-third')).toEqual('');
+ expect(div.attr('i-last')).toEqual('');
+
+ expect(div.text()).toEqual('3');
+ }));
+ });
+
+
+ it('should check that template has root element', inject(function($compile, $httpBackend) {
+ $httpBackend.expect('GET', 'hello.html').respond('before mid after');
+ $compile('');
+ expect(function(){
+ $httpBackend.flush();
+ }).toThrow('Template must have exactly one root element: before mid after');
+ }));
+
+
+ it('should allow multiple elements in template', inject(function($compile, $httpBackend) {
+ $httpBackend.expect('GET', 'hello.html').respond('before mid after');
+ element = jqLite('');
+ $compile(element);
+ $httpBackend.flush();
+ expect(element.text()).toEqual('before mid after');
+ }));
+
+
+ it('should work when widget is in root element', inject(
+ function($compile, $httpBackend, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond('3==<> ');
+ element = jqLite('{{1+2}}');
+ $compile(element)($rootScope);
+
+ $httpBackend.flush();
+ expect(element.text()).toEqual('3==3');
+ }
+ ));
+
+
+ it('should work when widget is a repeater', inject(
+ function($compile, $httpBackend, $rootScope) {
+ $httpBackend.expect('GET', 'hello.html').respond('i=<>; ');
+ element = jqLite('{{i}}');
+ $compile(element)($rootScope);
+
+ $httpBackend.flush();
+ expect(element.text()).toEqual('i=1;i=2;');
+ }
+ ));
+ });
+
+
+ describe('scope', function() {
+
+ beforeEach(module(function($compileProvider) {
+ forEach(['', 'a', 'b'], function(name) {
+ $compileProvider.directive('scope' + uppercase(name), function(log) {
+ return {
+ scope: true,
+ compile: function() {
+ return function (scope, element) {
+ log(scope.$id);
+ expect(element.data('$scope')).toBe(scope);
+ };
+ }
+ };
+ });
+ });
+ $compileProvider.directive('log', function(log) {
+ return function(scope) {
+ log('log-' + scope.$id + '-' + scope.$parent.$id);
+ };
+ });
+ }));
+
+
+ it('should allow creation of new scopes', inject(function($rootScope, $compile, log) {
+ element = $compile('')($rootScope);
+ expect(log).toEqual('LOG; log-002-001; 002');
+ }));
+
+ it('should correctly create the scope hierachy properly', inject(
+ function($rootScope, $compile, log) {
+ element = $compile(
+ '' + //1
+ '' + //2
+ '' + //3
+ '' +
+ '' +
+ '' + //4
+ '' +
+ '' +
+ ''
+ )($rootScope);
+ expect(log).toEqual('LOG; log-003-002; 003; LOG; log-002-001; 002; LOG; log-004-001; 004');
+ }));
- it('should process markup before directives', inject(function($rootScope, $compile) {
- textMarkup.push(function(text, textNode, parentNode) {
- if (text == 'middle') {
- expect(textNode.text()).toEqual(text);
- parentNode.attr('hello', text);
- textNode[0].nodeValue = 'replaced';
- }
+
+ it('should not allow more then one scope creation per element', inject(
+ function($rootScope, $compile) {
+ expect(function(){
+ $compile('');
+ }).toThrow('Multiple directives [scopeA, scopeB] asking for new scope on: ' +
+ '<' + (msie < 9 ? 'DIV' : 'div') + ' class="scope-a; scope-b">');
+ }));
+
+
+ it('should treat new scope on new template as noop', inject(
+ function($rootScope, $compile, log) {
+ element = $compile('')($rootScope);
+ expect(log).toEqual('001');
+ }));
+ });
});
- $compile('beforemiddleafter')($rootScope);
- expect(sortedHtml($rootScope.$element[0], true)).
- toEqual('beforereplacedafter');
- expect(log).toEqual("hello middle");
- }));
+ });
- it('should replace widgets', inject(function($rootScope, $compile) {
- widgets['NG:BUTTON'] = function(element) {
- expect(element.hasClass('ng-widget')).toEqual(true);
- element.replaceWith('button');
- return function(element) {
- log += 'init';
- };
- };
- $compile('push me ')($rootScope);
- expect(lowercase($rootScope.$element[0].innerHTML)).toEqual('button');
- expect(log).toEqual('init');
- }));
+ describe('interpolation', function() {
+ it('should compile and link both attribute and text bindings', inject(
+ function($rootScope, $compile) {
+ $rootScope.name = 'angular';
+ element = $compile('text: {{name}}')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('text: angular');
+ expect(element.attr('name')).toEqual('attr: angular');
+ }));
- it('should use the replaced element after calling widget', inject(function($rootScope, $compile) {
- widgets['H1'] = function(element) {
- // HTML elements which are augmented by acting as widgets, should not be marked as so
- expect(element.hasClass('ng-widget')).toEqual(false);
- var span = angular.element('{{1+2}}');
- element.replaceWith(span);
- this.descend(true);
- this.directives(true);
- return noop;
- };
- textMarkup.push(function(text, textNode, parent){
- if (text == '{{1+2}}')
- parent.text('3');
- });
- $compile('ignore me
')($rootScope);
- expect($rootScope.$element.text()).toEqual('3');
- }));
+ it('should decorate the binding with ng-binding and interpolation function', inject(
+ function($compile, $rootScope) {
+ element = $compile('{{1+2}}')($rootScope);
+ expect(element.hasClass('ng-binding')).toBe(true);
+ expect(element.data('$binding')[0].exp).toEqual('{{1+2}}');
+ }));
+ });
- it('should allow multiple markups per text element', inject(function($rootScope, $compile) {
- textMarkup.push(function(text, textNode, parent){
- var index = text.indexOf('---');
- if (index > -1) {
- textNode.after(text.substring(index + 3));
- textNode.after("
");
- textNode.after(text.substring(0, index));
- textNode.remove();
- }
- });
- textMarkup.push(function(text, textNode, parent){
- var index = text.indexOf('===');
- if (index > -1) {
- textNode.after(text.substring(index + 3));
- textNode.after("");
- textNode.after(text.substring(0, index));
- textNode.remove();
- }
- });
- $compile('
A---B---C===D')($rootScope);
- expect(sortedHtml($rootScope.$element)).toEqual('A
B
CD');
- }));
+ describe('link phase', function() {
- it('should add class for namespace elements', inject(function($rootScope, $compile) {
- var element = $compile('abc ')($rootScope);
- expect(element.hasClass('ng-space')).toEqual(true);
- }));
+ beforeEach(module(function($compileProvider) {
+
+ forEach(['a', 'b', 'c'], function(name) {
+ $compileProvider.directive(name, function(log) {
+ return {
+ compile: function() {
+ log('t' + uppercase(name))
+ return {
+ pre: function() {
+ log('pre' + uppercase(name));
+ },
+ post: function linkFn() {
+ log('post' + uppercase(name));
+ }
+ };
+ }
+ };
+ });
+ });
+ }));
+
+
+ it('should not store linkingFns for noop branches', inject(function ($rootScope, $compile) {
+ element = jqLite('ignore');
+ var linkingFn = $compile(element);
+ // Now prune the branches with no directives
+ element.find('span').remove();
+ expect(element.find('span').length).toBe(0);
+ // and we should still be able to compile without errors
+ linkingFn($rootScope);
+ }));
+
+
+ it('should compile from top to bottom but link from bottom up', inject(
+ function($compile, $rootScope, log) {
+ element = $compile(' ')($rootScope);
+ expect(log).toEqual('tA; tB; tC; preA; preB; preC; postC; postA; postB');
+ }
+ ));
+
+
+ it('should support link function on directive object', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('abc', valueFn({
+ link: function(scope, element, attrs) {
+ element.text(attrs.abc);
+ }
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('FAIL')($rootScope);
+ expect(element.text()).toEqual('WORKS');
+ });
+ });
+ });
+
+
+ describe('attrs', function() {
+
+ it('should allow setting of attributes', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ setter: valueFn(function(scope, element, attr) {
+ attr.$set('name', 'abc');
+ attr.$set('disabled', true);
+ expect(attr.name).toBe('abc');
+ expect(attr.disabled).toBe(true);
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ expect(element.attr('name')).toEqual('abc');
+ expect(element.attr('disabled')).toEqual('disabled');
+ });
+ });
+
+
+ it('should read boolean attributes as boolean', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ div: valueFn(function(scope, element, attr) {
+ element.text(attr.required);
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ expect(element.text()).toEqual('true');
+ });
+ });
+
+ it('should allow setting of attributes', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ setter: valueFn(function(scope, element, attr) {
+ attr.$set('name', 'abc');
+ attr.$set('disabled', true);
+ expect(attr.name).toBe('abc');
+ expect(attr.disabled).toBe(true);
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ expect(element.attr('name')).toEqual('abc');
+ expect(element.attr('disabled')).toEqual('disabled');
+ });
+ });
+
+
+ it('should read boolean attributes as boolean', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive({
+ div: valueFn(function(scope, element, attr) {
+ element.text(attr.required);
+ })
+ });
+ });
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ expect(element.text()).toEqual('true');
+ });
+ });
+
+
+ it('should create new instance of attr for each template stamping', function() {
+ module(function($compileProvider, $provide) {
+ var state = { first: [], second: [] };
+ $provide.value('state', state);
+ $compileProvider.directive({
+ first: valueFn({
+ priority: 1,
+ compile: function(templateElement, templateAttr) {
+ return function(scope, element, attr) {
+ state.first.push({
+ template: {element: templateElement, attr:templateAttr},
+ link: {element: element, attr: attr}
+ });
+ }
+ }
+ }),
+ second: valueFn({
+ priority: 2,
+ compile: function(templateElement, templateAttr) {
+ return function(scope, element, attr) {
+ state.second.push({
+ template: {element: templateElement, attr:templateAttr},
+ link: {element: element, attr: attr}
+ });
+ }
+ }
+ })
+ });
+ });
+ inject(function($rootScope, $compile, state) {
+ var template = $compile('');
+ dealoc(template($rootScope.$new(), noop));
+ dealoc(template($rootScope.$new(), noop));
+
+ // instance between directives should be shared
+ expect(state.first[0].template.element).toBe(state.second[0].template.element);
+ expect(state.first[0].template.attr).toBe(state.second[0].template.attr);
+
+ // the template and the link can not be the same instance
+ expect(state.first[0].template.element).not.toBe(state.first[0].link.element);
+ expect(state.first[0].template.attr).not.toBe(state.first[0].link.attr);
+
+ // each new template needs to be new instance
+ expect(state.first[0].link.element).not.toBe(state.first[1].link.element);
+ expect(state.first[0].link.attr).not.toBe(state.first[1].link.attr);
+ expect(state.second[0].link.element).not.toBe(state.second[1].link.element);
+ expect(state.second[0].link.attr).not.toBe(state.second[1].link.attr);
+ });
+ });
+
+
+ describe('$set', function() {
+ var attr;
+ beforeEach(function(){
+ module(function($compileProvider) {
+ $compileProvider.directive('div', valueFn(function(scope, element, attr){
+ scope.attr = attr;
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('')($rootScope);
+ attr = $rootScope.attr;
+ expect(attr).toBeDefined();
+ });
+ });
+
+
+ it('should set attributes', function() {
+ attr.$set('ngMyAttr', 'value');
+ expect(element.attr('ng-my-attr')).toEqual('value');
+ expect(attr.ngMyAttr).toEqual('value');
+ });
+
+
+ it('should allow overriding of attribute name and remember the name', function() {
+ attr.$set('ngOther', '123', 'other');
+ expect(element.attr('other')).toEqual('123');
+ expect(attr.ngOther).toEqual('123');
+
+ attr.$set('ngOther', '246');
+ expect(element.attr('other')).toEqual('246');
+ expect(attr.ngOther).toEqual('246');
+ });
+
+
+ it('should set boolean attributes', function() {
+ attr.$set('disabled', 'true');
+ attr.$set('readOnly', 'true');
+ expect(element.attr('disabled')).toEqual('disabled');
+ expect(element.attr('readonly')).toEqual('readonly');
+
+ attr.$set('disabled', 'false');
+ expect(element.attr('disabled')).toEqual(undefined);
+
+ attr.$set('disabled', false);
+ attr.$set('readOnly', false);
+ expect(element.attr('disabled')).toEqual(undefined);
+ expect(element.attr('readonly')).toEqual(undefined);
+ });
+
+
+ it('should remove attribute', function() {
+ attr.$set('ngMyAttr', 'value');
+ expect(element.attr('ng-my-attr')).toEqual('value');
+
+ attr.$set('ngMyAttr', undefined);
+ expect(element.attr('ng-my-attr')).toBe(undefined);
+
+ attr.$set('ngMyAttr', 'value');
+ attr.$set('ngMyAttr', null);
+ expect(element.attr('ng-my-attr')).toBe(undefined);
+ })
+ });
+ });
});
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index 13e726bb..85c844cb 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -67,7 +67,10 @@ function dealoc(obj) {
}
}
-
+/**
+ * @param {DOMElement} element
+ * @param {boolean=} showNgClass
+ */
function sortedHtml(element, showNgClass) {
var html = "";
forEach(jqLite(element), function toString(node) {
--
cgit v1.2.3