From 8af4fde18246ac1587b471a549e70d5d858bf0ee Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 29 Nov 2011 12:11:32 -0800 Subject: add($compile): add compiler v2.0 - not connected --- src/service/compiler.js | 1033 ++++++++++++++++++++++++++++++-------------- src/service/formFactory.js | 72 +-- 2 files changed, 752 insertions(+), 353 deletions(-) (limited to 'src/service') 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. + * + + + +
+
+
+
+
+
+ + it('should auto compile', function() { + expect(element('div[compile]').text()).toBe('Hello Angular'); + input('html').enter('{{name}}!'); + expect(element('div[compile]').text()).toBe('Angular!'); + }); + +
- 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:
`cloneAttachFn(clonedElement, scope)` where: + * + * * `clonedElement` - is a clone of the original `element` passed into the compiler. + * * `scope` - is the current scope with which the linking function is working with. + * + * Calling the linking function returns the element of the template. It is either the original element + * passed in, or the clone of the element if the `cloneAttachFn` is provided. + * + * After linking the view is not updateh until after a call to $digest which typically is done by + * Angular automatically. + * + * If you need access to the bound view, there are two ways to do it: + * + * - If you are not asking the linking function to clone the template, create the DOM element(s) + * before you send them to the compiler and keep this reference around. + *
+ *     var element = $compile('

{{total}}

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

{{total}}

'), + * scope = ....; + * + * var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) { + * //attach the clone to DOM document at the right place + * }); + * + * //now we have reference to the cloned DOM via `clone` + *
+ * + * + * 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 = /\<\\>/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 + 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. - * -
-          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('
click me
')($rootScope); - - // C: compile a piece of html and retain reference to both the dom and scope - var element = $compile('
click me
')(scope); - // at this point template was transformed into a view - }); -
- * - * - * @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:
`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. - *
-       *     var $injector = angular.injector(['ng']);
-       *     var scope = $injector.invoke(function($rootScope, $compile){
-       *       var element = $compile('

{{total}}

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

{{total}}

'), - * 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` - *
- * - * - * 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('
').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 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 -
-
-
- HTML:
- -
-
editorForm = {{editorForm}}
-
- - - it('should enter invalid HTML', function() { - expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); - input('html').enter('<'); - expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); - }); - - + angular.module('formModule', [], function($compileProvider){ + $compileProvider.directive('ngHtmlEditorModel', function ($formFactory) { + return function(scope, element, attr) { + var form = $formFactory.forElement(element), + widget; + element.attr('contentEditable', true); + widget = form.$createWidget({ + scope: scope, + model: attr.ngHtmlEditorModel, + controller: HTMLEditorWidget, + controllerArgs: {$element: element}}); + // if the element is destroyed, then we need to + // notify the form. + element.bind('$destroy', function() { + widget.$destroy(); + }); + }; + }); + }); + +
+
+
+ HTML:
+ +
+
editorForm = {{editorForm|json}}
+
+ + + it('should enter invalid HTML', function() { + expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); + input('htmlContent').enter('<'); + expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); + }); + + */ /** -- cgit v1.2.3