/src/ng/compile.js
JavaScript | 1144 lines | 671 code | 137 blank | 336 comment | 160 complexity | d6f8574c569ffe25050ab125a9202b8c MD5 | raw file
Possible License(s): JSON, Apache-2.0
- 'use strict';
- /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
- *
- * DOM-related variables:
- *
- * - "node" - DOM Node
- * - "element" - DOM Element or Node
- * - "$node" or "$element" - jqLite-wrapped node or element
- *
- *
- * Compiler related stuff:
- *
- * - "linkFn" - linking fn of a single directive
- * - "nodeLinkFn" - function that aggregates all linking fns for a particular node
- * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node
- * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList)
- */
- var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: ';
- /**
- * @ngdoc function
- * @name 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 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 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 ng.directive:ngRepeat repeater} many-times, in which
- * case each call results in a view that is a DOM clone of the original template.
- *
- <doc:example module="compile">
- <doc:source>
- <script>
- // declare a new module, and inject the $compileProvider
- angular.module('compile', [], function($compileProvider) {
- // configure new 'compile' directive by passing a directive
- // factory function. The factory function injects the '$compile'
- $compileProvider.directive('compile', function($compile) {
- // directive factory creates a link function
- return function(scope, element, attrs) {
- scope.$watch(
- function(scope) {
- // watch the 'compile' expression for changes
- return scope.$eval(attrs.compile);
- },
- function(value) {
- // when the 'compile' expression changes
- // assign it into the current DOM
- element.html(value);
- // compile the new DOM and link it to the current
- // scope.
- // NOTE: we only compile .childNodes so that
- // we don't get into infinite loop compiling ourselves
- $compile(element.contents())(scope);
- }
- );
- };
- })
- });
- function Ctrl($scope) {
- $scope.name = 'Angular';
- $scope.html = 'Hello {{name}}';
- }
- </script>
- <div ng-controller="Ctrl">
- <input ng-model="name"> <br>
- <textarea ng-model="html"></textarea> <br>
- <div compile="html"></div>
- </div>
- </doc:source>
- <doc:scenario>
- it('should auto compile', function() {
- expect(element('div[compile]').text()).toBe('Hello Angular');
- input('html').enter('{{name}}!');
- expect(element('div[compile]').text()).toBe('Angular!');
- });
- </doc:scenario>
- </doc:example>
- *
- *
- * @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 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: <br> `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.
- * <pre>
- * var element = $compile('<p>{{total}}</p>')(scope);
- * </pre>
- *
- * - 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:
- * <pre>
- * var templateHTML = angular.element('<p>{{total}}</p>'),
- * 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`
- * </pre>
- *
- *
- * For information on how the compiler works, see the
- * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
- */
- /**
- * @ngdoc service
- * @name ng.$compileProvider
- * @function
- *
- * @description
- */
- $CompileProvider.$inject = ['$provide'];
- function $CompileProvider($provide) {
- var hasDirectives = {},
- Suffix = 'Directive',
- COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
- CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
- MULTI_ROOT_TEMPLATE_ERROR = 'Template must have exactly one root element. was: ';
- /**
- * @ngdoc function
- * @name ng.$compileProvider#directive
- * @methodOf ng.$compileProvider
- * @function
- *
- * @description
- * Register a new directives with the compiler.
- *
- * @param {string} name Name of the directive in camel-case. (ie <code>ngBind</code> which will match as
- * <code>ng-bind</code>).
- * @param {function} directiveFactory An injectable directive factroy function. See {@link guide/directive} for more
- * info.
- * @returns {ng.$compileProvider} Self for chaining.
- */
- 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', '$rootScope',
- function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
- $controller, $rootScope) {
- var Attributes = function(element, attr) {
- this.$$element = element;
- 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 = getBooleanAttrName(this.$$element[0], key),
- $$observers = this.$$observers;
- 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
- $$observers && forEach($$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.
- * @returns {function(*)} the `fn` Function passed in.
- */
- $observe: function(key, fn) {
- var attrs = this,
- $$observers = (attrs.$$observers || (attrs.$$observers = {})),
- listeners = ($$observers[key] || ($$observers[key] = []));
- listeners.push(fn);
- $rootScope.$evalAsync(function() {
- if (!listeners.$$inter) {
- // no one registered attribute interpolation function, so lets call it manually
- fn(attrs[key]);
- }
- });
- return fn;
- }
- };
- var startSymbol = $interpolate.startSymbol(),
- endSymbol = $interpolate.endSymbol(),
- denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
- ? identity
- : function denormalizeTemplate(template) {
- return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
- };
- return compile;
- //================================
- function compile($compileNodes, transcludeFn, maxPriority) {
- if (!($compileNodes instanceof jqLite)) {
- // jquery always rewraps, where as we need to preserve the original selector so that we can modify it.
- $compileNodes = jqLite($compileNodes);
- }
- // 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 <span>
- forEach($compileNodes, function(node, index){
- if (node.nodeType == 3 /* text node */) {
- $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
- }
- });
- var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority);
- return function publicLinkFn(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 $linkNode = cloneConnectFn
- ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
- : $compileNodes;
- $linkNode.data('$scope', scope);
- safeAddClass($linkNode, 'ng-scope');
- if (cloneConnectFn) cloneConnectFn($linkNode, scope);
- if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
- return $linkNode;
- };
- }
- 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 linkFns = [],
- nodeLinkFn, childLinkFn, directives, attrs, linkFnFound;
- 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);
- nodeLinkFn = (directives.length)
- ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement)
- : null;
- childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes.length)
- ? null
- : compileNodes(nodeList[i].childNodes,
- nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
- linkFns.push(nodeLinkFn);
- linkFns.push(childLinkFn);
- linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn);
- }
- // return a linking function if we have found anything, null otherwise
- return linkFnFound ? compositeLinkFn : null;
- function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
- var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn;
- for(var i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
- node = nodeList[n];
- nodeLinkFn = linkFns[i++];
- childLinkFn = linkFns[i++];
- if (nodeLinkFn) {
- if (nodeLinkFn.scope) {
- childScope = scope.$new(isObject(nodeLinkFn.scope));
- jqLite(node).data('$scope', childScope);
- } else {
- childScope = scope;
- }
- childTranscludeFn = nodeLinkFn.transclude;
- if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) {
- nodeLinkFn(childLinkFn, childScope, node, $rootElement,
- (function(transcludeFn) {
- return function(cloneFn) {
- var transcludeScope = scope.$new();
- return transcludeFn(transcludeScope, cloneFn).
- bind('$destroy', bind(transcludeScope, transcludeScope.$destroy));
- };
- })(childTranscludeFn || transcludeFn)
- );
- } else {
- nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn);
- }
- } else if (childLinkFn) {
- childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
- }
- }
- }
- }
- /**
- * Looks for directives on the given node and adds them to the directive collection which is
- * sorted.
- *
- * @param node Node to search.
- * @param directives An array to which the directives are added to. This array is sorted before
- * the function returns.
- * @param attrs The shared attrs object which is used to populate the normalized attributes.
- * @param {number=} maxPriority Max directive priority.
- */
- function collectDirectives(node, directives, attrs, maxPriority) {
- var nodeType = node.nodeType,
- attrsMap = attrs.$attr,
- match,
- className;
- switch(nodeType) {
- case 1: /* Element */
- // use the node name: <directive>
- 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 (getBooleanAttrName(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) && 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} compileNode 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 linkFn
- */
- function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, $rootElement) {
- var terminalPriority = -Number.MAX_VALUE,
- preLinkFns = [],
- postLinkFns = [],
- newScopeDirective = null,
- newIsolateScopeDirective = null,
- templateDirective = null,
- $compileNode = templateAttrs.$$element = jqLite(compileNode),
- directive,
- directiveName,
- $template,
- transcludeDirective,
- childTranscludeFn = transcludeFn,
- controllerDirectives,
- linkFn,
- 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', newIsolateScopeDirective, directive, $compileNode);
- if (isObject(directiveValue)) {
- safeAddClass($compileNode, 'ng-isolate-scope');
- newIsolateScopeDirective = directive;
- }
- safeAddClass($compileNode, 'ng-scope');
- newScopeDirective = newScopeDirective || directive;
- }
- directiveName = directive.name;
- if (directiveValue = directive.controller) {
- controllerDirectives = controllerDirectives || {};
- assertNoDuplicate("'" + directiveName + "' controller",
- controllerDirectives[directiveName], directive, $compileNode);
- controllerDirectives[directiveName] = directive;
- }
- if (directiveValue = directive.transclude) {
- assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
- transcludeDirective = directive;
- terminalPriority = directive.priority;
- if (directiveValue == 'element') {
- $template = jqLite(compileNode);
- $compileNode = templateAttrs.$$element =
- jqLite('<!-- ' + directiveName + ': ' + templateAttrs[directiveName] + ' -->');
- compileNode = $compileNode[0];
- replaceWith($rootElement, jqLite($template[0]), compileNode);
- childTranscludeFn = compile($template, transcludeFn, terminalPriority);
- } else {
- $template = jqLite(JQLiteClone(compileNode)).contents();
- $compileNode.html(''); // clear contents
- childTranscludeFn = compile($template, transcludeFn);
- }
- }
- if ((directiveValue = directive.template)) {
- assertNoDuplicate('template', templateDirective, directive, $compileNode);
- templateDirective = directive;
- directiveValue = denormalizeTemplate(directiveValue);
- if (directive.replace) {
- $template = jqLite('<div>' +
- trim(directiveValue) +
- '</div>').contents();
- compileNode = $template[0];
- if ($template.length != 1 || compileNode.nodeType !== 1) {
- throw new Error(MULTI_ROOT_TEMPLATE_ERROR + directiveValue);
- }
- replaceWith($rootElement, $compileNode, compileNode);
- 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(
- compileNode,
- directives.splice(i + 1, directives.length - (i + 1)),
- newTemplateAttrs
- )
- );
- mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
- ii = directives.length;
- } else {
- $compileNode.html(directiveValue);
- }
- }
- if (directive.templateUrl) {
- assertNoDuplicate('template', templateDirective, directive, $compileNode);
- templateDirective = directive;
- nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i),
- nodeLinkFn, $compileNode, templateAttrs, $rootElement, directive.replace,
- childTranscludeFn);
- ii = directives.length;
- } else if (directive.compile) {
- try {
- linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
- if (isFunction(linkFn)) {
- addLinkFns(null, linkFn);
- } else if (linkFn) {
- addLinkFns(linkFn.pre, linkFn.post);
- }
- } catch (e) {
- $exceptionHandler(e, startingTag($compileNode));
- }
- }
- if (directive.terminal) {
- nodeLinkFn.terminal = true;
- terminalPriority = Math.max(terminalPriority, directive.priority);
- }
- }
- nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope;
- nodeLinkFn.transclude = transcludeDirective && childTranscludeFn;
- // might be normal or delayed nodeLinkFn depending on if templateUrl is present
- return nodeLinkFn;
- ////////////////////
- function addLinkFns(pre, post) {
- if (pre) {
- pre.require = directive.require;
- preLinkFns.push(pre);
- }
- if (post) {
- post.require = directive.require;
- postLinkFns.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;
- }
- function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
- var attrs, $element, i, ii, linkFn, controller;
- if (compileNode === linkNode) {
- attrs = templateAttrs;
- } else {
- attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr));
- }
- $element = attrs.$$element;
- if (newIsolateScopeDirective) {
- var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/;
- var parentScope = scope.$parent || scope;
- forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
- var match = definiton.match(LOCAL_REGEXP) || [],
- attrName = match[2]|| scopeName,
- mode = match[1], // @, =, or &
- lastValue,
- parentGet, parentSet;
- switch (mode) {
- case '@': {
- attrs.$observe(attrName, function(value) {
- scope[scopeName] = value;
- });
- attrs.$$observers[attrName].$$scope = parentScope;
- break;
- }
- case '=': {
- parentGet = $parse(attrs[attrName]);
- parentSet = parentGet.assign || function() {
- // reset the change, or we will throw this exception on every $digest
- lastValue = scope[scopeName] = parentGet(parentScope);
- throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] +
- ' (directive: ' + newIsolateScopeDirective.name + ')');
- };
- lastValue = scope[scopeName] = parentGet(parentScope);
- scope.$watch(function parentValueWatch() {
- var parentValue = parentGet(parentScope);
- if (parentValue !== scope[scopeName]) {
- // we are out of sync and need to copy
- if (parentValue !== lastValue) {
- // parent changed and it has precedence
- lastValue = scope[scopeName] = parentValue;
- } else {
- // if the parent can be assigned then do so
- parentSet(parentScope, parentValue = lastValue = scope[scopeName]);
- }
- }
- return parentValue;
- });
- break;
- }
- case '&': {
- parentGet = $parse(attrs[attrName]);
- scope[scopeName] = function(locals) {
- return parentGet(parentScope, locals);
- }
- break;
- }
- default: {
- throw Error('Invalid isolate scope definition for directive ' +
- newIsolateScopeDirective.name + ': ' + definiton);
- }
- }
- });
- }
- if (controllerDirectives) {
- forEach(controllerDirectives, function(directive) {
- var locals = {
- $scope: scope,
- $element: $element,
- $attrs: attrs,
- $transclude: boundTranscludeFn
- };
- controller = directive.controller;
- if (controller == '@') {
- controller = attrs[directive.name];
- }
- $element.data(
- '$' + directive.name + 'Controller',
- $controller(controller, locals));
- });
- }
- // PRELINKING
- for(i = 0, ii = preLinkFns.length; i < ii; i++) {
- try {
- linkFn = preLinkFns[i];
- linkFn(scope, $element, attrs,
- linkFn.require && getControllers(linkFn.require, $element));
- } catch (e) {
- $exceptionHandler(e, startingTag($element));
- }
- }
- // RECURSION
- childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
- // POSTLINKING
- for(i = 0, ii = postLinkFns.length; i < ii; i++) {
- try {
- linkFn = postLinkFns[i];
- linkFn(scope, $element, attrs,
- linkFn.require && getControllers(linkFn.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<ii; i++) {
- try {
- directive = directives[i];
- if ( (maxPriority === undefined || maxPriority > 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);
- dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + 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, beforeTemplateNodeLinkFn, $compileNode, tAttrs,
- $rootElement, replace, childTranscludeFn) {
- var linkQueue = [],
- afterTemplateNodeLinkFn,
- afterTemplateChildLinkFn,
- beforeTemplateCompileNode = $compileNode[0],
- origAsyncDirective = directives.shift(),
- // The fact that we have to copy and patch the directive seems wrong!
- derivedSyncDirective = extend({}, origAsyncDirective, {
- controller: null, templateUrl: null, transclude: null, scope: null
- });
- $compileNode.html('');
- $http.get(origAsyncDirective.templateUrl, {cache: $templateCache}).
- success(function(content) {
- var compileNode, tempTemplateAttrs, $template;
- content = denormalizeTemplate(content);
- if (replace) {
- $template = jqLite('<div>' + trim(content) + '</div>').contents();
- compileNode = $template[0];
- if ($template.length != 1 || compileNode.nodeType !== 1) {
- throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content);
- }
- tempTemplateAttrs = {$attr: {}};
- replaceWith($rootElement, $compileNode, compileNode);
- collectDirectives(compileNode, directives, tempTemplateAttrs);
- mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
- } else {
- compileNode = beforeTemplateCompileNode;
- $compileNode.html(content);
- }
- directives.unshift(derivedSyncDirective);
- afterTemplateNodeLinkFn = applyDirectivesToNode(directives, $compileNode, tAttrs, childTranscludeFn);
- afterTemplateChildLinkFn = compileNodes($compileNode.contents(), childTranscludeFn);
- while(linkQueue.length) {
- var controller = linkQueue.pop(),
- linkRootElement = linkQueue.pop(),
- beforeTemplateLinkNode = linkQueue.pop(),
- scope = linkQueue.pop(),
- linkNode = compileNode;
- if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
- // it was cloned therefore we have to clone as well.
- linkNode = JQLiteClone(compileNode);
- replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
- }
- afterTemplateNodeLinkFn(function() {
- beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller);
- }, scope, linkNode, $rootElement, controller);
- }
- linkQueue = null;
- }).
- error(function(response, code, headers, config) {
- throw Error('Failed to load template: ' + config.url);
- });
- return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) {
- if (linkQueue) {
- linkQueue.push(scope);
- linkQueue.push(node);
- linkQueue.push(rootElement);
- linkQueue.push(controller);
- } else {
- afterTemplateNodeLinkFn(function() {
- beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, 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 textInterpolateLinkFn(scope, node) {
- var parent = node.parent(),
- bindings = parent.data('$binding') || [];
- bindings.push(interpolateFn);
- safeAddClass(parent.data('$binding', bindings), 'ng-binding');
- scope.$watch(interpolateFn, function interpolateFnWatchAction(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 attrInterpolateLinkFn(scope, element, attr) {
- var $$observers = (attr.$$observers || (attr.$$observers = {}));
- 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);
- }
- attr[name] = undefined;
- ($$observers[name] || ($$observers[name] = [])).$$inter = true;
- (attr.$$observers && attr.$$observers[name].$$scope || scope).
- $watch(interpolateFn, function interpolateFnWatchAction(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 < ii; i++) {
- if ($rootElement[i] == oldNode) {
- $rootElement[i] = newNode;
- break;
- }
- }
- }
- if (parent) {
- parent.replaceChild(newNode, oldNode);
- }
- newNode[jqLite.expando] = oldNode[jqLite.expando];
- $element[0] = newNode;
- }
- }];
- }
- var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
- /**
- * Converts all accepted directives format into proper directive name.
- * All of these will become 'myDirective':
- * my:DiRective
- * my-directive
- * x-my-directive
- * data-my:directive
- *
- * Also there is special case for Moz prefix starting with upper case letter.
- * @param name Name to normalize
- */
- function directiveNormalize(name) {
- return camelCase(name.replace(PREFIX_REGEXP, ''));
- }
- /**
- * @ngdoc object
- * @name ng.$compile.directive.Attributes
- * @description
- *
- * A shared object between directive compile / linking functions which contains normalized DOM element
- * attributes. The the values reflect current binding state `{{ }}`. The normalization is needed
- * since all of these are treated as equivalent in Angular:
- *
- * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
- */
- /**
- * @ngdoc property
- * @name ng.$compile.directive.Attributes#$attr
- * @propertyOf ng.$compile.directive.Attributes
- * @returns {object} A map of DOM element attribute names to the normalized name. This is
- * needed to do reverse lookup from normalized name back to actual name.
- */
- /**
- * @ngdoc function
- * @name ng.$compile.directive.Attributes#$set
- * @methodOf ng.$compile.directive.Attributes
- * @function
- *
- * @description
- * Set DOM element attribute value.
- *
- *
- * @param {string} name Normalized element attribute name of the property to modify. The name is
- * revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
- * property to the original name.
- * @param {string} value Value to set the attribute to.
- */
- /**
- * Closure compiler type information
- */
- function nodesetLinkingFn(
- /* angular.Scope */ scope,
- /* NodeList */ nodeList,
- /* Element */ rootElement,
- /* function(Function) */ boundTranscludeFn
- ){}
- function directiveLinkingFn(
- /* nodesetLinkingFn */ nodesetLinkingFn,
- /* angular.Scope */ scope,
- /* Node */ node,
- /* Element */ rootElement,
- /* function(Function) */ boundTranscludeFn
- ){}