diff options
| -rw-r--r-- | src/Angular.js | 38 | ||||
| -rw-r--r-- | src/angular-mocks.js | 12 | ||||
| -rw-r--r-- | src/jqLite.js | 22 | ||||
| -rw-r--r-- | src/service/compiler.js | 1033 | ||||
| -rw-r--r-- | src/service/formFactory.js | 72 | ||||
| -rw-r--r-- | test/AngularSpec.js | 23 | ||||
| -rw-r--r-- | test/directivesSpec.js | 8 | ||||
| -rw-r--r-- | test/jqLiteSpec.js | 2 | ||||
| -rw-r--r-- | test/service/compilerSpec.js | 1340 | ||||
| -rw-r--r-- | test/testabilityPatch.js | 5 |
10 files changed, 2001 insertions, 554 deletions
diff --git a/src/Angular.js b/src/Angular.js index 17ede3aa..4a0589c3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -627,6 +627,22 @@ function copy(source, destination){ } /** + * Create a shallow copy of an object + * @param src + */ +function shallowCopy(src) { + var dst = {}, + key; + for(key in src) { + if (src.hasOwnProperty(key)) { + dst[key] = src[key]; + } + } + return dst; +} + + +/** * @ngdoc function * @name angular.equals * @function @@ -750,6 +766,19 @@ function toBoolean(value) { return value; } +/** + * @returns {string} Returns the string representation of the element. + */ +function startingTag(element) { + element = jqLite(element).clone(); + try { + // turns out IE does not let you set .html() on elements which + // are not allowed to have children. So we just ignore it. + element.html(''); + } catch(e) {}; + return jqLite('<div>').append(element).html().replace(/\<\/[\w\:\-]+\>$/, ''); +} + ///////////////////////////////////////////////// @@ -918,6 +947,14 @@ function bootstrap(element, modules) { return injector; } +var SNAKE_CASE_REGEXP = /[A-Z]/g; +function snake_case(name, separator){ + separator = separator || '_'; + return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { + return (pos ? separator : '') + letter.toLowerCase(); + }); +} + function bindJQuery() { // bind to jQuery if present; jQuery = window.jQuery; @@ -951,6 +988,7 @@ function assertArg(arg, name, reason) { } function assertArgFn(arg, name) { + assertArg(arg, name); assertArg(isFunction(arg), name, 'not a function, got ' + (typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; diff --git a/src/angular-mocks.js b/src/angular-mocks.js index 0645322b..c024343e 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -684,19 +684,17 @@ angular.mock.dump = function(object) { * * <pre> // controller - function MyController($http) { - var scope = this; - + function MyController($scope, $http) { $http.get('/auth.py').success(function(data) { - scope.user = data; + $scope.user = data; }); this.saveMessage = function(message) { - scope.status = 'Saving...'; + $scope.status = 'Saving...'; $http.post('/add-msg.py', message).success(function(response) { - scope.status = ''; + $scope.status = ''; }).error(function() { - scope.status = 'ERROR!'; + $scope.status = 'ERROR!'; }); }; } diff --git a/src/jqLite.js b/src/jqLite.js index 9e16f8ec..e48d250b 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -100,13 +100,27 @@ function getStyle(element) { } +var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; +var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; +var MOZ_HACK_REGEXP = /^moz([A-Z])/; /** - * Converts dash-separated names to camelCase. Useful for dealing with css properties. + * 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 camelCase(name) { - return name.replace(/\-(\w)/g, function(all, letter, offset){ - return (offset == 0 && letter == 'w') ? 'w' : letter.toUpperCase(); - }); + return name. + replace(PREFIX_REGEXP, ''). + replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }). + replace(MOZ_HACK_REGEXP, 'Moz$1'); } ///////////////////////////////////////////// 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, '')); } diff --git a/src/service/formFactory.js b/src/service/formFactory.js index 69c6d717..727a243c 100644 --- a/src/service/formFactory.js +++ b/src/service/formFactory.js @@ -56,41 +56,43 @@ }); } - angular.directive('ng:contenteditable', function() { - return ['$formFactory', '$element', function ($formFactory, element) { - var exp = element.attr('ng:contenteditable'), - form = $formFactory.forElement(element), - widget; - element.attr('contentEditable', true); - widget = form.$createWidget({ - scope: this, - model: exp, - controller: HTMLEditorWidget, - controllerArgs: {$element: element}}); - // if the element is destroyed, then we need to notify the form. - element.bind('$destroy', function() { - widget.$destroy(); - }); - }]; - }); - </script> - <form name='editorForm' ng:controller="EditorCntl"> - <div ng:contenteditable="html"></div> - <hr/> - HTML: <br/> - <textarea ng:model="html" cols=80></textarea> - <hr/> - <pre>editorForm = {{editorForm}}</pre> - </form> - </doc:source> - <doc:scenario> - 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/); - }); - </doc:scenario> - </doc:example> + 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(); + }); + }; + }); + }); + </script> + <form name='editorForm' ng:controller="EditorCntl"> + <div ng:html-editor-model="htmlContent"></div> + <hr/> + HTML: <br/> + <textarea ng:model="htmlContent" cols="80"></textarea> + <hr/> + <pre>editorForm = {{editorForm|json}}</pre> + </form> + </doc:source> + <doc:scenario> + 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/); + }); + </doc:scenario> + </doc:example> */ /** diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 0ff0508f..2d469698 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -75,6 +75,22 @@ describe('angular', function() { }); }); + describe('shallow copy', function() { + it('should make a copy', function() { + var original = {key:{}}; + var copy = shallowCopy(original); + expect(copy).toEqual(original); + expect(copy.key).toBe(original.key); + }); + }); + + describe('elementHTML', function() { + it('should dump element', function() { + expect(lowercase(startingTag('<div attr="123">something<span></span></div>'))). + toEqual('<div attr="123">'); + }); + }); + describe('equals', function() { it('should return true if same object', function() { var o = {}; @@ -501,4 +517,11 @@ describe('angular', function() { dealoc(element); }); }); + + + describe('startingElementHtml', function(){ + it('should show starting element tag only', function(){ + expect(startingTag('<ng:abc x="2"><div>text</div></ng:abc>')).toEqual('<ng:abc x="2">'); + }); + }); }); diff --git a/test/directivesSpec.js b/test/directivesSpec.js index e52d9fcb..5bd5f5bd 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -2,14 +2,18 @@ describe("directive", function() { - var $filterProvider; + var $filterProvider, element; beforeEach(module(['$filterProvider', function(provider){ $filterProvider = provider; }])); + afterEach(function() { + dealoc(element); + }); + it("should ng:init", inject(function($rootScope, $compile) { - var element = $compile('<div ng:init="a=123"></div>')($rootScope); + element = $compile('<div ng:init="a=123"></div>')($rootScope); expect($rootScope.a).toEqual(123); })); diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js index 3abe4549..8b49502f 100644 --- a/test/jqLiteSpec.js +++ b/test/jqLiteSpec.js @@ -1,4 +1,3 @@ -'use strict'; describe('jqLite', function() { var scope, a, b, c; @@ -880,6 +879,7 @@ describe('jqLite', function() { it('should covert dash-separated strings to camelCase', function() { expect(camelCase('foo-bar')).toBe('fooBar'); expect(camelCase('foo-bar-baz')).toBe('fooBarBaz'); + expect(camelCase('foo:bar_baz')).toBe('fooBarBaz'); }); diff --git a/test/service/compilerSpec.js b/test/service/compilerSpec.js index 2df15c51..8d91ed5d 100644 --- a/test/service/compilerSpec.js +++ b/test/service/compilerSpec.js @@ -1,216 +1,1184 @@ 'use strict'; -describe('compiler', function() { - var textMarkup, attrMarkup, directives, widgets, compile, log; - - beforeEach(module(function($provide){ - textMarkup = []; - attrMarkup = []; - widgets = extensionMap({}, 'widget'); - directives = { - hello: function(expression, element){ - log += "hello "; - return function() { - log += expression; - }; - }, - - observe: function(expression, element){ - return function() { - this.$watch(expression, function(val) { - if (val) - log += ":" + val; - }); - }; - } - - }; - log = ""; - $provide.value('$textMarkup', textMarkup); - $provide.value('$attrMarkup', attrMarkup); - $provide.value('$directive', directives); - $provide.value('$widget', widgets); - })); +describe('$compile', function() { + var element; + beforeEach(module(provideLog, function($provide, $compileProvider){ + element = null; - it('should not allow compilation of multiple roots', inject(function($rootScope, $compile) { - expect(function() { - $compile('<div>A</div><span></span>'); - }).toThrow("Cannot compile multiple element roots: " + ie("<div>A</div><span></span>")); - function ie(text) { - return msie < 9 ? uppercase(text) : text; - } - })); + $compileProvider.directive('log', function(log) { + return { + priority:0, + compile: valueFn(function(scope, element, attrs) { + log(attrs.log || 'LOG'); + }) + }; + }); + + $compileProvider.directive('highLog', function(log) { + return { priority:3, compile: valueFn(function(scope, element, attrs) { + log(attrs.highLog || 'HIGH'); + })}; + }); + + $compileProvider.directive('mediumLog', function(log) { + return { priority:2, compile: valueFn(function(scope, element, attrs) { + log(attrs.mediumLog || 'MEDIUM'); + })}; + }); + $compileProvider.directive('greet', function() { + return { priority:10, compile: valueFn(function(scope, element, attrs) { + element.text("Hello " + attrs.greet); + })}; + }); - it('should recognize a directive', inject(function($rootScope, $compile) { - var e = jqLite('<div directive="expr" ignore="me"></div>'); - directives.directive = function(expression, element){ - log += "found"; - expect(expression).toEqual("expr"); - expect(element).toEqual(e); - return function initFn() { - log += ":init"; + $compileProvider.directive('set', function() { + return function(scope, element, attrs) { + element.text(attrs.set); }; - }; - var linkFn = $compile(e); - expect(log).toEqual("found"); - linkFn($rootScope); - expect(e.hasClass('ng-directive')).toEqual(true); - expect(log).toEqual("found:init"); - })); + }); + $compileProvider.directive('mediumStop', valueFn({ + priority: 2, + terminal: true + })); - it('should recurse to children', inject(function($rootScope, $compile) { - $compile('<div><span hello="misko"/></div>')($rootScope); - expect(log).toEqual("hello misko"); + $compileProvider.directive('stop', valueFn({ + terminal: true + })); + + $compileProvider.directive('negativeStop', valueFn({ + priority: -100, // even with negative priority we still should be able to stop descend + terminal: true + })); })); - it('should observe scope', inject(function($rootScope, $compile) { - $compile('<span observe="name"></span>')($rootScope); - expect(log).toEqual(""); - $rootScope.$digest(); - $rootScope.name = 'misko'; - $rootScope.$digest(); - $rootScope.$digest(); - $rootScope.name = 'adam'; - $rootScope.$digest(); - $rootScope.$digest(); - expect(log).toEqual(":misko:adam"); - })); + afterEach(function(){ + dealoc(element); + }); - it('should prevent descend', inject(function($rootScope, $compile) { - directives.stop = function() { this.descend(false); }; - $compile('<span hello="misko" stop="true"><span hello="adam"/></span>')($rootScope); - expect(log).toEqual("hello misko"); - })); + describe('configuration', function() { + it('should register a directive', function() { + module(function($compileProvider) { + $compileProvider.directive('div', function(log) { + return function(scope, element) { + log('OK'); + element.text('SUCCESS'); + }; + }) + }); + inject(function($compile, $rootScope, log) { + element = $compile('<div></div>')($rootScope); + expect(element.text()).toEqual('SUCCESS'); + expect(log).toEqual('OK'); + }) + }); + it('should allow registration of multiple directives with same name', function() { + module(function($compileProvider) { + $compileProvider.directive('div', function(log) { + return log.fn('1'); + }); + $compileProvider.directive('div', function(log) { + return log.fn('2'); + }); + }); + inject(function($compile, $rootScope, log) { + element = $compile('<div></div>')($rootScope); + expect(log).toEqual('1; 2'); + }); + }); + }); + + + describe('compile phase', function() { + + describe('multiple directives per element', function() { + it('should allow multiple directives per element', inject(function($compile, $rootScope, log){ + element = $compile( + '<span greet="angular" log="L" x-high-log="H" data-medium-log="M"></span>') + ($rootScope); + expect(element.text()).toEqual('Hello angular'); + expect(log).toEqual('H; M; L'); + })); + + + it('should recurse to children', inject(function($compile, $rootScope){ + element = $compile('<div>0<a set="hello">1</a>2<b set="angular">3</b>4</div>')($rootScope); + expect(element.text()).toEqual('0hello2angular4'); + })); - it('should allow creation of templates', inject(function($rootScope, $compile) { - directives.duplicate = function(expr, element){ - element.replaceWith(document.createComment("marker")); - element.removeAttr("duplicate"); - var linker = this.compile(element); - return function(marker) { - this.$watch('value', function() { - var scope = $rootScope.$new; - linker(scope, noop); - marker.after(scope.$element); + + it('should allow directives in classes', inject(function($compile, $rootScope, log) { + element = $compile('<div class="greet: angular; log:123;"></div>')($rootScope); + expect(element.html()).toEqual('Hello angular'); + expect(log).toEqual('123'); + })); + + + it('should allow directives in comments', inject( + function($compile, $rootScope, log) { + element = $compile('<div>0<!-- directive: log angular -->1</div>')($rootScope); + expect(log).toEqual('angular'); + } + )); + + + it('should receive scope, element, and attributes', function() { + var injector; + module(function($compileProvider) { + $compileProvider.directive('log', function($injector, $rootScope) { + injector = $injector; + return { + compile: function(element, templateAttr) { + expect(typeof templateAttr.$normalize).toBe('function'); + expect(typeof templateAttr.$set).toBe('function'); + expect(isElement(templateAttr.$element)).toBeTruthy(); + expect(element.text()).toEqual('unlinked'); + expect(templateAttr.exp).toEqual('abc'); + expect(templateAttr.aa).toEqual('A'); + expect(templateAttr.bb).toEqual('B'); + expect(templateAttr.cc).toEqual('C'); + return function(scope, element, attr) { + expect(element.text()).toEqual('unlinked'); + expect(attr).toBe(templateAttr); + expect(scope).toEqual($rootScope); + element.text('worked'); + } + } + }; + }); }); - }; - }; - $compile('<div>before<span duplicate="expr">x</span>after</div>')($rootScope); - expect(sortedHtml($rootScope.$element)). - toEqual('<div>' + - 'before<#comment></#comment>' + - 'after' + + inject(function($rootScope, $compile, $injector) { + element = $compile( + '<div class="log" exp="abc" aa="A" x-Bb="B" daTa-cC="C">unlinked</div>')($rootScope); + expect(element.text()).toEqual('worked'); + expect(injector).toBe($injector); // verify that directive is injectable + }); + }); + }); + + describe('error handling', function() { + + it('should handle exceptions', function() { + module(function($compileProvider, $exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + $compileProvider.directive('factoryError', function() { throw 'FactoryError'; }); + $compileProvider.directive('templateError', + valueFn({ compile: function() { throw 'TemplateError'; } })); + $compileProvider.directive('linkingError', + valueFn(function() { throw 'LinkingError'; })); + }); + inject(function($rootScope, $compile, $exceptionHandler) { + element = $compile('<div factory-error template-error linking-error></div>')($rootScope); + expect($exceptionHandler.errors[0]).toEqual('FactoryError'); + expect($exceptionHandler.errors[1][0]).toEqual('TemplateError'); + expect(ie($exceptionHandler.errors[1][1])). + toEqual('<div factory-error linking-error template-error>'); + expect($exceptionHandler.errors[2][0]).toEqual('LinkingError'); + expect(ie($exceptionHandler.errors[2][1])). + toEqual('<div factory-error linking-error template-error>'); + + + // crazy stuff to make IE happy + function ie(text) { + var list = [], + parts; + + parts = lowercase(text). + replace('<', ''). + replace('>', ''). + split(' '); + parts.sort(); + forEach(parts, function(value, key){ + if (value.substring(0,3) == 'ng-') { + } else { + list.push(value.replace('=""', '')); + } + }); + return '<' + list.join(' ') + '>'; + } + }); + }); + + + it('should prevent changing of structure', inject( + function($compile, $rootScope){ + element = jqLite("<div><div log></div></div>"); + var linkFn = $compile(element); + element.append("<div></div>"); + expect(function() { + linkFn($rootScope); + }).toThrow('Template changed structure!'); + } + )); + }); + + describe('compiler control', function() { + describe('priority', function() { + it('should honor priority', inject(function($compile, $rootScope, log){ + element = $compile( + '<span log="L" x-high-log="H" data-medium-log="M"></span>') + ($rootScope); + expect(log).toEqual('H; M; L'); + })); + }); + + + describe('terminal', function() { + + it('should prevent further directives from running', inject(function($rootScope, $compile) { + element = $compile('<div negative-stop><a set="FAIL">OK</a></div>')($rootScope); + expect(element.text()).toEqual('OK'); + } + )); + + + it('should prevent further directives from running, but finish current priority level', + inject(function($rootScope, $compile, log) { + // class is processed after attrs, so putting log in class will put it after + // the stop in the current level. This proves that the log runs after stop + element = $compile( + '<div high-log medium-stop log class="medium-log"><a set="FAIL">OK</a></div>')($rootScope); + expect(element.text()).toEqual('OK'); + expect(log.toArray().sort()).toEqual(['HIGH', 'MEDIUM']); + }) + ); + }); + + + describe('restrict', function() { + + it('should allow restriction of attributes', function() { + module(function($compileProvider, $provide) { + forEach({div:'E', attr:'A', clazz:'C', all:'EAC'}, function(restrict, name) { + $compileProvider.directive(name, function(log) { + return { + restrict: restrict, + compile: valueFn(function(scope, element, attr) { + log(name); + }) + }; + }); + }); + }); + inject(function($rootScope, $compile, log) { + dealoc($compile('<span div class="div"></span>')($rootScope)); + expect(log).toEqual(''); + log.reset(); + + dealoc($compile('<div></div>')($rootScope)); + expect(log).toEqual('div'); + log.reset(); + + dealoc($compile('<attr class=""attr"></attr>')($rootScope)); + expect(log).toEqual(''); + log.reset(); + + dealoc($compile('<span attr></span>')($rootScope)); + expect(log).toEqual('attr'); + log.reset(); + + dealoc($compile('<clazz clazz></clazz>')($rootScope)); + expect(log).toEqual(''); + log.reset(); + + dealoc($compile('<span class="clazz"></span>')($rootScope)); + expect(log).toEqual('clazz'); + log.reset(); + + dealoc($compile('<all class="all" all></all>')($rootScope)); + expect(log).toEqual('all; all; all'); + }); + }); + }); + + + describe('template', function() { + + + beforeEach(module(function($compileProvider) { + $compileProvider.directive('replace', valueFn({ + replace: true, + template: '<div class="log" style="width: 10px" high-log>Hello: <<CONTENT>></div>', + compile: function(element, attr) { + attr.$set('compiled', 'COMPILED'); + expect(element).toBe(attr.$element); + } + })); + $compileProvider.directive('append', valueFn({ + template: '<div class="log" style="width: 10px" high-log>Hello: <<CONTENT>></div>', + compile: function(element, attr) { + attr.$set('compiled', 'COMPILED'); + expect(element).toBe(attr.$element); + } + })); + })); + + + it('should replace element with template', inject(function($compile, $rootScope) { + element = $compile('<div><div replace>content</div><div>')($rootScope); + expect(element.text()).toEqual('Hello: content'); + expect(element.find('div').attr('compiled')).toEqual('COMPILED'); + })); + + + it('should append element with template', inject(function($compile, $rootScope) { + element = $compile('<div><div append>content</div><div>')($rootScope); + expect(element.text()).toEqual('Hello: content'); + expect(element.find('div').attr('compiled')).toEqual('COMPILED'); + })); + + + it('should compile replace template', inject(function($compile, $rootScope, log) { + element = $compile('<div><div replace medium-log>{{ "angular" }}</div><div>') + ($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('Hello: angular'); + // HIGH goes after MEDIUM since it executes as part of replaced template + expect(log).toEqual('MEDIUM; HIGH; LOG'); + })); + + + it('should compile append template', inject(function($compile, $rootScope, log) { + element = $compile('<div><div append medium-log>{{ "angular" }}</div><div>') + ($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('Hello: angular'); + expect(log).toEqual('HIGH; LOG; MEDIUM'); + })); + + + it('should merge attributes', inject(function($compile, $rootScope) { + element = $compile( + '<div><div replace class="medium-log" style="height: 20px" ></div><div>') + ($rootScope); + var div = element.find('div'); + expect(div.hasClass('medium-log')).toBe(true); + expect(div.hasClass('log')).toBe(true); + expect(div.css('width')).toBe('10px'); + expect(div.css('height')).toBe('20px'); + expect(div.attr('replace')).toEqual(''); + expect(div.attr('high-log')).toEqual(''); + })); + + it('should prevent multiple templates per element', inject(function($compile) { + try { + $compile('<div><span replace class="replace"></span></div>') + fail(); + } catch(e) { + expect(e.message).toMatch(/Multiple directives .* asking for template/); + } + })); + + it('should play nice with repeater when inline', inject(function($compile, $rootScope) { + element = $compile( + '<div>' + + '<div ng-repeat="i in [1,2]" replace>{{i}}; </div>' + + '</div>')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('Hello: 1; Hello: 2; '); + })); + + + it('should play nice with repeater when append', inject(function($compile, $rootScope) { + element = $compile( + '<div>' + + '<div ng-repeat="i in [1,2]" append>{{i}}; </div>' + + '</div>')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('Hello: 1; Hello: 2; '); + })); + }); + + + describe('async templates', function() { + + beforeEach(module( + function($compileProvider) { + $compileProvider.directive('hello', valueFn({ templateUrl: 'hello.html' })); + $compileProvider.directive('cau', valueFn({ templateUrl:'cau.html' })); + + $compileProvider.directive('cError', valueFn({ + templateUrl:'error.html', + compile: function() { + throw Error('cError'); + } + })); + $compileProvider.directive('lError', valueFn({ + templateUrl: 'error.html', + compile: function() { + throw Error('lError'); + } + })); + + + $compileProvider.directive('iHello', valueFn({ + replace: true, + templateUrl: 'hello.html' + })); + $compileProvider.directive('iCau', valueFn({ + replace: true, + templateUrl:'cau.html' + })); + + $compileProvider.directive('iCError', valueFn({ + replace: true, + templateUrl:'error.html', + compile: function() { + throw Error('cError'); + } + })); + $compileProvider.directive('iLError', valueFn({ + replace: true, + templateUrl: 'error.html', + compile: function() { + throw Error('lError'); + } + })); + + } + )); + + + it('should append template via $http and cache it in $templateCache', inject( + function($compile, $httpBackend, $templateCache, $rootScope, $browser) { + $httpBackend.expect('GET', 'hello.html').respond('<span>Hello!</span> World!'); + $templateCache.put('cau.html', '<span>Cau!</span>'); + element = $compile('<div><b class="hello">ignore</b><b class="cau">ignore</b></div>')($rootScope); + expect(sortedHtml(element)). + toEqual('<div><b class="hello"></b><b class="cau"></b></div>'); + + $rootScope.$digest(); + + + expect(sortedHtml(element)). + toEqual('<div><b class="hello"></b><b class="cau"><span>Cau!</span></b></div>'); + + $httpBackend.flush(); + expect(sortedHtml(element)).toEqual( + '<div>' + + '<b class="hello"><span>Hello!</span> World!</b>' + + '<b class="cau"><span>Cau!</span></b>' + + '</div>'); + } + )); + + + it('should inline template via $http and cache it in $templateCache', inject( + function($compile, $httpBackend, $templateCache, $rootScope) { + $httpBackend.expect('GET', 'hello.html').respond('<span>Hello!</span>'); + $templateCache.put('cau.html', '<span>Cau!</span>'); + element = $compile('<div><b class=i-hello>ignore</b><b class=i-cau>ignore</b></div>')($rootScope); + expect(sortedHtml(element)). + toEqual('<div><b class="i-hello"></b><b class="i-cau"></b></div>'); + + $rootScope.$digest(); + + + expect(sortedHtml(element)). + toEqual('<div><b class="i-hello"></b><span class="i-cau">Cau!</span></div>'); + + $httpBackend.flush(); + expect(sortedHtml(element)). + toEqual('<div><span class="i-hello">Hello!</span><span class="i-cau">Cau!</span></div>'); + } + )); + + + it('should compile, link and flush the template append', inject( + function($compile, $templateCache, $rootScope, $browser) { + $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>'); + $rootScope.name = 'Elvis'; + element = $compile('<div><b class="hello"></b></div>')($rootScope); + + $rootScope.$digest(); + + expect(sortedHtml(element)). + toEqual('<div><b class="hello"><span>Hello, Elvis!</span></b></div>'); + } + )); + + + it('should compile, link and flush the template inline', inject( + function($compile, $templateCache, $rootScope) { + $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>'); + $rootScope.name = 'Elvis'; + element = $compile('<div><b class=i-hello></b></div>')($rootScope); + + $rootScope.$digest(); + + expect(sortedHtml(element)). + toEqual('<div><span class="i-hello">Hello, Elvis!</span></div>'); + } + )); + + + it('should compile, flush and link the template append', inject( + function($compile, $templateCache, $rootScope) { + $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>'); + $rootScope.name = 'Elvis'; + var template = $compile('<div><b class="hello"></b></div>'); + + element = template($rootScope); + $rootScope.$digest(); + + expect(sortedHtml(element)). + toEqual('<div><b class="hello"><span>Hello, Elvis!</span></b></div>'); + } + )); + + + it('should compile, flush and link the template inline', inject( + function($compile, $templateCache, $rootScope) { + $templateCache.put('hello.html', '<span>Hello, {{name}}!</span>'); + $rootScope.name = 'Elvis'; + var template = $compile('<div><b class=i-hello></b></div>'); + + element = template($rootScope); + $rootScope.$digest(); + + expect(sortedHtml(element)). + toEqual('<div><span class="i-hello">Hello, Elvis!</span></div>'); + } + )); + + + it('should resolve widgets after cloning in append mode', function() { + module(function($exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + }); + inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser, + $exceptionHandler) { + $httpBackend.expect('GET', 'hello.html').respond('<span>{{greeting}} </span>'); + $httpBackend.expect('GET', 'error.html').respond('<div></div>'); + $templateCache.put('cau.html', '<span>{{name}}</span>'); + $rootScope.greeting = 'Hello'; + $rootScope.name = 'Elvis'; + var template = $compile( + '<div>' + + '<b class="hello"></b>' + + '<b class="cau"></b>' + + '<b class=c-error></b>' + + '<b class=l-error></b>' + '</div>'); - $rootScope.value = 1; - $rootScope.$digest(); - expect(sortedHtml($rootScope.$element)). - toEqual('<div>' + - 'before<#comment></#comment>' + - '<span>x</span>' + - 'after' + - '</div>'); - $rootScope.value = 2; - $rootScope.$digest(); - expect(sortedHtml($rootScope.$element)). - toEqual('<div>' + - 'before<#comment></#comment>' + - '<span>x</span>' + - '<span>x</span>' + - 'after' + - '</div>'); - $rootScope.value = 3; - $rootScope.$digest(); - expect(sortedHtml($rootScope.$element)). - toEqual('<div>' + - 'before<#comment></#comment>' + - '<span>x</span>' + - '<span>x</span>' + - '<span>x</span>' + - 'after' + - '</div>'); - })); + var e1; + var e2; + + e1 = template($rootScope.$new(), noop); // clone + expect(e1.text()).toEqual(''); + + $httpBackend.flush(); + + e2 = template($rootScope.$new(), noop); // clone + $rootScope.$digest(); + expect(e1.text()).toEqual('Hello Elvis'); + expect(e2.text()).toEqual('Hello Elvis'); + + expect($exceptionHandler.errors.length).toEqual(2); + expect($exceptionHandler.errors[0][0].message).toEqual('cError'); + expect($exceptionHandler.errors[1][0].message).toEqual('lError'); + + dealoc(e1); + dealoc(e2); + }); + }); + + + it('should resolve widgets after cloning in inline mode', function() { + module(function($exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); + }); + inject(function($compile, $templateCache, $rootScope, $httpBackend, $browser, + $exceptionHandler) { + $httpBackend.expect('GET', 'hello.html').respond('<span>{{greeting}} </span>'); + $httpBackend.expect('GET', 'error.html').respond('<div></div>'); + $templateCache.put('cau.html', '<span>{{name}}</span>'); + $rootScope.greeting = 'Hello'; + $rootScope.name = 'Elvis'; + var template = $compile( + '<div>' + + '<b class=i-hello></b>' + + '<b class=i-cau></b>' + + '<b class=i-c-error></b>' + + '<b class=i-l-error></b>' + + '</div>'); + var e1; + var e2; + + e1 = template($rootScope.$new(), noop); // clone + expect(e1.text()).toEqual(''); + + $httpBackend.flush(); + + e2 = template($rootScope.$new(), noop); // clone + $rootScope.$digest(); + expect(e1.text()).toEqual('Hello Elvis'); + expect(e2.text()).toEqual('Hello Elvis'); + + expect($exceptionHandler.errors.length).toEqual(2); + expect($exceptionHandler.errors[0][0].message).toEqual('cError'); + expect($exceptionHandler.errors[1][0].message).toEqual('lError'); + + dealoc(e1); + dealoc(e2); + }); + }); + + + it('should be implicitly terminal and not compile placeholder content in append', inject( + function($compile, $templateCache, $rootScope, log) { + // we can't compile the contents because that would result in a memory leak + + $templateCache.put('hello.html', 'Hello!'); + element = $compile('<div><b class="hello"><div log></div></b></div>')($rootScope); + + expect(log).toEqual(''); + } + )); + + + it('should be implicitly terminal and not compile placeholder content in inline', inject( + function($compile, $templateCache, $rootScope, log) { + // we can't compile the contents because that would result in a memory leak + + $templateCache.put('hello.html', 'Hello!'); + element = $compile('<div><b class=i-hello><div log></div></b></div>')($rootScope); + + expect(log).toEqual(''); + } + )); + + + it('should throw an error and clear element content if the template fails to load', inject( + function($compile, $httpBackend, $rootScope) { + $httpBackend.expect('GET', 'hello.html').respond(404, 'Not Found!'); + element = $compile('<div><b class="hello">content</b></div>')($rootScope); + + expect(function() { + $httpBackend.flush(); + }).toThrow('Failed to load template: hello.html'); + expect(sortedHtml(element)).toBe('<div><b class="hello"></b></div>'); + } + )); + + + it('should prevent multiple templates per element', function() { + module(function($compileProvider) { + $compileProvider.directive('sync', valueFn({ + template: '<span></span>' + })); + $compileProvider.directive('async', valueFn({ + templateUrl: 'template.html' + })); + }); + inject(function($compile){ + expect(function() { + $compile('<div><div class="sync async"></div></div>'); + }).toThrow('Multiple directives [sync, async] asking for template on: <'+ + (msie <= 8 ? 'DIV' : 'div') + ' class="sync async">'); + }); + }); + + + describe('delay compile / linking functions until after template is resolved', function(){ + var template; + beforeEach(module(function($compileProvider) { + function directive (name, priority, options) { + $compileProvider.directive(name, function(log) { + return (extend({ + priority: priority, + compile: function() { + log(name + '-C'); + return function() { log(name + '-L'); } + } + }, options || {})); + }); + } + + directive('first', 10); + directive('second', 5, { templateUrl: 'second.html' }); + directive('third', 3); + directive('last', 0); + + directive('iFirst', 10, {replace: true}); + directive('iSecond', 5, {replace: true, templateUrl: 'second.html' }); + directive('iThird', 3, {replace: true}); + directive('iLast', 0, {replace: true}); + })); + + it('should flush after link append', inject( + function($compile, $rootScope, $httpBackend, log) { + $httpBackend.expect('GET', 'second.html').respond('<div third>{{1+2}}</div>'); + template = $compile('<div><span first second last></span></div>'); + element = template($rootScope); + expect(log).toEqual('first-C'); + + log('FLUSH'); + $httpBackend.flush(); + $rootScope.$digest(); + expect(log).toEqual( + 'first-C; FLUSH; second-C; last-C; third-C; ' + + 'third-L; first-L; second-L; last-L'); + + var span = element.find('span'); + expect(span.attr('first')).toEqual(''); + expect(span.attr('second')).toEqual(''); + expect(span.find('div').attr('third')).toEqual(''); + expect(span.attr('last')).toEqual(''); + + expect(span.text()).toEqual('3'); + })); + + + it('should flush after link inline', inject( + function($compile, $rootScope, $httpBackend, log) { + $httpBackend.expect('GET', 'second.html').respond('<div i-third>{{1+2}}</div>'); + template = $compile('<div><span i-first i-second i-last></span></div>'); + element = template($rootScope); + expect(log).toEqual('iFirst-C'); + + log('FLUSH'); + $httpBackend.flush(); + $rootScope.$digest(); + expect(log).toEqual( + 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' + + 'iFirst-L; iSecond-L; iThird-L; iLast-L'); + + var div = element.find('div'); + expect(div.attr('i-first')).toEqual(''); + expect(div.attr('i-second')).toEqual(''); + expect(div.attr('i-third')).toEqual(''); + expect(div.attr('i-last')).toEqual(''); + + expect(div.text()).toEqual('3'); + })); + + + it('should flush before link append', inject( + function($compile, $rootScope, $httpBackend, log) { + $httpBackend.expect('GET', 'second.html').respond('<div third>{{1+2}}</div>'); + template = $compile('<div><span first second last></span></div>'); + expect(log).toEqual('first-C'); + log('FLUSH'); + $httpBackend.flush(); + expect(log).toEqual('first-C; FLUSH; second-C; last-C; third-C'); + + element = template($rootScope); + $rootScope.$digest(); + expect(log).toEqual( + 'first-C; FLUSH; second-C; last-C; third-C; ' + + 'third-L; first-L; second-L; last-L'); + + var span = element.find('span'); + expect(span.attr('first')).toEqual(''); + expect(span.attr('second')).toEqual(''); + expect(span.find('div').attr('third')).toEqual(''); + expect(span.attr('last')).toEqual(''); + + expect(span.text()).toEqual('3'); + })); + + + it('should flush before link inline', inject( + function($compile, $rootScope, $httpBackend, log) { + $httpBackend.expect('GET', 'second.html').respond('<div i-third>{{1+2}}</div>'); + template = $compile('<div><span i-first i-second i-last></span></div>'); + expect(log).toEqual('iFirst-C'); + log('FLUSH'); + $httpBackend.flush(); + expect(log).toEqual('iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C'); + + element = template($rootScope); + $rootScope.$digest(); + expect(log).toEqual( + 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' + + 'iFirst-L; iSecond-L; iThird-L; iLast-L'); + + var div = element.find('div'); + expect(div.attr('i-first')).toEqual(''); + expect(div.attr('i-second')).toEqual(''); + expect(div.attr('i-third')).toEqual(''); + expect(div.attr('i-last')).toEqual(''); + + expect(div.text()).toEqual('3'); + })); + }); + + + it('should check that template has root element', inject(function($compile, $httpBackend) { + $httpBackend.expect('GET', 'hello.html').respond('before <b>mid</b> after'); + $compile('<div i-hello></div>'); + expect(function(){ + $httpBackend.flush(); + }).toThrow('Template must have exactly one root element: before <b>mid</b> after'); + })); + + + it('should allow multiple elements in template', inject(function($compile, $httpBackend) { + $httpBackend.expect('GET', 'hello.html').respond('before <b>mid</b> after'); + element = jqLite('<div hello></div>'); + $compile(element); + $httpBackend.flush(); + expect(element.text()).toEqual('before mid after'); + })); + + + it('should work when widget is in root element', inject( + function($compile, $httpBackend, $rootScope) { + $httpBackend.expect('GET', 'hello.html').respond('<span>3==<<content>></span>'); + element = jqLite('<b class="hello">{{1+2}}</b>'); + $compile(element)($rootScope); + + $httpBackend.flush(); + expect(element.text()).toEqual('3==3'); + } + )); + + + it('should work when widget is a repeater', inject( + function($compile, $httpBackend, $rootScope) { + $httpBackend.expect('GET', 'hello.html').respond('<span>i=<<content>>;</span>'); + element = jqLite('<div><b class=hello ng-repeat="i in [1,2]">{{i}}</b></div>'); + $compile(element)($rootScope); + + $httpBackend.flush(); + expect(element.text()).toEqual('i=1;i=2;'); + } + )); + }); + + + describe('scope', function() { + + beforeEach(module(function($compileProvider) { + forEach(['', 'a', 'b'], function(name) { + $compileProvider.directive('scope' + uppercase(name), function(log) { + return { + scope: true, + compile: function() { + return function (scope, element) { + log(scope.$id); + expect(element.data('$scope')).toBe(scope); + }; + } + }; + }); + }); + $compileProvider.directive('log', function(log) { + return function(scope) { + log('log-' + scope.$id + '-' + scope.$parent.$id); + }; + }); + })); + + + it('should allow creation of new scopes', inject(function($rootScope, $compile, log) { + element = $compile('<div><span scope><a log></a></span></div>')($rootScope); + expect(log).toEqual('LOG; log-002-001; 002'); + })); + + it('should correctly create the scope hierachy properly', inject( + function($rootScope, $compile, log) { + element = $compile( + '<div>' + //1 + '<b class=scope>' + //2 + '<b class=scope><b class=log></b></b>' + //3 + '<b class=log></b>' + + '</b>' + + '<b class=scope>' + //4 + '<b class=log></b>' + + '</b>' + + '</div>' + )($rootScope); + expect(log).toEqual('LOG; log-003-002; 003; LOG; log-002-001; 002; LOG; log-004-001; 004'); + })); - it('should process markup before directives', inject(function($rootScope, $compile) { - textMarkup.push(function(text, textNode, parentNode) { - if (text == 'middle') { - expect(textNode.text()).toEqual(text); - parentNode.attr('hello', text); - textNode[0].nodeValue = 'replaced'; - } + + it('should not allow more then one scope creation per element', inject( + function($rootScope, $compile) { + expect(function(){ + $compile('<div class="scope-a; scope-b"></div>'); + }).toThrow('Multiple directives [scopeA, scopeB] asking for new scope on: ' + + '<' + (msie < 9 ? 'DIV' : 'div') + ' class="scope-a; scope-b">'); + })); + + + it('should treat new scope on new template as noop', inject( + function($rootScope, $compile, log) { + element = $compile('<div scope-a></div>')($rootScope); + expect(log).toEqual('001'); + })); + }); }); - $compile('<div>before<span>middle</span>after</div>')($rootScope); - expect(sortedHtml($rootScope.$element[0], true)). - toEqual('<div>before<span class="ng-directive" hello="middle">replaced</span>after</div>'); - expect(log).toEqual("hello middle"); - })); + }); - it('should replace widgets', inject(function($rootScope, $compile) { - widgets['NG:BUTTON'] = function(element) { - expect(element.hasClass('ng-widget')).toEqual(true); - element.replaceWith('<div>button</div>'); - return function(element) { - log += 'init'; - }; - }; - $compile('<div><ng:button>push me</ng:button></div>')($rootScope); - expect(lowercase($rootScope.$element[0].innerHTML)).toEqual('<div>button</div>'); - expect(log).toEqual('init'); - })); + describe('interpolation', function() { + it('should compile and link both attribute and text bindings', inject( + function($rootScope, $compile) { + $rootScope.name = 'angular'; + element = $compile('<div name="attr: {{name}}">text: {{name}}</div>')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('text: angular'); + expect(element.attr('name')).toEqual('attr: angular'); + })); - it('should use the replaced element after calling widget', inject(function($rootScope, $compile) { - widgets['H1'] = function(element) { - // HTML elements which are augmented by acting as widgets, should not be marked as so - expect(element.hasClass('ng-widget')).toEqual(false); - var span = angular.element('<span>{{1+2}}</span>'); - element.replaceWith(span); - this.descend(true); - this.directives(true); - return noop; - }; - textMarkup.push(function(text, textNode, parent){ - if (text == '{{1+2}}') - parent.text('3'); - }); - $compile('<div><h1>ignore me</h1></div>')($rootScope); - expect($rootScope.$element.text()).toEqual('3'); - })); + it('should decorate the binding with ng-binding and interpolation function', inject( + function($compile, $rootScope) { + element = $compile('<div>{{1+2}}</div>')($rootScope); + expect(element.hasClass('ng-binding')).toBe(true); + expect(element.data('$binding')[0].exp).toEqual('{{1+2}}'); + })); + }); - it('should allow multiple markups per text element', inject(function($rootScope, $compile) { - textMarkup.push(function(text, textNode, parent){ - var index = text.indexOf('---'); - if (index > -1) { - textNode.after(text.substring(index + 3)); - textNode.after("<hr/>"); - textNode.after(text.substring(0, index)); - textNode.remove(); - } - }); - textMarkup.push(function(text, textNode, parent){ - var index = text.indexOf('==='); - if (index > -1) { - textNode.after(text.substring(index + 3)); - textNode.after("<p>"); - textNode.after(text.substring(0, index)); - textNode.remove(); - } - }); - $compile('<div>A---B---C===D</div>')($rootScope); - expect(sortedHtml($rootScope.$element)).toEqual('<div>A<hr></hr>B<hr></hr>C<p></p>D</div>'); - })); + describe('link phase', function() { - it('should add class for namespace elements', inject(function($rootScope, $compile) { - var element = $compile('<ng:space>abc</ng:space>')($rootScope); - expect(element.hasClass('ng-space')).toEqual(true); - })); + beforeEach(module(function($compileProvider) { + + forEach(['a', 'b', 'c'], function(name) { + $compileProvider.directive(name, function(log) { + return { + compile: function() { + log('t' + uppercase(name)) + return { + pre: function() { + log('pre' + uppercase(name)); + }, + post: function linkFn() { + log('post' + uppercase(name)); + } + }; + } + }; + }); + }); + })); + + + it('should not store linkingFns for noop branches', inject(function ($rootScope, $compile) { + element = jqLite('<div name="{{a}}"><span>ignore</span></div>'); + var linkingFn = $compile(element); + // Now prune the branches with no directives + element.find('span').remove(); + expect(element.find('span').length).toBe(0); + // and we should still be able to compile without errors + linkingFn($rootScope); + })); + + + it('should compile from top to bottom but link from bottom up', inject( + function($compile, $rootScope, log) { + element = $compile('<a b><c></c></a>')($rootScope); + expect(log).toEqual('tA; tB; tC; preA; preB; preC; postC; postA; postB'); + } + )); + + + it('should support link function on directive object', function() { + module(function($compileProvider) { + $compileProvider.directive('abc', valueFn({ + link: function(scope, element, attrs) { + element.text(attrs.abc); + } + })); + }); + inject(function($compile, $rootScope) { + element = $compile('<div abc="WORKS">FAIL</div>')($rootScope); + expect(element.text()).toEqual('WORKS'); + }); + }); + }); + + + describe('attrs', function() { + + it('should allow setting of attributes', function() { + module(function($compileProvider) { + $compileProvider.directive({ + setter: valueFn(function(scope, element, attr) { + attr.$set('name', 'abc'); + attr.$set('disabled', true); + expect(attr.name).toBe('abc'); + expect(attr.disabled).toBe(true); + }) + }); + }); + inject(function($rootScope, $compile) { + element = $compile('<div setter></div>')($rootScope); + expect(element.attr('name')).toEqual('abc'); + expect(element.attr('disabled')).toEqual('disabled'); + }); + }); + + + it('should read boolean attributes as boolean', function() { + module(function($compileProvider) { + $compileProvider.directive({ + div: valueFn(function(scope, element, attr) { + element.text(attr.required); + }) + }); + }); + inject(function($rootScope, $compile) { + element = $compile('<div required></div>')($rootScope); + expect(element.text()).toEqual('true'); + }); + }); + + it('should allow setting of attributes', function() { + module(function($compileProvider) { + $compileProvider.directive({ + setter: valueFn(function(scope, element, attr) { + attr.$set('name', 'abc'); + attr.$set('disabled', true); + expect(attr.name).toBe('abc'); + expect(attr.disabled).toBe(true); + }) + }); + }); + inject(function($rootScope, $compile) { + element = $compile('<div setter></div>')($rootScope); + expect(element.attr('name')).toEqual('abc'); + expect(element.attr('disabled')).toEqual('disabled'); + }); + }); + + + it('should read boolean attributes as boolean', function() { + module(function($compileProvider) { + $compileProvider.directive({ + div: valueFn(function(scope, element, attr) { + element.text(attr.required); + }) + }); + }); + inject(function($rootScope, $compile) { + element = $compile('<div required></div>')($rootScope); + expect(element.text()).toEqual('true'); + }); + }); + + + it('should create new instance of attr for each template stamping', function() { + module(function($compileProvider, $provide) { + var state = { first: [], second: [] }; + $provide.value('state', state); + $compileProvider.directive({ + first: valueFn({ + priority: 1, + compile: function(templateElement, templateAttr) { + return function(scope, element, attr) { + state.first.push({ + template: {element: templateElement, attr:templateAttr}, + link: {element: element, attr: attr} + }); + } + } + }), + second: valueFn({ + priority: 2, + compile: function(templateElement, templateAttr) { + return function(scope, element, attr) { + state.second.push({ + template: {element: templateElement, attr:templateAttr}, + link: {element: element, attr: attr} + }); + } + } + }) + }); + }); + inject(function($rootScope, $compile, state) { + var template = $compile('<div first second>'); + dealoc(template($rootScope.$new(), noop)); + dealoc(template($rootScope.$new(), noop)); + + // instance between directives should be shared + expect(state.first[0].template.element).toBe(state.second[0].template.element); + expect(state.first[0].template.attr).toBe(state.second[0].template.attr); + + // the template and the link can not be the same instance + expect(state.first[0].template.element).not.toBe(state.first[0].link.element); + expect(state.first[0].template.attr).not.toBe(state.first[0].link.attr); + + // each new template needs to be new instance + expect(state.first[0].link.element).not.toBe(state.first[1].link.element); + expect(state.first[0].link.attr).not.toBe(state.first[1].link.attr); + expect(state.second[0].link.element).not.toBe(state.second[1].link.element); + expect(state.second[0].link.attr).not.toBe(state.second[1].link.attr); + }); + }); + + + describe('$set', function() { + var attr; + beforeEach(function(){ + module(function($compileProvider) { + $compileProvider.directive('div', valueFn(function(scope, element, attr){ + scope.attr = attr; + })); + }); + inject(function($compile, $rootScope) { + element = $compile('<div></div>')($rootScope); + attr = $rootScope.attr; + expect(attr).toBeDefined(); + }); + }); + + + it('should set attributes', function() { + attr.$set('ngMyAttr', 'value'); + expect(element.attr('ng-my-attr')).toEqual('value'); + expect(attr.ngMyAttr).toEqual('value'); + }); + + + it('should allow overriding of attribute name and remember the name', function() { + attr.$set('ngOther', '123', 'other'); + expect(element.attr('other')).toEqual('123'); + expect(attr.ngOther).toEqual('123'); + + attr.$set('ngOther', '246'); + expect(element.attr('other')).toEqual('246'); + expect(attr.ngOther).toEqual('246'); + }); + + + it('should set boolean attributes', function() { + attr.$set('disabled', 'true'); + attr.$set('readOnly', 'true'); + expect(element.attr('disabled')).toEqual('disabled'); + expect(element.attr('readonly')).toEqual('readonly'); + + attr.$set('disabled', 'false'); + expect(element.attr('disabled')).toEqual(undefined); + + attr.$set('disabled', false); + attr.$set('readOnly', false); + expect(element.attr('disabled')).toEqual(undefined); + expect(element.attr('readonly')).toEqual(undefined); + }); + + + it('should remove attribute', function() { + attr.$set('ngMyAttr', 'value'); + expect(element.attr('ng-my-attr')).toEqual('value'); + + attr.$set('ngMyAttr', undefined); + expect(element.attr('ng-my-attr')).toBe(undefined); + + attr.$set('ngMyAttr', 'value'); + attr.$set('ngMyAttr', null); + expect(element.attr('ng-my-attr')).toBe(undefined); + }) + }); + }); }); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index 13e726bb..85c844cb 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -67,7 +67,10 @@ function dealoc(obj) { } } - +/** + * @param {DOMElement} element + * @param {boolean=} showNgClass + */ function sortedHtml(element, showNgClass) { var html = ""; forEach(jqLite(element), function toString(node) { |
