aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMisko Hevery2012-01-27 16:18:16 -0800
committerMisko Hevery2012-02-21 22:46:00 -0800
commit78656fe0dfc99c341ce02d71e7006e9c05b1fe3f (patch)
treea68731c4c1675047da65b23ccf3d562324324081
parentcb10ccc44fa78b82c80afa1cb5dac2c34fdf24b7 (diff)
downloadangular.js-78656fe0dfc99c341ce02d71e7006e9c05b1fe3f.tar.bz2
feat($compile) add locals, isolate scope, transclusion
-rw-r--r--src/AngularPublic.js3
-rw-r--r--src/angular-bootstrap.js1
-rw-r--r--src/angular-mocks.js3
-rw-r--r--src/directives.js69
-rw-r--r--src/service/compiler.js330
-rw-r--r--src/service/controller.js17
-rw-r--r--src/service/formFactory.js2
-rw-r--r--src/service/route.js2
-rw-r--r--src/service/scope.js34
-rw-r--r--src/widgets.js2
-rw-r--r--test/service/compilerSpec.js471
-rw-r--r--test/service/controllerSpec.js2
-rw-r--r--test/service/scopeSpec.js9
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);
+ }));
});