diff --git a/src/loader.js b/src/loader.js index 93ce8e720a99..2a818f7f26f4 100644 --- a/src/loader.js +++ b/src/loader.js @@ -282,6 +282,118 @@ 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 (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. + * + * @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 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) { + 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 || {}, + 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); + }, + /** * @ngdoc method * @name angular.Module#config diff --git a/test/loaderSpec.js b/test/loaderSpec.js index 1c4d0bb45d84..3204cf27b4a0 100644 --- a/test/loaderSpec.js +++ b/test/loaderSpec.js @@ -87,3 +87,114 @@ 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: {}, + 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: false, + 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: false, + 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 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' + })); + }); + }); +});