diff --git a/src/auto/injector.js b/src/auto/injector.js index c1cea832acde..b4995af8f6f6 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -70,8 +70,12 @@ var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); +function stringifyFn(fn) { + return Function.prototype.toString.call(fn); +} + function extractArgs(fn) { - var fnText = Function.prototype.toString.call(fn).replace(STRIP_COMMENTS, ''), + var fnText = stringifyFn(fn).replace(STRIP_COMMENTS, ''), args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS); return args; } @@ -910,6 +914,19 @@ function createInjector(modulesToLoad, strictDi) { return args; } + function isClass(func) { + // Support: IE 9-11 only + // IE 9-11 do not support classes and IE9 leaks with the code below. + if (msie || typeof func !== 'function') { + return false; + } + var result = func.$$ngIsClass; + if (!isBoolean(result)) { + result = func.$$ngIsClass = /^class\b/.test(stringifyFn(func)); + } + return result; + } + function invoke(fn, self, locals, serviceName) { if (typeof locals === 'string') { serviceName = locals; @@ -921,9 +938,14 @@ function createInjector(modulesToLoad, strictDi) { fn = fn[fn.length - 1]; } - // http://jsperf.com/angularjs-invoke-apply-vs-switch - // #5388 - return fn.apply(self, args); + if (!isClass(fn)) { + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); + } else { + args.unshift(null); + return new (Function.prototype.bind.apply(fn, args))(); + } } diff --git a/src/ng/compile.js b/src/ng/compile.js index 470640d2d329..a257c0b5f096 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -2790,6 +2790,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }; } + if (controllerDirectives) { + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective); + } + if (newIsolateScopeDirective) { // Initialize isolate scope bindings for new isolate scope directive. compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || @@ -2805,69 +2809,53 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - if (controllerDirectives) { - elementControllers = createMap(); - for (var name in controllerDirectives) { - var directive = controllerDirectives[name]; - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }; - - var controllerConstructor = directive.controller; - if (controllerConstructor === '@') { - controllerConstructor = attrs[name]; - } + // Initialize bindToController bindings + for (var name in elementControllers) { + var controllerDirective = controllerDirectives[name]; + var controller = elementControllers[name]; + var bindings = controllerDirective.$$bindings.bindToController; - var instance = $controller(controllerConstructor, locals, directive.controllerAs); - - $element.data('$' + name + 'Controller', instance); - - // Initialize bindToController bindings - var bindings = directive.$$bindings.bindToController; - var bindingInfo = initializeDirectiveBindings(controllerScope, attrs, instance, bindings, directive); - - elementControllers[name] = { instance: instance, bindingInfo: bindingInfo }; + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } - // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy - forEach(controllerDirectives, function(controllerDirective, name) { - var require = controllerDirective.require; - if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { - extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); - } - }); + // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy + forEach(controllerDirectives, function(controllerDirective, name) { + var require = controllerDirective.require; + if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { + extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); + } + }); - // Handle the init and destroy lifecycle hooks on all controllers that have them - forEach(elementControllers, function(controller) { - var controllerInstance = controller.instance; - if (isFunction(controllerInstance.$onChanges)) { - try { - controllerInstance.$onChanges(controller.bindingInfo.initialChanges); - } catch (e) { - $exceptionHandler(e); - } - } - if (isFunction(controllerInstance.$onInit)) { - try { - controllerInstance.$onInit(); - } catch (e) { - $exceptionHandler(e); - } - } - if (isFunction(controllerInstance.$doCheck)) { - controllerScope.$watch(function() { controllerInstance.$doCheck(); }); - controllerInstance.$doCheck(); + // Handle the init and destroy lifecycle hooks on all controllers that have them + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$onChanges)) { + try { + controllerInstance.$onChanges(controller.bindingInfo.initialChanges); + } catch (e) { + $exceptionHandler(e); } - if (isFunction(controllerInstance.$onDestroy)) { - controllerScope.$on('$destroy', function callOnDestroyHook() { - controllerInstance.$onDestroy(); - }); + } + if (isFunction(controllerInstance.$onInit)) { + try { + controllerInstance.$onInit(); + } catch (e) { + $exceptionHandler(e); } - }); - } + } + if (isFunction(controllerInstance.$doCheck)) { + controllerScope.$watch(function() { controllerInstance.$doCheck(); }); + controllerInstance.$doCheck(); + } + if (isFunction(controllerInstance.$onDestroy)) { + controllerScope.$on('$destroy', function callOnDestroyHook() { + controllerInstance.$onDestroy(); + }); + } + }); // PRELINKING for (i = 0, ii = preLinkFns.length; i < ii; i++) { @@ -2995,6 +2983,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return value || null; } + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) { + var elementControllers = createMap(); + for (var controllerKey in controllerDirectives) { + var directive = controllerDirectives[controllerKey]; + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }; + + var controller = directive.controller; + if (controller === '@') { + controller = attrs[directive.name]; + } + + var controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment. + // In this case .data will not attach any data. + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); + } + return elementControllers; + } + // Depending upon the context in which a directive finds itself it might need to have a new isolated // or child scope created. For instance: // * if the directive has been pulled into a template because another directive with a higher priority diff --git a/src/ng/controller.js b/src/ng/controller.js index 8148307bd431..3b8d6196449b 100644 --- a/src/ng/controller.js +++ b/src/ng/controller.js @@ -81,11 +81,16 @@ function $ControllerProvider() { * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function $controller(expression, locals, ident) { + return function $controller(expression, locals, later, ident) { // PRIVATE API: + // param `later` --- indicates that the controller's constructor is invoked at a later time. + // If true, $controller will allocate the object with the correct + // prototype chain, but will not invoke the controller until a returned + // callback is invoked. // param `ident` --- An optional label which overrides the label parsed from the controller // expression, if any. var instance, match, constructor, identifier; + later = later === true; if (ident && isString(ident)) { identifier = ident; } @@ -111,6 +116,41 @@ function $ControllerProvider() { assertArgFn(expression, constructor, true); } + if (later) { + // Instantiate controller later: + // This machinery is used to create an instance of the object before calling the + // controller's constructor itself. + // + // This allows properties to be added to the controller before the constructor is + // invoked. Primarily, this is used for isolate scope bindings in $compile. + // + // This feature is not intended for use by applications, and is thus not documented + // publicly. + // Object creation: http://jsperf.com/create-constructor/2 + var controllerPrototype = (isArray(expression) ? + expression[expression.length - 1] : expression).prototype; + instance = Object.create(controllerPrototype || null); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); + } + + return extend(function $controllerInit() { + var result = $injector.invoke(expression, instance, locals, constructor); + if (result !== instance && (isObject(result) || isFunction(result))) { + instance = result; + if (identifier) { + // If result changed, re-assign controllerAs value to scope. + addIdentifier(locals, identifier, instance, constructor || expression.name); + } + } + return instance; + }, { + instance: instance, + identifier: identifier + }); + } + instance = $injector.instantiate(expression, locals, constructor); if (identifier) { diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index d051ab18a7ef..4be9d7ab2d47 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -2345,11 +2345,14 @@ angular.mock.$RootElementProvider = function() { */ function createControllerDecorator() { angular.mock.$ControllerDecorator = ['$delegate', function($delegate) { - return function(expression, locals, bindings, ident) { - if (angular.isString(bindings)) ident = bindings; - var instance = $delegate(expression, locals, ident); - angular.extend(instance, bindings); - return instance; + return function(expression, locals, later, ident) { + if (later && typeof later === 'object') { + var instantiate = $delegate(expression, locals, true, ident); + var instance = instantiate(); + angular.extend(instance, later); + return instance; + } + return $delegate(expression, locals, later, ident); }; }]; diff --git a/test/auto/injectorSpec.js b/test/auto/injectorSpec.js index 7189c95de5f7..7f5254e201e6 100644 --- a/test/auto/injectorSpec.js +++ b/test/auto/injectorSpec.js @@ -482,6 +482,25 @@ describe('injector', function() { expect(instance).toEqual(new Clazz('a-value')); expect(instance.aVal()).toEqual('a-value'); }); + + they('should detect ES6 classes regardless of whitespace/comments ($prop)', [ + 'class Test {}', + 'class Test{}', + 'class //<--ES6 stuff\nTest {}', + 'class//<--ES6 stuff\nTest {}', + 'class {}', + 'class{}', + 'class //<--ES6 stuff\n {}', + 'class//<--ES6 stuff\n {}', + 'class/* Test */{}', + 'class /* Test */ {}' + ], function(classDefinition) { + // eslint-disable-next-line no-eval + var Clazz = eval('(' + classDefinition + ')'); + var instance = injector.invoke(Clazz); + + expect(instance).toEqual(jasmine.any(Clazz)); + }); } });