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