Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 54e8165

Browse files
Shahar Talmipetebacondarwin
Shahar Talmi
authored andcommitted
feat(Module): add helper method, component(...) for creating component directives
Since we are promoting component directives as the building blocks of Angular applications, this new helper provides a simpler method for defining such directives. By using sensible, widely accepted, conventions the number of parameters needed has been cut down dramatically. Many component directives can now be defined by simply providing a `name`, `template`/`templateUrl`, a `controller`, and `bindings`: ```js myMod.component('myComp', { template: '<div>My name is {{myComp.name}}</div>', controller: function() { }, bindings: { name: '=' } }); ``` Closes #10007 Closes #12933
1 parent 4fc7346 commit 54e8165

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

src/loader.js

+113
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,119 @@ function setupModuleLoader(window) {
282282
*/
283283
directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'),
284284

285+
/**
286+
* @ngdoc method
287+
* @name angular.Module#component
288+
* @module ng
289+
* @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
290+
* @param {Object} options Component definition object (a simplified
291+
* {@link ng.$compile#directive-definition-object directive definition object}),
292+
* has the following properties (all optional):
293+
*
294+
* - `controller` – `{(string|function()=}` – Controller constructor function that should be
295+
* associated with newly created scope or the name of a {@link ng.$compile#-controller-
296+
* registered controller} if passed as a string. Empty function by default.
297+
* - `controllerAs` – `{string=}` – An identifier name for a reference to the controller.
298+
* If present, the controller will be published to scope under the `controllerAs` name.
299+
* If not present, this will default to be the same as the component name.
300+
* - `template` – `{string=|function()=}` – html template as a string or a function that
301+
* returns an html template as a string which should be used as the contents of this component.
302+
* Empty string by default.
303+
*
304+
* If `template` is a function, then it is {@link auto.$injector#invoke injected} with
305+
* the following locals:
306+
*
307+
* - `$element` - Current element
308+
* - `$attrs` - Current attributes object for the element
309+
*
310+
* - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
311+
* template that should be used as the contents of this component.
312+
*
313+
* If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with
314+
* the following locals:
315+
*
316+
* - `$element` - Current element
317+
* - `$attrs` - Current attributes object for the element
318+
* - `bindings` – `{object=}` – Define DOM attribute binding to component properties.
319+
* Component properties are always bound to the component controller and not to the scope.
320+
* - `transclude` – `{boolean=}` – Whether {@link $compile#transclusion transclusion} is enabled.
321+
* Enabled by default.
322+
* - `isolate` – `{boolean=}` – Whether the new scope is isolated. Isolated by default.
323+
* - `restrict` - `{string=}` - String of subset of {@link ng.$compile#-restrict- EACM} which
324+
* restricts the component to specific directive declaration style. If omitted, this defaults to 'E'.
325+
* - `$canActivate` – `{function()=}` – TBD.
326+
* - `$routeConfig` – `{object=}` – TBD.
327+
*
328+
* @description
329+
* Register a component definition with the compiler. This is short for registering a specific
330+
* subset of directives which represents actual UI components in your application. Component
331+
* definitions are very simple and do not require the complexity behind defining directives.
332+
* Component definitions usually consist only of the template and the controller backing it.
333+
* In order to make the definition easier, components enforce best practices like controllerAs
334+
* and default behaviors like scope isolation, restrict to elements and allow transclusion.
335+
*
336+
* Here are a few examples of how you would usually define components:
337+
*
338+
* ```js
339+
* var myMod = angular.module(...);
340+
* myMod.component('myComp', {
341+
* template: '<div>My name is {{myComp.name}}</div>',
342+
* controller: function() {
343+
* this.name = 'shahar';
344+
* }
345+
* });
346+
*
347+
* myMod.component('myComp', {
348+
* template: '<div>My name is {{myComp.name}}</div>',
349+
* bindings: {name: '@'}
350+
* });
351+
*
352+
* myMod.component('myComp', {
353+
* templateUrl: 'views/my-comp.html',
354+
* controller: 'MyCtrl as ctrl',
355+
* bindings: {name: '@'}
356+
* });
357+
*
358+
* ```
359+
*
360+
* See {@link ng.$compileProvider#directive $compileProvider.directive()}.
361+
*/
362+
component: function(name, options) {
363+
function factory($injector) {
364+
function makeInjectable(fn) {
365+
if (angular.isFunction(fn)) {
366+
return function(tElement, tAttrs) {
367+
return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
368+
};
369+
} else {
370+
return fn;
371+
}
372+
}
373+
374+
var template = (!options.template && !options.templateUrl ? '' : options.template);
375+
return {
376+
controller: options.controller || function() {},
377+
controllerAs: identifierForController(options.controller) || options.controllerAs || name,
378+
template: makeInjectable(template),
379+
templateUrl: makeInjectable(options.templateUrl),
380+
transclude: options.transclude === undefined ? true : options.transclude,
381+
scope: options.isolate === false ? true : {},
382+
bindToController: options.bindings || {},
383+
restrict: options.restrict || 'E'
384+
};
385+
}
386+
387+
if (options.$canActivate) {
388+
factory.$canActivate = options.$canActivate;
389+
}
390+
if (options.$routeConfig) {
391+
factory.$routeConfig = options.$routeConfig;
392+
}
393+
factory.$inject = ['$injector'];
394+
395+
return moduleInstance.directive(name, factory);
396+
},
397+
285398
/**
286399
* @ngdoc method
287400
* @name angular.Module#config

test/loaderSpec.js

+111
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,114 @@ describe('module loader', function() {
8787
expect(window.angular.$$minErr).toEqual(jasmine.any(Function));
8888
});
8989
});
90+
91+
92+
describe('component', function() {
93+
it('should return the module', function() {
94+
var myModule = window.angular.module('my', []);
95+
expect(myModule.component('myComponent', {})).toBe(myModule);
96+
});
97+
98+
it('should register a directive', function() {
99+
var myModule = window.angular.module('my', []).component('myComponent', {});
100+
expect(myModule._invokeQueue).toEqual(
101+
[['$compileProvider', 'directive', ['myComponent', jasmine.any(Function)]]]);
102+
});
103+
104+
it('should add router annotations to directive factory', function() {
105+
var myModule = window.angular.module('my', []).component('myComponent', {
106+
$canActivate: 'canActivate',
107+
$routeConfig: 'routeConfig'
108+
});
109+
expect(myModule._invokeQueue.pop().pop()[1]).toEqual(jasmine.objectContaining({
110+
$canActivate: 'canActivate',
111+
$routeConfig: 'routeConfig'
112+
}));
113+
});
114+
115+
it('should return ddo with reasonable defaults', function() {
116+
window.angular.module('my', []).component('myComponent', {});
117+
module('my');
118+
inject(function(myComponentDirective) {
119+
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
120+
controller: jasmine.any(Function),
121+
controllerAs: 'myComponent',
122+
template: '',
123+
templateUrl: undefined,
124+
transclude: true,
125+
scope: {},
126+
bindToController: {},
127+
restrict: 'E'
128+
}));
129+
});
130+
});
131+
132+
it('should return ddo with assigned options', function() {
133+
function myCtrl() {}
134+
window.angular.module('my', []).component('myComponent', {
135+
controller: myCtrl,
136+
controllerAs: 'ctrl',
137+
template: 'abc',
138+
templateUrl: 'def.html',
139+
transclude: false,
140+
isolate: false,
141+
bindings: {abc: '='},
142+
restrict: 'EA'
143+
});
144+
module('my');
145+
inject(function(myComponentDirective) {
146+
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
147+
controller: myCtrl,
148+
controllerAs: 'ctrl',
149+
template: 'abc',
150+
templateUrl: 'def.html',
151+
transclude: false,
152+
scope: true,
153+
bindToController: {abc: '='},
154+
restrict: 'EA'
155+
}));
156+
});
157+
});
158+
159+
it('should allow passing injectable functions as template/templateUrl', function() {
160+
var log = '';
161+
window.angular.module('my', []).component('myComponent', {
162+
template: function($element, $attrs, myValue) {
163+
log += 'template,' + $element + ',' + $attrs + ',' + myValue + '\n';
164+
},
165+
templateUrl: function($element, $attrs, myValue) {
166+
log += 'templateUrl,' + $element + ',' + $attrs + ',' + myValue + '\n';
167+
}
168+
}).value('myValue', 'blah');
169+
module('my');
170+
inject(function(myComponentDirective) {
171+
myComponentDirective[0].template('a', 'b');
172+
myComponentDirective[0].templateUrl('c', 'd');
173+
expect(log).toEqual('template,a,b,blah\ntemplateUrl,c,d,blah\n');
174+
});
175+
});
176+
177+
it('should allow passing transclude as object', function() {
178+
window.angular.module('my', []).component('myComponent', {
179+
transclude: {}
180+
});
181+
module('my');
182+
inject(function(myComponentDirective) {
183+
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
184+
transclude: {}
185+
}));
186+
});
187+
});
188+
189+
it('should give ctrl as syntax priority over controllerAs', function() {
190+
window.angular.module('my', []).component('myComponent', {
191+
controller: 'MyCtrl as vm'
192+
});
193+
module('my');
194+
inject(function(myComponentDirective) {
195+
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
196+
controllerAs: 'vm'
197+
}));
198+
});
199+
});
200+
});

0 commit comments

Comments
 (0)