'use strict'; /** * @ngdoc function * @name angular.module.ng.$compile * @function * * @description * Compiles a piece of HTML string or DOM into a template and produces a template function, which * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together. * * The compilation is a process of walking the DOM tree and trying to match DOM elements to * {@link angular.module.ng.$compileProvider.directive directives}. For each match it * executes corresponding template function and collects the * instance functions into a single template function which is then returned. * * The template function can then be used once to produce the view or as it is the case with * {@link angular.module.ng.$compileProvider.directive.ng-repeat repeater} many-times, in which * case each call results in a view that is a DOM clone of the original template. *


it('should auto compile', function() { expect(element('div[compile]').text()).toBe('Hello Angular'); input('html').enter('{{name}}!'); expect(element('div[compile]').text()).toBe('Angular!'); });
* * * @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 updateh until after a call to $digest which typically is done by * Angular automatically. * * If you need access to the bound view, there are two ways to do it: * * - If you are not asking the linking function to clone the template, create the DOM element(s) * before you send them to the compiler and keep this reference around. *
 *     var element = $compile('

{{total}}

')(scope); *
* * - if on the other hand, you need the element to be cloned, the view reference from the original * example would not point to the clone, but rather to the original template that was cloned. In * this case, you can access the clone via the cloneAttachFn: *
 *     var templateHTML = angular.element('

{{total}}

'), * scope = ....; * * var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) { * //attach the clone to DOM document at the right place * }); * * //now we have reference to the cloned DOM via `clone` *
* * * For information on how the compiler works, see the * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide. */ $CompileProvider.$inject = ['$provide']; function $CompileProvider($provide) { var hasDirectives = {}, Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, CONTENT_REGEXP = /\<\\>/i, HAS_ROOT_ELEMENT = /^\<[\s\S]*\>$/, SIDE_EFFECT_ATTRS = {}; forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) { SIDE_EFFECT_ATTRS[name] = name; SIDE_EFFECT_ATTRS[directiveNormalize('ng_' + name)] = name; }); this.directive = function registerDirective(name, directiveFactory) { if (isString(name)) { assertArg(directiveFactory, 'directive'); if (!hasDirectives.hasOwnProperty(name)) { hasDirectives[name] = []; $provide.factory(name + Suffix, ['$injector', '$exceptionHandler', function($injector, $exceptionHandler) { var directives = []; forEach(hasDirectives[name], function(directiveFactory) { try { var directive = $injector.invoke(directiveFactory); if (isFunction(directive)) { directive = { compile: valueFn(directive) }; } else if (!directive.compile && directive.link) { directive.compile = valueFn(directive.link); } directive.priority = directive.priority || 0; directive.name = 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); }; } }; return compile; //================================ function compile(templateElement, transcludeFn, maxPriority) { 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, ii = nodeList.length; i < ii; i++) { attrs = { $attr: {}, $normalize: directiveNormalize, $set: attrSetter, $observe: interpolatedAttrObserve, $observers: {} }; // 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) { if (linkingFns.length != nodeList.length * 2) { throw Error('Template changed structure!'); } 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]; 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 */ match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); if (match) { nName = directiveNormalize(match[1]); if (addDirective(directives, nName, 'M', maxPriority)) { attrs[nName] = trim(match[2]); } } 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]; template.replaceWith(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); attrs.$element = jqLite(linkNode); } 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) != '$') { dst.$set(key, value, 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); if (SIDE_EFFECT_ATTRS[name]) { name = SIDE_EFFECT_ATTRS[name]; if (isBooleanAttr(node, name)) { value = true; } } else if (!interpolateFn) { // we are not a side-effect attr, and we have no side-effects -> ignore return; } directives.push({ priority: 100, compile: function(element, attr) { if (interpolateFn) { return function(scope, element, attr) { // 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); }); }; } else { 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