PageRenderTime 51ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/ng/compile.js

https://github.com/cedricss/angular.js
JavaScript | 1150 lines | 675 code | 138 blank | 337 comment | 162 complexity | 38a03b1dc05ce591184bd9855b480696 MD5 | raw file
Possible License(s): JSON, Apache-2.0
  1. 'use strict';
  2. /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
  3. *
  4. * DOM-related variables:
  5. *
  6. * - "node" - DOM Node
  7. * - "element" - DOM Element or Node
  8. * - "$node" or "$element" - jqLite-wrapped node or element
  9. *
  10. *
  11. * Compiler related stuff:
  12. *
  13. * - "linkFn" - linking fn of a single directive
  14. * - "nodeLinkFn" - function that aggregates all linking fns for a particular node
  15. * - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node
  16. * - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList)
  17. */
  18. var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: ';
  19. /**
  20. * @ngdoc function
  21. * @name ng.$compile
  22. * @function
  23. *
  24. * @description
  25. * Compiles a piece of HTML string or DOM into a template and produces a template function, which
  26. * can then be used to link {@link ng.$rootScope.Scope scope} and the template together.
  27. *
  28. * The compilation is a process of walking the DOM tree and trying to match DOM elements to
  29. * {@link ng.$compileProvider#directive directives}. For each match it
  30. * executes corresponding template function and collects the
  31. * instance functions into a single template function which is then returned.
  32. *
  33. * The template function can then be used once to produce the view or as it is the case with
  34. * {@link ng.directive:ngRepeat repeater} many-times, in which
  35. * case each call results in a view that is a DOM clone of the original template.
  36. *
  37. <doc:example module="compile">
  38. <doc:source>
  39. <script>
  40. // declare a new module, and inject the $compileProvider
  41. angular.module('compile', [], function($compileProvider) {
  42. // configure new 'compile' directive by passing a directive
  43. // factory function. The factory function injects the '$compile'
  44. $compileProvider.directive('compile', function($compile) {
  45. // directive factory creates a link function
  46. return function(scope, element, attrs) {
  47. scope.$watch(
  48. function(scope) {
  49. // watch the 'compile' expression for changes
  50. return scope.$eval(attrs.compile);
  51. },
  52. function(value) {
  53. // when the 'compile' expression changes
  54. // assign it into the current DOM
  55. element.html(value);
  56. // compile the new DOM and link it to the current
  57. // scope.
  58. // NOTE: we only compile .childNodes so that
  59. // we don't get into infinite loop compiling ourselves
  60. $compile(element.contents())(scope);
  61. }
  62. );
  63. };
  64. })
  65. });
  66. function Ctrl($scope) {
  67. $scope.name = 'Angular';
  68. $scope.html = 'Hello {{name}}';
  69. }
  70. </script>
  71. <div ng-controller="Ctrl">
  72. <input ng-model="name"> <br>
  73. <textarea ng-model="html"></textarea> <br>
  74. <div compile="html"></div>
  75. </div>
  76. </doc:source>
  77. <doc:scenario>
  78. it('should auto compile', function() {
  79. expect(element('div[compile]').text()).toBe('Hello Angular');
  80. input('html').enter('{{name}}!');
  81. expect(element('div[compile]').text()).toBe('Angular!');
  82. });
  83. </doc:scenario>
  84. </doc:example>
  85. *
  86. *
  87. * @param {string|DOMElement} element Element or HTML string to compile into a template function.
  88. * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives.
  89. * @param {number} maxPriority only apply directives lower then given priority (Only effects the
  90. * root element(s), not their children)
  91. * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template
  92. * (a DOM element/tree) to a scope. Where:
  93. *
  94. * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
  95. * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
  96. * `template` and call the `cloneAttachFn` function allowing the caller to attach the
  97. * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
  98. * called as: <br> `cloneAttachFn(clonedElement, scope)` where:
  99. *
  100. * * `clonedElement` - is a clone of the original `element` passed into the compiler.
  101. * * `scope` - is the current scope with which the linking function is working with.
  102. *
  103. * Calling the linking function returns the element of the template. It is either the original element
  104. * passed in, or the clone of the element if the `cloneAttachFn` is provided.
  105. *
  106. * After linking the view is not updated until after a call to $digest which typically is done by
  107. * Angular automatically.
  108. *
  109. * If you need access to the bound view, there are two ways to do it:
  110. *
  111. * - If you are not asking the linking function to clone the template, create the DOM element(s)
  112. * before you send them to the compiler and keep this reference around.
  113. * <pre>
  114. * var element = $compile('<p>{{total}}</p>')(scope);
  115. * </pre>
  116. *
  117. * - if on the other hand, you need the element to be cloned, the view reference from the original
  118. * example would not point to the clone, but rather to the original template that was cloned. In
  119. * this case, you can access the clone via the cloneAttachFn:
  120. * <pre>
  121. * var templateHTML = angular.element('<p>{{total}}</p>'),
  122. * scope = ....;
  123. *
  124. * var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) {
  125. * //attach the clone to DOM document at the right place
  126. * });
  127. *
  128. * //now we have reference to the cloned DOM via `clone`
  129. * </pre>
  130. *
  131. *
  132. * For information on how the compiler works, see the
  133. * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
  134. */
  135. /**
  136. * @ngdoc service
  137. * @name ng.$compileProvider
  138. * @function
  139. *
  140. * @description
  141. */
  142. $CompileProvider.$inject = ['$provide'];
  143. function $CompileProvider($provide) {
  144. var hasDirectives = {},
  145. Suffix = 'Directive',
  146. COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
  147. CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
  148. MULTI_ROOT_TEMPLATE_ERROR = 'Template must have exactly one root element. was: ';
  149. /**
  150. * @ngdoc function
  151. * @name ng.$compileProvider#directive
  152. * @methodOf ng.$compileProvider
  153. * @function
  154. *
  155. * @description
  156. * Register a new directives with the compiler.
  157. *
  158. * @param {string} name Name of the directive in camel-case. (ie <code>ngBind</code> which will match as
  159. * <code>ng-bind</code>).
  160. * @param {function} directiveFactory An injectable directive factroy function. See {@link guide/directive} for more
  161. * info.
  162. * @returns {ng.$compileProvider} Self for chaining.
  163. */
  164. this.directive = function registerDirective(name, directiveFactory) {
  165. if (isString(name)) {
  166. assertArg(directiveFactory, 'directive');
  167. if (!hasDirectives.hasOwnProperty(name)) {
  168. hasDirectives[name] = [];
  169. $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
  170. function($injector, $exceptionHandler) {
  171. var directives = [];
  172. forEach(hasDirectives[name], function(directiveFactory) {
  173. try {
  174. var directive = $injector.invoke(directiveFactory);
  175. if (isFunction(directive)) {
  176. directive = { compile: valueFn(directive) };
  177. } else if (!directive.compile && directive.link) {
  178. directive.compile = valueFn(directive.link);
  179. }
  180. directive.priority = directive.priority || 0;
  181. directive.name = directive.name || name;
  182. directive.require = directive.require || (directive.controller && directive.name);
  183. directive.restrict = directive.restrict || 'A';
  184. directives.push(directive);
  185. } catch (e) {
  186. $exceptionHandler(e);
  187. }
  188. });
  189. return directives;
  190. }]);
  191. }
  192. hasDirectives[name].push(directiveFactory);
  193. } else {
  194. forEach(name, reverseParams(registerDirective));
  195. }
  196. return this;
  197. };
  198. this.$get = [
  199. '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
  200. '$controller', '$rootScope',
  201. function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
  202. $controller, $rootScope) {
  203. var Attributes = function(element, attr) {
  204. this.$$element = element;
  205. this.$attr = attr || {};
  206. };
  207. Attributes.prototype = {
  208. $normalize: directiveNormalize,
  209. /**
  210. * Set a normalized attribute on the element in a way such that all directives
  211. * can share the attribute. This function properly handles boolean attributes.
  212. * @param {string} key Normalized key. (ie ngAttribute)
  213. * @param {string|boolean} value The value to set. If `null` attribute will be deleted.
  214. * @param {boolean=} writeAttr If false, does not write the value to DOM element attribute.
  215. * Defaults to true.
  216. * @param {string=} attrName Optional none normalized name. Defaults to key.
  217. */
  218. $set: function(key, value, writeAttr, attrName) {
  219. var booleanKey = getBooleanAttrName(this.$$element[0], key),
  220. $$observers = this.$$observers;
  221. if (booleanKey) {
  222. this.$$element.prop(key, value);
  223. attrName = booleanKey;
  224. }
  225. this[key] = value;
  226. // translate normalized key to actual key
  227. if (attrName) {
  228. this.$attr[key] = attrName;
  229. } else {
  230. attrName = this.$attr[key];
  231. if (!attrName) {
  232. this.$attr[key] = attrName = snake_case(key, '-');
  233. }
  234. }
  235. if (writeAttr !== false) {
  236. if (value === null || value === undefined) {
  237. this.$$element.removeAttr(attrName);
  238. } else {
  239. this.$$element.attr(attrName, value);
  240. }
  241. }
  242. // fire observers
  243. $$observers && forEach($$observers[key], function(fn) {
  244. try {
  245. fn(value);
  246. } catch (e) {
  247. $exceptionHandler(e);
  248. }
  249. });
  250. },
  251. /**
  252. * Observe an interpolated attribute.
  253. * The observer will never be called, if given attribute is not interpolated.
  254. *
  255. * @param {string} key Normalized key. (ie ngAttribute) .
  256. * @param {function(*)} fn Function that will be called whenever the attribute value changes.
  257. * @returns {function(*)} the `fn` Function passed in.
  258. */
  259. $observe: function(key, fn) {
  260. var attrs = this,
  261. $$observers = (attrs.$$observers || (attrs.$$observers = {})),
  262. listeners = ($$observers[key] || ($$observers[key] = []));
  263. listeners.push(fn);
  264. $rootScope.$evalAsync(function() {
  265. if (!listeners.$$inter) {
  266. // no one registered attribute interpolation function, so lets call it manually
  267. fn(attrs[key]);
  268. }
  269. });
  270. return fn;
  271. }
  272. };
  273. var startSymbol = $interpolate.startSymbol(),
  274. endSymbol = $interpolate.endSymbol(),
  275. denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
  276. ? identity
  277. : function denormalizeTemplate(template) {
  278. return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
  279. };
  280. return compile;
  281. //================================
  282. function compile($compileNodes, transcludeFn, maxPriority) {
  283. if (!($compileNodes instanceof jqLite)) {
  284. // jquery always rewraps, where as we need to preserve the original selector so that we can modify it.
  285. $compileNodes = jqLite($compileNodes);
  286. }
  287. // We can not compile top level text elements since text nodes can be merged and we will
  288. // not be able to attach scope data to them, so we will wrap them in <span>
  289. forEach($compileNodes, function(node, index){
  290. if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
  291. $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
  292. }
  293. });
  294. var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority);
  295. return function publicLinkFn(scope, cloneConnectFn){
  296. assertArg(scope, 'scope');
  297. // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
  298. // and sometimes changes the structure of the DOM.
  299. var $linkNode = cloneConnectFn
  300. ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
  301. : $compileNodes;
  302. $linkNode.data('$scope', scope);
  303. safeAddClass($linkNode, 'ng-scope');
  304. if (cloneConnectFn) cloneConnectFn($linkNode, scope);
  305. if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
  306. return $linkNode;
  307. };
  308. }
  309. function wrongMode(localName, mode) {
  310. throw Error("Unsupported '" + mode + "' for '" + localName + "'.");
  311. }
  312. function safeAddClass($element, className) {
  313. try {
  314. $element.addClass(className);
  315. } catch(e) {
  316. // ignore, since it means that we are trying to set class on
  317. // SVG element, where class name is read-only.
  318. }
  319. }
  320. /**
  321. * Compile function matches each node in nodeList against the directives. Once all directives
  322. * for a particular node are collected their compile functions are executed. The compile
  323. * functions return values - the linking functions - are combined into a composite linking
  324. * function, which is the a linking function for the node.
  325. *
  326. * @param {NodeList} nodeList an array of nodes to compile
  327. * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
  328. * scope argument is auto-generated to the new child of the transcluded parent scope.
  329. * @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then the
  330. * rootElement must be set the jqLite collection of the compile root. This is
  331. * needed so that the jqLite collection items can be replaced with widgets.
  332. * @param {number=} max directive priority
  333. * @returns {?function} A composite linking function of all of the matched directives or null.
  334. */
  335. function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority) {
  336. var linkFns = [],
  337. nodeLinkFn, childLinkFn, directives, attrs, linkFnFound;
  338. for(var i = 0; i < nodeList.length; i++) {
  339. attrs = new Attributes();
  340. // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
  341. directives = collectDirectives(nodeList[i], [], attrs, maxPriority);
  342. nodeLinkFn = (directives.length)
  343. ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement)
  344. : null;
  345. childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes.length)
  346. ? null
  347. : compileNodes(nodeList[i].childNodes,
  348. nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
  349. linkFns.push(nodeLinkFn);
  350. linkFns.push(childLinkFn);
  351. linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn);
  352. }
  353. // return a linking function if we have found anything, null otherwise
  354. return linkFnFound ? compositeLinkFn : null;
  355. function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
  356. var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn, i, ii, n;
  357. // copy nodeList so that linking doesn't break due to live list updates.
  358. var stableNodeList = [];
  359. for (i = 0, ii = nodeList.length; i < ii; i++) {
  360. stableNodeList.push(nodeList[i]);
  361. }
  362. for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
  363. node = stableNodeList[n];
  364. nodeLinkFn = linkFns[i++];
  365. childLinkFn = linkFns[i++];
  366. if (nodeLinkFn) {
  367. if (nodeLinkFn.scope) {
  368. childScope = scope.$new(isObject(nodeLinkFn.scope));
  369. jqLite(node).data('$scope', childScope);
  370. } else {
  371. childScope = scope;
  372. }
  373. childTranscludeFn = nodeLinkFn.transclude;
  374. if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) {
  375. nodeLinkFn(childLinkFn, childScope, node, $rootElement,
  376. (function(transcludeFn) {
  377. return function(cloneFn) {
  378. var transcludeScope = scope.$new();
  379. return transcludeFn(transcludeScope, cloneFn).
  380. bind('$destroy', bind(transcludeScope, transcludeScope.$destroy));
  381. };
  382. })(childTranscludeFn || transcludeFn)
  383. );
  384. } else {
  385. nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn);
  386. }
  387. } else if (childLinkFn) {
  388. childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
  389. }
  390. }
  391. }
  392. }
  393. /**
  394. * Looks for directives on the given node and adds them to the directive collection which is
  395. * sorted.
  396. *
  397. * @param node Node to search.
  398. * @param directives An array to which the directives are added to. This array is sorted before
  399. * the function returns.
  400. * @param attrs The shared attrs object which is used to populate the normalized attributes.
  401. * @param {number=} maxPriority Max directive priority.
  402. */
  403. function collectDirectives(node, directives, attrs, maxPriority) {
  404. var nodeType = node.nodeType,
  405. attrsMap = attrs.$attr,
  406. match,
  407. className;
  408. switch(nodeType) {
  409. case 1: /* Element */
  410. // use the node name: <directive>
  411. addDirective(directives,
  412. directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority);
  413. // iterate over the attributes
  414. for (var attr, name, nName, value, nAttrs = node.attributes,
  415. j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
  416. attr = nAttrs[j];
  417. if (attr.specified) {
  418. name = attr.name;
  419. nName = directiveNormalize(name.toLowerCase());
  420. attrsMap[nName] = name;
  421. attrs[nName] = value = trim((msie && name == 'href')
  422. ? decodeURIComponent(node.getAttribute(name, 2))
  423. : attr.value);
  424. if (getBooleanAttrName(node, nName)) {
  425. attrs[nName] = true; // presence means true
  426. }
  427. addAttrInterpolateDirective(node, directives, value, nName);
  428. addDirective(directives, nName, 'A', maxPriority);
  429. }
  430. }
  431. // use class as directive
  432. className = node.className;
  433. if (isString(className) && className !== '') {
  434. while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
  435. nName = directiveNormalize(match[2]);
  436. if (addDirective(directives, nName, 'C', maxPriority)) {
  437. attrs[nName] = trim(match[3]);
  438. }
  439. className = className.substr(match.index + match[0].length);
  440. }
  441. }
  442. break;
  443. case 3: /* Text Node */
  444. addTextInterpolateDirective(directives, node.nodeValue);
  445. break;
  446. case 8: /* Comment */
  447. try {
  448. match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
  449. if (match) {
  450. nName = directiveNormalize(match[1]);
  451. if (addDirective(directives, nName, 'M', maxPriority)) {
  452. attrs[nName] = trim(match[2]);
  453. }
  454. }
  455. } catch (e) {
  456. // turns out that under some circumstances IE9 throws errors when one attempts to read comment's node value.
  457. // Just ignore it and continue. (Can't seem to reproduce in test case.)
  458. }
  459. break;
  460. }
  461. directives.sort(byPriority);
  462. return directives;
  463. }
  464. /**
  465. * Once the directives have been collected their compile functions is executed. This method
  466. * is responsible for inlining directive templates as well as terminating the application
  467. * of the directives if the terminal directive has been reached..
  468. *
  469. * @param {Array} directives Array of collected directives to execute their compile function.
  470. * this needs to be pre-sorted by priority order.
  471. * @param {Node} compileNode The raw DOM node to apply the compile functions to
  472. * @param {Object} templateAttrs The shared attribute function
  473. * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the
  474. * scope argument is auto-generated to the new child of the transcluded parent scope.
  475. * @param {DOMElement} $rootElement If we are working on the root of the compile tree then this
  476. * argument has the root jqLite array so that we can replace widgets on it.
  477. * @returns linkFn
  478. */
  479. function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, $rootElement) {
  480. var terminalPriority = -Number.MAX_VALUE,
  481. preLinkFns = [],
  482. postLinkFns = [],
  483. newScopeDirective = null,
  484. newIsolateScopeDirective = null,
  485. templateDirective = null,
  486. $compileNode = templateAttrs.$$element = jqLite(compileNode),
  487. directive,
  488. directiveName,
  489. $template,
  490. transcludeDirective,
  491. childTranscludeFn = transcludeFn,
  492. controllerDirectives,
  493. linkFn,
  494. directiveValue;
  495. // executes all directives on the current element
  496. for(var i = 0, ii = directives.length; i < ii; i++) {
  497. directive = directives[i];
  498. $template = undefined;
  499. if (terminalPriority > directive.priority) {
  500. break; // prevent further processing of directives
  501. }
  502. if (directiveValue = directive.scope) {
  503. assertNoDuplicate('isolated scope', newIsolateScopeDirective, directive, $compileNode);
  504. if (isObject(directiveValue)) {
  505. safeAddClass($compileNode, 'ng-isolate-scope');
  506. newIsolateScopeDirective = directive;
  507. }
  508. safeAddClass($compileNode, 'ng-scope');
  509. newScopeDirective = newScopeDirective || directive;
  510. }
  511. directiveName = directive.name;
  512. if (directiveValue = directive.controller) {
  513. controllerDirectives = controllerDirectives || {};
  514. assertNoDuplicate("'" + directiveName + "' controller",
  515. controllerDirectives[directiveName], directive, $compileNode);
  516. controllerDirectives[directiveName] = directive;
  517. }
  518. if (directiveValue = directive.transclude) {
  519. assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
  520. transcludeDirective = directive;
  521. terminalPriority = directive.priority;
  522. if (directiveValue == 'element') {
  523. $template = jqLite(compileNode);
  524. $compileNode = templateAttrs.$$element =
  525. jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
  526. compileNode = $compileNode[0];
  527. replaceWith($rootElement, jqLite($template[0]), compileNode);
  528. childTranscludeFn = compile($template, transcludeFn, terminalPriority);
  529. } else {
  530. $template = jqLite(JQLiteClone(compileNode)).contents();
  531. $compileNode.html(''); // clear contents
  532. childTranscludeFn = compile($template, transcludeFn);
  533. }
  534. }
  535. if ((directiveValue = directive.template)) {
  536. assertNoDuplicate('template', templateDirective, directive, $compileNode);
  537. templateDirective = directive;
  538. directiveValue = denormalizeTemplate(directiveValue);
  539. if (directive.replace) {
  540. $template = jqLite('<div>' +
  541. trim(directiveValue) +
  542. '</div>').contents();
  543. compileNode = $template[0];
  544. if ($template.length != 1 || compileNode.nodeType !== 1) {
  545. throw new Error(MULTI_ROOT_TEMPLATE_ERROR + directiveValue);
  546. }
  547. replaceWith($rootElement, $compileNode, compileNode);
  548. var newTemplateAttrs = {$attr: {}};
  549. // combine directives from the original node and from the template:
  550. // - take the array of directives for this element
  551. // - split it into two parts, those that were already applied and those that weren't
  552. // - collect directives from the template, add them to the second group and sort them
  553. // - append the second group with new directives to the first group
  554. directives = directives.concat(
  555. collectDirectives(
  556. compileNode,
  557. directives.splice(i + 1, directives.length - (i + 1)),
  558. newTemplateAttrs
  559. )
  560. );
  561. mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
  562. ii = directives.length;
  563. } else {
  564. $compileNode.html(directiveValue);
  565. }
  566. }
  567. if (directive.templateUrl) {
  568. assertNoDuplicate('template', templateDirective, directive, $compileNode);
  569. templateDirective = directive;
  570. nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i),
  571. nodeLinkFn, $compileNode, templateAttrs, $rootElement, directive.replace,
  572. childTranscludeFn);
  573. ii = directives.length;
  574. } else if (directive.compile) {
  575. try {
  576. linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
  577. if (isFunction(linkFn)) {
  578. addLinkFns(null, linkFn);
  579. } else if (linkFn) {
  580. addLinkFns(linkFn.pre, linkFn.post);
  581. }
  582. } catch (e) {
  583. $exceptionHandler(e, startingTag($compileNode));
  584. }
  585. }
  586. if (directive.terminal) {
  587. nodeLinkFn.terminal = true;
  588. terminalPriority = Math.max(terminalPriority, directive.priority);
  589. }
  590. }
  591. nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope;
  592. nodeLinkFn.transclude = transcludeDirective && childTranscludeFn;
  593. // might be normal or delayed nodeLinkFn depending on if templateUrl is present
  594. return nodeLinkFn;
  595. ////////////////////
  596. function addLinkFns(pre, post) {
  597. if (pre) {
  598. pre.require = directive.require;
  599. preLinkFns.push(pre);
  600. }
  601. if (post) {
  602. post.require = directive.require;
  603. postLinkFns.push(post);
  604. }
  605. }
  606. function getControllers(require, $element) {
  607. var value, retrievalMethod = 'data', optional = false;
  608. if (isString(require)) {
  609. while((value = require.charAt(0)) == '^' || value == '?') {
  610. require = require.substr(1);
  611. if (value == '^') {
  612. retrievalMethod = 'inheritedData';
  613. }
  614. optional = optional || value == '?';
  615. }
  616. value = $element[retrievalMethod]('$' + require + 'Controller');
  617. if (!value && !optional) {
  618. throw Error("No controller: " + require);
  619. }
  620. return value;
  621. } else if (isArray(require)) {
  622. value = [];
  623. forEach(require, function(require) {
  624. value.push(getControllers(require, $element));
  625. });
  626. }
  627. return value;
  628. }
  629. function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
  630. var attrs, $element, i, ii, linkFn, controller;
  631. if (compileNode === linkNode) {
  632. attrs = templateAttrs;
  633. } else {
  634. attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr));
  635. }
  636. $element = attrs.$$element;
  637. if (newIsolateScopeDirective) {
  638. var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/;
  639. var parentScope = scope.$parent || scope;
  640. forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
  641. var match = definiton.match(LOCAL_REGEXP) || [],
  642. attrName = match[2]|| scopeName,
  643. mode = match[1], // @, =, or &
  644. lastValue,
  645. parentGet, parentSet;
  646. switch (mode) {
  647. case '@': {
  648. attrs.$observe(attrName, function(value) {
  649. scope[scopeName] = value;
  650. });
  651. attrs.$$observers[attrName].$$scope = parentScope;
  652. break;
  653. }
  654. case '=': {
  655. parentGet = $parse(attrs[attrName]);
  656. parentSet = parentGet.assign || function() {
  657. // reset the change, or we will throw this exception on every $digest
  658. lastValue = scope[scopeName] = parentGet(parentScope);
  659. throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] +
  660. ' (directive: ' + newIsolateScopeDirective.name + ')');
  661. };
  662. lastValue = scope[scopeName] = parentGet(parentScope);
  663. scope.$watch(function parentValueWatch() {
  664. var parentValue = parentGet(parentScope);
  665. if (parentValue !== scope[scopeName]) {
  666. // we are out of sync and need to copy
  667. if (parentValue !== lastValue) {
  668. // parent changed and it has precedence
  669. lastValue = scope[scopeName] = parentValue;
  670. } else {
  671. // if the parent can be assigned then do so
  672. parentSet(parentScope, parentValue = lastValue = scope[scopeName]);
  673. }
  674. }
  675. return parentValue;
  676. });
  677. break;
  678. }
  679. case '&': {
  680. parentGet = $parse(attrs[attrName]);
  681. scope[scopeName] = function(locals) {
  682. return parentGet(parentScope, locals);
  683. }
  684. break;
  685. }
  686. default: {
  687. throw Error('Invalid isolate scope definition for directive ' +
  688. newIsolateScopeDirective.name + ': ' + definiton);
  689. }
  690. }
  691. });
  692. }
  693. if (controllerDirectives) {
  694. forEach(controllerDirectives, function(directive) {
  695. var locals = {
  696. $scope: scope,
  697. $element: $element,
  698. $attrs: attrs,
  699. $transclude: boundTranscludeFn
  700. };
  701. controller = directive.controller;
  702. if (controller == '@') {
  703. controller = attrs[directive.name];
  704. }
  705. $element.data(
  706. '$' + directive.name + 'Controller',
  707. $controller(controller, locals));
  708. });
  709. }
  710. // PRELINKING
  711. for(i = 0, ii = preLinkFns.length; i < ii; i++) {
  712. try {
  713. linkFn = preLinkFns[i];
  714. linkFn(scope, $element, attrs,
  715. linkFn.require && getControllers(linkFn.require, $element));
  716. } catch (e) {
  717. $exceptionHandler(e, startingTag($element));
  718. }
  719. }
  720. // RECURSION
  721. childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
  722. // POSTLINKING
  723. for(i = 0, ii = postLinkFns.length; i < ii; i++) {
  724. try {
  725. linkFn = postLinkFns[i];
  726. linkFn(scope, $element, attrs,
  727. linkFn.require && getControllers(linkFn.require, $element));
  728. } catch (e) {
  729. $exceptionHandler(e, startingTag($element));
  730. }
  731. }
  732. }
  733. }
  734. /**
  735. * looks up the directive and decorates it with exception handling and proper parameters. We
  736. * call this the boundDirective.
  737. *
  738. * @param {string} name name of the directive to look up.
  739. * @param {string} location The directive must be found in specific format.
  740. * String containing any of theses characters:
  741. *
  742. * * `E`: element name
  743. * * `A': attribute
  744. * * `C`: class
  745. * * `M`: comment
  746. * @returns true if directive was added.
  747. */
  748. function addDirective(tDirectives, name, location, maxPriority) {
  749. var match = false;
  750. if (hasDirectives.hasOwnProperty(name)) {
  751. for(var directive, directives = $injector.get(name + Suffix),
  752. i = 0, ii = directives.length; i<ii; i++) {
  753. try {
  754. directive = directives[i];
  755. if ( (maxPriority === undefined || maxPriority > directive.priority) &&
  756. directive.restrict.indexOf(location) != -1) {
  757. tDirectives.push(directive);
  758. match = true;
  759. }
  760. } catch(e) { $exceptionHandler(e); }
  761. }
  762. }
  763. return match;
  764. }
  765. /**
  766. * When the element is replaced with HTML template then the new attributes
  767. * on the template need to be merged with the existing attributes in the DOM.
  768. * The desired effect is to have both of the attributes present.
  769. *
  770. * @param {object} dst destination attributes (original DOM)
  771. * @param {object} src source attributes (from the directive template)
  772. */
  773. function mergeTemplateAttributes(dst, src) {
  774. var srcAttr = src.$attr,
  775. dstAttr = dst.$attr,
  776. $element = dst.$$element;
  777. // reapply the old attributes to the new element
  778. forEach(dst, function(value, key) {
  779. if (key.charAt(0) != '$') {
  780. if (src[key]) {
  781. value += (key === 'style' ? ';' : ' ') + src[key];
  782. }
  783. dst.$set(key, value, true, srcAttr[key]);
  784. }
  785. });
  786. // copy the new attributes on the old attrs object
  787. forEach(src, function(value, key) {
  788. if (key == 'class') {
  789. safeAddClass($element, value);
  790. dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
  791. } else if (key == 'style') {
  792. $element.attr('style', $element.attr('style') + ';' + value);
  793. } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
  794. dst[key] = value;
  795. dstAttr[key] = srcAttr[key];
  796. }
  797. });
  798. }
  799. function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs,
  800. $rootElement, replace, childTranscludeFn) {
  801. var linkQueue = [],
  802. afterTemplateNodeLinkFn,
  803. afterTemplateChildLinkFn,
  804. beforeTemplateCompileNode = $compileNode[0],
  805. origAsyncDirective = directives.shift(),
  806. // The fact that we have to copy and patch the directive seems wrong!
  807. derivedSyncDirective = extend({}, origAsyncDirective, {
  808. controller: null, templateUrl: null, transclude: null, scope: null
  809. });
  810. $compileNode.html('');
  811. $http.get(origAsyncDirective.templateUrl, {cache: $templateCache}).
  812. success(function(content) {
  813. var compileNode, tempTemplateAttrs, $template;
  814. content = denormalizeTemplate(content);
  815. if (replace) {
  816. $template = jqLite('<div>' + trim(content) + '</div>').contents();
  817. compileNode = $template[0];
  818. if ($template.length != 1 || compileNode.nodeType !== 1) {
  819. throw new Error(MULTI_ROOT_TEMPLATE_ERROR + content);
  820. }
  821. tempTemplateAttrs = {$attr: {}};
  822. replaceWith($rootElement, $compileNode, compileNode);
  823. collectDirectives(compileNode, directives, tempTemplateAttrs);
  824. mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
  825. } else {
  826. compileNode = beforeTemplateCompileNode;
  827. $compileNode.html(content);
  828. }
  829. directives.unshift(derivedSyncDirective);
  830. afterTemplateNodeLinkFn = applyDirectivesToNode(directives, $compileNode, tAttrs, childTranscludeFn);
  831. afterTemplateChildLinkFn = compileNodes($compileNode.contents(), childTranscludeFn);
  832. while(linkQueue.length) {
  833. var controller = linkQueue.pop(),
  834. linkRootElement = linkQueue.pop(),
  835. beforeTemplateLinkNode = linkQueue.pop(),
  836. scope = linkQueue.pop(),
  837. linkNode = compileNode;
  838. if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
  839. // it was cloned therefore we have to clone as well.
  840. linkNode = JQLiteClone(compileNode);
  841. replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
  842. }
  843. afterTemplateNodeLinkFn(function() {
  844. beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller);
  845. }, scope, linkNode, $rootElement, controller);
  846. }
  847. linkQueue = null;
  848. }).
  849. error(function(response, code, headers, config) {
  850. throw Error('Failed to load template: ' + config.url);
  851. });
  852. return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) {
  853. if (linkQueue) {
  854. linkQueue.push(scope);
  855. linkQueue.push(node);
  856. linkQueue.push(rootElement);
  857. linkQueue.push(controller);
  858. } else {
  859. afterTemplateNodeLinkFn(function() {
  860. beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller);
  861. }, scope, node, rootElement, controller);
  862. }
  863. };
  864. }
  865. /**
  866. * Sorting function for bound directives.
  867. */
  868. function byPriority(a, b) {
  869. return b.priority - a.priority;
  870. }
  871. function assertNoDuplicate(what, previousDirective, directive, element) {
  872. if (previousDirective) {
  873. throw Error('Multiple directives [' + previousDirective.name + ', ' +
  874. directive.name + '] asking for ' + what + ' on: ' + startingTag(element));
  875. }
  876. }
  877. function addTextInterpolateDirective(directives, text) {
  878. var interpolateFn = $interpolate(text, true);
  879. if (interpolateFn) {
  880. directives.push({
  881. priority: 0,
  882. compile: valueFn(function textInterpolateLinkFn(scope, node) {
  883. var parent = node.parent(),
  884. bindings = parent.data('$binding') || [];
  885. bindings.push(interpolateFn);
  886. safeAddClass(parent.data('$binding', bindings), 'ng-binding');
  887. scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
  888. node[0].nodeValue = value;
  889. });
  890. })
  891. });
  892. }
  893. }
  894. function addAttrInterpolateDirective(node, directives, value, name) {
  895. var interpolateFn = $interpolate(value, true);
  896. // no interpolation found -> ignore
  897. if (!interpolateFn) return;
  898. directives.push({
  899. priority: 100,
  900. compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) {
  901. var $$observers = (attr.$$observers || (attr.$$observers = {}));
  902. if (name === 'class') {
  903. // we need to interpolate classes again, in the case the element was replaced
  904. // and therefore the two class attrs got merged - we want to interpolate the result
  905. interpolateFn = $interpolate(attr[name], true);
  906. }
  907. attr[name] = undefined;
  908. ($$observers[name] || ($$observers[name] = [])).$$inter = true;
  909. (attr.$$observers && attr.$$observers[name].$$scope || scope).
  910. $watch(interpolateFn, function interpolateFnWatchAction(value) {
  911. attr.$set(name, value);
  912. });
  913. })
  914. });
  915. }
  916. /**
  917. * This is a special jqLite.replaceWith, which can replace items which
  918. * have no parents, provided that the containing jqLite collection is provided.
  919. *
  920. * @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
  921. * in the root of the tree.
  922. * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell,
  923. * but replace its DOM node reference.
  924. * @param {Node} newNode The new DOM node.
  925. */
  926. function replaceWith($rootElement, $element, newNode) {
  927. var oldNode = $element[0],
  928. parent = oldNode.parentNode,
  929. i, ii;
  930. if ($rootElement) {
  931. for(i = 0, ii = $rootElement.length; i < ii; i++) {
  932. if ($rootElement[i] == oldNode) {
  933. $rootElement[i] = newNode;
  934. break;
  935. }
  936. }
  937. }
  938. if (parent) {
  939. parent.replaceChild(newNode, oldNode);
  940. }
  941. newNode[jqLite.expando] = oldNode[jqLite.expando];
  942. $element[0] = newNode;
  943. }
  944. }];
  945. }
  946. var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
  947. /**
  948. * Converts all accepted directives format into proper directive name.
  949. * All of these will become 'myDirective':
  950. * my:DiRective
  951. * my-directive
  952. * x-my-directive
  953. * data-my:directive
  954. *
  955. * Also there is special case for Moz prefix starting with upper case letter.
  956. * @param name Name to normalize
  957. */
  958. function directiveNormalize(name) {
  959. return camelCase(name.replace(PREFIX_REGEXP, ''));
  960. }
  961. /**
  962. * @ngdoc object
  963. * @name ng.$compile.directive.Attributes
  964. * @description
  965. *
  966. * A shared object between directive compile / linking functions which contains normalized DOM element
  967. * attributes. The the values reflect current binding state `{{ }}`. The normalization is needed
  968. * since all of these are treated as equivalent in Angular:
  969. *
  970. * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a">
  971. */
  972. /**
  973. * @ngdoc property
  974. * @name ng.$compile.directive.Attributes#$attr
  975. * @propertyOf ng.$compile.directive.Attributes
  976. * @returns {object} A map of DOM element attribute names to the normalized name. This is
  977. * needed to do reverse lookup from normalized name back to actual name.
  978. */
  979. /**
  980. * @ngdoc function
  981. * @name ng.$compile.directive.Attributes#$set
  982. * @methodOf ng.$compile.directive.Attributes
  983. * @function
  984. *
  985. * @description
  986. * Set DOM element attribute value.
  987. *
  988. *
  989. * @param {string} name Normalized element attribute name of the property to modify. The name is
  990. * revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr}
  991. * property to the original name.
  992. * @param {string} value Value to set the attribute to.
  993. */
  994. /**
  995. * Closure compiler type information
  996. */
  997. function nodesetLinkingFn(
  998. /* angular.Scope */ scope,
  999. /* NodeList */ nodeList,
  1000. /* Element */ rootElement,
  1001. /* function(Function) */ boundTranscludeFn
  1002. ){}
  1003. function directiveLinkingFn(
  1004. /* nodesetLinkingFn */ nodesetLinkingFn,
  1005. /* angular.Scope */ scope,
  1006. /* Node */ node,
  1007. /* Element */ rootElement,
  1008. /* function(Function) */ boundTranscludeFn
  1009. ){}