aboutsummaryrefslogtreecommitdiffstats
path: root/src/service/compiler.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/service/compiler.js')
-rw-r--r--src/service/compiler.js1033
1 files changed, 715 insertions, 318 deletions
diff --git a/src/service/compiler.js b/src/service/compiler.js
index adf1ffa9..6185c909 100644
--- a/src/service/compiler.js
+++ b/src/service/compiler.js
@@ -1,356 +1,753 @@
'use strict';
+/**
+ * @ngdoc function
+ * @name angular.module.ng.$compile
+ * @function
+ *
+ * @description
+ * Compiles a piece of HTML string or DOM into a template and produces a template function, which
+ * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together.
+ *
+ * The compilation is a process of walking the DOM tree and trying to match DOM elements to
+ * {@link angular.module.ng.$compileProvider.directive directives}. For each match it
+ * executes corresponding template function and collects the
+ * instance functions into a single template function which is then returned.
+ *
+ * The template function can then be used once to produce the view or as it is the case with
+ * {@link angular.module.ng.$compileProvider.directive.ng:repeat repeater} many-times, in which
+ * case each call results in a view that is a DOM clone of the original template.
+ *
+ <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(scope, value) {
+ // when the 'compile' expression changes
+ // assign it into the current DOM
+ element.html(value);
-function $CompileProvider(){
- this.$get = ['$injector', '$exceptionHandler', '$textMarkup', '$attrMarkup', '$directive', '$widget',
- function( $injector, $exceptionHandler, $textMarkup, $attrMarkup, $directive, $widget){
- /**
- * Template provides directions an how to bind to a given element.
- * It contains a list of init functions which need to be called to
- * bind to a new instance of elements. It also provides a list
- * of child paths which contain child templates
- */
- function Template() {
- this.paths = [];
- this.children = [];
- this.linkFns = [];
- this.newScope = false;
+ // 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() {
+ this.name = 'Angular';
+ this.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>
- Template.prototype = {
- link: function(element, scope) {
- var childScope = scope,
- locals = {$element: element};
- if (this.newScope) {
- childScope = scope.$new();
- element.data($$scope, childScope);
- }
- forEach(this.linkFns, function(fn) {
- try {
- if (isArray(fn) || fn.$inject) {
- $injector.invoke(fn, childScope, locals);
- } else {
- fn.call(childScope, element);
+ *
+ *
+ * @param {string|DOMElement} element Element or HTML string to compile into a template function.
+ * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template
+ * (a DOM element/tree) to a scope. Where:
+ *
+ * * `scope` - A {@link angular.module.ng.$rootScope.Scope Scope} to bind to.
+ * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
+ * `template` and call the `cloneAttachFn` function allowing the caller to attach the
+ * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
+ * called as: <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 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.
+ * <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>
+ *
+ *
+ * Compiler Methods For Widgets and Directives:
+ *
+ * The following methods are available for use when you write your own widgets, directives,
+ * and markup.
+ *
+ *
+ * For information on how the compiler works, see the
+ * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide.
+ */
+
+
+$CompileProvider.$inject = ['$provide'];
+function $CompileProvider($provide) {
+ var hasDirectives = {},
+ Suffix = 'Directive',
+ COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,
+ CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/,
+ CONTENT_REGEXP = /\<\<content\>\>/i,
+ HAS_ROOT_ELEMENT = /^\<[\s\S]*\>$/,
+ SIDE_EFFECT_ATTRS = {};
+
+ forEach('src,href,multiple,selected,checked,disabled,readonly,required'.split(','), function(name) {
+ SIDE_EFFECT_ATTRS[name] = name;
+ SIDE_EFFECT_ATTRS[directiveNormalize('ng_' + name)] = name;
+ });
+
+
+ this.directive = function registerDirective(name, directiveFactory) {
+ if (isString(name)) {
+ assertArg(directiveFactory, 'directive');
+ if (!hasDirectives.hasOwnProperty(name)) {
+ hasDirectives[name] = [];
+ $provide.factory(name + Suffix, ['$injector', '$exceptionHandler',
+ function($injector, $exceptionHandler) {
+ var directives = [];
+ forEach(hasDirectives[name], function(directiveFactory) {
+ try {
+ var directive = $injector.invoke(directiveFactory);
+ if (isFunction(directive)) {
+ directive = { compile: valueFn(directive) };
+ } else if (!directive.compile && directive.link) {
+ directive.compile = valueFn(directive.link);
+ }
+ directive.priority = directive.priority || 0;
+ directive.name = name;
+ directive.restrict = directive.restrict || 'EACM';
+ directives.push(directive);
+ } catch (e) {
+ $exceptionHandler(e);
}
- } catch (e) {
- $exceptionHandler(e);
+ });
+ return directives;
+ }]);
+ }
+ hasDirectives[name].push(directiveFactory);
+ } else {
+ forEach(name, reverseParams(registerDirective));
+ }
+ return this;
+ };
+
+
+ this.$get = ['$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache',
+ function($injector, $interpolate, $exceptionHandler, $http, $templateCache) {
+
+ return function(templateElement) {
+ templateElement = jqLite(templateElement);
+ var linkingFn = compileNodes(templateElement, templateElement);
+ return function(scope, cloneConnectFn){
+ assertArg(scope, 'scope');
+ // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
+ // and sometimes changes the structure of the DOM.
+ var element = cloneConnectFn
+ ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!!
+ : templateElement;
+ element.data('$scope', scope);
+ if (cloneConnectFn) cloneConnectFn(element, scope);
+ if (linkingFn) linkingFn(scope, element, element);
+ return element;
+ };
+ };
+
+ //================================
+
+ /**
+ * Compile function matches each node in nodeList against the directives. Once all directives
+ * for a particular node are collected their compile functions are executed. The compile
+ * functions return values - the linking functions - are combined into a composite linking
+ * function, which is the a linking function for the node.
+ *
+ * @param {NodeList} nodeList an array of nodes to compile
+ * @param {DOMElement=} rootElement If the nodeList is the root of the compilation tree then the
+ * rootElement must be set the jqLite collection of the compile root. This is
+ * needed so that the jqLite collection items can be replaced with widgets.
+ * @returns {?function} A composite linking function of all of the matched directives or null.
+ */
+ function compileNodes(nodeList, rootElement) {
+ var linkingFns = [],
+ directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound;
+
+ for(var i = 0, ii = nodeList.length; i < ii; i++) {
+ attrs = {
+ $attr: {},
+ $normalize: directiveNormalize,
+ $set: attrSetter
+ };
+ // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
+ directives = collectDirectives(nodeList[i], [], attrs);
+
+ directiveLinkingFn = (directives.length)
+ ? applyDirectivesToNode(directives, nodeList[i], attrs, rootElement)
+ : null;
+
+ childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal)
+ ? null
+ : compileNodes(nodeList[i].childNodes);
+
+ linkingFns.push(directiveLinkingFn);
+ linkingFns.push(childLinkingFn);
+ linkingFnFound = (linkingFnFound || directiveLinkingFn || childLinkingFn);
+ }
+
+ // return a linking function if we have found anything, null otherwise
+ return linkingFnFound ? linkingFn : null;
+
+ function linkingFn(scope, nodeList, rootElement) {
+ if (linkingFns.length != nodeList.length * 2) {
+ throw Error('Template changed structure!');
+ }
+
+ var childLinkingFn, directiveLinkingFn, node, childScope;
+
+ for(var i=0, n=0, ii=linkingFns.length; i<ii; n++) {
+ node = nodeList[n];
+ directiveLinkingFn = linkingFns[i++];
+ childLinkingFn = linkingFns[i++];
+
+ if (directiveLinkingFn) {
+ if (directiveLinkingFn.scope && !rootElement) {
+ childScope = scope.$new();
+ jqLite(node).data('$scope', childScope);
+ } else {
+ childScope = scope;
+ }
+ directiveLinkingFn(childLinkingFn, childScope, node, rootElement);
+ } else if (childLinkingFn) {
+ childLinkingFn(scope, node.childNodes);
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Looks for directives on the given node ands 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.
+ */
+ function collectDirectives(node, directives, attrs) {
+ 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');
+
+ // iterate over the attributes
+ for (var attr, name, nName, value, nAttrs = node.attributes,
+ j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
+ attr = nAttrs[j];
+ name = attr.name;
+ nName = directiveNormalize(name.toLowerCase());
+ attrsMap[nName] = name;
+ attrs[nName] = value = trim((msie && name == 'href')
+ ? decodeURIComponent(node.getAttribute(name, 2))
+ : attr.value);
+ if (BOOLEAN_ATTR[nName]) {
+ attrs[nName] = true; // presence means true
}
- });
- var i,
- childNodes = element[0].childNodes,
- children = this.children,
- paths = this.paths,
- length = paths.length;
- for (i = 0; i < length; i++) {
- // sometimes `element` can be modified by one of the linker functions in `this.linkFns`
- // and childNodes may be added or removed
- // TODO: element structure needs to be re-evaluated if new children added
- // if the childNode still exists
- if (childNodes[paths[i]])
- children[i].link(jqLite(childNodes[paths[i]]), childScope);
- else
- delete paths[i]; // if child no longer available, delete path
+ addAttrInterpolateDirective(directives, value, nName);
+ addDirective(directives, nName, 'A');
}
- },
-
- addLinkFn:function(linkingFn) {
- if (linkingFn) {
- this.linkFns.push(linkingFn);
+ // use class as directive
+ className = node.className;
+ while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
+ nName = directiveNormalize(match[2]);
+ if (addDirective(directives, nName, 'C')) {
+ attrs[nName] = trim(match[3]);
+ }
+ className = className.substr(match.index + match[0].length);
}
- },
+ break;
+ case 3: /* Text Node */
+ addTextInterpolateDirective(directives, node.nodeValue);
+ break;
+ case 8: /* Comment */
+ match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
+ if (match) {
+ nName = directiveNormalize(match[1]);
+ if (addDirective(directives, nName, 'M')) {
+ attrs[nName] = trim(match[2]);
+ }
+ }
+ break;
+ }
+
+ directives.sort(byPriority);
+ return directives;
+ }
- addChild: function(index, template) {
- if (template) {
- this.paths.push(index);
- this.children.push(template);
+ /**
+ * Once the directives have been collected their compile functions is executed. This method
+ * is responsible for inlining widget templates as well as terminating the application
+ * of the directives if the terminal directive has been reached..
+ *
+ * @param {Array} directives Array of collected directives to execute their compile function.
+ * this needs to be pre-sorted by priority order.
+ * @param {Node} templateNode The raw DOM node to apply the compile functions to
+ * @param {Object} templateAttrs The shared attribute function
+ * @param {DOMElement} rootElement If we are working on the root of the compile tree then this
+ * argument has the root jqLite array so that we can replace widgets on it.
+ * @returns linkingFn
+ */
+ function applyDirectivesToNode(directives, templateNode, templateAttrs, rootElement) {
+ var terminalPriority = -Number.MAX_VALUE,
+ preLinkingFns = [],
+ postLinkingFns = [],
+ newScopeDirective = null,
+ templateDirective = null,
+ delayedLinkingFn = null,
+ element = templateAttrs.$element = jqLite(templateNode),
+ directive, linkingFn;
+
+ // executes all directives on the current element
+ for(var i = 0, ii = directives.length; i < ii; i++) {
+ directive = directives[i];
+
+ if (terminalPriority > directive.priority) {
+ break; // prevent further processing of directives
+ }
+
+ if (directive.scope) {
+ assertNoDuplicate('new scope', newScopeDirective, directive, element);
+ newScopeDirective = directive;
+ }
+
+ if (directive.template) {
+ assertNoDuplicate('template', templateDirective, directive, element);
+ templateDirective = directive;
+
+ // include the contents of the original element into the template and replace the element
+ var content = directive.template.replace(CONTENT_REGEXP, element.html());
+ templateNode = jqLite(content)[0];
+ if (directive.replace) {
+ replaceWith(rootElement, element, templateNode);
+
+ var newTemplateAttrs = {$attr: {}};
+
+ // combine directives from the original node and from the template:
+ // - take the array of directives for this element
+ // - split it into two parts, those that were already applied and those that weren't
+ // - collect directives from the template, add them to the second group and sort them
+ // - append the second group with new directives to the first group
+ directives = directives.concat(
+ collectDirectives(
+ templateNode,
+ directives.splice(i + 1, directives.length - (i + 1)),
+ newTemplateAttrs
+ )
+ );
+ mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
+
+ ii = directives.length;
+ } else {
+ element.html(content);
+ }
+ }
+
+ if (directive.templateUrl) {
+ assertNoDuplicate('template', templateDirective, directive, element);
+ templateDirective = directive;
+ delayedLinkingFn = compileTemplateUrl(directives.splice(i, directives.length - i),
+ compositeLinkFn, element, templateAttrs, rootElement, directive.replace);
+ ii = directives.length;
+ } else if (directive.compile) {
+ try {
+ linkingFn = directive.compile(element, templateAttrs);
+ if (isFunction(linkingFn)) {
+ postLinkingFns.push(linkingFn);
+ } else if (linkingFn) {
+ if (linkingFn.pre) preLinkingFns.push(linkingFn.pre);
+ if (linkingFn.post) postLinkingFns.push(linkingFn.post);
+ }
+ } catch (e) {
+ $exceptionHandler(e, startingTag(element));
}
- },
+ }
- empty: function() {
- return this.linkFns.length === 0 && this.paths.length === 0;
+ if (directive.terminal) {
+ compositeLinkFn.terminal = true;
+ terminalPriority = Math.max(terminalPriority, directive.priority);
}
- };
- ///////////////////////////////////
- //Compiler
- //////////////////////////////////
-
- /**
- * @ngdoc function
- * @name angular.module.ng.$compile
- * @function
- *
- * @description
- * Compiles a piece of HTML string or DOM into a template and produces a template function, which
- * can then be used to link {@link angular.module.ng.$rootScope.Scope scope} and the template together.
- *
- * The compilation is a process of walking the DOM tree and trying to match DOM elements to
- * {@link angular.markup markup}, {@link angular.attrMarkup attrMarkup},
- * {@link angular.widget widgets}, and {@link angular.directive directives}. For each match it
- * executes corresponding markup, attrMarkup, widget or directive template function and collects the
- * instance functions into a single template function which is then returned.
- *
- * The template function can then be used once to produce the view or as it is the case with
- * {@link angular.widget.@ng:repeat repeater} many-times, in which case each call results in a view
- * that is a DOM clone of the original template.
- *
- <pre>
- angular.injector(['ng']).invoke(function($rootScope, $compile) {
- // Chose one:
-
- // A: compile the entire window.document.
- var element = $compile(window.document)($rootScope);
-
- // B: compile a piece of html
- var element = $compile('<div ng:click="clicked = true">click me</div>')($rootScope);
-
- // C: compile a piece of html and retain reference to both the dom and scope
- var element = $compile('<div ng:click="clicked = true">click me</div>')(scope);
- // at this point template was transformed into a view
- });
- </pre>
- *
- *
- * @param {string|DOMElement} element Element or HTML to compile into a template function.
- * @returns {function(scope[, cloneAttachFn])} a template function which is used to bind template
- * (a DOM element/tree) to a scope. Where:
- *
- * * `scope` - A {@link angular.module.ng.$rootScope.Scope Scope} to bind to.
- * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
- * `template` and call the `cloneAttachFn` function allowing the caller to attach the
- * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
- * called as: <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 template function returns the element of the template. It is either the original element
- * passed in, or the clone of the element if the `cloneAttachFn` is provided.
- *
- * It is important to understand that the returned scope is "linked" to the view DOM, but no linking
- * (instance) functions registered by {@link angular.directive directives} or
- * {@link angular.widget widgets} found in the template have been executed yet. This means that the
- * view is likely empty and doesn't contain any values that result from evaluation on the scope. To
- * bring the view to life, the scope needs to run through a $digest phase which typically is done by
- * Angular automatically, except for the case when an application is being
- * {@link guide/dev_guide.bootstrap.manual_bootstrap} manually bootstrapped, in which case the
- * $digest phase must be invoked by calling {@link angular.module.ng.$rootScope.Scope#$apply}.
- *
- * If you need access to the bound view, there are two ways to do it:
- *
- * - If you are not asking the linking function to clone the template, create the DOM element(s)
- * before you send them to the compiler and keep this reference around.
- * <pre>
- * var $injector = angular.injector(['ng']);
- * var scope = $injector.invoke(function($rootScope, $compile){
- * var element = $compile('<p>{{total}}</p>')($rootScope);
- * });
- * </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 original = angular.element('<p>{{total}}</p>'),
- * scope = someParentScope.$new(),
- * clone;
- *
- * $compile(original)(scope, function(clonedElement, scope) {
- * clone = clonedElement;
- * //attach the clone to DOM document at the right place
- * });
- *
- * //now we have reference to the cloned DOM via `clone`
- * </pre>
- *
- *
- * Compiler Methods For Widgets and Directives:
- *
- * The following methods are available for use when you write your own widgets, directives,
- * and markup. (Recall that the compile function's this is a reference to the compiler.)
- *
- * `compile(element)` - returns linker -
- * Invoke a new instance of the compiler to compile a DOM element and return a linker function.
- * You can apply the linker function to the original element or a clone of the original element.
- * The linker function returns a scope.
- *
- * * `comment(commentText)` - returns element - Create a comment element.
- *
- * * `element(elementName)` - returns element - Create an element by name.
- *
- * * `text(text)` - returns element - Create a text element.
- *
- * * `descend([set])` - returns descend state (true or false). Get or set the current descend
- * state. If true the compiler will descend to children elements.
- *
- * * `directives([set])` - returns directive state (true or false). Get or set the current
- * directives processing state. The compiler will process directives only when directives set to
- * true.
- *
- * For information on how the compiler works, see the
- * {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide.
- */
- function Compiler(markup, attrMarkup, directives, widgets){
- this.markup = markup;
- this.attrMarkup = attrMarkup;
- this.directives = directives;
- this.widgets = widgets;
}
+ compositeLinkFn.scope = !!newScopeDirective;
+
+ // if we have templateUrl, then we have to delay linking
+ return delayedLinkingFn || compositeLinkFn;
- Compiler.prototype = {
- compile: function(templateElement) {
- templateElement = jqLite(templateElement);
- var index = 0,
- template,
- parent = templateElement.parent();
- if (templateElement.length > 1) {
- // https://github.com/angular/angular.js/issues/338
- throw Error("Cannot compile multiple element roots: " +
- jqLite('<div>').append(templateElement.clone()).html());
+ ////////////////////
+
+
+ function compositeLinkFn(childLinkingFn, scope, linkNode) {
+ var attrs, element, i, ii;
+
+ if (templateNode === linkNode) {
+ attrs = templateAttrs;
+ } else {
+ attrs = shallowCopy(templateAttrs);
+ attrs.$element = jqLite(linkNode);
+ }
+ element = attrs.$element;
+
+ // PRELINKING
+ for(i = 0, ii = preLinkingFns.length; i < ii; i++) {
+ try {
+ preLinkingFns[i](scope, element, attrs);
+ } catch (e) {
+ $exceptionHandler(e, startingTag(element));
}
- if (parent && parent[0]) {
- parent = parent[0];
- for(var i = 0; i < parent.childNodes.length; i++) {
- if (parent.childNodes[i] == templateElement[0]) {
- index = i;
- }
- }
+ }
+
+ // RECURSION
+ childLinkingFn && childLinkingFn(scope, linkNode.childNodes);
+
+ // POSTLINKING
+ for(i = 0, ii = postLinkingFns.length; i < ii; i++) {
+ try {
+ postLinkingFns[i](scope, element, attrs);
+ } catch (e) {
+ $exceptionHandler(e, startingTag(element));
}
- template = this.templatize(templateElement, index) || new Template();
- return function(scope, cloneConnectFn){
- assertArg(scope, 'scope');
- // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
- // and sometimes changes the structure of the DOM.
- var element = cloneConnectFn
- ? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!!
- : templateElement;
- element.data($$scope, scope);
- scope.$element = element;
- (cloneConnectFn||noop)(element, scope);
- template.link(element, scope);
- return element;
- };
- },
-
- templatize: function(element, elementIndex){
- var self = this,
- widget,
- fn,
- directiveFns = self.directives,
- descend = true,
- directives = true,
- elementName = nodeName_(element),
- elementNamespace = elementName.indexOf(':') > 0 ? lowercase(elementName).replace(':', '-') : '',
- template,
- locals = {$element: element},
- selfApi = {
- compile: bind(self, self.compile),
- descend: function(value){ if(isDefined(value)) descend = value; return descend;},
- directives: function(value){ if(isDefined(value)) directives = value; return directives;},
- scope: function(value){ if(isDefined(value)) template.newScope = template.newScope || value; return template.newScope;}
- };
- element.addClass(elementNamespace);
- template = new Template();
- eachAttribute(element, function(value, name){
- if (!widget) {
- if ((widget = self.widgets('@' + name))) {
- element.addClass('ng-attr-widget');
- if (isFunction(widget) && !widget.$inject) {
- widget.$inject = ['$value', '$element'];
- }
- locals.$value = value;
- }
- }
- });
- if (!widget) {
- if ((widget = self.widgets(elementName))) {
- if (elementNamespace)
- element.addClass('ng-widget');
- if (isFunction(widget) && !widget.$inject) {
- widget.$inject = ['$element'];
- }
+ }
+ }
+ }
+
+
+ /**
+ * looks up the directive and decorates it with exception handling and proper parameters. We
+ * call this the boundDirective.
+ *
+ * @param {string} name name of the directive to look up.
+ * @param {string} location The directive must be found in specific format.
+ * String containing any of theses characters:
+ *
+ * * `E`: element name
+ * * `A': attribute
+ * * `C`: class
+ * * `M`: comment
+ * @returns true if directive was added.
+ */
+ function addDirective(tDirectives, name, location) {
+ var match = false;
+ if (hasDirectives.hasOwnProperty(name)) {
+ for(var directive, directives = $injector.get(name + Suffix),
+ i=0, ii = directives.length; i<ii; i++) {
+ try {
+ directive = directives[i];
+ if (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') {
+ element.addClass(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, beforeWidgetLinkFn, tElement, tAttrs, rootElement,
+ replace) {
+ 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}),
+ 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);
}
- if (widget) {
- descend = false;
- directives = false;
- var parent = element.parent();
- template.addLinkFn($injector.invoke(widget, selfApi, locals));
- if (parent && parent[0]) {
- element = jqLite(parent[0].childNodes[elementIndex]);
- }
+
+ 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);
}
- if (descend){
- // process markup for text nodes only
- for(var i=0, child=element[0].childNodes;
- i<child.length; i++) {
- if (isTextNode(child[i])) {
- forEach(self.markup, function(markup){
- if (i<child.length) {
- var textNode = jqLite(child[i]);
- markup.call(selfApi, textNode.text(), textNode, element);
- }
- });
- }
+
+ directives.unshift(syncWidgetDirective);
+ afterWidgetLinkFn = applyDirectivesToNode(directives, tElement, tAttrs);
+ afterWidgetChildrenLinkFn = compileNodes(tElement.contents());
+
+
+ while(linkQueue.length) {
+ var 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);
+ }, scope, node);
}
+ linkQueue = null;
+ }).
+ error(function(response, code, headers, config) {
+ throw Error('Failed to load template: ' + config.url);
+ });
- if (directives) {
- // Process attributes/directives
- eachAttribute(element, function(value, name){
- forEach(self.attrMarkup, function(markup){
- markup.call(selfApi, value, name, element);
- });
- });
- eachAttribute(element, function(value, name){
- name = lowercase(name);
- fn = directiveFns[name];
- if (fn) {
- element.addClass('ng-directive');
- template.addLinkFn((isArray(fn) || fn.$inject)
- ? $injector.invoke(fn, selfApi, {$value:value, $element: element})
- : fn.call(selfApi, value, element));
- }
+ return function(ignoreChildLinkingFn, scope, node, rootElement) {
+ if (linkQueue) {
+ linkQueue.push(scope);
+ linkQueue.push(node);
+ linkQueue.push(rootElement);
+ } else {
+ afterWidgetLinkFn(function() {
+ beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node);
+ }, scope, node);
+ }
+ };
+ }
+
+
+ /**
+ * 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);
+ parent.data('$binding', bindings).addClass('ng-binding');
+ scope.$watch(interpolateFn, function(scope, value) {
+ node[0].nodeValue = value;
});
+ })
+ });
+ }
+ }
+
+
+ function addAttrInterpolateDirective(directives, value, name) {
+ var interpolateFn = $interpolate(value, true);
+ if (SIDE_EFFECT_ATTRS[name]) {
+ name = SIDE_EFFECT_ATTRS[name];
+ if (BOOLEAN_ATTR[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) {
+ scope.$watch(interpolateFn, function(scope, value){
+ attr.$set(name, value);
+ });
+ };
+ } else {
+ attr.$set(name, value);
}
- // Process non text child nodes
- if (descend) {
- eachNode(element, function(child, i){
- template.addChild(i, self.templatize(child, i));
- });
+ }
+ });
+ }
+
+
+ /**
+ * This is a special jqLite.replaceWith, which can replace items which
+ * have no parents, provided that the containing jqLite collection is provided.
+ *
+ * @param {JqLite=} rootElement The root of the compile tree. Used so that we can replace nodes
+ * in the root of the tree.
+ * @param {JqLite} element The jqLite element which we are going to replace. We keep the shell,
+ * but replace its DOM node reference.
+ * @param {Node} newNode The new DOM node.
+ */
+ function replaceWith(rootElement, element, newNode) {
+ var oldNode = element[0],
+ parent = oldNode.parentNode,
+ i, ii;
+
+ if (rootElement) {
+ for(i = 0, ii = rootElement.length; i<ii; i++) {
+ if (rootElement[i] == oldNode) {
+ rootElement[i] = newNode;
}
- return template.empty() ? null : template;
}
- };
+ }
+ if (parent) {
+ parent.replaceChild(newNode, oldNode);
+ }
+ element[0] = newNode;
+ }
+ }];
- /////////////////////////////////////////////////////////////////////
- var compiler = new Compiler($textMarkup, $attrMarkup, $directive, $widget);
- return bind(compiler, compiler.compile);
- }];
-};
+ /**
+ * 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 {string=} attrName Optional none normalized name. Defaults to key.
+ */
+ function attrSetter(key, value, attrName) {
+ var booleanKey = BOOLEAN_ATTR[key.toLowerCase()];
-function eachNode(element, fn){
- var i, chldNodes = element[0].childNodes || [], chld;
- for (i = 0; i < chldNodes.length; i++) {
- if(!isTextNode(chld = chldNodes[i])) {
- fn(jqLite(chld), i);
+ if (booleanKey) {
+ value = toBoolean(value);
+ this.$element.prop(key, value);
+ this[key] = value;
+ attrName = key = booleanKey;
+ value = value ? booleanKey : undefined;
+ } else {
+ 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, '-');
+ }
}
- }
-}
-function eachAttribute(element, fn){
- var i, attrs = element[0].attributes || [], chld, attr, name, value, attrValue = {};
- for (i = 0; i < attrs.length; i++) {
- attr = attrs[i];
- name = attr.name;
- value = attr.value;
- if (msie && name == 'href') {
- value = decodeURIComponent(element[0].getAttribute(name, 2));
+ if (value === null || value === undefined) {
+ this.$element.removeAttr(attrName);
+ } else {
+ this.$element.attr(attrName, value);
}
- attrValue[name] = value;
}
- forEachSorted(attrValue, fn);
+}
+
+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, ''));
}