diff options
| author | Misko Hevery | 2012-01-27 16:18:16 -0800 | 
|---|---|---|
| committer | Misko Hevery | 2012-02-21 22:46:00 -0800 | 
| commit | 78656fe0dfc99c341ce02d71e7006e9c05b1fe3f (patch) | |
| tree | a68731c4c1675047da65b23ccf3d562324324081 | |
| parent | cb10ccc44fa78b82c80afa1cb5dac2c34fdf24b7 (diff) | |
| download | angular.js-78656fe0dfc99c341ce02d71e7006e9c05b1fe3f.tar.bz2 | |
feat($compile) add locals, isolate scope, transclusion
| -rw-r--r-- | src/AngularPublic.js | 3 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 1 | ||||
| -rw-r--r-- | src/angular-mocks.js | 3 | ||||
| -rw-r--r-- | src/directives.js | 69 | ||||
| -rw-r--r-- | src/service/compiler.js | 330 | ||||
| -rw-r--r-- | src/service/controller.js | 17 | ||||
| -rw-r--r-- | src/service/formFactory.js | 2 | ||||
| -rw-r--r-- | src/service/route.js | 2 | ||||
| -rw-r--r-- | src/service/scope.js | 34 | ||||
| -rw-r--r-- | src/widgets.js | 2 | ||||
| -rw-r--r-- | test/service/compilerSpec.js | 471 | ||||
| -rw-r--r-- | test/service/controllerSpec.js | 2 | ||||
| -rw-r--r-- | test/service/scopeSpec.js | 9 | 
13 files changed, 838 insertions, 107 deletions
| diff --git a/src/AngularPublic.js b/src/AngularPublic.js index d052c35b..ac7d4243 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -94,7 +94,8 @@ function publishExternalAPI(angular){              ngStyle: ngStyleDirective,              ngSwitch: ngSwitchDirective,              ngOptions: ngOptionsDirective, -            ngView: ngViewDirective +            ngView: ngViewDirective, +            ngTransclude: ngTranscludeDirective            }).          directive(ngEventDirectives).          directive(ngAttributeAliasDirectives); diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index 778eee6b..5b6e5937 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -101,6 +101,7 @@      globalVars = {};      bindJQuery(); +    publishExternalAPI(window.angular);      angularInit(document, angular.bootstrap);    } diff --git a/src/angular-mocks.js b/src/angular-mocks.js index c024343e..0a8b573b 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -1458,6 +1458,9 @@ window.jstestdriver && (function(window) {        args.push(angular.mock.dump(arg));      });      jstestdriver.console.log.apply(jstestdriver.console, args); +    if (window.console) { +      window.console.log.apply(window.console, args); +    }    };  })(window); diff --git a/src/directives.js b/src/directives.js index c6cc0b15..39308b1a 100644 --- a/src/directives.js +++ b/src/directives.js @@ -133,17 +133,7 @@ var ngInitDirective = valueFn({  var ngControllerDirective = ['$controller', '$window', function($controller, $window) {    return {      scope: true, -    compile: function() { -      return { -        pre: function(scope, element, attr) { -          var expression = attr.ngController, -              Controller = getter(scope, expression, true) || getter($window, expression, true); - -          assertArgFn(Controller, expression); -          $controller(Controller, scope); -        } -      }; -    } +    controller: '@'    }  }]; @@ -264,6 +254,7 @@ var ngBindHtmlDirective = ['$sanitize', function($sanitize) {  var ngBindTemplateDirective = ['$interpolate', function($interpolate) {    return function(scope, element, attr) {      var interpolateFn = $interpolate(attr.ngBindTemplate); +    var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate));      element.addClass('ng-binding').data('$binding', interpolateFn);      scope.$watch(interpolateFn, function(value) {        element.text(value); @@ -921,3 +912,59 @@ function ngAttributeAliasDirective(propName, attrName) {  var ngAttributeAliasDirectives = {};  forEach(BOOLEAN_ATTR, ngAttributeAliasDirective);  ngAttributeAliasDirective(null, 'src'); + +/** + * @ngdoc directive + * @name angular.module.ng.$compileProvider.directive.ng:transclude + * + * @description + * Insert the transcluded DOM here. + * + * @element ANY + * + * @example +   <doc:example module="transclude"> +     <doc:source> +       <script> +         function Ctrl($scope) { +           $scope.title = 'Lorem Ipsum'; +           $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; +         } + +         angular.module('transclude', []) +          .directive('pane', function(){ +             return { +               transclude: true, +               scope: 'isolate', +               locals: { title:'bind' }, +               template: '<div style="border: 1px solid black;">' + +                           '<div style="background-color: gray">{{title}}</div>' + +                           '<div ng-transclude></div>' + +                         '</div>' +             }; +         }); +       </script> +       <div ng:controller="Ctrl"> +         <input ng:model="title"><br> +         <textarea ng:model="text"></textarea> <br/> +         <pane title="{{title}}">{{text}}</pane> +       </div> +     </doc:source> +     <doc:scenario> +        it('should have transcluded', function() { +          input('title').enter('TITLE'); +          input('text').enter('TEXT'); +          expect(binding('title')).toEqual('TITLE'); +          expect(binding('text')).toEqual('TEXT'); +        }); +     </doc:scenario> +   </doc:example> + * + */ +var ngTranscludeDirective = valueFn({ +  controller: ['$transclude', '$element', function($transclude, $element) { +    $transclude(function(clone) { +      $element.append(clone); +    }); +  }] +}); diff --git a/src/service/compiler.js b/src/service/compiler.js index ed453749..ef049b50 100644 --- a/src/service/compiler.js +++ b/src/service/compiler.js @@ -72,6 +72,9 @@   *   *   * @param {string|DOMElement} element Element or HTML string to compile into a template function. + * @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. + * @param {number} maxPriority only apply directives lower then given priority (Only effects the + *                 root element(s), not their children)   * @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template   * (a DOM element/tree) to a scope. Where:   * @@ -157,7 +160,8 @@ function $CompileProvider($provide) {                    directive.compile = valueFn(directive.link);                  }                  directive.priority = directive.priority || 0; -                directive.name = name; +                directive.name = directive.name || name; +                directive.require = directive.require || (directive.controller && directive.name);                  directive.restrict = directive.restrict || 'EACM';                  directives.push(directive);                } catch (e) { @@ -175,10 +179,58 @@ function $CompileProvider($provide) {    }; -  this.$get = ['$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', -       function($injector,   $interpolate,   $exceptionHandler,   $http,   $templateCache) { +  this.$get = [ +            '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', +            '$controller', +    function($injector,   $interpolate,   $exceptionHandler,   $http,   $templateCache,   $parse, +             $controller) { + +    var LOCAL_MODE = { +      attribute: function(localName, mode, parentScope, scope, attr) { +        scope[localName] = attr[localName]; +      }, + +      evaluate: function(localName, mode, parentScope, scope, attr) { +        scope[localName] = parentScope.$eval(attr[localName]); +      }, + +      bind: function(localName, mode, parentScope, scope, attr) { +        var getter = $interpolate(attr[localName]); +        scope.$watch( +          function() { return getter(parentScope); }, +          function(v) { scope[localName] = v; } +        ); +      }, + +      accessor: function(localName, mode, parentScope, scope, attr) { +        var getter = noop, +            setter = noop, +            exp = attr[localName]; + +        if (exp) { +          getter = $parse(exp); +          setter = getter.assign || function() { +            throw Error("Expression '" + exp + "' not assignable."); +          }; +        } + +        scope[localName] = function(value) { +          return arguments.length ? setter(parentScope, value) : getter(parentScope); +        }; +      }, + +      expression: function(localName, mode, parentScope, scope, attr) { +        scope[localName] = function(locals) { +          $parse(attr[localName])(parentScope, locals); +        }; +      } +    }; + +    return compile; -    return function(templateElement) { +    //================================ + +    function compile(templateElement, transcludeFn, maxPriority) {        templateElement = jqLite(templateElement);        // We can not compile top level text elements since text nodes can be merged and we will        // not be able to attach scope data to them, so we will wrap them in <span> @@ -187,7 +239,7 @@ function $CompileProvider($provide) {            templateElement[index] = jqLite(node).wrap('<span>').parent()[0];          }        }); -      var linkingFn = compileNodes(templateElement, templateElement); +      var linkingFn = compileNodes(templateElement, transcludeFn, templateElement, maxPriority);        return function(scope, cloneConnectFn){          assertArg(scope, 'scope');          // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart @@ -200,9 +252,11 @@ function $CompileProvider($provide) {          if (linkingFn) linkingFn(scope, element, element);          return element;        }; -    }; +    } -    //================================ +    function wrongMode(localName, mode) { +      throw Error("Unsupported '" + mode + "' for '" + localName + "'."); +    }      /**       * Compile function matches each node in nodeList against the directives. Once all directives @@ -211,12 +265,15 @@ function $CompileProvider($provide) {       * function, which is the a linking function for the node.       *       * @param {NodeList} nodeList an array of nodes to compile +     * @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the +     *        scope argument is auto-generated to the new child of the transcluded parent scope.       * @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. +     * @param {number=} max directive priority       * @returns {?function} A composite linking function of all of the matched directives or null.       */ -    function compileNodes(nodeList, rootElement) { +    function compileNodes(nodeList, transcludeFn, rootElement, maxPriority) {       var linkingFns = [],           directiveLinkingFn, childLinkingFn, directives, attrs, linkingFnFound; @@ -227,15 +284,16 @@ function $CompileProvider($provide) {           $set: attrSetter         };         // we must always refer to nodeList[i] since the nodes can be replaced underneath us. -       directives = collectDirectives(nodeList[i], [], attrs); +       directives = collectDirectives(nodeList[i], [], attrs, maxPriority);         directiveLinkingFn = (directives.length) -           ? applyDirectivesToNode(directives, nodeList[i], attrs, rootElement) +           ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, rootElement)             : null;         childLinkingFn = (directiveLinkingFn && directiveLinkingFn.terminal)             ? null -           : compileNodes(nodeList[i].childNodes); +           : compileNodes(nodeList[i].childNodes, +                directiveLinkingFn ? directiveLinkingFn.transclude : transcludeFn);         linkingFns.push(directiveLinkingFn);         linkingFns.push(childLinkingFn); @@ -245,28 +303,42 @@ function $CompileProvider($provide) {       // return a linking function if we have found anything, null otherwise       return linkingFnFound ? linkingFn : null; -     function linkingFn(scope, nodeList, rootElement) { +     /* nodesetLinkingFn */ function linkingFn(scope, nodeList, rootElement, boundTranscludeFn) {         if (linkingFns.length != nodeList.length * 2) {           throw Error('Template changed structure!');         } -       var childLinkingFn, directiveLinkingFn, node, childScope; +       var childLinkingFn, directiveLinkingFn, node, childScope, childTransclusionFn;         for(var i=0, n=0, ii=linkingFns.length; i<ii; n++) {           node = nodeList[n]; -         directiveLinkingFn = linkingFns[i++]; -         childLinkingFn = linkingFns[i++]; +         directiveLinkingFn = /* directiveLinkingFn */ linkingFns[i++]; +         childLinkingFn = /* nodesetLinkingFn */ linkingFns[i++];           if (directiveLinkingFn) {             if (directiveLinkingFn.scope && !rootElement) { -             childScope = scope.$new(); +             childScope = scope.$new(isObject(directiveLinkingFn.scope));               jqLite(node).data('$scope', childScope);             } else {               childScope = scope;             } -           directiveLinkingFn(childLinkingFn, childScope, node, rootElement); +           childTransclusionFn = directiveLinkingFn.transclude; +           if (childTransclusionFn || (!boundTranscludeFn && transcludeFn)) { +             directiveLinkingFn(childLinkingFn, childScope, node, rootElement, +                 (function(transcludeFn) { +                   return function(cloneFn) { +                     var transcludeScope = scope.$new(); + +                     return transcludeFn(transcludeScope, cloneFn). +                         bind('$destroy', bind(transcludeScope, transcludeScope.$destroy)); +                    }; +                  })(childTransclusionFn || transcludeFn) +             ); +           } else { +             directiveLinkingFn(childLinkingFn, childScope, node, undefined, boundTranscludeFn); +           }           } else if (childLinkingFn) { -           childLinkingFn(scope, node.childNodes); +           childLinkingFn(scope, node.childNodes, undefined, boundTranscludeFn);           }         }       } @@ -280,8 +352,9 @@ function $CompileProvider($provide) {       * @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. +     * @param {number=} max directive priority       */ -    function collectDirectives(node, directives, attrs) { +    function collectDirectives(node, directives, attrs, maxPriority) {        var nodeType = node.nodeType,            attrsMap = attrs.$attr,            match, @@ -290,7 +363,8 @@ function $CompileProvider($provide) {        switch(nodeType) {          case 1: /* Element */            // use the node name: <directive> -          addDirective(directives, directiveNormalize(nodeName_(node).toLowerCase()), 'E'); +          addDirective(directives, +              directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority);            // iterate over the attributes            for (var attr, name, nName, value, nAttrs = node.attributes, @@ -305,15 +379,15 @@ function $CompileProvider($provide) {              if (BOOLEAN_ATTR[nName]) {                attrs[nName] = true; // presence means true              } -            addAttrInterpolateDirective(directives, value, nName); -            addDirective(directives, nName, 'A'); +            addAttrInterpolateDirective(directives, value, nName) +            addDirective(directives, nName, 'A', maxPriority);            }            // use class as directive            className = node.className;            while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {              nName = directiveNormalize(match[2]); -            if (addDirective(directives, nName, 'C')) { +            if (addDirective(directives, nName, 'C', maxPriority)) {                attrs[nName] = trim(match[3]);              }              className = className.substr(match.index + match[0].length); @@ -326,7 +400,7 @@ function $CompileProvider($provide) {            match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);            if (match) {              nName = directiveNormalize(match[1]); -            if (addDirective(directives, nName, 'M')) { +            if (addDirective(directives, nName, 'M', maxPriority)) {                attrs[nName] = trim(match[2]);              }            } @@ -347,40 +421,81 @@ function $CompileProvider($provide) {       *        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 {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the +     *        scope argument is auto-generated to the new child of the transcluded parent scope.       * @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) { +    function applyDirectivesToNode(directives, templateNode, templateAttrs, transcludeFn, rootElement) {        var terminalPriority = -Number.MAX_VALUE,            preLinkingFns = [],            postLinkingFns = [],            newScopeDirective = null, +          newIsolatedScopeDirective = null,            templateDirective = null,            delayedLinkingFn = null,            element = templateAttrs.$element = jqLite(templateNode), -          directive, linkingFn; +          directive, +          directiveName, +          template, +          transcludeDirective, +          childTranscludeFn = transcludeFn, +          controllerDirectives, +          linkingFn, +          directiveValue;        // executes all directives on the current element        for(var i = 0, ii = directives.length; i < ii; i++) {          directive = directives[i]; +        template = undefined;          if (terminalPriority > directive.priority) {            break; // prevent further processing of directives          } -        if (directive.scope) { -          assertNoDuplicate('new scope', newScopeDirective, directive, element); +        if (directiveValue = directive.scope) { +          assertNoDuplicate('isolated scope', newIsolatedScopeDirective, directive, element); +          if (isObject(directiveValue)) { +            element.addClass('ng-isolate-scope'); +            newIsolatedScopeDirective = directive; +          }            element.addClass('ng-scope'); -          newScopeDirective = directive; +          newScopeDirective = newScopeDirective || directive;          } -        if (directive.template) { +        directiveName = directive.name; + +        if (directiveValue = directive.controller) { +          controllerDirectives = controllerDirectives || {}; +          assertNoDuplicate("'" + directiveName + "' controller", +              controllerDirectives[directiveName], directive, element); +          controllerDirectives[directiveName] = directive; +        } + +        if (directiveValue = directive.transclude) { +          assertNoDuplicate('transclusion', transcludeDirective, directive, element); +          transcludeDirective = directive; +          terminalPriority = directive.priority; +          if (directiveValue == 'element') { +            template = jqLite(templateNode); +            templateNode = (element = templateAttrs.$element = jqLite( +                '<!-- ' + directiveName + ': ' + templateAttrs[directiveName]  + ' -->'))[0]; +            template.replaceWith(templateNode); +            childTranscludeFn = compile(template, transcludeFn, terminalPriority); +          } else { +            template = jqLite(JQLiteClone(templateNode)); +            element.html(''); // clear contents +            childTranscludeFn = compile(template.contents(), transcludeFn); +          } +        } + +        if (directiveValue = 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()); +          var content = directiveValue.replace(CONTENT_REGEXP, element.html());            templateNode = jqLite(content)[0];            if (directive.replace) {              replaceWith(rootElement, element, templateNode); @@ -411,16 +526,16 @@ function $CompileProvider($provide) {            assertNoDuplicate('template', templateDirective, directive, element);            templateDirective = directive;            delayedLinkingFn = compileTemplateUrl(directives.splice(i, directives.length - i), -              compositeLinkFn, element, templateAttrs, rootElement, directive.replace); +              /* directiveLinkingFn */ compositeLinkFn, element, templateAttrs, rootElement, +              directive.replace, childTranscludeFn);            ii = directives.length;          } else if (directive.compile) {            try { -            linkingFn = directive.compile(element, templateAttrs); +            linkingFn = directive.compile(element, templateAttrs, childTranscludeFn);              if (isFunction(linkingFn)) { -              postLinkingFns.push(linkingFn); +              addLinkingFns(null, linkingFn);              } else if (linkingFn) { -              if (linkingFn.pre) preLinkingFns.push(linkingFn.pre); -              if (linkingFn.post) postLinkingFns.push(linkingFn.post); +              addLinkingFns(linkingFn.pre, linkingFn.post);              }            } catch (e) {              $exceptionHandler(e, startingTag(element)); @@ -433,16 +548,57 @@ function $CompileProvider($provide) {          }        } -      compositeLinkFn.scope = !!newScopeDirective; + +      linkingFn = delayedLinkingFn || compositeLinkFn; +      linkingFn.scope = newScopeDirective && newScopeDirective.scope; +      linkingFn.transclude = transcludeDirective && childTranscludeFn;        // if we have templateUrl, then we have to delay linking -      return delayedLinkingFn || compositeLinkFn; +      return linkingFn;        //////////////////// +      function addLinkingFns(pre, post) { +        if (pre) { +          pre.require = directive.require; +          preLinkingFns.push(pre); +        } +        if (post) { +          post.require = directive.require; +          postLinkingFns.push(post); +        } +      } + -      function compositeLinkFn(childLinkingFn, scope, linkNode) { -        var attrs, element, i, ii; +      function getControllers(require, element) { +        var value, retrievalMethod = 'data', optional = false; +        if (isString(require)) { +          while((value = require.charAt(0)) == '^' || value == '?') { +            require = require.substr(1); +            if (value == '^') { +              retrievalMethod = 'inheritedData'; +            } +            optional = optional || value == '?'; +          } +          value = element[retrievalMethod]('$' + require + 'Controller'); +          if (!value && !optional) { +            throw Error("No controller: " + require); +          } +          return value; +        } else if (isArray(require)) { +          value = []; +          forEach(require, function(require) { +            value.push(getControllers(require, element)); +          }); +        } +        return value; +      } + + +      /* directiveLinkingFn */ +      function compositeLinkFn(/* nodesetLinkingFn */ childLinkingFn, +                               scope, linkNode, rootElement, boundTranscludeFn) { +        var attrs, element, i, ii, linkingFn, controller;          if (templateNode === linkNode) {            attrs = templateAttrs; @@ -452,22 +608,59 @@ function $CompileProvider($provide) {          }          element = attrs.$element; +        if (newScopeDirective && isObject(newScopeDirective.scope)) { +          forEach(newScopeDirective.scope, function(mode, name) { +            (LOCAL_MODE[mode] || wrongMode)(name, mode, +                scope.$parent || scope, scope, attrs); +          }); +        } + +        if (controllerDirectives) { +          forEach(controllerDirectives, function(directive) { +            var locals = { +              $scope: scope, +              $element: element, +              $attrs: attrs, +              $transclude: boundTranscludeFn +            }; + + +            forEach(directive.inject || {}, function(mode, name) { +              (LOCAL_MODE[mode] || wrongMode)(name, mode, +                  newScopeDirective ? scope.$parent || scope : scope, locals, attrs); +            }); + +            controller = directive.controller; +            if (controller == '@') { +              controller = attrs[directive.name]; +            } + +            element.data( +                '$' + directive.name + 'Controller', +                $controller(controller, locals)); +          }); +        } +          // PRELINKING          for(i = 0, ii = preLinkingFns.length; i < ii; i++) {            try { -            preLinkingFns[i](scope, element, attrs); +            linkingFn = preLinkingFns[i]; +            linkingFn(scope, element, attrs, +                linkingFn.require && getControllers(linkingFn.require, element));            } catch (e) {              $exceptionHandler(e, startingTag(element));            }          }          // RECURSION -        childLinkingFn && childLinkingFn(scope, linkNode.childNodes); +        childLinkingFn && childLinkingFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);          // POSTLINKING          for(i = 0, ii = postLinkingFns.length; i < ii; i++) {            try { -            postLinkingFns[i](scope, element, attrs); +            linkingFn = postLinkingFns[i]; +            linkingFn(scope, element, attrs, +                linkingFn.require && getControllers(linkingFn.require, element));            } catch (e) {              $exceptionHandler(e, startingTag(element));            } @@ -490,14 +683,15 @@ function $CompileProvider($provide) {       *   * `M`: comment       * @returns true if directive was added.       */ -    function addDirective(tDirectives, name, location) { +    function addDirective(tDirectives, name, location, maxPriority) {        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) { +            if ( (maxPriority === undefined || maxPriority > directive.priority) && +                 directive.restrict.indexOf(location) != -1) {                tDirectives.push(directive);                match = true;              } @@ -540,15 +734,15 @@ function $CompileProvider($provide) {      } -    function compileTemplateUrl(directives, beforeWidgetLinkFn, tElement, tAttrs, rootElement, -                                replace) { +    function compileTemplateUrl(directives, /* directiveLinkingFn */ beforeWidgetLinkFn, +                                tElement, tAttrs, rootElement, replace, transcludeFn) {        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}), +          syncWidgetDirective = extend({}, asyncWidgetDirective, {templateUrl:null, transclude:null}),            html = tElement.html();        tElement.html(''); @@ -574,12 +768,13 @@ function $CompileProvider($provide) {            }            directives.unshift(syncWidgetDirective); -          afterWidgetLinkFn = applyDirectivesToNode(directives, tElement, tAttrs); -          afterWidgetChildrenLinkFn = compileNodes(tElement.contents()); +          afterWidgetLinkFn = /* directiveLinkingFn */ applyDirectivesToNode(directives, tElement, tAttrs, transcludeFn); +          afterWidgetChildrenLinkFn = /* nodesetLinkingFn */ compileNodes(tElement.contents(), transcludeFn);            while(linkQueue.length) { -            var linkRootElement = linkQueue.pop(), +            var controller = linkQueue.pop(), +                linkRootElement = linkQueue.pop(),                  cLinkNode = linkQueue.pop(),                  scope = linkQueue.pop(),                  node = templateNode; @@ -590,8 +785,8 @@ function $CompileProvider($provide) {                replaceWith(linkRootElement, jqLite(cLinkNode), node);              }              afterWidgetLinkFn(function() { -              beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node); -            }, scope, node); +              beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node, rootElement, controller); +            }, scope, node, rootElement, controller);            }            linkQueue = null;          }). @@ -599,15 +794,17 @@ function $CompileProvider($provide) {            throw Error('Failed to load template: ' + config.url);          }); -      return function(ignoreChildLinkingFn, scope, node, rootElement) { +      return /* directiveLinkingFn */ function(ignoreChildLinkingFn, scope, node, rootElement, +                                               controller) {          if (linkQueue) {            linkQueue.push(scope);            linkQueue.push(node);            linkQueue.push(rootElement); +          linkQueue.push(controller);          } else {            afterWidgetLinkFn(function() { -            beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node); -          }, scope, node); +            beforeWidgetLinkFn(afterWidgetChildrenLinkFn, scope, node, rootElement, controller); +          }, scope, node, rootElement, controller);          }        };      } @@ -759,3 +956,24 @@ var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;  function directiveNormalize(name) {    return camelCase(name.replace(PREFIX_REGEXP, ''));  } + + + +/** + * Closure compiler type information + */ + +function nodesetLinkingFn( +  /* angular.Scope */ scope, +  /* NodeList */ nodeList, +  /* Element */ rootElement, +  /* function(Function) */ boundTranscludeFn +){} + +function directiveLinkingFn( +  /* nodesetLinkingFn */ nodesetLinkingFn, +  /* angular.Scope */ scope, +  /* Node */ node, +  /* Element */ rootElement, +  /* function(Function) */ boundTranscludeFn +){} diff --git a/src/service/controller.js b/src/service/controller.js index 22fb3b02..229ce14a 100644 --- a/src/service/controller.js +++ b/src/service/controller.js @@ -1,15 +1,16 @@  'use strict';  function $ControllerProvider() { -  this.$get = ['$injector', function($injector) { +  this.$get = ['$injector', '$window', function($injector, $window) {      /**       * @ngdoc function       * @name angular.module.ng.$controller       * @requires $injector       * -     * @param {Function} Class Constructor function of a controller to instantiate. -     * @param {Object} scope Related scope. +     * @param {Function|string} Class Constructor function of a controller to instantiate, or +     *        expression to read from current scope or window. +     * @param {Object} locals Injection locals for Controller.       * @return {Object} Instance of given controller.       *       * @description @@ -19,8 +20,14 @@ function $ControllerProvider() {       * a service, so that one can override this service with {@link https://gist.github.com/1649788       * BC version}.       */ -    return function(Class, scope) { -      return $injector.instantiate(Class, {$scope: scope}); +    return function(Class, locals) { +      if(isString(Class)) { +        var expression = Class; +        Class = getter(locals.$scope, expression, true) || getter($window, expression, true); +        assertArgFn(Class, expression); +      } + +      return $injector.instantiate(Class, locals);      };    }];  } diff --git a/src/service/formFactory.js b/src/service/formFactory.js index 807f4113..b051f7b9 100644 --- a/src/service/formFactory.js +++ b/src/service/formFactory.js @@ -139,7 +139,7 @@ function $FormFactoryProvider() {      function formFactory(parent) {        var scope = (parent || formFactory.rootForm).$new(); -      $controller(FormController, scope); +      $controller(FormController, {$scope: scope});        return scope;      } diff --git a/src/service/route.js b/src/service/route.js index 9b52c4b0..932e26d5 100644 --- a/src/service/route.js +++ b/src/service/route.js @@ -280,7 +280,7 @@ function $RouteProvider(){              copy(next.params, $routeParams);              next.scope = parentScope.$new();              if (next.controller) { -              $controller(next.controller, next.scope); +              $controller(next.controller, {$scope: next.scope});              }            }          } diff --git a/src/service/scope.js b/src/service/scope.js index 9b9e9215..da1062a8 100644 --- a/src/service/scope.js +++ b/src/service/scope.js @@ -136,20 +136,36 @@ function $RootScopeProvider(){         * the scope and its child scopes to be permanently detached from the parent and thus stop         * participating in model change detection and listener notification by invoking.         * +       * @params {boolean} isolate if true then the scoped does not prototypically inherit from the +       *         parent scope. The scope is isolated, as it can not se parent scope properties. +       *         When creating widgets it is useful for the widget to not accidently read parent +       *         state. +       *         * @returns {Object} The newly created child scope.         *         */ -      $new: function() { -        var Child = function() {}; // should be anonymous; This is so that when the minifier munges -          // the name it does not become random set of chars. These will then show up as class -          // name in the debugger. -        var child; -        Child.prototype = this; -        child = new Child(); +      $new: function(isolate) { +        var Child, +            child; + +        if (isFunction(isolate)) { +          // TODO: remove at some point +          throw Error('API-CHANGE: Use $controller to instantiate controllers.'); +        } +        if (isolate) { +          child = new Scope(); +          child.$root = this.$root; +        } else { +          Child = function() {}; // should be anonymous; This is so that when the minifier munges +            // the name it does not become random set of chars. These will then show up as class +            // name in the debugger. +          Child.prototype = this; +          child = new Child(); +          child.$id = nextUid(); +        }          child['this'] = child;          child.$$listeners = {};          child.$parent = this; -        child.$id = nextUid();          child.$$asyncQueue = [];          child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;          child.$$prevSibling = this.$$childTail; @@ -277,7 +293,7 @@ function $RootScopeProvider(){         * `'Maximum iteration limit exceeded.'` if the number of iterations exceeds 100.         *         * Usually you don't call `$digest()` directly in -       * {@link angular.module.ng.$compileProvider.directive.ng:controller controllers} or in  +       * {@link angular.module.ng.$compileProvider.directive.ng:controller controllers} or in         * {@link angular.module.ng.$compileProvider.directive directives}.         * Instead a call to {@link angular.module.ng.$rootScope.Scope#$apply $apply()} (typically from within a         * {@link angular.module.ng.$compileProvider.directive directives}) will force a `$digest()`. diff --git a/src/widgets.js b/src/widgets.js index 18ac27c3..a465bc88 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -760,7 +760,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp    var BRACE = /{}/g;    return function(scope, element, attr) {      var numberExp = attr.count, -        whenExp = attr.when, +        whenExp = element.attr(attr.$attr.when), // this is becaues we have {{}} in attrs          offset = attr.offset || 0,          whens = scope.$eval(whenExp),          whensExpFns = {}; diff --git a/test/service/compilerSpec.js b/test/service/compilerSpec.js index 4de4641a..c998765c 100644 --- a/test/service/compilerSpec.js +++ b/test/service/compilerSpec.js @@ -207,7 +207,12 @@ describe('$compile', function() {              forEach(parts, function(value, key){                if (value.substring(0,3) == 'ng-') {                } else { -                list.push(value.replace('=""', '')); +                value = value.replace('=""', ''); +                var match = value.match(/=(.*)/); +                if (match && match[1].charAt(0) != '"') { +                  value = value.replace(/=(.*)/, '="$1"'); +                } +                list.push(value);                }              });              return '<' + list.join(' ') + '>'; @@ -864,6 +869,7 @@ describe('$compile', function() {        describe('scope', function() { +        var iscope;          beforeEach(module(function($compileProvider) {            forEach(['', 'a', 'b'], function(name) { @@ -878,6 +884,31 @@ describe('$compile', function() {                  }                };              }); +            $compileProvider.directive('iscope' + uppercase(name), function(log) { +              return { +                scope: {}, +                compile: function() { +                  return function (scope, element) { +                    iscope = scope; +                    log(scope.$id); +                    expect(element.data('$scope')).toBe(scope); +                  }; +                } +              }; +            }); +            $compileProvider.directive('tiscope' + uppercase(name), function(log) { +              return { +                scope: {}, +                templateUrl: 'tiscope.html', +                compile: function() { +                  return function (scope, element) { +                    iscope = scope; +                    log(scope.$id); +                    expect(element.data('$scope')).toBe(scope); +                  }; +                } +              }; +            });            });            $compileProvider.directive('log', function(log) {              return function(scope) { @@ -894,37 +925,80 @@ describe('$compile', function() {          })); -        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 allow creation of new isolated scopes', inject(function($rootScope, $compile, log) { +          element = $compile('<div><span iscope><a log></a></span></div>')($rootScope); +          expect(log).toEqual('LOG; log-002-001; 002'); +          $rootScope.name = 'abc'; +          expect(iscope.$parent).toBe($rootScope); +          expect(iscope.name).toBeUndefined(); +        })); + + +        it('should allow creation of new isolated scopes', inject( +            function($rootScope, $compile, log, $httpBackend) { +          $httpBackend.expect('GET', 'tiscope.html').respond('<a log></a>'); +          element = $compile('<div><span tiscope></span></div>')($rootScope); +          $httpBackend.flush(); +          expect(log).toEqual('LOG; log-002-001; 002'); +          $rootScope.name = 'abc'; +          expect(iscope.$parent).toBe($rootScope); +          expect(iscope.name).toBeUndefined();          })); -        it('should not allow more then one scope creation per element', inject( +        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 allow more then one scope creation per element', inject( +          function($rootScope, $compile, log) { +            $compile('<div class="scope-a; scope-b"></div>')($rootScope); +            expect(log).toEqual('001; 001'); +          }) +        ); + +        it('should not allow more then one isolate scope creation per element', inject( +          function($rootScope, $compile) { +            expect(function(){ +              $compile('<div class="iscope-a; scope-b"></div>'); +            }).toThrow('Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + +                '<' + (msie < 9 ? 'DIV' : 'div') + +                ' class="iscope-a; scope-b ng-isolate-scope ng-scope">'); +          }) +        ); + + +        it('should not allow more then one isolate 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 ng-scope">'); -          })); +              $compile('<div class="iscope-a; iscope-b"></div>'); +            }).toThrow('Multiple directives [iscopeA, iscopeB] asking for isolated scope on: ' + +                '<' + (msie < 9 ? 'DIV' : 'div') + +                ' class="iscope-a; iscope-b ng-isolate-scope ng-scope">'); +          }) +        );          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'); -          })); +          }) +        );        });      });    }); @@ -1193,4 +1267,359 @@ describe('$compile', function() {        })      });    }); + + +  describe('locals', function() { +    it('should marshal to locals', function() { +      module(function($compileProvider) { +        $compileProvider.directive('widget', function(log) { +          return { +            scope: { +              attr: 'attribute', +              prop: 'evaluate', +              bind: 'bind', +              assign: 'accessor', +              read: 'accessor', +              exp: 'expression', +              nonExist: 'accessor', +              nonExistExpr: 'expression' +            }, +            link: function(scope, element, attrs) { +              scope.nonExist(); // noop +              scope.nonExist(123); // noop +              scope.nonExistExpr(); // noop +              scope.nonExistExpr(123); // noop +              log(scope.attr); +              log(scope.prop); +              log(scope.assign()); +              log(scope.read()); +              log(scope.assign('ng')); +              scope.exp({myState:'OK'}); +              expect(function() { scope.read(undefined); }). +                  toThrow("Expression ''D'' not assignable."); +              scope.$watch('bind', log); +            } +          }; +        }); +      }); +      inject(function(log, $compile, $rootScope) { +        $rootScope.myProp = 'B'; +        $rootScope.bi = {nd: 'C'}; +        $rootScope.name = 'C'; +        element = $compile( +            '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' + +                'exp="state=myState">{{bind}}</div></div>') +            ($rootScope); +        expect(log).toEqual('A; B; C; D; ng'); +        expect($rootScope.name).toEqual('ng'); +        expect($rootScope.state).toEqual('OK'); +        log.reset(); +        $rootScope.$apply(); +        expect(element.text()).toEqual('C'); +        expect(log).toEqual('C'); +        $rootScope.bi.nd = 'c'; +        $rootScope.$apply(); +        expect(log).toEqual('C; c'); +      }); +    }); +  }); + + +  describe('controller', function() { +    it('should inject locals to controller', function() { +      module(function($compileProvider) { +        $compileProvider.directive('widget', function(log) { +          return { +            controller: function(attr, prop, assign, read, exp){ +              log(attr); +              log(prop); +              log(assign()); +              log(read()); +              log(assign('ng')); +              exp(); +              expect(function() { read(undefined); }). +                  toThrow("Expression ''D'' not assignable."); +              this.result = 'OK'; +            }, +            inject: { +              attr: 'attribute', +              prop: 'evaluate', +              assign: 'accessor', +              read: 'accessor', +              exp: 'expression' +            }, +            link: function(scope, element, attrs, controller) { +              log(controller.result); +            } +          }; +        }); +      }); +      inject(function(log, $compile, $rootScope) { +        $rootScope.myProp = 'B'; +        $rootScope.bi = {nd: 'C'}; +        $rootScope.name = 'C'; +        element = $compile( +            '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' + +                'exp="state=\'OK\'">{{bind}}</div></div>') +            ($rootScope); +        expect(log).toEqual('A; B; C; D; ng; OK'); +        expect($rootScope.name).toEqual('ng'); +      }); +    }); + + +    it('should get required controller', function() { +      module(function($compileProvider) { +        $compileProvider.directive('main', function(log) { +          return { +            priority: 2, +            controller: function() { +              this.name = 'main'; +            }, +            link: function(scope, element, attrs, controller) { +              log(controller.name); +            } +          }; +        }); +        $compileProvider.directive('dep', function(log) { +          return { +            priority: 1, +            require: 'main', +            link: function(scope, element, attrs, controller) { +              log('dep:' + controller.name); +            } +          }; +        }); +        $compileProvider.directive('other', function(log) { +          return { +            link: function(scope, element, attrs, controller) { +              log(!!controller); // should be false +            } +          }; +        }); +      }); +      inject(function(log, $compile, $rootScope) { +        element = $compile('<div main dep other></div>')($rootScope); +        expect(log).toEqual('main; dep:main; false'); +      }); +    }); + + +    it('should require controller on parent element',function() { +      module(function($compileProvider) { +        $compileProvider.directive('main', function(log) { +          return { +            controller: function() { +              this.name = 'main'; +            } +          }; +        }); +        $compileProvider.directive('dep', function(log) { +          return { +            require: '^main', +            link: function(scope, element, attrs, controller) { +              log('dep:' + controller.name); +            } +          }; +        }); +      }); +      inject(function(log, $compile, $rootScope) { +        element = $compile('<div main><div dep></div></div>')($rootScope); +        expect(log).toEqual('dep:main'); +      }); +    }); + + +    it('should have optional controller on current element', function() { +      module(function($compileProvider) { +        $compileProvider.directive('dep', function(log) { +          return { +            require: '?main', +            link: function(scope, element, attrs, controller) { +              log('dep:' + !!controller); +            } +          }; +        }); +      }); +      inject(function(log, $compile, $rootScope) { +        element = $compile('<div main><div dep></div></div>')($rootScope); +        expect(log).toEqual('dep:false'); +      }); +    }); + + +    it('should support multiple controllers', function() { +      module(function($compileProvider) { +        $compileProvider.directive('c1', valueFn({ +          controller: function() { this.name = 'c1'; } +        })); +        $compileProvider.directive('c2', valueFn({ +          controller: function() { this.name = 'c2'; } +        })); +        $compileProvider.directive('dep', function(log) { +          return { +            require: ['^c1', '^c2'], +            link: function(scope, element, attrs, controller) { +              log('dep:' + controller[0].name + '-' + controller[1].name); +            } +          }; +        }); +      }); +      inject(function(log, $compile, $rootScope) { +        element = $compile('<div c1 c2><div dep></div></div>')($rootScope); +        expect(log).toEqual('dep:c1-c2'); +      }); + +    }); +  }); + + +  describe('transclude', function() { +    it('should compile get templateFn', function() { +      module(function($compileProvider) { +        $compileProvider.directive('trans', function(log) { +          return { +            transclude: 'element', +            priority: 2, +            controller: function($transclude) { this.$transclude = $transclude; }, +            compile: function(element, attrs, template) { +              log('compile: ' + angular.mock.dump(element)); +              return function(scope, element, attrs, ctrl) { +                log('link'); +                var cursor = element; +                template(scope.$new(), function(clone) {cursor.after(cursor = clone)}); +                ctrl.$transclude(function(clone) {cursor.after(clone)}); +              }; +            } +          } +        }); +      }); +      inject(function(log, $rootScope, $compile) { +        element = $compile('<div><div high-log trans="text" log>{{$parent.$id}}-{{$id}};</div></div>') +            ($rootScope); +        $rootScope.$apply(); +        expect(log).toEqual('compile: <!-- trans: text -->; HIGH; link; LOG; LOG'); +        expect(element.text()).toEqual('001-002;001-003;'); +      }); +    }); + + +    it('should support transclude directive', function() { +      module(function($compileProvider) { +        $compileProvider.directive('trans', function() { +          return { +            transclude: 'content', +            replace: true, +            scope: true, +            template: '<ul><li>W:{{$parent.$id}}-{{$id}};</li><li ng-transclude></li></ul>' +          } +        }); +      }); +      inject(function(log, $rootScope, $compile) { +        element = $compile('<div><div trans>T:{{$parent.$id}}-{{$id}}<span>;</span></div></div>') +            ($rootScope); +        $rootScope.$apply(); +        expect(element.text()).toEqual('W:001-002;T:001-003;'); +        expect(jqLite(element.find('span')[0]).text()).toEqual('T:001-003'); +        expect(jqLite(element.find('span')[1]).text()).toEqual(';'); +      }); +    }); + + +    it('should transclude transcluded content', function() { +      module(function($compileProvider) { +        $compileProvider.directive('book', valueFn({ +          transclude: 'content', +          template: '<div>book-<div chapter>(<div ng-transclude></div>)</div></div>' +        })); +        $compileProvider.directive('chapter', valueFn({ +          transclude: 'content', +          templateUrl: 'chapter.html' +        })); +        $compileProvider.directive('section', valueFn({ +          transclude: 'content', +          template: '<div>section-!<div ng-transclude></div>!</div></div>' +        })); +        return function($httpBackend) { +          $httpBackend. +              expect('GET', 'chapter.html'). +              respond('<div>chapter-<div section>[<div ng-transclude></div>]</div></div>'); +        } +      }); +      inject(function(log, $rootScope, $compile, $httpBackend) { +        element = $compile('<div><div book>paragraph</div></div>')($rootScope); +        $rootScope.$apply(); + +        expect(element.text()).toEqual('book-'); + +        $httpBackend.flush(); +        $rootScope.$apply(); +        expect(element.text()).toEqual('book-chapter-section-![(paragraph)]!'); +      }); +    }); + + +    it('should only allow one transclude per element', function() { +      module(function($compileProvider) { +        $compileProvider.directive('first', valueFn({ +          scope: {}, +          transclude: 'content' +        })); +        $compileProvider.directive('second', valueFn({ +          transclude: 'content' +        })); +      }); +      inject(function($compile) { +        expect(function() { +          $compile('<div class="first second"></div>'); +        }).toThrow('Multiple directives [first, second] asking for transclusion on: <' + +            (msie <= 8 ? 'DIV' : 'div') + ' class="first second ng-isolate-scope ng-scope">'); +      }); +    }); + + +    it('should remove transclusion scope, when the DOM is destroyed', function() { +      module(function($compileProvider) { +        $compileProvider.directive('box', valueFn({ +          transclude: 'content', +          scope: { name: 'evaluate', show: 'accessor' }, +          template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>', +          link: function(scope, element) { +            scope.$watch( +                function() { return scope.show(); }, +                function(show) { +                  if (!show) { +                    element.find('div').find('div').remove(); +                  } +                } +            ); +          } +        })); +      }); +      inject(function($compile, $rootScope) { +        $rootScope.username = 'Misko'; +        $rootScope.select = true; +        element = $compile( +            '<div><div box name="username" show="select">user: {{username}}</div></div>') +              ($rootScope); +        $rootScope.$apply(); +        expect(element.text()).toEqual('Hello: Misko!user: Misko'); + +        var widgetScope = $rootScope.$$childHead; +        var transcludeScope = widgetScope.$$nextSibling; +        expect(widgetScope.name).toEqual('Misko'); +        expect(widgetScope.$parent).toEqual($rootScope); +        expect(transcludeScope.$parent).toEqual($rootScope); + +        var removed = 0; +        $rootScope.$on('$destroy', function() { removed++; }); +        $rootScope.select = false; +        $rootScope.$apply(); +        expect(element.text()).toEqual('Hello: Misko!'); +        expect(removed).toEqual(1); +        expect(widgetScope.$$nextSibling).toEqual(null); +      }); +    }); + +  });  }); diff --git a/test/service/controllerSpec.js b/test/service/controllerSpec.js index 8b12eceb..2c0f8c62 100644 --- a/test/service/controllerSpec.js +++ b/test/service/controllerSpec.js @@ -31,7 +31,7 @@ describe('$controller', function() {      };      var scope = {}, -        ctrl = $controller(MyClass, scope); +        ctrl = $controller(MyClass, {$scope: scope});      expect(ctrl.$scope).toBe(scope);    }); diff --git a/test/service/scopeSpec.js b/test/service/scopeSpec.js index c3a09cc8..179ff162 100644 --- a/test/service/scopeSpec.js +++ b/test/service/scopeSpec.js @@ -53,6 +53,15 @@ describe('Scope', function() {        $rootScope.a = 123;        expect(child.a).toEqual(123);      })); + +    it('should create a non prototypically inherited child scope', inject(function($rootScope) { +      var child = $rootScope.$new(true); +      $rootScope.a = 123; +      expect(child.a).toBeUndefined(); +      expect(child.$parent).toEqual($rootScope); +      expect(child.$new).toBe($rootScope.$new); +      expect(child.$root).toBe($rootScope); +    }));    }); | 
