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'
+ }));
+ });
+ });
+ });
});