From 8955cfb6462f79a32caa641ffc002f1522f08220 Mon Sep 17 00:00:00 2001 From: Lucas Mirelmann Date: Mon, 14 Dec 2015 23:45:19 +0100 Subject: [PATCH] feat($compile): Allow ES6 classes as controllers with `bindToController: true` Modify `$injector.invoke` so ES6 classes would be invoked using `new` Closes: #13510 Closes: #13540 Closes: #13682 --- src/auto/injector.js | 23 +++++++++++++++++---- src/ng/compile.js | 2 +- test/ng/compileSpec.js | 47 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/auto/injector.js b/src/auto/injector.js index 9ab248acba1f..d16bfb829f45 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -821,6 +821,14 @@ function createInjector(modulesToLoad, strictDi) { return args; } + function isClass(func) { + // IE 9-11 do not support classes and IE9 leaks with the code below. + if (msie <= 11) { + return false; + } + return typeof func === 'function' + && /^class\s/.test(Function.prototype.toString.call(func)); + } function invoke(fn, self, locals, serviceName) { if (typeof locals === 'string') { @@ -833,9 +841,16 @@ 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); + /*jshint -W058 */ // Applying a constructor without immediate parentheses is the point here. + return new (Function.prototype.bind.apply(fn, args)); + /*jshint +W058 */ + } } @@ -845,7 +860,7 @@ function createInjector(modulesToLoad, strictDi) { var ctor = (isArray(Type) ? Type[Type.length - 1] : Type); var args = injectionArgs(Type, locals, serviceName); // Empty object at position 0 is ignored for invocation with `new`, but required. - args.unshift({}); + args.unshift(null); /*jshint -W058 */ // Applying a constructor without immediate parentheses is the point here. return new (Function.prototype.bind.apply(ctor, args)); /*jshint +W058 */ diff --git a/src/ng/compile.js b/src/ng/compile.js index b6331ad69105..87d6fb66b67f 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -214,7 +214,7 @@ * #### `bindToController` * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will * allow a component to have its properties bound to the controller, rather than to scope. When the controller - * is instantiated, the initial values of the isolate scope bindings are already available. + * is instantiated, the initial values of the isolate scope bindings will be available if the controller is not an ES6 class. * * #### `controller` * Controller constructor function. The controller is instantiated before the diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index c420b3e2d090..093b192b8ed8 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4235,6 +4235,53 @@ describe('$compile', function() { }); + it('should eventually expose isolate scope variables on ES6 class controller with controllerAs when bindToController is true', function() { + if (!/chrome/i.test(navigator.userAgent)) return; + /*jshint -W061 */ + var controllerCalled = false; + module(function($compileProvider) { + $compileProvider.directive('fooDir', valueFn({ + template: '

isolate

', + scope: { + 'data': '=dirData', + 'str': '@dirStr', + 'fn': '&dirFn' + }, + controller: eval( + "class Foo {" + + " constructor($scope) {}" + + " check() {" + + " expect(this.data).toEqualData({" + + " 'foo': 'bar'," + + " 'baz': 'biz'" + + " });" + + " expect(this.str).toBe('Hello, world!');" + + " expect(this.fn()).toBe('called!');" + + " controllerCalled = true;" + + " }" + + "}" + ), + controllerAs: 'test', + bindToController: true + })); + }); + inject(function($compile, $rootScope) { + $rootScope.fn = valueFn('called!'); + $rootScope.whom = 'world'; + $rootScope.remoteData = { + 'foo': 'bar', + 'baz': 'biz' + }; + element = $compile('
')($rootScope); + element.data('$fooDirController').check(); + expect(controllerCalled).toBe(true); + }); + /*jshint +W061 */ + }); + + it('should update @-bindings on controller when bindToController and attribute change observed', function() { module(function($compileProvider) { $compileProvider.directive('atBinding', valueFn({