From beea3a4beda0aaed5fc54af1a992b1c161db7752 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Wed, 2 May 2012 14:32:27 -0700 Subject: style($compile): rename compiler.js to compile.js --- angularFiles.js | 2 +- src/ng/compile.js | 1018 ++++++++++++++++++++++++++ src/ng/compiler.js | 1018 -------------------------- test/ng/compileSpec.js | 1869 +++++++++++++++++++++++++++++++++++++++++++++++ test/ng/compilerSpec.js | 1869 ----------------------------------------------- 5 files changed, 2888 insertions(+), 2888 deletions(-) create mode 100644 src/ng/compile.js delete mode 100644 src/ng/compiler.js create mode 100644 test/ng/compileSpec.js delete mode 100644 test/ng/compilerSpec.js diff --git a/angularFiles.js b/angularFiles.js index fb332a8a..fa633be7 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -11,7 +11,7 @@ angularFiles = { 'src/ng/anchorScroll.js', 'src/ng/browser.js', 'src/ng/cacheFactory.js', - 'src/ng/compiler.js', + 'src/ng/compile.js', 'src/ng/controller.js', 'src/ng/defer.js', 'src/ng/document.js', diff --git a/src/ng/compile.js b/src/ng/compile.js new file mode 100644 index 00000000..59a70145 --- /dev/null +++ b/src/ng/compile.js @@ -0,0 +1,1018 @@ +'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.ngRepeat 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!'); + }); + +
+ + * + * + * @param {string|DOMElement} element Element or HTML string to compile into a template function. + * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. + * @param {number} maxPriority only apply directives lower then given priority (Only effects the + * root element(s), not their children) + * @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 updated 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` + *
+ * + * + * 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]*\>$/; + + + 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 = directive.name || name; + directive.require = directive.require || (directive.controller && directive.name); + directive.restrict = directive.restrict || 'A'; + directives.push(directive); + } catch (e) { + $exceptionHandler(e); + } + }); + return directives; + }]); + } + hasDirectives[name].push(directiveFactory); + } else { + forEach(name, reverseParams(registerDirective)); + } + return this; + }; + + + this.$get = [ + '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', + '$controller', + function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, + $controller) { + + var LOCAL_MODE = { + attribute: function(localName, mode, parentScope, scope, attr) { + scope[localName] = attr[localName]; + }, + + evaluate: function(localName, mode, parentScope, scope, attr) { + scope[localName] = parentScope.$eval(attr[localName]); + }, + + bind: function(localName, mode, parentScope, scope, attr) { + var getter = $interpolate(attr[localName]); + scope.$watch( + function() { return getter(parentScope); }, + function(v) { scope[localName] = v; } + ); + }, + + accessor: function(localName, mode, parentScope, scope, attr) { + var getter = noop, + setter = noop, + exp = attr[localName]; + + if (exp) { + getter = $parse(exp); + setter = getter.assign || function() { + throw Error("Expression '" + exp + "' not assignable."); + }; + } + + scope[localName] = function(value) { + return arguments.length ? setter(parentScope, value) : getter(parentScope); + }; + }, + + expression: function(localName, mode, parentScope, scope, attr) { + scope[localName] = function(locals) { + $parse(attr[localName])(parentScope, locals); + }; + } + }; + + var Attributes = function(element, attr) { + this.$$element = element; + this.$$observers = {}; + this.$attr = attr || {}; + }; + + Attributes.prototype = { + $normalize: directiveNormalize, + + + /** + * Set a normalized attribute on the element in a way such that all directives + * can share the attribute. This function properly handles boolean attributes. + * @param {string} key Normalized key. (ie ngAttribute) + * @param {string|boolean} value The value to set. If `null` attribute will be deleted. + * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. + * Defaults to true. + * @param {string=} attrName Optional none normalized name. Defaults to key. + */ + $set: function(key, value, writeAttr, attrName) { + var booleanKey = isBooleanAttr(this.$$element[0], key.toLowerCase()); + + if (booleanKey) { + this.$$element.prop(key, value); + attrName = booleanKey; + } + + this[key] = value; + + // translate normalized key to actual key + if (attrName) { + this.$attr[key] = attrName; + } else { + attrName = this.$attr[key]; + if (!attrName) { + this.$attr[key] = attrName = snake_case(key, '-'); + } + } + + if (writeAttr !== false) { + if (value === null || value === undefined) { + this.$$element.removeAttr(attrName); + } else { + this.$$element.attr(attrName, value); + } + } + + // fire observers + forEach(this.$$observers[key], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + }, + + + /** + * Observe an interpolated attribute. + * The observer will never be called, if given attribute is not interpolated. + * + * @param {string} key Normalized key. (ie ngAttribute) . + * @param {function(*)} fn Function that will be called whenever the attribute value changes. + */ + $observe: function(key, fn) { + // keep only observers for interpolated attrs + if (this.$$observers[key]) { + this.$$observers[key].push(fn); + } + } + }; + + return compile; + + //================================ + + function compile(templateElement, transcludeFn, maxPriority) { + if (!(templateElement instanceof jqLite)) { + // jquery always rewraps, where as we need to preserve the original selector so that we can modify it. + templateElement = jqLite(templateElement); + } + // We can not compile top level text elements since text nodes can be merged and we will + // not be able to attach scope data to them, so we will wrap them in + forEach(templateElement, function(node, index){ + if (node.nodeType == 3 /* text node */) { + templateElement[index] = jqLite(node).wrap('').parent()[0]; + } + }); + var linkingFn = compileNodes(templateElement, transcludeFn, templateElement, maxPriority); + 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; + safeAddClass(element.data('$scope', scope), 'ng-scope'); + if (cloneConnectFn) cloneConnectFn(element, scope); + if (linkingFn) linkingFn(scope, element, element); + return element; + }; + } + + function wrongMode(localName, mode) { + throw Error("Unsupported '" + mode + "' for '" + localName + "'."); + } + + function safeAddClass(element, className) { + try { + element.addClass(className); + } catch(e) { + // ignore, since it means that we are trying to set class on + // SVG element, where class name is read-only. + } + } + + /** + * 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 {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the + * scope argument is auto-generated to the new child of the transcluded parent scope. + * @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. + * @param {number=} max directive priority + * @returns {?function} A composite linking function of all of the matched directives or null. + */ + function compileNodes(nodeList, transcludeFn, rootElement, maxPriority) { + var linkingFns = [], + directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound; + + for(var i = 0; i < nodeList.length; i++) { + attrs = new Attributes(); + + // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + directives = collectDirectives(nodeList[i], [], attrs, maxPriority); + + directiveLinkingFn = (directives.length) + ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, rootElement) + : null; + + childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal) + ? null + : compileNodes(nodeList[i].childNodes, + directiveLinkingFn ? directiveLinkingFn.transclude : transcludeFn); + + 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; + + /* nodesetLinkingFn */ function linkingFn(scope, nodeList, rootElement, boundTranscludeFn) { + var childLinkingFn, directiveLinkingFn, node, childScope, childTransclusionFn; + + for(var i=0, n=0, ii=linkingFns.length; i + addDirective(directives, + directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority); + + // 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]; + if (attr.specified) { + 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 (isBooleanAttr(node, nName)) { + attrs[nName] = true; // presence means true + } + addAttrInterpolateDirective(node, directives, value, nName); + addDirective(directives, nName, 'A', maxPriority); + } + } + + // use class as directive + className = node.className; + if (isString(className)) { + while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + nName = directiveNormalize(match[2]); + if (addDirective(directives, nName, 'C', maxPriority)) { + 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 */ + try { + match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); + if (match) { + nName = directiveNormalize(match[1]); + if (addDirective(directives, nName, 'M', maxPriority)) { + attrs[nName] = trim(match[2]); + } + } + } catch (e) { + // turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value. + // Just ignore it and continue. (Can't seem to reproduce in test case.) + } + break; + } + + directives.sort(byPriority); + return directives; + } + + + /** + * Once the directives have been collected their compile functions is executed. This method + * is responsible for inlining directive 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 {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the + * scope argument is auto-generated to the new child of the transcluded parent scope. + * @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, transcludeFn, rootElement) { + var terminalPriority = -Number.MAX_VALUE, + preLinkingFns = [], + postLinkingFns = [], + newScopeDirective = null, + newIsolatedScopeDirective = null, + templateDirective = null, + delayedLinkingFn = null, + element = templateAttrs.$$element = jqLite(templateNode), + directive, + directiveName, + template, + transcludeDirective, + childTranscludeFn = transcludeFn, + controllerDirectives, + linkingFn, + directiveValue; + + // executes all directives on the current element + for(var i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + template = undefined; + + if (terminalPriority > directive.priority) { + break; // prevent further processing of directives + } + + if (directiveValue = directive.scope) { + assertNoDuplicate('isolated scope', newIsolatedScopeDirective, directive, element); + if (isObject(directiveValue)) { + safeAddClass(element, 'ng-isolate-scope'); + newIsolatedScopeDirective = directive; + } + safeAddClass(element, 'ng-scope'); + newScopeDirective = newScopeDirective || directive; + } + + directiveName = directive.name; + + if (directiveValue = directive.controller) { + controllerDirectives = controllerDirectives || {}; + assertNoDuplicate("'" + directiveName + "' controller", + controllerDirectives[directiveName], directive, element); + controllerDirectives[directiveName] = directive; + } + + if (directiveValue = directive.transclude) { + assertNoDuplicate('transclusion', transcludeDirective, directive, element); + transcludeDirective = directive; + terminalPriority = directive.priority; + if (directiveValue == 'element') { + template = jqLite(templateNode); + templateNode = (element = templateAttrs.$$element = jqLite( + ''))[0]; + replaceWith(rootElement, jqLite(template[0]), templateNode); + childTranscludeFn = compile(template, transcludeFn, terminalPriority); + } else { + template = jqLite(JQLiteClone(templateNode)); + element.html(''); // clear contents + childTranscludeFn = compile(template.contents(), transcludeFn); + } + } + + if (directiveValue = 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 = directiveValue.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), + /* directiveLinkingFn */ compositeLinkFn, element, templateAttrs, rootElement, + directive.replace, childTranscludeFn); + ii = directives.length; + } else if (directive.compile) { + try { + linkingFn = directive.compile(element, templateAttrs, childTranscludeFn); + if (isFunction(linkingFn)) { + addLinkingFns(null, linkingFn); + } else if (linkingFn) { + addLinkingFns(linkingFn.pre, linkingFn.post); + } + } catch (e) { + $exceptionHandler(e, startingTag(element)); + } + } + + if (directive.terminal) { + compositeLinkFn.terminal = true; + terminalPriority = Math.max(terminalPriority, directive.priority); + } + + } + + linkingFn = delayedLinkingFn || compositeLinkFn; + linkingFn.scope = newScopeDirective && newScopeDirective.scope; + linkingFn.transclude = transcludeDirective && childTranscludeFn; + + // if we have templateUrl, then we have to delay linking + return linkingFn; + + //////////////////// + + function addLinkingFns(pre, post) { + if (pre) { + pre.require = directive.require; + preLinkingFns.push(pre); + } + if (post) { + post.require = directive.require; + postLinkingFns.push(post); + } + } + + + function getControllers(require, element) { + var value, retrievalMethod = 'data', optional = false; + if (isString(require)) { + while((value = require.charAt(0)) == '^' || value == '?') { + require = require.substr(1); + if (value == '^') { + retrievalMethod = 'inheritedData'; + } + optional = optional || value == '?'; + } + value = element[retrievalMethod]('$' + require + 'Controller'); + if (!value && !optional) { + throw Error("No controller: " + require); + } + return value; + } else if (isArray(require)) { + value = []; + forEach(require, function(require) { + value.push(getControllers(require, element)); + }); + } + return value; + } + + + /* directiveLinkingFn */ + function compositeLinkFn(/* nodesetLinkingFn */ childLinkingFn, + scope, linkNode, rootElement, boundTranscludeFn) { + var attrs, element, i, ii, linkingFn, controller; + + if (templateNode === linkNode) { + attrs = templateAttrs; + } else { + attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); + } + element = attrs.$$element; + + if (newScopeDirective && isObject(newScopeDirective.scope)) { + forEach(newScopeDirective.scope, function(mode, name) { + (LOCAL_MODE[mode] || wrongMode)(name, mode, + scope.$parent || scope, scope, attrs); + }); + } + + if (controllerDirectives) { + forEach(controllerDirectives, function(directive) { + var locals = { + $scope: scope, + $element: element, + $attrs: attrs, + $transclude: boundTranscludeFn + }; + + + forEach(directive.inject || {}, function(mode, name) { + (LOCAL_MODE[mode] || wrongMode)(name, mode, + newScopeDirective ? scope.$parent || scope : scope, locals, attrs); + }); + + controller = directive.controller; + if (controller == '@') { + controller = attrs[directive.name]; + } + + element.data( + '$' + directive.name + 'Controller', + $controller(controller, locals)); + }); + } + + // PRELINKING + for(i = 0, ii = preLinkingFns.length; i < ii; i++) { + try { + linkingFn = preLinkingFns[i]; + linkingFn(scope, element, attrs, + linkingFn.require && getControllers(linkingFn.require, element)); + } catch (e) { + $exceptionHandler(e, startingTag(element)); + } + } + + // RECURSION + childLinkingFn && childLinkingFn(scope, linkNode.childNodes, undefined, boundTranscludeFn); + + // POSTLINKING + for(i = 0, ii = postLinkingFns.length; i < ii; i++) { + try { + linkingFn = postLinkingFns[i]; + linkingFn(scope, element, attrs, + linkingFn.require && getControllers(linkingFn.require, element)); + } catch (e) { + $exceptionHandler(e, startingTag(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, maxPriority) { + var match = false; + if (hasDirectives.hasOwnProperty(name)) { + for(var directive, directives = $injector.get(name + Suffix), + i=0, ii = directives.length; i directive.priority) && + directive.restrict.indexOf(location) != -1) { + tDirectives.push(directive); + match = true; + } + } catch(e) { $exceptionHandler(e); } + } + } + return match; + } + + + /** + * When the element is replaced with HTML template then the new attributes + * on the template need to be merged with the existing attributes in the DOM. + * The desired effect is to have both of the attributes present. + * + * @param {object} dst destination attributes (original DOM) + * @param {object} src source attributes (from the directive template) + */ + function mergeTemplateAttributes(dst, src) { + var srcAttr = src.$attr, + dstAttr = dst.$attr, + element = dst.$$element; + // reapply the old attributes to the new element + forEach(dst, function(value, key) { + if (key.charAt(0) != '$') { + if (src[key]) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } + dst.$set(key, value, true, srcAttr[key]); + } + }); + // copy the new attributes on the old attrs object + forEach(src, function(value, key) { + if (key == 'class') { + safeAddClass(element, value); + } else if (key == 'style') { + element.attr('style', element.attr('style') + ';' + value); + } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { + dst[key] = value; + dstAttr[key] = srcAttr[key]; + } + }); + } + + + function compileTemplateUrl(directives, /* directiveLinkingFn */ beforeWidgetLinkFn, + tElement, tAttrs, rootElement, replace, transcludeFn) { + var linkQueue = [], + afterWidgetLinkFn, + afterWidgetChildrenLinkFn, + originalWidgetNode = tElement[0], + asyncWidgetDirective = directives.shift(), + // The fact that we have to copy and patch the directive seems wrong! + syncWidgetDirective = extend({}, asyncWidgetDirective, {templateUrl:null, transclude:null}), + html = tElement.html(); + + tElement.html(''); + + $http.get(asyncWidgetDirective.templateUrl, {cache: $templateCache}). + success(function(content) { + content = trim(content).replace(CONTENT_REGEXP, html); + if (replace && !content.match(HAS_ROOT_ELEMENT)) { + throw Error('Template must have exactly one root element: ' + content); + } + + var templateNode, tempTemplateAttrs; + + if (replace) { + tempTemplateAttrs = {$attr: {}}; + templateNode = jqLite(content)[0]; + replaceWith(rootElement, tElement, templateNode); + collectDirectives(tElement[0], directives, tempTemplateAttrs); + mergeTemplateAttributes(tAttrs, tempTemplateAttrs); + } else { + templateNode = tElement[0]; + tElement.html(content); + } + + directives.unshift(syncWidgetDirective); + afterWidgetLinkFn = /* directiveLinkingFn */ applyDirectivesToNode(directives, tElement, tAttrs, transcludeFn); + afterWidgetChildrenLinkFn = /* nodesetLinkingFn */ compileNodes(tElement.contents(), transcludeFn); + + + while(linkQueue.length) { + var controller = linkQueue.pop(), + linkRootElement = linkQueue.pop(), + cLinkNode = linkQueue.pop(), + scope = linkQueue.pop(), + node = templateNode; + + if (cLinkNode !== originalWidgetNode) { + // it was cloned therefore we have to clone as well. + node = JQLiteClone(templateNode); + replaceWith(linkRootElement, jqLite(cLinkNode), node); + } + afterWidgetLinkFn(function() { + beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node, rootElement, controller); + }, scope, node, rootElement, controller); + } + linkQueue = null; + }). + error(function(response, code, headers, config) { + throw Error('Failed to load template: ' + config.url); + }); + + return /* directiveLinkingFn */ function(ignoreChildLinkingFn, scope, node, rootElement, + controller) { + if (linkQueue) { + linkQueue.push(scope); + linkQueue.push(node); + linkQueue.push(rootElement); + linkQueue.push(controller); + } else { + afterWidgetLinkFn(function() { + beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node, rootElement, controller); + }, scope, node, rootElement, controller); + } + }; + } + + + /** + * Sorting function for bound directives. + */ + function byPriority(a, b) { + return b.priority - a.priority; + } + + + function assertNoDuplicate(what, previousDirective, directive, element) { + if (previousDirective) { + throw Error('Multiple directives [' + previousDirective.name + ', ' + + directive.name + '] asking for ' + what + ' on: ' + startingTag(element)); + } + } + + + function addTextInterpolateDirective(directives, text) { + var interpolateFn = $interpolate(text, true); + if (interpolateFn) { + directives.push({ + priority: 0, + compile: valueFn(function(scope, node) { + var parent = node.parent(), + bindings = parent.data('$binding') || []; + bindings.push(interpolateFn); + safeAddClass(parent.data('$binding', bindings), 'ng-binding'); + scope.$watch(interpolateFn, function(value) { + node[0].nodeValue = value; + }); + }) + }); + } + } + + + function addAttrInterpolateDirective(node, directives, value, name) { + var interpolateFn = $interpolate(value, true); + + + // no interpolation found -> ignore + if (!interpolateFn) return; + + directives.push({ + priority: 100, + compile: valueFn(function(scope, element, attr) { + if (name === 'class') { + // we need to interpolate classes again, in the case the element was replaced + // and therefore the two class attrs got merged - we want to interpolate the result + interpolateFn = $interpolate(attr[name], true); + } + + // we define observers array only for interpolated attrs + // and ignore observers for non interpolated attrs to save some memory + attr.$$observers[name] = []; + attr[name] = undefined; + scope.$watch(interpolateFn, function(value) { + attr.$set(name, value); + }); + }) + }); + } + + + /** + * 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 auto compile', function() { - expect(element('div[compile]').text()).toBe('Hello Angular'); - input('html').enter('{{name}}!'); - expect(element('div[compile]').text()).toBe('Angular!'); - }); - - - - * - * - * @param {string|DOMElement} element Element or HTML string to compile into a template function. - * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. - * @param {number} maxPriority only apply directives lower then given priority (Only effects the - * root element(s), not their children) - * @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 updated 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` - *
- * - * - * 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]*\>$/; - - - 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 = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; - directives.push(directive); - } catch (e) { - $exceptionHandler(e); - } - }); - return directives; - }]); - } - hasDirectives[name].push(directiveFactory); - } else { - forEach(name, reverseParams(registerDirective)); - } - return this; - }; - - - this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', - function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller) { - - var LOCAL_MODE = { - attribute: function(localName, mode, parentScope, scope, attr) { - scope[localName] = attr[localName]; - }, - - evaluate: function(localName, mode, parentScope, scope, attr) { - scope[localName] = parentScope.$eval(attr[localName]); - }, - - bind: function(localName, mode, parentScope, scope, attr) { - var getter = $interpolate(attr[localName]); - scope.$watch( - function() { return getter(parentScope); }, - function(v) { scope[localName] = v; } - ); - }, - - accessor: function(localName, mode, parentScope, scope, attr) { - var getter = noop, - setter = noop, - exp = attr[localName]; - - if (exp) { - getter = $parse(exp); - setter = getter.assign || function() { - throw Error("Expression '" + exp + "' not assignable."); - }; - } - - scope[localName] = function(value) { - return arguments.length ? setter(parentScope, value) : getter(parentScope); - }; - }, - - expression: function(localName, mode, parentScope, scope, attr) { - scope[localName] = function(locals) { - $parse(attr[localName])(parentScope, locals); - }; - } - }; - - var Attributes = function(element, attr) { - this.$$element = element; - this.$$observers = {}; - this.$attr = attr || {}; - }; - - Attributes.prototype = { - $normalize: directiveNormalize, - - - /** - * Set a normalized attribute on the element in a way such that all directives - * can share the attribute. This function properly handles boolean attributes. - * @param {string} key Normalized key. (ie ngAttribute) - * @param {string|boolean} value The value to set. If `null` attribute will be deleted. - * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. - * Defaults to true. - * @param {string=} attrName Optional none normalized name. Defaults to key. - */ - $set: function(key, value, writeAttr, attrName) { - var booleanKey = isBooleanAttr(this.$$element[0], key.toLowerCase()); - - if (booleanKey) { - this.$$element.prop(key, value); - attrName = booleanKey; - } - - this[key] = value; - - // translate normalized key to actual key - if (attrName) { - this.$attr[key] = attrName; - } else { - attrName = this.$attr[key]; - if (!attrName) { - this.$attr[key] = attrName = snake_case(key, '-'); - } - } - - if (writeAttr !== false) { - if (value === null || value === undefined) { - this.$$element.removeAttr(attrName); - } else { - this.$$element.attr(attrName, value); - } - } - - // fire observers - forEach(this.$$observers[key], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); - }, - - - /** - * Observe an interpolated attribute. - * The observer will never be called, if given attribute is not interpolated. - * - * @param {string} key Normalized key. (ie ngAttribute) . - * @param {function(*)} fn Function that will be called whenever the attribute value changes. - */ - $observe: function(key, fn) { - // keep only observers for interpolated attrs - if (this.$$observers[key]) { - this.$$observers[key].push(fn); - } - } - }; - - return compile; - - //================================ - - function compile(templateElement, transcludeFn, maxPriority) { - if (!(templateElement instanceof jqLite)) { - // jquery always rewraps, where as we need to preserve the original selector so that we can modify it. - templateElement = jqLite(templateElement); - } - // We can not compile top level text elements since text nodes can be merged and we will - // not be able to attach scope data to them, so we will wrap them in - forEach(templateElement, function(node, index){ - if (node.nodeType == 3 /* text node */) { - templateElement[index] = jqLite(node).wrap('').parent()[0]; - } - }); - var linkingFn = compileNodes(templateElement, transcludeFn, templateElement, maxPriority); - 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; - safeAddClass(element.data('$scope', scope), 'ng-scope'); - if (cloneConnectFn) cloneConnectFn(element, scope); - if (linkingFn) linkingFn(scope, element, element); - return element; - }; - } - - function wrongMode(localName, mode) { - throw Error("Unsupported '" + mode + "' for '" + localName + "'."); - } - - function safeAddClass(element, className) { - try { - element.addClass(className); - } catch(e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. - } - } - - /** - * 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 {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the - * scope argument is auto-generated to the new child of the transcluded parent scope. - * @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. - * @param {number=} max directive priority - * @returns {?function} A composite linking function of all of the matched directives or null. - */ - function compileNodes(nodeList, transcludeFn, rootElement, maxPriority) { - var linkingFns = [], - directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound; - - for(var i = 0; i < nodeList.length; i++) { - attrs = new Attributes(); - - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. - directives = collectDirectives(nodeList[i], [], attrs, maxPriority); - - directiveLinkingFn = (directives.length) - ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, rootElement) - : null; - - childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal) - ? null - : compileNodes(nodeList[i].childNodes, - directiveLinkingFn ? directiveLinkingFn.transclude : transcludeFn); - - 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; - - /* nodesetLinkingFn */ function linkingFn(scope, nodeList, rootElement, boundTranscludeFn) { - var childLinkingFn, directiveLinkingFn, node, childScope, childTransclusionFn; - - for(var i=0, n=0, ii=linkingFns.length; i - addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority); - - // 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]; - if (attr.specified) { - 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 (isBooleanAttr(node, nName)) { - attrs[nName] = true; // presence means true - } - addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority); - } - } - - // use class as directive - className = node.className; - if (isString(className)) { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { - nName = directiveNormalize(match[2]); - if (addDirective(directives, nName, 'C', maxPriority)) { - 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 */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } - break; - } - - directives.sort(byPriority); - return directives; - } - - - /** - * Once the directives have been collected their compile functions is executed. This method - * is responsible for inlining directive 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 {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the - * scope argument is auto-generated to the new child of the transcluded parent scope. - * @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, transcludeFn, rootElement) { - var terminalPriority = -Number.MAX_VALUE, - preLinkingFns = [], - postLinkingFns = [], - newScopeDirective = null, - newIsolatedScopeDirective = null, - templateDirective = null, - delayedLinkingFn = null, - element = templateAttrs.$$element = jqLite(templateNode), - directive, - directiveName, - template, - transcludeDirective, - childTranscludeFn = transcludeFn, - controllerDirectives, - linkingFn, - directiveValue; - - // executes all directives on the current element - for(var i = 0, ii = directives.length; i < ii; i++) { - directive = directives[i]; - template = undefined; - - if (terminalPriority > directive.priority) { - break; // prevent further processing of directives - } - - if (directiveValue = directive.scope) { - assertNoDuplicate('isolated scope', newIsolatedScopeDirective, directive, element); - if (isObject(directiveValue)) { - safeAddClass(element, 'ng-isolate-scope'); - newIsolatedScopeDirective = directive; - } - safeAddClass(element, 'ng-scope'); - newScopeDirective = newScopeDirective || directive; - } - - directiveName = directive.name; - - if (directiveValue = directive.controller) { - controllerDirectives = controllerDirectives || {}; - assertNoDuplicate("'" + directiveName + "' controller", - controllerDirectives[directiveName], directive, element); - controllerDirectives[directiveName] = directive; - } - - if (directiveValue = directive.transclude) { - assertNoDuplicate('transclusion', transcludeDirective, directive, element); - transcludeDirective = directive; - terminalPriority = directive.priority; - if (directiveValue == 'element') { - template = jqLite(templateNode); - templateNode = (element = templateAttrs.$$element = jqLite( - ''))[0]; - replaceWith(rootElement, jqLite(template[0]), templateNode); - childTranscludeFn = compile(template, transcludeFn, terminalPriority); - } else { - template = jqLite(JQLiteClone(templateNode)); - element.html(''); // clear contents - childTranscludeFn = compile(template.contents(), transcludeFn); - } - } - - if (directiveValue = 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 = directiveValue.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), - /* directiveLinkingFn */ compositeLinkFn, element, templateAttrs, rootElement, - directive.replace, childTranscludeFn); - ii = directives.length; - } else if (directive.compile) { - try { - linkingFn = directive.compile(element, templateAttrs, childTranscludeFn); - if (isFunction(linkingFn)) { - addLinkingFns(null, linkingFn); - } else if (linkingFn) { - addLinkingFns(linkingFn.pre, linkingFn.post); - } - } catch (e) { - $exceptionHandler(e, startingTag(element)); - } - } - - if (directive.terminal) { - compositeLinkFn.terminal = true; - terminalPriority = Math.max(terminalPriority, directive.priority); - } - - } - - linkingFn = delayedLinkingFn || compositeLinkFn; - linkingFn.scope = newScopeDirective && newScopeDirective.scope; - linkingFn.transclude = transcludeDirective && childTranscludeFn; - - // if we have templateUrl, then we have to delay linking - return linkingFn; - - //////////////////// - - function addLinkingFns(pre, post) { - if (pre) { - pre.require = directive.require; - preLinkingFns.push(pre); - } - if (post) { - post.require = directive.require; - postLinkingFns.push(post); - } - } - - - function getControllers(require, element) { - var value, retrievalMethod = 'data', optional = false; - if (isString(require)) { - while((value = require.charAt(0)) == '^' || value == '?') { - require = require.substr(1); - if (value == '^') { - retrievalMethod = 'inheritedData'; - } - optional = optional || value == '?'; - } - value = element[retrievalMethod]('$' + require + 'Controller'); - if (!value && !optional) { - throw Error("No controller: " + require); - } - return value; - } else if (isArray(require)) { - value = []; - forEach(require, function(require) { - value.push(getControllers(require, element)); - }); - } - return value; - } - - - /* directiveLinkingFn */ - function compositeLinkFn(/* nodesetLinkingFn */ childLinkingFn, - scope, linkNode, rootElement, boundTranscludeFn) { - var attrs, element, i, ii, linkingFn, controller; - - if (templateNode === linkNode) { - attrs = templateAttrs; - } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); - } - element = attrs.$$element; - - if (newScopeDirective && isObject(newScopeDirective.scope)) { - forEach(newScopeDirective.scope, function(mode, name) { - (LOCAL_MODE[mode] || wrongMode)(name, mode, - scope.$parent || scope, scope, attrs); - }); - } - - if (controllerDirectives) { - forEach(controllerDirectives, function(directive) { - var locals = { - $scope: scope, - $element: element, - $attrs: attrs, - $transclude: boundTranscludeFn - }; - - - forEach(directive.inject || {}, function(mode, name) { - (LOCAL_MODE[mode] || wrongMode)(name, mode, - newScopeDirective ? scope.$parent || scope : scope, locals, attrs); - }); - - controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - element.data( - '$' + directive.name + 'Controller', - $controller(controller, locals)); - }); - } - - // PRELINKING - for(i = 0, ii = preLinkingFns.length; i < ii; i++) { - try { - linkingFn = preLinkingFns[i]; - linkingFn(scope, element, attrs, - linkingFn.require && getControllers(linkingFn.require, element)); - } catch (e) { - $exceptionHandler(e, startingTag(element)); - } - } - - // RECURSION - childLinkingFn && childLinkingFn(scope, linkNode.childNodes, undefined, boundTranscludeFn); - - // POSTLINKING - for(i = 0, ii = postLinkingFns.length; i < ii; i++) { - try { - linkingFn = postLinkingFns[i]; - linkingFn(scope, element, attrs, - linkingFn.require && getControllers(linkingFn.require, element)); - } catch (e) { - $exceptionHandler(e, startingTag(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, maxPriority) { - var match = false; - if (hasDirectives.hasOwnProperty(name)) { - for(var directive, directives = $injector.get(name + Suffix), - i=0, ii = directives.length; i directive.priority) && - directive.restrict.indexOf(location) != -1) { - tDirectives.push(directive); - match = true; - } - } catch(e) { $exceptionHandler(e); } - } - } - return match; - } - - - /** - * When the element is replaced with HTML template then the new attributes - * on the template need to be merged with the existing attributes in the DOM. - * The desired effect is to have both of the attributes present. - * - * @param {object} dst destination attributes (original DOM) - * @param {object} src source attributes (from the directive template) - */ - function mergeTemplateAttributes(dst, src) { - var srcAttr = src.$attr, - dstAttr = dst.$attr, - element = dst.$$element; - // reapply the old attributes to the new element - forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { - if (src[key]) { - value += (key === 'style' ? ';' : ' ') + src[key]; - } - dst.$set(key, value, true, srcAttr[key]); - } - }); - // copy the new attributes on the old attrs object - forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass(element, value); - } else if (key == 'style') { - element.attr('style', element.attr('style') + ';' + value); - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { - dst[key] = value; - dstAttr[key] = srcAttr[key]; - } - }); - } - - - function compileTemplateUrl(directives, /* directiveLinkingFn */ beforeWidgetLinkFn, - tElement, tAttrs, rootElement, replace, transcludeFn) { - var linkQueue = [], - afterWidgetLinkFn, - afterWidgetChildrenLinkFn, - originalWidgetNode = tElement[0], - asyncWidgetDirective = directives.shift(), - // The fact that we have to copy and patch the directive seems wrong! - syncWidgetDirective = extend({}, asyncWidgetDirective, {templateUrl:null, transclude:null}), - html = tElement.html(); - - tElement.html(''); - - $http.get(asyncWidgetDirective.templateUrl, {cache: $templateCache}). - success(function(content) { - content = trim(content).replace(CONTENT_REGEXP, html); - if (replace && !content.match(HAS_ROOT_ELEMENT)) { - throw Error('Template must have exactly one root element: ' + content); - } - - var templateNode, tempTemplateAttrs; - - if (replace) { - tempTemplateAttrs = {$attr: {}}; - templateNode = jqLite(content)[0]; - replaceWith(rootElement, tElement, templateNode); - collectDirectives(tElement[0], directives, tempTemplateAttrs); - mergeTemplateAttributes(tAttrs, tempTemplateAttrs); - } else { - templateNode = tElement[0]; - tElement.html(content); - } - - directives.unshift(syncWidgetDirective); - afterWidgetLinkFn = /* directiveLinkingFn */ applyDirectivesToNode(directives, tElement, tAttrs, transcludeFn); - afterWidgetChildrenLinkFn = /* nodesetLinkingFn */ compileNodes(tElement.contents(), transcludeFn); - - - while(linkQueue.length) { - var controller = linkQueue.pop(), - linkRootElement = linkQueue.pop(), - cLinkNode = linkQueue.pop(), - scope = linkQueue.pop(), - node = templateNode; - - if (cLinkNode !== originalWidgetNode) { - // it was cloned therefore we have to clone as well. - node = JQLiteClone(templateNode); - replaceWith(linkRootElement, jqLite(cLinkNode), node); - } - afterWidgetLinkFn(function() { - beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node, rootElement, controller); - }, scope, node, rootElement, controller); - } - linkQueue = null; - }). - error(function(response, code, headers, config) { - throw Error('Failed to load template: ' + config.url); - }); - - return /* directiveLinkingFn */ function(ignoreChildLinkingFn, scope, node, rootElement, - controller) { - if (linkQueue) { - linkQueue.push(scope); - linkQueue.push(node); - linkQueue.push(rootElement); - linkQueue.push(controller); - } else { - afterWidgetLinkFn(function() { - beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node, rootElement, controller); - }, scope, node, rootElement, controller); - } - }; - } - - - /** - * Sorting function for bound directives. - */ - function byPriority(a, b) { - return b.priority - a.priority; - } - - - function assertNoDuplicate(what, previousDirective, directive, element) { - if (previousDirective) { - throw Error('Multiple directives [' + previousDirective.name + ', ' + - directive.name + '] asking for ' + what + ' on: ' + startingTag(element)); - } - } - - - function addTextInterpolateDirective(directives, text) { - var interpolateFn = $interpolate(text, true); - if (interpolateFn) { - directives.push({ - priority: 0, - compile: valueFn(function(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - safeAddClass(parent.data('$binding', bindings), 'ng-binding'); - scope.$watch(interpolateFn, function(value) { - node[0].nodeValue = value; - }); - }) - }); - } - } - - - function addAttrInterpolateDirective(node, directives, value, name) { - var interpolateFn = $interpolate(value, true); - - - // no interpolation found -> ignore - if (!interpolateFn) return; - - directives.push({ - priority: 100, - compile: valueFn(function(scope, element, attr) { - if (name === 'class') { - // we need to interpolate classes again, in the case the element was replaced - // and therefore the two class attrs got merged - we want to interpolate the result - interpolateFn = $interpolate(attr[name], true); - } - - // we define observers array only for interpolated attrs - // and ignore observers for non interpolated attrs to save some memory - attr.$$observers[name] = []; - attr[name] = undefined; - scope.$watch(interpolateFn, function(value) { - attr.$set(name, value); - }); - }) - }); - } - - - /** - * 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')($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 { + restrict: 'ECA', + link: log.fn('1') + }; + }); + $compileProvider.directive('div', function(log) { + return { + restrict: 'ECA', + link: log.fn('2') + }; + }); + }); + inject(function($compile, $rootScope, log) { + element = $compile('
')($rootScope); + expect(log).toEqual('1; 2'); + }); + }); + }); + + + describe('compile phase', function() { + + it('should wrap root text nodes in spans', inject(function($compile, $rootScope) { + element = jqLite('
A<a>B</a>C
'); + var text = element.contents(); + expect(text[0].nodeName).toEqual('#text'); + text = $compile(text)($rootScope); + expect(lowercase(text[0].nodeName)).toEqual('span'); + expect(element.find('span').text()).toEqual('ABC'); + })); + + 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 directives in classes', inject(function($compile, $rootScope, log) { + element = $compile('
')($rootScope); + expect(element.html()).toEqual('Hello angular'); + expect(log).toEqual('123'); + })); + + + it('should ignore not set CSS classes on SVG elements', inject(function($compile, $rootScope, log) { + if (!window.SVGElement) return; + // According to spec SVG element className property is readonly, but only FF + // implements it this way which causes compile exceptions. + element = $compile('{{1}}')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('1'); + })); + + + 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 { + restrict: 'CA', + 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'); + } + } + }; + }); + }); + 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, elementName; + + parts = lowercase(text). + replace('<', ''). + replace('>', ''). + split(' '); + elementName = parts.shift(); + parts.sort(); + parts.unshift(elementName); + forEach(parts, function(value, key){ + if (value.substring(0,3) == 'ng-') { + } else { + value = value.replace('=""', ''); + var match = value.match(/=(.*)/); + if (match && match[1].charAt(0) != '"') { + value = value.replace(/=(.*)/, '="$1"'); + } + list.push(value); + } + }); + return '<' + list.join(' ') + '>'; + } + }); + }); + + + it('should allow changing the template structure after the current node', function() { + module(function($compileProvider){ + $compileProvider.directive('after', valueFn({ + compile: function(element) { + element.after('B'); + } + })); + }); + inject(function($compile, $rootScope, log){ + element = jqLite("
A
"); + $compile(element)($rootScope); + expect(element.text()).toBe('AB'); + expect(log).toEqual('LOG'); + }); + }); + + + it('should allow changing the template structure after the current node inside ngRepeat', function() { + module(function($compileProvider){ + $compileProvider.directive('after', valueFn({ + compile: function(element) { + element.after('B'); + } + })); + }); + inject(function($compile, $rootScope, log){ + element = jqLite('
A
'); + $compile(element)($rootScope); + $rootScope.$digest(); + expect(element.text()).toBe('ABAB'); + expect(log).toEqual('LOG; LOG'); + }); + }); + }); + + 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({ + restrict: 'CAM', + replace: true, + template: '
Hello: <>
', + compile: function(element, attr) { + attr.$set('compiled', 'COMPILED'); + expect(element).toBe(attr.$$element); + } + })); + $compileProvider.directive('append', valueFn({ + restrict: 'CAM', + 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 including style attr', 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; '); + })); + + + it('should merge interpolated css class', inject(function($compile, $rootScope) { + element = $compile('
')($rootScope); + + $rootScope.$apply(function() { + $rootScope.cls = 'two'; + }); + + expect(element).toHaveClass('one'); + expect(element).toHaveClass('two'); // interpolated + expect(element).toHaveClass('three'); + expect(element).toHaveClass('log'); // merged from replace directive template + })); + + + it('should merge interpolated css class with ngRepeat', + inject(function($compile, $rootScope) { + element = $compile( + '
' + + '
' + + '
')($rootScope); + + $rootScope.$apply(function() { + $rootScope.cls = 'two'; + }); + + var child = element.find('div').eq(0); + expect(child).toHaveClass('one'); + expect(child).toHaveClass('two'); // interpolated + expect(child).toHaveClass('three'); + expect(child).toHaveClass('log'); // merged from replace directive template + })); + }); + + + describe('async templates', function() { + + beforeEach(module( + function($compileProvider) { + $compileProvider.directive('hello', valueFn({ restrict: 'CAM', templateUrl: 'hello.html' })); + $compileProvider.directive('cau', valueFn({ restrict: 'CAM', templateUrl:'cau.html' })); + + $compileProvider.directive('cError', valueFn({ + restrict: 'CAM', + templateUrl:'error.html', + compile: function() { + throw Error('cError'); + } + })); + $compileProvider.directive('lError', valueFn({ + restrict: 'CAM', + templateUrl: 'error.html', + compile: function() { + throw Error('lError'); + } + })); + + + $compileProvider.directive('iHello', valueFn({ + restrict: 'CAM', + replace: true, + templateUrl: 'hello.html' + })); + $compileProvider.directive('iCau', valueFn({ + restrict: 'CAM', + replace: true, + templateUrl:'cau.html' + })); + + $compileProvider.directive('iCError', valueFn({ + restrict: 'CAM', + replace: true, + templateUrl:'error.html', + compile: function() { + throw Error('cError'); + } + })); + $compileProvider.directive('iLError', valueFn({ + restrict: 'CAM', + 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( + '
' + + '' + + '' + + '' + + '' + + '
'); + 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({ + restrict: 'C', + template: '' + })); + $compileProvider.directive('async', valueFn({ + restrict: 'C', + 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() { + var iscope; + + beforeEach(module(function($compileProvider) { + forEach(['', 'a', 'b'], function(name) { + $compileProvider.directive('scope' + uppercase(name), function(log) { + return { + scope: true, + restrict: 'CA', + compile: function() { + return function (scope, element) { + log(scope.$id); + expect(element.data('$scope')).toBe(scope); + }; + } + }; + }); + $compileProvider.directive('iscope' + uppercase(name), function(log) { + return { + scope: {}, + restrict: 'CA', + compile: function() { + return function (scope, element) { + iscope = scope; + log(scope.$id); + expect(element.data('$scope')).toBe(scope); + }; + } + }; + }); + $compileProvider.directive('tiscope' + uppercase(name), function(log) { + return { + scope: {}, + restrict: 'CA', + templateUrl: 'tiscope.html', + compile: function() { + return function (scope, element) { + iscope = scope; + log(scope.$id); + expect(element.data('$scope')).toBe(scope); + }; + } + }; + }); + }); + $compileProvider.directive('log', function(log) { + return { + restrict: 'CA', + link: 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'); + expect(element.find('span').hasClass('ng-scope')).toBe(true); + })); + + + it('should allow creation of new isolated scopes for directives', inject( + function($rootScope, $compile, log) { + element = $compile('
')($rootScope); + expect(log).toEqual('LOG; log-002-001; 002'); + $rootScope.name = 'abc'; + expect(iscope.$parent).toBe($rootScope); + expect(iscope.name).toBeUndefined(); + })); + + + it('should allow creation of new isolated scopes for directives with templates', inject( + function($rootScope, $compile, log, $httpBackend) { + $httpBackend.expect('GET', 'tiscope.html').respond(''); + element = $compile('
')($rootScope); + $httpBackend.flush(); + expect(log).toEqual('LOG; log-002-001; 002'); + $rootScope.name = 'abc'; + expect(iscope.$parent).toBe($rootScope); + expect(iscope.name).toBeUndefined(); + })); + + + it('should correctly create the scope hierachy', 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 allow more one new scope directives per element, but directives should share' + + 'the scope', inject( + function($rootScope, $compile, log) { + element = $compile('
')($rootScope); + expect(log).toEqual('002; 002'); + }) + ); + + it('should not allow more then one isolate scope creation per element', inject( + function($rootScope, $compile) { + expect(function(){ + $compile('
'); + }).toThrow('Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + + '<' + (msie < 9 ? 'DIV' : 'div') + + ' class="iscope-a; scope-b ng-isolate-scope ng-scope">'); + }) + ); + + + it('should not allow more then one isolate scope creation per element', inject( + function($rootScope, $compile) { + expect(function(){ + $compile('
'); + }).toThrow('Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' + + '<' + (msie < 9 ? 'DIV' : 'div') + + ' class="iscope-a; iscope-b ng-isolate-scope ng-scope">'); + }) + ); + + + it('should create new scope even at the root of the template', inject( + function($rootScope, $compile, log) { + element = $compile('
')($rootScope); + expect(log).toEqual('002'); + }) + ); + + + it('should create isolate scope even at the root of the template', inject( + function($rootScope, $compile, log) { + element = $compile('
')($rootScope); + expect(log).toEqual('002'); + }) + ); + }); + }); + }); + + + describe('interpolation', function() { + var observeSpy, attrValueDuringLinking; + + beforeEach(module(function($compileProvider) { + $compileProvider.directive('observer', function() { + return function(scope, elm, attr) { + observeSpy = jasmine.createSpy('$observe attr'); + + attr.$observe('someAttr', observeSpy); + attrValueDuringLinking = attr.someAttr; + }; + }); + })); + + + 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 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 observe interpolated attrs', inject(function($rootScope, $compile) { + $compile('
')($rootScope); + + // should be async + expect(observeSpy).not.toHaveBeenCalled(); + + $rootScope.$apply(function() { + $rootScope.value = 'bound-value'; + }); + expect(observeSpy).toHaveBeenCalledOnceWith('bound-value'); + })); + + + it('should set interpolated attrs to undefined', inject(function($rootScope, $compile) { + attrValueDuringLinking = null; + $compile('
')($rootScope); + expect(attrValueDuringLinking).toBeUndefined(); + })); + + + it('should not call observer of non-interpolated attr', inject(function($rootScope, $compile) { + $compile('
')($rootScope); + expect(attrValueDuringLinking).toBe('nonBound'); + + $rootScope.$digest(); + expect(observeSpy).not.toHaveBeenCalled(); + })); + + + it('should delegate exceptions to $exceptionHandler', function() { + observeSpy = jasmine.createSpy('$observe attr').andThrow('ERROR'); + + module(function($compileProvider, $exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + $compileProvider.directive('error', function() { + return function(scope, elm, attr) { + attr.$observe('someAttr', observeSpy); + attr.$observe('someAttr', observeSpy); + }; + }); + }); + + inject(function($compile, $rootScope, $exceptionHandler) { + $compile('
')($rootScope); + $rootScope.$digest(); + + expect(observeSpy).toHaveBeenCalled(); + expect(observeSpy.callCount).toBe(2); + expect($exceptionHandler.errors).toEqual(['ERROR', 'ERROR']); + }); + }); + + + it('should translate {{}} in terminal nodes', inject(function($rootScope, $compile) { + element = $compile('')($rootScope) + $rootScope.$digest(); + expect(sortedHtml(element).replace(' selected="true"', '')). + toEqual(''); + $rootScope.name = 'Misko'; + $rootScope.$digest(); + expect(sortedHtml(element).replace(' selected="true"', '')). + toEqual(''); + })); + }); + + + describe('link phase', function() { + + beforeEach(module(function($compileProvider) { + + forEach(['a', 'b', 'c'], function(name) { + $compileProvider.directive(name, function(log) { + return { + restrict: 'ECA', + 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 only on control elements', function() { + var value; + module(function($compileProvider) { + $compileProvider.directive({ + input: valueFn({ + restrict: 'ECA', + link:function(scope, element, attr) { + value = attr.required; + } + }) + }); + }); + inject(function($rootScope, $compile) { + element = $compile('')($rootScope); + expect(value).toEqual(true); + }); + }); + + it('should read boolean attributes as text on non-controll elements', function() { + var value; + module(function($compileProvider) { + $compileProvider.directive({ + div: valueFn({ + restrict: 'ECA', + link:function(scope, element, attr) { + value = attr.required; + } + }) + }); + }); + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + expect(value).toEqual('some text'); + }); + }); + + 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 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); + }); + }); + + + it('should properly $observe inside ng-repeat', function() { + var spies = []; + + module(function($compileProvider) { + $compileProvider.directive('observer', function() { + return function(scope, elm, attr) { + spies.push(jasmine.createSpy('observer ' + spies.length)); + attr.$observe('some', spies[spies.length - 1]); + }; + }); + }); + + inject(function($compile, $rootScope) { + element = $compile('
'+ + ''+ + '
')($rootScope); + + $rootScope.$apply(function() { + $rootScope.items = [{id: 1}, {id: 2}]; + }); + + expect(spies[0]).toHaveBeenCalledOnceWith('id_1'); + expect(spies[1]).toHaveBeenCalledOnceWith('id_2'); + spies[0].reset(); + spies[1].reset(); + + $rootScope.$apply(function() { + $rootScope.items[0].id = 5; + }); + + expect(spies[0]).toHaveBeenCalledOnceWith('id_5'); + }); + }); + + + describe('$set', function() { + var attr; + beforeEach(function(){ + module(function($compileProvider) { + $compileProvider.directive('input', valueFn({ + restrict: 'ECA', + link: 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', true, '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 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); + }); + + + it('should not set DOM element attr if writeAttr false', function() { + attr.$set('test', 'value', false); + + expect(element.attr('test')).toBeUndefined(); + expect(attr.test).toBe('value'); + }); + }); + }); + + + describe('locals', function() { + it('should marshal to locals', function() { + module(function($compileProvider) { + $compileProvider.directive('widget', function(log) { + return { + scope: { + attr: 'attribute', + prop: 'evaluate', + bind: 'bind', + assign: 'accessor', + read: 'accessor', + exp: 'expression', + nonExist: 'accessor', + nonExistExpr: 'expression' + }, + link: function(scope, element, attrs) { + scope.nonExist(); // noop + scope.nonExist(123); // noop + scope.nonExistExpr(); // noop + scope.nonExistExpr(123); // noop + log(scope.attr); + log(scope.prop); + log(scope.assign()); + log(scope.read()); + log(scope.assign('ng')); + scope.exp({myState:'OK'}); + expect(function() { scope.read(undefined); }). + toThrow("Expression ''D'' not assignable."); + scope.$watch('bind', log); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + $rootScope.myProp = 'B'; + $rootScope.bi = {nd: 'C'}; + $rootScope.name = 'C'; + element = $compile( + '
{{bind}}
') + ($rootScope); + expect(log).toEqual('A; B; C; D; ng'); + expect($rootScope.name).toEqual('ng'); + expect($rootScope.state).toEqual('OK'); + log.reset(); + $rootScope.$apply(); + expect(element.text()).toEqual('C'); + expect(log).toEqual('C'); + $rootScope.bi.nd = 'c'; + $rootScope.$apply(); + expect(log).toEqual('C; c'); + }); + }); + }); + + + describe('controller', function() { + it('should inject locals to controller', function() { + module(function($compileProvider) { + $compileProvider.directive('widget', function(log) { + return { + controller: function(attr, prop, assign, read, exp){ + log(attr); + log(prop); + log(assign()); + log(read()); + log(assign('ng')); + exp(); + expect(function() { read(undefined); }). + toThrow("Expression ''D'' not assignable."); + this.result = 'OK'; + }, + inject: { + attr: 'attribute', + prop: 'evaluate', + assign: 'accessor', + read: 'accessor', + exp: 'expression' + }, + link: function(scope, element, attrs, controller) { + log(controller.result); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + $rootScope.myProp = 'B'; + $rootScope.bi = {nd: 'C'}; + $rootScope.name = 'C'; + element = $compile( + '
{{bind}}
') + ($rootScope); + expect(log).toEqual('A; B; C; D; ng; OK'); + expect($rootScope.name).toEqual('ng'); + }); + }); + + + it('should get required controller', function() { + module(function($compileProvider) { + $compileProvider.directive('main', function(log) { + return { + priority: 2, + controller: function() { + this.name = 'main'; + }, + link: function(scope, element, attrs, controller) { + log(controller.name); + } + }; + }); + $compileProvider.directive('dep', function(log) { + return { + priority: 1, + require: 'main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller.name); + } + }; + }); + $compileProvider.directive('other', function(log) { + return { + link: function(scope, element, attrs, controller) { + log(!!controller); // should be false + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('main; dep:main; false'); + }); + }); + + + it('should require controller on parent element',function() { + module(function($compileProvider) { + $compileProvider.directive('main', function(log) { + return { + controller: function() { + this.name = 'main'; + } + }; + }); + $compileProvider.directive('dep', function(log) { + return { + require: '^main', + link: function(scope, element, attrs, controller) { + log('dep:' + controller.name); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:main'); + }); + }); + + + it('should have optional controller on current element', function() { + module(function($compileProvider) { + $compileProvider.directive('dep', function(log) { + return { + require: '?main', + link: function(scope, element, attrs, controller) { + log('dep:' + !!controller); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:false'); + }); + }); + + + it('should support multiple controllers', function() { + module(function($compileProvider) { + $compileProvider.directive('c1', valueFn({ + controller: function() { this.name = 'c1'; } + })); + $compileProvider.directive('c2', valueFn({ + controller: function() { this.name = 'c2'; } + })); + $compileProvider.directive('dep', function(log) { + return { + require: ['^c1', '^c2'], + link: function(scope, element, attrs, controller) { + log('dep:' + controller[0].name + '-' + controller[1].name); + } + }; + }); + }); + inject(function(log, $compile, $rootScope) { + element = $compile('
')($rootScope); + expect(log).toEqual('dep:c1-c2'); + }); + + }); + }); + + + describe('transclude', function() { + it('should compile get templateFn', function() { + module(function($compileProvider) { + $compileProvider.directive('trans', function(log) { + return { + transclude: 'element', + priority: 2, + controller: function($transclude) { this.$transclude = $transclude; }, + compile: function(element, attrs, template) { + log('compile: ' + angular.mock.dump(element)); + return function(scope, element, attrs, ctrl) { + log('link'); + var cursor = element; + template(scope.$new(), function(clone) {cursor.after(cursor = clone)}); + ctrl.$transclude(function(clone) {cursor.after(clone)}); + }; + } + } + }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
{{$parent.$id}}-{{$id}};
') + ($rootScope); + $rootScope.$apply(); + expect(log).toEqual('compile: ; HIGH; link; LOG; LOG'); + expect(element.text()).toEqual('001-002;001-003;'); + }); + }); + + + it('should support transclude directive', function() { + module(function($compileProvider) { + $compileProvider.directive('trans', function() { + return { + transclude: 'content', + replace: true, + scope: true, + template: '
  • W:{{$parent.$id}}-{{$id}};
' + } + }); + }); + inject(function(log, $rootScope, $compile) { + element = $compile('
T:{{$parent.$id}}-{{$id}};
') + ($rootScope); + $rootScope.$apply(); + expect(element.text()).toEqual('W:001-002;T:001-003;'); + expect(jqLite(element.find('span')[0]).text()).toEqual('T:001-003'); + expect(jqLite(element.find('span')[1]).text()).toEqual(';'); + }); + }); + + + it('should transclude transcluded content', function() { + module(function($compileProvider) { + $compileProvider.directive('book', valueFn({ + transclude: 'content', + template: '
book-
(
)
' + })); + $compileProvider.directive('chapter', valueFn({ + transclude: 'content', + templateUrl: 'chapter.html' + })); + $compileProvider.directive('section', valueFn({ + transclude: 'content', + template: '
section-!
!
' + })); + return function($httpBackend) { + $httpBackend. + expect('GET', 'chapter.html'). + respond('
chapter-
[
]
'); + } + }); + inject(function(log, $rootScope, $compile, $httpBackend) { + element = $compile('
paragraph
')($rootScope); + $rootScope.$apply(); + + expect(element.text()).toEqual('book-'); + + $httpBackend.flush(); + $rootScope.$apply(); + expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); + }); + }); + + + it('should only allow one transclude per element', function() { + module(function($compileProvider) { + $compileProvider.directive('first', valueFn({ + scope: {}, + restrict: 'CA', + transclude: 'content' + })); + $compileProvider.directive('second', valueFn({ + restrict: 'CA', + transclude: 'content' + })); + }); + inject(function($compile) { + expect(function() { + $compile('
'); + }).toThrow('Multiple directives [first, second] asking for transclusion on: <' + + (msie <= 8 ? 'DIV' : 'div') + ' class="first second ng-isolate-scope ng-scope">'); + }); + }); + + + it('should remove transclusion scope, when the DOM is destroyed', function() { + module(function($compileProvider) { + $compileProvider.directive('box', valueFn({ + transclude: 'content', + scope: { name: 'evaluate', show: 'accessor' }, + template: '

Hello: {{name}}!

', + link: function(scope, element) { + scope.$watch( + function() { return scope.show(); }, + function(show) { + if (!show) { + element.find('div').find('div').remove(); + } + } + ); + } + })); + }); + inject(function($compile, $rootScope) { + $rootScope.username = 'Misko'; + $rootScope.select = true; + element = $compile( + '
user: {{username}}
') + ($rootScope); + $rootScope.$apply(); + expect(element.text()).toEqual('Hello: Misko!user: Misko'); + + var widgetScope = $rootScope.$$childHead; + var transcludeScope = widgetScope.$$nextSibling; + expect(widgetScope.name).toEqual('Misko'); + expect(widgetScope.$parent).toEqual($rootScope); + expect(transcludeScope.$parent).toEqual($rootScope); + + $rootScope.select = false; + $rootScope.$apply(); + expect(element.text()).toEqual('Hello: Misko!'); + expect(widgetScope.$$nextSibling).toEqual(null); + }); + }); + + + it('should support transcluded element on root content', function() { + var comment; + module(function($compileProvider) { + $compileProvider.directive('transclude', valueFn({ + transclude: 'element', + compile: function(element, attr, linker) { + return function(scope, element, attr) { + comment = element; + }; + } + })); + }); + inject(function($compile, $rootScope) { + var element = jqLite('
before
after
').contents(); + expect(element.length).toEqual(3); + expect(nodeName_(element[1])).toBe('DIV'); + $compile(element)($rootScope); + expect(nodeName_(element[1])).toBe('#comment'); + expect(nodeName_(comment)).toBe('#comment'); + }); + }); + }); +}); diff --git a/test/ng/compilerSpec.js b/test/ng/compilerSpec.js deleted file mode 100644 index 1aef24fe..00000000 --- a/test/ng/compilerSpec.js +++ /dev/null @@ -1,1869 +0,0 @@ -'use strict'; - -describe('$compile', function() { - var element; - - beforeEach(module(provideLog, function($provide, $compileProvider){ - element = null; - - $compileProvider.directive('log', function(log) { - return { - restrict: 'CAM', - priority:0, - compile: valueFn(function(scope, element, attrs) { - log(attrs.log || 'LOG'); - }) - }; - }); - - $compileProvider.directive('highLog', function(log) { - return { restrict: 'CAM', priority:3, compile: valueFn(function(scope, element, attrs) { - log(attrs.highLog || 'HIGH'); - })}; - }); - - $compileProvider.directive('mediumLog', function(log) { - return { restrict: 'CAM', priority:2, compile: valueFn(function(scope, element, attrs) { - log(attrs.mediumLog || 'MEDIUM'); - })}; - }); - - $compileProvider.directive('greet', function() { - return { restrict: 'CAM', priority:10, compile: valueFn(function(scope, element, attrs) { - element.text("Hello " + attrs.greet); - })}; - }); - - $compileProvider.directive('set', function() { - return function(scope, element, attrs) { - element.text(attrs.set); - }; - }); - - $compileProvider.directive('mediumStop', valueFn({ - priority: 2, - terminal: true - })); - - $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 - })); - })); - - - afterEach(function(){ - dealoc(element); - }); - - - describe('configuration', function() { - it('should register a directive', function() { - module(function($compileProvider) { - $compileProvider.directive('div', function(log) { - return { - restrict: 'ECA', - link: 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 { - restrict: 'ECA', - link: log.fn('1') - }; - }); - $compileProvider.directive('div', function(log) { - return { - restrict: 'ECA', - link: log.fn('2') - }; - }); - }); - inject(function($compile, $rootScope, log) { - element = $compile('
')($rootScope); - expect(log).toEqual('1; 2'); - }); - }); - }); - - - describe('compile phase', function() { - - it('should wrap root text nodes in spans', inject(function($compile, $rootScope) { - element = jqLite('
A<a>B</a>C
'); - var text = element.contents(); - expect(text[0].nodeName).toEqual('#text'); - text = $compile(text)($rootScope); - expect(lowercase(text[0].nodeName)).toEqual('span'); - expect(element.find('span').text()).toEqual('ABC'); - })); - - 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 directives in classes', inject(function($compile, $rootScope, log) { - element = $compile('
')($rootScope); - expect(element.html()).toEqual('Hello angular'); - expect(log).toEqual('123'); - })); - - - it('should ignore not set CSS classes on SVG elements', inject(function($compile, $rootScope, log) { - if (!window.SVGElement) return; - // According to spec SVG element className property is readonly, but only FF - // implements it this way which causes compile exceptions. - element = $compile('{{1}}')($rootScope); - $rootScope.$digest(); - expect(element.text()).toEqual('1'); - })); - - - 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 { - restrict: 'CA', - 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'); - } - } - }; - }); - }); - 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, elementName; - - parts = lowercase(text). - replace('<', ''). - replace('>', ''). - split(' '); - elementName = parts.shift(); - parts.sort(); - parts.unshift(elementName); - forEach(parts, function(value, key){ - if (value.substring(0,3) == 'ng-') { - } else { - value = value.replace('=""', ''); - var match = value.match(/=(.*)/); - if (match && match[1].charAt(0) != '"') { - value = value.replace(/=(.*)/, '="$1"'); - } - list.push(value); - } - }); - return '<' + list.join(' ') + '>'; - } - }); - }); - - - it('should allow changing the template structure after the current node', function() { - module(function($compileProvider){ - $compileProvider.directive('after', valueFn({ - compile: function(element) { - element.after('B'); - } - })); - }); - inject(function($compile, $rootScope, log){ - element = jqLite("
A
"); - $compile(element)($rootScope); - expect(element.text()).toBe('AB'); - expect(log).toEqual('LOG'); - }); - }); - - - it('should allow changing the template structure after the current node inside ngRepeat', function() { - module(function($compileProvider){ - $compileProvider.directive('after', valueFn({ - compile: function(element) { - element.after('B'); - } - })); - }); - inject(function($compile, $rootScope, log){ - element = jqLite('
A
'); - $compile(element)($rootScope); - $rootScope.$digest(); - expect(element.text()).toBe('ABAB'); - expect(log).toEqual('LOG; LOG'); - }); - }); - }); - - 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({ - restrict: 'CAM', - replace: true, - template: '
Hello: <>
', - compile: function(element, attr) { - attr.$set('compiled', 'COMPILED'); - expect(element).toBe(attr.$$element); - } - })); - $compileProvider.directive('append', valueFn({ - restrict: 'CAM', - 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 including style attr', 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; '); - })); - - - it('should merge interpolated css class', inject(function($compile, $rootScope) { - element = $compile('
')($rootScope); - - $rootScope.$apply(function() { - $rootScope.cls = 'two'; - }); - - expect(element).toHaveClass('one'); - expect(element).toHaveClass('two'); // interpolated - expect(element).toHaveClass('three'); - expect(element).toHaveClass('log'); // merged from replace directive template - })); - - - it('should merge interpolated css class with ngRepeat', - inject(function($compile, $rootScope) { - element = $compile( - '
' + - '
' + - '
')($rootScope); - - $rootScope.$apply(function() { - $rootScope.cls = 'two'; - }); - - var child = element.find('div').eq(0); - expect(child).toHaveClass('one'); - expect(child).toHaveClass('two'); // interpolated - expect(child).toHaveClass('three'); - expect(child).toHaveClass('log'); // merged from replace directive template - })); - }); - - - describe('async templates', function() { - - beforeEach(module( - function($compileProvider) { - $compileProvider.directive('hello', valueFn({ restrict: 'CAM', templateUrl: 'hello.html' })); - $compileProvider.directive('cau', valueFn({ restrict: 'CAM', templateUrl:'cau.html' })); - - $compileProvider.directive('cError', valueFn({ - restrict: 'CAM', - templateUrl:'error.html', - compile: function() { - throw Error('cError'); - } - })); - $compileProvider.directive('lError', valueFn({ - restrict: 'CAM', - templateUrl: 'error.html', - compile: function() { - throw Error('lError'); - } - })); - - - $compileProvider.directive('iHello', valueFn({ - restrict: 'CAM', - replace: true, - templateUrl: 'hello.html' - })); - $compileProvider.directive('iCau', valueFn({ - restrict: 'CAM', - replace: true, - templateUrl:'cau.html' - })); - - $compileProvider.directive('iCError', valueFn({ - restrict: 'CAM', - replace: true, - templateUrl:'error.html', - compile: function() { - throw Error('cError'); - } - })); - $compileProvider.directive('iLError', valueFn({ - restrict: 'CAM', - 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( - '
' + - '' + - '' + - '' + - '' + - '
'); - 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({ - restrict: 'C', - template: '' - })); - $compileProvider.directive('async', valueFn({ - restrict: 'C', - 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() { - var iscope; - - beforeEach(module(function($compileProvider) { - forEach(['', 'a', 'b'], function(name) { - $compileProvider.directive('scope' + uppercase(name), function(log) { - return { - scope: true, - restrict: 'CA', - compile: function() { - return function (scope, element) { - log(scope.$id); - expect(element.data('$scope')).toBe(scope); - }; - } - }; - }); - $compileProvider.directive('iscope' + uppercase(name), function(log) { - return { - scope: {}, - restrict: 'CA', - compile: function() { - return function (scope, element) { - iscope = scope; - log(scope.$id); - expect(element.data('$scope')).toBe(scope); - }; - } - }; - }); - $compileProvider.directive('tiscope' + uppercase(name), function(log) { - return { - scope: {}, - restrict: 'CA', - templateUrl: 'tiscope.html', - compile: function() { - return function (scope, element) { - iscope = scope; - log(scope.$id); - expect(element.data('$scope')).toBe(scope); - }; - } - }; - }); - }); - $compileProvider.directive('log', function(log) { - return { - restrict: 'CA', - link: 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'); - expect(element.find('span').hasClass('ng-scope')).toBe(true); - })); - - - it('should allow creation of new isolated scopes for directives', inject( - function($rootScope, $compile, log) { - element = $compile('
')($rootScope); - expect(log).toEqual('LOG; log-002-001; 002'); - $rootScope.name = 'abc'; - expect(iscope.$parent).toBe($rootScope); - expect(iscope.name).toBeUndefined(); - })); - - - it('should allow creation of new isolated scopes for directives with templates', inject( - function($rootScope, $compile, log, $httpBackend) { - $httpBackend.expect('GET', 'tiscope.html').respond(''); - element = $compile('
')($rootScope); - $httpBackend.flush(); - expect(log).toEqual('LOG; log-002-001; 002'); - $rootScope.name = 'abc'; - expect(iscope.$parent).toBe($rootScope); - expect(iscope.name).toBeUndefined(); - })); - - - it('should correctly create the scope hierachy', 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 allow more one new scope directives per element, but directives should share' + - 'the scope', inject( - function($rootScope, $compile, log) { - element = $compile('
')($rootScope); - expect(log).toEqual('002; 002'); - }) - ); - - it('should not allow more then one isolate scope creation per element', inject( - function($rootScope, $compile) { - expect(function(){ - $compile('
'); - }).toThrow('Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + - '<' + (msie < 9 ? 'DIV' : 'div') + - ' class="iscope-a; scope-b ng-isolate-scope ng-scope">'); - }) - ); - - - it('should not allow more then one isolate scope creation per element', inject( - function($rootScope, $compile) { - expect(function(){ - $compile('
'); - }).toThrow('Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' + - '<' + (msie < 9 ? 'DIV' : 'div') + - ' class="iscope-a; iscope-b ng-isolate-scope ng-scope">'); - }) - ); - - - it('should create new scope even at the root of the template', inject( - function($rootScope, $compile, log) { - element = $compile('
')($rootScope); - expect(log).toEqual('002'); - }) - ); - - - it('should create isolate scope even at the root of the template', inject( - function($rootScope, $compile, log) { - element = $compile('
')($rootScope); - expect(log).toEqual('002'); - }) - ); - }); - }); - }); - - - describe('interpolation', function() { - var observeSpy, attrValueDuringLinking; - - beforeEach(module(function($compileProvider) { - $compileProvider.directive('observer', function() { - return function(scope, elm, attr) { - observeSpy = jasmine.createSpy('$observe attr'); - - attr.$observe('someAttr', observeSpy); - attrValueDuringLinking = attr.someAttr; - }; - }); - })); - - - 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 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 observe interpolated attrs', inject(function($rootScope, $compile) { - $compile('
')($rootScope); - - // should be async - expect(observeSpy).not.toHaveBeenCalled(); - - $rootScope.$apply(function() { - $rootScope.value = 'bound-value'; - }); - expect(observeSpy).toHaveBeenCalledOnceWith('bound-value'); - })); - - - it('should set interpolated attrs to undefined', inject(function($rootScope, $compile) { - attrValueDuringLinking = null; - $compile('
')($rootScope); - expect(attrValueDuringLinking).toBeUndefined(); - })); - - - it('should not call observer of non-interpolated attr', inject(function($rootScope, $compile) { - $compile('
')($rootScope); - expect(attrValueDuringLinking).toBe('nonBound'); - - $rootScope.$digest(); - expect(observeSpy).not.toHaveBeenCalled(); - })); - - - it('should delegate exceptions to $exceptionHandler', function() { - observeSpy = jasmine.createSpy('$observe attr').andThrow('ERROR'); - - module(function($compileProvider, $exceptionHandlerProvider) { - $exceptionHandlerProvider.mode('log'); - $compileProvider.directive('error', function() { - return function(scope, elm, attr) { - attr.$observe('someAttr', observeSpy); - attr.$observe('someAttr', observeSpy); - }; - }); - }); - - inject(function($compile, $rootScope, $exceptionHandler) { - $compile('
')($rootScope); - $rootScope.$digest(); - - expect(observeSpy).toHaveBeenCalled(); - expect(observeSpy.callCount).toBe(2); - expect($exceptionHandler.errors).toEqual(['ERROR', 'ERROR']); - }); - }); - - - it('should translate {{}} in terminal nodes', inject(function($rootScope, $compile) { - element = $compile('')($rootScope) - $rootScope.$digest(); - expect(sortedHtml(element).replace(' selected="true"', '')). - toEqual(''); - $rootScope.name = 'Misko'; - $rootScope.$digest(); - expect(sortedHtml(element).replace(' selected="true"', '')). - toEqual(''); - })); - }); - - - describe('link phase', function() { - - beforeEach(module(function($compileProvider) { - - forEach(['a', 'b', 'c'], function(name) { - $compileProvider.directive(name, function(log) { - return { - restrict: 'ECA', - 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 only on control elements', function() { - var value; - module(function($compileProvider) { - $compileProvider.directive({ - input: valueFn({ - restrict: 'ECA', - link:function(scope, element, attr) { - value = attr.required; - } - }) - }); - }); - inject(function($rootScope, $compile) { - element = $compile('')($rootScope); - expect(value).toEqual(true); - }); - }); - - it('should read boolean attributes as text on non-controll elements', function() { - var value; - module(function($compileProvider) { - $compileProvider.directive({ - div: valueFn({ - restrict: 'ECA', - link:function(scope, element, attr) { - value = attr.required; - } - }) - }); - }); - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - expect(value).toEqual('some text'); - }); - }); - - 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 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); - }); - }); - - - it('should properly $observe inside ng-repeat', function() { - var spies = []; - - module(function($compileProvider) { - $compileProvider.directive('observer', function() { - return function(scope, elm, attr) { - spies.push(jasmine.createSpy('observer ' + spies.length)); - attr.$observe('some', spies[spies.length - 1]); - }; - }); - }); - - inject(function($compile, $rootScope) { - element = $compile('
'+ - ''+ - '
')($rootScope); - - $rootScope.$apply(function() { - $rootScope.items = [{id: 1}, {id: 2}]; - }); - - expect(spies[0]).toHaveBeenCalledOnceWith('id_1'); - expect(spies[1]).toHaveBeenCalledOnceWith('id_2'); - spies[0].reset(); - spies[1].reset(); - - $rootScope.$apply(function() { - $rootScope.items[0].id = 5; - }); - - expect(spies[0]).toHaveBeenCalledOnceWith('id_5'); - }); - }); - - - describe('$set', function() { - var attr; - beforeEach(function(){ - module(function($compileProvider) { - $compileProvider.directive('input', valueFn({ - restrict: 'ECA', - link: 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', true, '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 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); - }); - - - it('should not set DOM element attr if writeAttr false', function() { - attr.$set('test', 'value', false); - - expect(element.attr('test')).toBeUndefined(); - expect(attr.test).toBe('value'); - }); - }); - }); - - - describe('locals', function() { - it('should marshal to locals', function() { - module(function($compileProvider) { - $compileProvider.directive('widget', function(log) { - return { - scope: { - attr: 'attribute', - prop: 'evaluate', - bind: 'bind', - assign: 'accessor', - read: 'accessor', - exp: 'expression', - nonExist: 'accessor', - nonExistExpr: 'expression' - }, - link: function(scope, element, attrs) { - scope.nonExist(); // noop - scope.nonExist(123); // noop - scope.nonExistExpr(); // noop - scope.nonExistExpr(123); // noop - log(scope.attr); - log(scope.prop); - log(scope.assign()); - log(scope.read()); - log(scope.assign('ng')); - scope.exp({myState:'OK'}); - expect(function() { scope.read(undefined); }). - toThrow("Expression ''D'' not assignable."); - scope.$watch('bind', log); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - $rootScope.myProp = 'B'; - $rootScope.bi = {nd: 'C'}; - $rootScope.name = 'C'; - element = $compile( - '
{{bind}}
') - ($rootScope); - expect(log).toEqual('A; B; C; D; ng'); - expect($rootScope.name).toEqual('ng'); - expect($rootScope.state).toEqual('OK'); - log.reset(); - $rootScope.$apply(); - expect(element.text()).toEqual('C'); - expect(log).toEqual('C'); - $rootScope.bi.nd = 'c'; - $rootScope.$apply(); - expect(log).toEqual('C; c'); - }); - }); - }); - - - describe('controller', function() { - it('should inject locals to controller', function() { - module(function($compileProvider) { - $compileProvider.directive('widget', function(log) { - return { - controller: function(attr, prop, assign, read, exp){ - log(attr); - log(prop); - log(assign()); - log(read()); - log(assign('ng')); - exp(); - expect(function() { read(undefined); }). - toThrow("Expression ''D'' not assignable."); - this.result = 'OK'; - }, - inject: { - attr: 'attribute', - prop: 'evaluate', - assign: 'accessor', - read: 'accessor', - exp: 'expression' - }, - link: function(scope, element, attrs, controller) { - log(controller.result); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - $rootScope.myProp = 'B'; - $rootScope.bi = {nd: 'C'}; - $rootScope.name = 'C'; - element = $compile( - '
{{bind}}
') - ($rootScope); - expect(log).toEqual('A; B; C; D; ng; OK'); - expect($rootScope.name).toEqual('ng'); - }); - }); - - - it('should get required controller', function() { - module(function($compileProvider) { - $compileProvider.directive('main', function(log) { - return { - priority: 2, - controller: function() { - this.name = 'main'; - }, - link: function(scope, element, attrs, controller) { - log(controller.name); - } - }; - }); - $compileProvider.directive('dep', function(log) { - return { - priority: 1, - require: 'main', - link: function(scope, element, attrs, controller) { - log('dep:' + controller.name); - } - }; - }); - $compileProvider.directive('other', function(log) { - return { - link: function(scope, element, attrs, controller) { - log(!!controller); // should be false - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('main; dep:main; false'); - }); - }); - - - it('should require controller on parent element',function() { - module(function($compileProvider) { - $compileProvider.directive('main', function(log) { - return { - controller: function() { - this.name = 'main'; - } - }; - }); - $compileProvider.directive('dep', function(log) { - return { - require: '^main', - link: function(scope, element, attrs, controller) { - log('dep:' + controller.name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:main'); - }); - }); - - - it('should have optional controller on current element', function() { - module(function($compileProvider) { - $compileProvider.directive('dep', function(log) { - return { - require: '?main', - link: function(scope, element, attrs, controller) { - log('dep:' + !!controller); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:false'); - }); - }); - - - it('should support multiple controllers', function() { - module(function($compileProvider) { - $compileProvider.directive('c1', valueFn({ - controller: function() { this.name = 'c1'; } - })); - $compileProvider.directive('c2', valueFn({ - controller: function() { this.name = 'c2'; } - })); - $compileProvider.directive('dep', function(log) { - return { - require: ['^c1', '^c2'], - link: function(scope, element, attrs, controller) { - log('dep:' + controller[0].name + '-' + controller[1].name); - } - }; - }); - }); - inject(function(log, $compile, $rootScope) { - element = $compile('
')($rootScope); - expect(log).toEqual('dep:c1-c2'); - }); - - }); - }); - - - describe('transclude', function() { - it('should compile get templateFn', function() { - module(function($compileProvider) { - $compileProvider.directive('trans', function(log) { - return { - transclude: 'element', - priority: 2, - controller: function($transclude) { this.$transclude = $transclude; }, - compile: function(element, attrs, template) { - log('compile: ' + angular.mock.dump(element)); - return function(scope, element, attrs, ctrl) { - log('link'); - var cursor = element; - template(scope.$new(), function(clone) {cursor.after(cursor = clone)}); - ctrl.$transclude(function(clone) {cursor.after(clone)}); - }; - } - } - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
{{$parent.$id}}-{{$id}};
') - ($rootScope); - $rootScope.$apply(); - expect(log).toEqual('compile: ; HIGH; link; LOG; LOG'); - expect(element.text()).toEqual('001-002;001-003;'); - }); - }); - - - it('should support transclude directive', function() { - module(function($compileProvider) { - $compileProvider.directive('trans', function() { - return { - transclude: 'content', - replace: true, - scope: true, - template: '
  • W:{{$parent.$id}}-{{$id}};
' - } - }); - }); - inject(function(log, $rootScope, $compile) { - element = $compile('
T:{{$parent.$id}}-{{$id}};
') - ($rootScope); - $rootScope.$apply(); - expect(element.text()).toEqual('W:001-002;T:001-003;'); - expect(jqLite(element.find('span')[0]).text()).toEqual('T:001-003'); - expect(jqLite(element.find('span')[1]).text()).toEqual(';'); - }); - }); - - - it('should transclude transcluded content', function() { - module(function($compileProvider) { - $compileProvider.directive('book', valueFn({ - transclude: 'content', - template: '
book-
(
)
' - })); - $compileProvider.directive('chapter', valueFn({ - transclude: 'content', - templateUrl: 'chapter.html' - })); - $compileProvider.directive('section', valueFn({ - transclude: 'content', - template: '
section-!
!
' - })); - return function($httpBackend) { - $httpBackend. - expect('GET', 'chapter.html'). - respond('
chapter-
[
]
'); - } - }); - inject(function(log, $rootScope, $compile, $httpBackend) { - element = $compile('
paragraph
')($rootScope); - $rootScope.$apply(); - - expect(element.text()).toEqual('book-'); - - $httpBackend.flush(); - $rootScope.$apply(); - expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); - }); - }); - - - it('should only allow one transclude per element', function() { - module(function($compileProvider) { - $compileProvider.directive('first', valueFn({ - scope: {}, - restrict: 'CA', - transclude: 'content' - })); - $compileProvider.directive('second', valueFn({ - restrict: 'CA', - transclude: 'content' - })); - }); - inject(function($compile) { - expect(function() { - $compile('
'); - }).toThrow('Multiple directives [first, second] asking for transclusion on: <' + - (msie <= 8 ? 'DIV' : 'div') + ' class="first second ng-isolate-scope ng-scope">'); - }); - }); - - - it('should remove transclusion scope, when the DOM is destroyed', function() { - module(function($compileProvider) { - $compileProvider.directive('box', valueFn({ - transclude: 'content', - scope: { name: 'evaluate', show: 'accessor' }, - template: '

Hello: {{name}}!

', - link: function(scope, element) { - scope.$watch( - function() { return scope.show(); }, - function(show) { - if (!show) { - element.find('div').find('div').remove(); - } - } - ); - } - })); - }); - inject(function($compile, $rootScope) { - $rootScope.username = 'Misko'; - $rootScope.select = true; - element = $compile( - '
user: {{username}}
') - ($rootScope); - $rootScope.$apply(); - expect(element.text()).toEqual('Hello: Misko!user: Misko'); - - var widgetScope = $rootScope.$$childHead; - var transcludeScope = widgetScope.$$nextSibling; - expect(widgetScope.name).toEqual('Misko'); - expect(widgetScope.$parent).toEqual($rootScope); - expect(transcludeScope.$parent).toEqual($rootScope); - - $rootScope.select = false; - $rootScope.$apply(); - expect(element.text()).toEqual('Hello: Misko!'); - expect(widgetScope.$$nextSibling).toEqual(null); - }); - }); - - - it('should support transcluded element on root content', function() { - var comment; - module(function($compileProvider) { - $compileProvider.directive('transclude', valueFn({ - transclude: 'element', - compile: function(element, attr, linker) { - return function(scope, element, attr) { - comment = element; - }; - } - })); - }); - inject(function($compile, $rootScope) { - var element = jqLite('
before
after
').contents(); - expect(element.length).toEqual(3); - expect(nodeName_(element[1])).toBe('DIV'); - $compile(element)($rootScope); - expect(nodeName_(element[1])).toBe('#comment'); - expect(nodeName_(comment)).toBe('#comment'); - }); - }); - }); -}); -- cgit v1.2.3