feat($compile) add locals, isolate scope, transclusion

This commit is contained in:
Misko Hevery 2012-01-27 16:18:16 -08:00
parent cb10ccc44f
commit 78656fe0df
13 changed files with 838 additions and 107 deletions

View file

@ -94,7 +94,8 @@ function publishExternalAPI(angular){
ngStyle: ngStyleDirective,
ngSwitch: ngSwitchDirective,
ngOptions: ngOptionsDirective,
ngView: ngViewDirective
ngView: ngViewDirective,
ngTransclude: ngTranscludeDirective
}).
directive(ngEventDirectives).
directive(ngAttributeAliasDirectives);

View file

@ -101,6 +101,7 @@
globalVars = {};
bindJQuery();
publishExternalAPI(window.angular);
angularInit(document, angular.bootstrap);
}

View file

@ -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);

View file

@ -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);
});
}]
});

View file

@ -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) {
return function(templateElement) {
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;
//================================
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
){}

View file

@ -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);
};
}];
}

View file

@ -139,7 +139,7 @@ function $FormFactoryProvider() {
function formFactory(parent) {
var scope = (parent || formFactory.rootForm).$new();
$controller(FormController, scope);
$controller(FormController, {$scope: scope});
return scope;
}

View file

@ -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});
}
}
}

View file

@ -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()`.

View file

@ -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 = {};

View file

@ -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 not allow more then one scope creation per element', inject(
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 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="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; 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="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);
});
});
});
});

View file

@ -31,7 +31,7 @@ describe('$controller', function() {
};
var scope = {},
ctrl = $controller(MyClass, scope);
ctrl = $controller(MyClass, {$scope: scope});
expect(ctrl.$scope).toBe(scope);
});

View file

@ -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);
}));
});