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) { | 
