diff --git a/src/loader.js b/src/loader.js index f46916c2c71b..336517783889 100644 --- a/src/loader.js +++ b/src/loader.js @@ -288,155 +288,12 @@ function setupModuleLoader(window) { * @module ng * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp) * @param {Object} options Component definition object (a simplified - * {@link ng.$compile#directive-definition-object directive definition object}), - * has the following properties (all optional): - * - * - `controller` – `{(string|function()=}` – Controller constructor function that should be - * associated with newly created scope or the name of a {@link ng.$compile#-controller- - * registered controller} if passed as a string. Empty function by default. - * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. - * If present, the controller will be published to scope under the `controllerAs` name. - * If not present, this will default to be the same as the component name. - * - `template` – `{string=|function()=}` – html template as a string or a function that - * returns an html template as a string which should be used as the contents of this component. - * Empty string by default. - * - * If `template` is a function, then it is {@link guide/di injectable}, and receives - * the following locals: - * - * - `$element` - Current element - * - `$attrs` - Current attributes object for the element - * - * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html - * template that should be used as the contents of this component. - * - * If `templateUrl` is a function, then it is {@link guide/di injectable}, and receives - * the following locals: - * - * - `$element` - Current element - * - `$attrs` - Current attributes object for the element - * - `bindings` – `{object=}` – Define DOM attribute binding to component properties. - * Component properties are always bound to the component controller and not to the scope. - * - `transclude` – `{boolean=}` – Whether {@link $compile#transclusion transclusion} is enabled. - * Disabled by default. - * - `isolate` – `{boolean=}` – Whether the new scope is isolated. Isolated by default. - * - `restrict` - `{string=}` - String of subset of {@link ng.$compile#-restrict- EACM} which - * restricts the component to specific directive declaration style. If omitted, this defaults to 'E'. - * - `$canActivate` – `{function()=}` – TBD. - * - `$routeConfig` – `{object=}` – TBD. + * {@link ng.$compile#directive-definition-object directive definition object}) * * @description - * Register a component definition with the compiler. This is short for registering a specific - * subset of directives which represents actual UI components in your application. Component - * definitions are very simple and do not require the complexity behind defining directives. - * Component definitions usually consist only of the template and the controller backing it. - * In order to make the definition easier, components enforce best practices like controllerAs - * and default behaviors like scope isolation, restrict to elements. - * - *
- * Here are a few examples of how you would usually define components: - * - * ```js - * var myMod = angular.module(...); - * myMod.component('myComp', { - * template: '
My name is {{myComp.name}}
', - * controller: function() { - * this.name = 'shahar'; - * } - * }); - * - * myMod.component('myComp', { - * template: '
My name is {{myComp.name}}
', - * bindings: {name: '@'} - * }); - * - * myMod.component('myComp', { - * templateUrl: 'views/my-comp.html', - * controller: 'MyCtrl as ctrl', - * bindings: {name: '@'} - * }); - * - * ``` - * - *
- * Components are also useful as route templates (e.g. when using - * {@link ngRoute ngRoute}): - * - * ```js - * var myMod = angular.module('myMod', ['ngRoute']); - * - * myMod.component('home', { - * template: '

Home

Hello, {{ home.user.name }} !

', - * controller: function() { - * this.user = {name: 'world'}; - * } - * }); - * - * myMod.config(function($routeProvider) { - * $routeProvider.when('/', { - * template: '' - * }); - * }); - * ``` - * - *
- * When using {@link ngRoute.$routeProvider $routeProvider}, you can often avoid some - * boilerplate, by assigning the resolved dependencies directly on the route scope: - * - * ```js - * var myMod = angular.module('myMod', ['ngRoute']); - * - * myMod.component('home', { - * template: '

Home

Hello, {{ home.user.name }} !

', - * bindings: {user: '='} - * }); - * - * myMod.config(function($routeProvider) { - * $routeProvider.when('/', { - * template: '', - * resolve: {user: function($http) { return $http.get('...'); }} - * }); - * }); - * ``` - * - *
- * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. + * See {@link ng.$compileProvider#component $compileProvider.component()}. */ - component: function(name, options) { - function factory($injector) { - function makeInjectable(fn) { - if (isFunction(fn) || Array.isArray(fn)) { - return function(tElement, tAttrs) { - return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); - }; - } else { - return fn; - } - } - - var template = (!options.template && !options.templateUrl ? '' : options.template); - return { - controller: options.controller || function() {}, - controllerAs: identifierForController(options.controller) || options.controllerAs || name, - template: makeInjectable(template), - templateUrl: makeInjectable(options.templateUrl), - transclude: options.transclude === undefined ? false : options.transclude, - scope: options.isolate === false ? true : {}, - bindToController: options.bindings || {}, - restrict: options.restrict || 'E' - }; - } - - if (options.$canActivate) { - factory.$canActivate = options.$canActivate; - } - if (options.$routeConfig) { - factory.$routeConfig = options.$routeConfig; - } - factory.$inject = ['$injector']; - - return moduleInstance.directive(name, factory); - }, + component: invokeLaterAndSetModuleName('$compileProvider', 'component'), /** * @ngdoc method diff --git a/src/ng/compile.js b/src/ng/compile.js index 87d6fb66b67f..407c4c56db47 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -867,8 +867,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which * will match as ng-bind), or an object map of directives where the keys are the * names and the values are the factories. - * @param {Function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. + * @param {Function|Array} directiveFactory An injectable directive factory function. See the + * {@link guide/directive directive guide} and the {@link $compile compile API} for more info. * @returns {ng.$compileProvider} Self for chaining. */ this.directive = function registerDirective(name, directiveFactory) { @@ -915,6 +915,162 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return this; }; + /** + * @ngdoc method + * @name $compileProvider#component + * @module ng + * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp) + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}), + * has the following properties (all optional): + * + * - `controller` – `{(string|function()=}` – Controller constructor function that should be + * associated with newly created scope or the name of a {@link ng.$compile#-controller- + * registered controller} if passed as a string. Empty function by default. + * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. + * If present, the controller will be published to scope under the `controllerAs` name. + * If not present, this will default to be the same as the component name. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used as the contents of this component. + * Empty string by default. + * + * If `template` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used as the contents of this component. + * + * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * - `bindings` – `{object=}` – Define DOM attribute binding to component properties. + * Component properties are always bound to the component controller and not to the scope. + * - `transclude` – `{boolean=}` – Whether {@link $compile#transclusion transclusion} is enabled. + * Enabled by default. + * - `isolate` – `{boolean=}` – Whether the new scope is isolated. Isolated by default. + * - `restrict` - `{string=}` - String of subset of {@link ng.$compile#-restrict- EACM} which + * restricts the component to specific directive declaration style. If omitted, this defaults to 'E'. + * - `$canActivate` – `{function()=}` – TBD. + * - `$routeConfig` – `{object=}` – TBD. + * + * @returns {ng.$compileProvider} Self for chaining. + * @description + * Register a component definition with the compiler. This is short for registering a specific + * type of directive which represents a self-contained UI component in your application. Component + * definitions are very simple and do not require the complexity behind defining directives. + * Component definitions usually consist only of the template and the controller backing it. + * In order to make the definition easier, components enforce best practices like controllerAs + * and default behaviors like scope isolation and restriction to elements. + * + * Here are a few examples of how you would usually define components: + * + * ```js + * var myMod = angular.module(...); + * myMod.component('myComp', { + * template: '
My name is {{myComp.name}}
', + * controller: function() { + * this.name = 'shahar'; + * } + * }); + * + * myMod.component('myComp', { + * template: '
My name is {{myComp.name}}
', + * bindings: {name: '@'} + * }); + * + * myMod.component('myComp', { + * templateUrl: 'views/my-comp.html', + * controller: 'MyCtrl as ctrl', + * bindings: {name: '@'} + * }); + * + * ``` + * + *
+ * Components are also useful as route templates (e.g. when using + * {@link ngRoute ngRoute}): + * + * ```js + * var myMod = angular.module('myMod', ['ngRoute']); + * + * myMod.component('home', { + * template: '

Home

Hello, {{ home.user.name }} !

', + * controller: function() { + * this.user = {name: 'world'}; + * } + * }); + * + * myMod.config(function($routeProvider) { + * $routeProvider.when('/', { + * template: '' + * }); + * }); + * ``` + * + *
+ * When using {@link ngRoute.$routeProvider $routeProvider}, you can often avoid some + * boilerplate, by assigning the resolved dependencies directly on the route scope: + * + * ```js + * var myMod = angular.module('myMod', ['ngRoute']); + * + * myMod.component('home', { + * template: '

Home

Hello, {{ home.user.name }} !

', + * bindings: {user: '='} + * }); + * + * myMod.config(function($routeProvider) { + * $routeProvider.when('/', { + * template: '', + * resolve: {user: function($http) { return $http.get('...'); }} + * }); + * }); + * ``` + * + *
+ * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + this.component = function registerComponent(name, options) { + function factory($injector) { + function makeInjectable(fn) { + if (isFunction(fn) || Array.isArray(fn)) { + return function(tElement, tAttrs) { + return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); + }; + } else { + return fn; + } + } + + var template = (!options.template && !options.templateUrl ? '' : options.template); + return { + controller: options.controller || function() {}, + controllerAs: identifierForController(options.controller) || options.controllerAs || name, + template: makeInjectable(template), + templateUrl: makeInjectable(options.templateUrl), + transclude: options.transclude === undefined ? false : options.transclude, + scope: options.isolate === false ? true : {}, + bindToController: options.bindings || {}, + restrict: options.restrict || 'E' + }; + } + + if (options.$canActivate) { + factory.$canActivate = options.$canActivate; + } + if (options.$routeConfig) { + factory.$routeConfig = options.$routeConfig; + } + factory.$inject = ['$injector']; + + return this.directive(name, factory); + }; + /** * @ngdoc method diff --git a/test/loaderSpec.js b/test/loaderSpec.js index 3794dbe68461..34eef0f20183 100644 --- a/test/loaderSpec.js +++ b/test/loaderSpec.js @@ -39,6 +39,7 @@ describe('module loader', function() { value('k', 'v'). filter('f', 'ff'). directive('d', 'dd'). + component('c', 'cc'). controller('ctrl', 'ccc'). config('init2'). constant('abc', 123). @@ -54,6 +55,7 @@ describe('module loader', function() { ['$provide', 'value', ['k', 'v']], ['$filterProvider', 'register', ['f', 'ff']], ['$compileProvider', 'directive', ['d', 'dd']], + ['$compileProvider', 'component', ['c', 'cc']], ['$controllerProvider', 'register', ['ctrl', 'ccc']] ]); expect(myModule._configBlocks).toEqual([ @@ -87,132 +89,3 @@ describe('module loader', function() { expect(window.angular.$$minErr).toEqual(jasmine.any(Function)); }); }); - - -describe('component', function() { - it('should return the module', function() { - var myModule = window.angular.module('my', []); - expect(myModule.component('myComponent', {})).toBe(myModule); - }); - - it('should register a directive', function() { - var myModule = window.angular.module('my', []).component('myComponent', {}); - expect(myModule._invokeQueue).toEqual( - [['$compileProvider', 'directive', ['myComponent', jasmine.any(Function)]]]); - }); - - it('should add router annotations to directive factory', function() { - var myModule = window.angular.module('my', []).component('myComponent', { - $canActivate: 'canActivate', - $routeConfig: 'routeConfig' - }); - expect(myModule._invokeQueue.pop().pop()[1]).toEqual(jasmine.objectContaining({ - $canActivate: 'canActivate', - $routeConfig: 'routeConfig' - })); - }); - - it('should return ddo with reasonable defaults', function() { - window.angular.module('my', []).component('myComponent', {}); - module('my'); - inject(function(myComponentDirective) { - expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ - controller: jasmine.any(Function), - controllerAs: 'myComponent', - template: '', - templateUrl: undefined, - transclude: false, - scope: {}, - bindToController: {}, - restrict: 'E' - })); - }); - }); - - it('should return ddo with assigned options', function() { - function myCtrl() {} - window.angular.module('my', []).component('myComponent', { - controller: myCtrl, - controllerAs: 'ctrl', - template: 'abc', - templateUrl: 'def.html', - transclude: true, - isolate: false, - bindings: {abc: '='}, - restrict: 'EA' - }); - module('my'); - inject(function(myComponentDirective) { - expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ - controller: myCtrl, - controllerAs: 'ctrl', - template: 'abc', - templateUrl: 'def.html', - transclude: true, - scope: true, - bindToController: {abc: '='}, - restrict: 'EA' - })); - }); - }); - - it('should allow passing injectable functions as template/templateUrl', function() { - var log = ''; - window.angular.module('my', []).component('myComponent', { - template: function($element, $attrs, myValue) { - log += 'template,' + $element + ',' + $attrs + ',' + myValue + '\n'; - }, - templateUrl: function($element, $attrs, myValue) { - log += 'templateUrl,' + $element + ',' + $attrs + ',' + myValue + '\n'; - } - }).value('myValue', 'blah'); - module('my'); - inject(function(myComponentDirective) { - myComponentDirective[0].template('a', 'b'); - myComponentDirective[0].templateUrl('c', 'd'); - expect(log).toEqual('template,a,b,blah\ntemplateUrl,c,d,blah\n'); - }); - }); - - it('should allow passing injectable arrays as template/templateUrl', function() { - var log = ''; - window.angular.module('my', []).component('myComponent', { - template: ['$element', '$attrs', 'myValue', function($element, $attrs, myValue) { - log += 'template,' + $element + ',' + $attrs + ',' + myValue + '\n'; - }], - templateUrl: ['$element', '$attrs', 'myValue', function($element, $attrs, myValue) { - log += 'templateUrl,' + $element + ',' + $attrs + ',' + myValue + '\n'; - }] - }).value('myValue', 'blah'); - module('my'); - inject(function(myComponentDirective) { - myComponentDirective[0].template('a', 'b'); - myComponentDirective[0].templateUrl('c', 'd'); - expect(log).toEqual('template,a,b,blah\ntemplateUrl,c,d,blah\n'); - }); - }); - - it('should allow passing transclude as object', function() { - window.angular.module('my', []).component('myComponent', { - transclude: {} - }); - module('my'); - inject(function(myComponentDirective) { - expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ - transclude: {} - })); - }); - }); - - it('should give ctrl as syntax priority over controllerAs', function() { - window.angular.module('my', []).component('myComponent', { - controller: 'MyCtrl as vm' - }); - module('my'); - inject(function(myComponentDirective) { - expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ - controllerAs: 'vm' - })); - }); - }); -}); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 093b192b8ed8..3e57bd459d44 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -9276,4 +9276,159 @@ describe('$compile', function() { testReplaceElementCleanup({replace: true, asyncTemplate: true}); }); }); + + describe('component helper', function() { + it('should return the module', function() { + var myModule = angular.module('my', []); + expect(myModule.component('myComponent', {})).toBe(myModule); + }); + + it('should register a directive', function() { + angular.module('my', []).component('myComponent', { + template: '
SUCCESS
', + controller: function(log) { + log('OK'); + } + }); + module('my'); + + inject(function($compile, $rootScope, log) { + element = $compile('')($rootScope); + expect(element.find('div').text()).toEqual('SUCCESS'); + expect(log).toEqual('OK'); + }); + }); + + it('should register a directive via $compileProvider.component()', function() { + module(function($compileProvider) { + $compileProvider.component('myComponent', { + template: '
SUCCESS
', + controller: function(log) { + log('OK'); + } + }); + }); + + inject(function($compile, $rootScope, log) { + element = $compile('')($rootScope); + expect(element.find('div').text()).toEqual('SUCCESS'); + expect(log).toEqual('OK'); + }); + }); + + it('should add router annotations to directive factory', function() { + var myModule = angular.module('my', []).component('myComponent', { + $canActivate: 'canActivate', + $routeConfig: 'routeConfig' + }); + expect(myModule._invokeQueue.pop().pop()[1]).toEqual(jasmine.objectContaining({ + $canActivate: 'canActivate', + $routeConfig: 'routeConfig' + })); + }); + + it('should return ddo with reasonable defaults', function() { + angular.module('my', []).component('myComponent', {}); + module('my'); + inject(function(myComponentDirective) { + expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ + controller: jasmine.any(Function), + controllerAs: 'myComponent', + template: '', + templateUrl: undefined, + transclude: false, + scope: {}, + bindToController: {}, + restrict: 'E' + })); + }); + }); + + it('should return ddo with assigned options', function() { + function myCtrl() {} + angular.module('my', []).component('myComponent', { + controller: myCtrl, + controllerAs: 'ctrl', + template: 'abc', + templateUrl: 'def.html', + transclude: true, + isolate: false, + bindings: {abc: '='}, + restrict: 'EA' + }); + module('my'); + inject(function(myComponentDirective) { + expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ + controller: myCtrl, + controllerAs: 'ctrl', + template: 'abc', + templateUrl: 'def.html', + transclude: true, + scope: true, + bindToController: {abc: '='}, + restrict: 'EA' + })); + }); + }); + + it('should allow passing injectable functions as template/templateUrl', function() { + var log = ''; + angular.module('my', []).component('myComponent', { + template: function($element, $attrs, myValue) { + log += 'template,' + $element + ',' + $attrs + ',' + myValue + '\n'; + }, + templateUrl: function($element, $attrs, myValue) { + log += 'templateUrl,' + $element + ',' + $attrs + ',' + myValue + '\n'; + } + }).value('myValue', 'blah'); + module('my'); + inject(function(myComponentDirective) { + myComponentDirective[0].template('a', 'b'); + myComponentDirective[0].templateUrl('c', 'd'); + expect(log).toEqual('template,a,b,blah\ntemplateUrl,c,d,blah\n'); + }); + }); + + it('should allow passing injectable arrays as template/templateUrl', function() { + var log = ''; + angular.module('my', []).component('myComponent', { + template: ['$element', '$attrs', 'myValue', function($element, $attrs, myValue) { + log += 'template,' + $element + ',' + $attrs + ',' + myValue + '\n'; + }], + templateUrl: ['$element', '$attrs', 'myValue', function($element, $attrs, myValue) { + log += 'templateUrl,' + $element + ',' + $attrs + ',' + myValue + '\n'; + }] + }).value('myValue', 'blah'); + module('my'); + inject(function(myComponentDirective) { + myComponentDirective[0].template('a', 'b'); + myComponentDirective[0].templateUrl('c', 'd'); + expect(log).toEqual('template,a,b,blah\ntemplateUrl,c,d,blah\n'); + }); + }); + + it('should allow passing transclude as object', function() { + angular.module('my', []).component('myComponent', { + transclude: {} + }); + module('my'); + inject(function(myComponentDirective) { + expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ + transclude: {} + })); + }); + }); + + it('should give ctrl as syntax priority over controllerAs', function() { + angular.module('my', []).component('myComponent', { + controller: 'MyCtrl as vm' + }); + module('my'); + inject(function(myComponentDirective) { + expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ + controllerAs: 'vm' + })); + }); + }); + }); });