From 292b37d48e4a8457ddfd3812c156bab7599e4214 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Thu, 24 Sep 2015 09:35:54 +0300 Subject: [PATCH 1/2] feat(loader): add convenience method for creating components Closes #10007 --- src/loader.js | 79 +++++++++++++++++++++++++++++++++ test/loaderSpec.js | 108 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/src/loader.js b/src/loader.js index 93ce8e720a99..6c7a4bfffb12 100644 --- a/src/loader.js +++ b/src/loader.js @@ -282,6 +282,85 @@ function setupModuleLoader(window) { */ directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), + /** + * @ngdoc method + * @name angular.Module#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, has the following properties (all optional): + * + * - `controller` – `{(string|function()=}` – Controller fn that should be associated with + * newly created scope or the name of a {@link angular.Module#controller registered + * controller} if passed as a string. + * - `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. + * + * 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 + * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion transclusion} is enabled. + * enabled by default. + * - `isolate` – `{boolean=}` – whether the new scope is isolated. Isolated by default. + * - `bindings` – `{object=}` – define DOM attribute binding to component properties. + * component properties are always bound to the component controller and not to the scope. + * - `$canActivate` – `{function()=}` – TBD. + * - `$routeConfig` – `{object=}` – TBD. + * + * @description + * Register a component definition with the compiler. This is short for registering a directive + * where its definition object is isolated, allows transclusion and bound to controller as the + * component name by default. + * See {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + component: function(name, options) { + function factory($injector) { + function makeInjectable(fn) { + if (angular.isFunction(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 ? true : options.transclude, + scope: options.isolate === false ? true : {}, + bindToController: options.bindings || {} + }; + } + + if (options.$canActivate) { + factory.$canActivate = options.$canActivate; + } + if (options.$routeConfig) { + factory.$routeConfig = options.$routeConfig; + } + factory.$inject = ['$injector']; + + return moduleInstance.directive(name, factory); + }, + /** * @ngdoc method * @name angular.Module#config diff --git a/test/loaderSpec.js b/test/loaderSpec.js index 1c4d0bb45d84..6ec842d39d69 100644 --- a/test/loaderSpec.js +++ b/test/loaderSpec.js @@ -87,3 +87,111 @@ 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: true, + scope: {}, + bindToController: {} + })); + }); + }); + + 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: false, + isolate: false, + bindings: {abc: '='} + }); + module('my'); + inject(function(myComponentDirective) { + expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({ + controller: myCtrl, + controllerAs: 'ctrl', + template: 'abc', + templateUrl: 'def.html', + transclude: false, + scope: true, + bindToController: {abc: '='} + })); + }); + }); + + 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 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' + })); + }); + }); +}); From d90f0494907771962f3c6b43e4534259a9eec7cb Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Tue, 13 Oct 2015 22:17:12 +0300 Subject: [PATCH 2/2] doc updates + adding restrict --- src/loader.js | 59 ++++++++++++++++++++++++++++++++++++---------- test/loaderSpec.js | 9 ++++--- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/loader.js b/src/loader.js index 6c7a4bfffb12..2a818f7f26f4 100644 --- a/src/loader.js +++ b/src/loader.js @@ -287,16 +287,19 @@ function setupModuleLoader(window) { * @name angular.Module#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, has the following properties (all optional): + * @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 fn that should be associated with - * newly created scope or the name of a {@link angular.Module#controller registered - * controller} if passed as a string. + * - `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: @@ -312,18 +315,47 @@ function setupModuleLoader(window) { * * - `$element` - Current element * - `$attrs` - Current attributes object for the element - * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion transclusion} is enabled. - * enabled by default. - * - `isolate` – `{boolean=}` – whether the new scope is isolated. Isolated by default. - * - `bindings` – `{object=}` – define DOM attribute binding to component properties. - * component properties are always bound to the component controller and not to the scope. + * - `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. * * @description - * Register a component definition with the compiler. This is short for registering a directive - * where its definition object is isolated, allows transclusion and bound to controller as the - * component name by default. + * 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 and allow transclusion. + * + * Here are a few examples of how you would usually define components: + * + * ```js + * angular.module(...).component.('myComp', { + * template: '
My name is {{myComp.name}}
', + * controller: function MyCtrl() { + * this.name = 'shahar'; + * } + * }); + * + * angular.module(...).component.('myComp', { + * template: '
My name is {{myComp.name}}
', + * bindings: {name: '@'} + * }); + * + * angular.module(...).component.('myComp', { + * templateUrl: 'views/my-comp.html', + * controller: 'MyCtrl as ctrl', + * bindings: {name: '@'} + * }); + * + * ``` + * * See {@link ng.$compileProvider#directive $compileProvider.directive()}. */ component: function(name, options) { @@ -346,7 +378,8 @@ function setupModuleLoader(window) { templateUrl: makeInjectable(options.templateUrl), transclude: options.transclude === undefined ? true : options.transclude, scope: options.isolate === false ? true : {}, - bindToController: options.bindings || {} + bindToController: options.bindings || {}, + restrict: options.restrict || 'E' }; } diff --git a/test/loaderSpec.js b/test/loaderSpec.js index 6ec842d39d69..3204cf27b4a0 100644 --- a/test/loaderSpec.js +++ b/test/loaderSpec.js @@ -123,7 +123,8 @@ describe('component', function() { templateUrl: undefined, transclude: true, scope: {}, - bindToController: {} + bindToController: {}, + restrict: 'E' })); }); }); @@ -137,7 +138,8 @@ describe('component', function() { templateUrl: 'def.html', transclude: false, isolate: false, - bindings: {abc: '='} + bindings: {abc: '='}, + restrict: 'EA' }); module('my'); inject(function(myComponentDirective) { @@ -148,7 +150,8 @@ describe('component', function() { templateUrl: 'def.html', transclude: false, scope: true, - bindToController: {abc: '='} + bindToController: {abc: '='}, + restrict: 'EA' })); }); });