diff --git a/README.md b/README.md index f6b4d80..022ee81 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,14 @@ What ? **angular-typescript** provides annotation like decorators: ``` -@at.service(moduleName: string, serviceName: string) +@at.service(moduleName: string, serviceName?: string) @at.inject(dependencyOne: string, ...dependencies?: string[]) -@at.controller(moduleName: string, controllerName: string) +@at.controller(moduleName: string, controllerName?: string) @at.directive(moduleName: string, directiveName: string) -@at.classFactory(moduleName: string, className: string) -@at.resource(moduleName: string, resourceClassName: string) +@at.directive(moduleName: string, directiveProperties: at.IDirectiveProperties) +@at.classFactory(moduleName: string, className?: string) +@at.resource(moduleName: string, resourceClassName?: string) +@at.decorator(moduleName: string, serviceBaseName: string) ``` Why ? @@ -62,7 +64,7 @@ angular.module('ngModuleName').service('someService', SomeService); Using **angular-typescript** it will look like: ```typescript -@service('ngModuleName', 'someService') +@service('ngModuleName') class SomeService { constructor() { @@ -74,6 +76,22 @@ class SomeService { } } + +// or + +@service('ngModuleName','AliasSomeService') +class SomeService { + + constructor() { + // do stuff + } + + public someMethod(anArg: number): boolean { + // do some stuff + } + +} + ``` *** @@ -123,9 +141,27 @@ class SomeService { ### Controller - ```typescript -@controller('ngModuleName', 'SomeController') +@controller('ngModuleName') +class SomeController { + + constructor( + @inject('$scope') $scope: angular.IScope, + @inject('$parse') private $$parse: angular.IParseService + ) { + // do stuff with $scope and $$parse; + } + + public someMethod(anArg: number): boolean { + // do some stuff with this.$$parse(); + } + +} + + +//or + +@controller('ngModuleName', 'AliasSomeCtrl') class SomeController { constructor( @@ -146,7 +182,35 @@ class SomeController { ### Directive -Static class members of directive controller are used as config directive config. +You can configure your directive with annotation : + +```typescript +@directive('ngModuleName', { + selector: 'atSomeDirective', //required + controllerAs: 'someDirectiveCtrl', //optional + templateUrl: '/partials/some-directive.html' +}) +class SomeDirectiveController { + + public static link: angular.IDirectiveLinkFn = (scope, element, attrs, ctrl: SomeDirectiveController) => { + ctrl.init(attrs.atSomeDirective); + }; + + constructor( + @inject('$scope') private $$scope: angular.IScope, + @inject('$parse') private $$parse: angular.IParseService + ) { + // do stuff with $$scope and $$parse; + } + + public init(anArg: string): boolean { + // do some stuff with this.$$parse and this.$$scope + } + +} +``` + +or with static class members of directive controller are used as config directive config : ```typescript @directive('ngModuleName', 'atSomeDirective') @@ -242,3 +306,29 @@ class UserResource extends at.Resource { *** +### Decorator + +Angular provide a decorator feature to extends a service/factory. Use @decorator to use them and adding some others methods : + +```typescript +@decorator('ngModuleName', 'SomeService') +class SomeServiceDecorator { + + constructor( + @at.inject('SomeService') private parent:SomeService; + ) { + // do stuff + } + + public someMethod2(anArg: number): boolean { + // do some stuff + + console.log(this.parent.someMethod(anArg)); + + // do some stuff + return true; + } + +} + +``` \ No newline at end of file diff --git a/at-angular-resource.ts b/at-angular-resource.ts index 5452f8c..1b010c5 100644 --- a/at-angular-resource.ts +++ b/at-angular-resource.ts @@ -14,6 +14,11 @@ module at { angular.extend(instance, new instance.$_Resource(model)); } + function getFuncName(target: any): string { + /* istanbul ignore next */ + return target.name || target.toString().match(/^function\s*([^\s(]+)/)[1]; + } + /* istanbul ignore next */ export class Resource implements angular.resource.IResource { public static get: (params?: Object) => Resource; @@ -43,10 +48,10 @@ module at { } export interface IResourceAnnotation { - (moduleName: string, className: string): IClassAnnotationDecorator; + (moduleName: string, className?: string): IClassAnnotationDecorator; } - export function resource(moduleName: string, className: string): IClassAnnotationDecorator { + export function resource(moduleName: string, className?: string): IClassAnnotationDecorator { return (target: any): void => { function resourceClassFactory($resource: ResourceService, ...args: any[]): any { const newResource: ResourceClass = $resource(target.url, target.params, target.actions, target.options); @@ -59,7 +64,7 @@ module at { })), ...args); } resourceClassFactory.$inject = (['$resource']).concat(target.$inject /* istanbul ignore next */ || []); - angular.module(moduleName).factory(className, resourceClassFactory); + angular.module(moduleName).factory(className || getFuncName(target), resourceClassFactory); }; } /* tslint:enable:no-any */ diff --git a/at-angular.ts b/at-angular.ts index ea099d9..abceb0a 100644 --- a/at-angular.ts +++ b/at-angular.ts @@ -26,9 +26,15 @@ module at { (t: any, key: string, index: number): void; } - function instantiate(moduleName: string, name: string, mode: string): IClassAnnotationDecorator { + function getFuncName(target: any): string { + /* istanbul ignore next */ + return target.name || target.toString().match(/^function\s*([^\s(]+)/)[1]; + } + + function instantiate(moduleName: string, mode: string, name?: string): IClassAnnotationDecorator { return (target: any): void => { - angular.module(moduleName)[mode](name, target); + let fnName: string = getFuncName(target); + angular.module(moduleName)[mode](name || fnName, target); }; } @@ -55,50 +61,81 @@ module at { } export interface IServiceAnnotation { - (moduleName: string, serviceName: string): IClassAnnotationDecorator; + (moduleName: string, serviceName?: string): IClassAnnotationDecorator; } - export function service(moduleName: string, serviceName: string): at.IClassAnnotationDecorator { - return instantiate(moduleName, serviceName, 'service'); + export function service(moduleName: string, serviceName?: string): at.IClassAnnotationDecorator { + return instantiate(moduleName, 'service', serviceName); } export interface IControllerAnnotation { - (moduleName: string, ctrlName: string): IClassAnnotationDecorator; + (moduleName: string, ctrlName?: string): IClassAnnotationDecorator; } - export function controller(moduleName: string, ctrlName: string): at.IClassAnnotationDecorator { - return instantiate(moduleName, ctrlName, 'controller'); + export function controller(moduleName: string, ctrlName?: string): at.IClassAnnotationDecorator { + return instantiate(moduleName, 'controller', ctrlName); } export interface IDirectiveAnnotation { - (moduleName: string, directiveName: string): IClassAnnotationDecorator; + (moduleName: string, directiveName: string | IDirectiveProperties): IClassAnnotationDecorator; } - export function directive(moduleName: string, directiveName: string): at.IClassAnnotationDecorator { + export interface IDirectiveProperties extends angular.IDirective { + selector: string; + } + + export function directive( + moduleName: string, + directiveSettings: string | IDirectiveProperties + ): at.IClassAnnotationDecorator { + return (target: any): void => { - let config: angular.IDirective; - const ctrlName: string = angular.isString(target.controller) ? target.controller.split(' ').shift() : null; - /* istanbul ignore else */ + + let config: IDirectiveProperties; + + const ctrlName: string = angular.isString(target.controller) + ? target.controller.split(' ').shift() + : null; + + let controllerAs: string; + if (ctrlName) { controller(moduleName, ctrlName)(target); } - config = directiveProperties.reduce(( - config: angular.IDirective, - property: string - ) => { - return angular.isDefined(target[property]) ? angular.extend(config, {[property]: target[property]}) : - config; /* istanbul ignore next */ - }, {controller: target, scope: Boolean(target.templateUrl)}); - - angular.module(moduleName).directive(directiveName, () => (config)); + // Retrocompatibilty + + if (typeof directiveSettings === 'string') { + directiveSettings = { + selector: directiveSettings + }; + } else { + controllerAs = ( directiveSettings).controllerAs || getFuncName(target); + } + + config = directiveProperties.reduce((config: angular.IDirective, property: string) => { + return angular.isDefined(target[property]) + ? angular.extend(config, {[property]: target[property]}) + : config; /* istanbul ignore next */ + + }, angular.extend({}, directiveSettings, { + controllerAs: controllerAs, + controller: target + })); + + /* istanbul ignore else */ + + angular + .module(moduleName) + .directive(config.selector, () => (config)); }; } export interface IClassFactoryAnnotation { - (moduleName: string, className: string): IClassAnnotationDecorator; + (moduleName: string, className?: string): IClassAnnotationDecorator; } - export function classFactory(moduleName: string, className: string): at.IClassAnnotationDecorator { + export function classFactory(moduleName: string, className?: string): at.IClassAnnotationDecorator { + return (target: any): void => { function factory(...args: any[]): any { return at.attachInjects(target, ...args); @@ -107,8 +144,43 @@ module at { if (target.$inject && target.$inject.length > 0) { factory.$inject = target.$inject.slice(0); } - angular.module(moduleName).factory(className, factory); + angular.module(moduleName).factory(className || getFuncName(target), factory); + }; + } + + export interface IDecoratorAnnotation { + (moduleName: string, targetProvider: string): IClassAnnotationDecorator; + } + + export function decorator(moduleName: string, targetProvider: string): at.IClassAnnotationDecorator { + + return (targetClass: any): void => { + + angular + .module(moduleName) + .config([ + '$provide', + function($provide: angular.auto.IProvideService): void { + + delegation.$inject = ['$delegate', '$injector']; + + function delegation( + $delegate: angular.ISCEDelegateProvider, + $injector: angular.auto.IInjectorService + ): any { + + let instance: any = $injector.instantiate(targetClass, { + $delegate: $delegate + }); + + return angular.extend($delegate, targetClass.prototype, instance); + } + + $provide.decorator(targetProvider, delegation); + }]); + }; + } /* tslint:enable:no-any */ diff --git a/bower.json b/bower.json index 0d9b5f9..23a2f7d 100644 --- a/bower.json +++ b/bower.json @@ -2,11 +2,11 @@ "name": "angular-typescript", "version": "0.0.8", "dependencies": { - "angular": "^1.2.0", - "angular-resource": "^1.2.0" + "angular": "1.4.0", + "angular-resource": "1.4.0" }, "devDependencies": { - "angular-mocks": "^1.2.0" + "angular-mocks": "1.4.0" }, "authors": ["Jakub Strojewski "], "ignore": [ diff --git a/package.json b/package.json index f8d4468..42935c4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "gulp-tslint": "4.2.2", "gulp-typescript": "2.10.0", "jasmine-core": "2.4.1", - "karma": "0.13.16", + "karma": "0.13.19", "karma-chrome-launcher": "0.2.2", "karma-coffee-preprocessor": "0.3.0", "karma-coverage": "0.5.3", diff --git a/test/class-factory-spec.coffee b/test/class-factory-spec.coffee index 4e71506..4758de5 100644 --- a/test/class-factory-spec.coffee +++ b/test/class-factory-spec.coffee @@ -49,4 +49,17 @@ describe 'annotations:', -> expect testInstanceOne.accept .toBe 'application/json, text/plain, */*' + describe '@classFactory (without Factory name)', -> + TestClassSecond = null + + beforeEach inject (_TestClassSecond_) -> + TestClassSecond = _TestClassSecond_ + + it 'should create class as a service', -> + + expect TestClassSecond + .toBeDefined() + + expect TestClassSecond + .toBe test.TestClassSecond diff --git a/test/class-factory.ts b/test/class-factory.ts index df31163..d793117 100644 --- a/test/class-factory.ts +++ b/test/class-factory.ts @@ -19,4 +19,7 @@ module test { } + @classFactory('test') + export class TestClassSecond {} + } diff --git a/test/controller-spec.coffee b/test/controller-spec.coffee index d6b1478..7e3c85b 100644 --- a/test/controller-spec.coffee +++ b/test/controller-spec.coffee @@ -40,4 +40,39 @@ describe 'annotations:', -> expect $scope.name .toBe 'FirstTestCtrl' + describe '@controller (without ControllerName)', -> + $scope = null + $parse = null + secondTestCtrl = null + + beforeEach inject ($controller, $rootScope, _$parse_) -> + $scope = $rootScope.$new() + $parse = _$parse_ + secondTestCtrl = $controller 'SecondTestCtrl', $scope: $scope + + it 'should be defined', -> + + expect at.controller + .toEqual jasmine.any Function + + it 'should instantiate decorated class as new service', -> + + expect secondTestCtrl + .toBeDefined() + + expect secondTestCtrl + .toEqual jasmine.any test.SecondTestCtrl + + it 'should assign proper $inject array to service constructor', -> + + expect test.SecondTestCtrl.$inject + .toEqual ['$scope', '$parse'] + + it 'should make proper dependencies are passed to service constructor on instantiation', -> + + expect secondTestCtrl.$$parse + .toBe $parse + + expect $scope.name + .toBe 'SecondTestCtrl' diff --git a/test/controller.ts b/test/controller.ts index b7fa4cb..75c01dc 100644 --- a/test/controller.ts +++ b/test/controller.ts @@ -21,4 +21,19 @@ module test { } + @controller('test') + @inject('$scope', '$parse') + export class SecondTestCtrl { + + constructor( + $scope: IFirstScope, + /* tslint:disable:variable-name */ + private $$parse: angular.IParseService + /* tslint:enable:variable-name */ + ) { + $scope.name = 'SecondTestCtrl'; + } + + } + } diff --git a/test/decorator-spec.coffee b/test/decorator-spec.coffee new file mode 100644 index 0000000..fe13e07 --- /dev/null +++ b/test/decorator-spec.coffee @@ -0,0 +1,44 @@ +'use strict'; + +describe 'annotations:', -> + + beforeEach module 'test' + + describe '@decorator', -> + TestServiceFive = null + + beforeEach inject (_TestServiceFive_) -> + TestServiceFive = _TestServiceFive_ + + it 'should be defined', -> + + expect at.service + .toEqual jasmine.any Function + + it 'should instantiate decorated class as new service', -> + + expect TestServiceFive + .toBeDefined() + + expect TestServiceFive + .toEqual jasmine.any test.TestServiceFive + + it 'should have a name', -> + + expect TestServiceFive.name + .toEqual 'TestServiceFive' + + it 'should have a method changeName() provided by the decorator', -> + expect TestServiceFive.changeName + .toBeDefined() + + TestServiceFive.changeName(); + expect TestServiceFive.name + .toEqual 'TestServiceFiveDecorated' + + it 'should inject $delegate service', -> + expect TestServiceFive.$delegate + .toBeDefined() + + expect TestServiceFive.$delegate + .toEqual jasmine.any test.TestServiceFive diff --git a/test/decorator.ts b/test/decorator.ts new file mode 100644 index 0000000..b49ec2b --- /dev/null +++ b/test/decorator.ts @@ -0,0 +1,34 @@ +/* istanbul ignore if else */ + +module test { + + 'use strict'; + + @service('test') + export class TestServiceFive { + private name: string; + + constructor() { + this.name = 'TestServiceFive'; + } + + getName(): string { + return this.name; + } + } + + @decorator('test', 'TestServiceFive') + export class TestDecorator { + private name: string; + constructor( + /* tslint:disable:variable-name */ + @inject('$delegate') private $delegate: TestServiceFive + /* tslint:enable:variable-name */ + ) {} + + changeName(): void { + this.name = 'TestServiceFiveDecorated'; + } + } + +} diff --git a/test/directive-spec.coffee b/test/directive-spec.coffee index 5f63387..7f0722d 100644 --- a/test/directive-spec.coffee +++ b/test/directive-spec.coffee @@ -45,4 +45,87 @@ describe 'annotations:', -> expect element.text() .toBe $scope.name + $scope.ctrl.name + describe '@directive (case B : Annotation)', -> + $scope = null + element = null + + beforeEach inject ($compile, $rootScope) -> + $scope = $rootScope.$new() + element = $compile('')($scope) + $rootScope.$digest() + + it 'should be defined', -> + + expect at.directive + .toEqual jasmine.any Function + + it 'should instantiate decorated class as new service', -> + + expect element + .toBeDefined() + + expect $scope.ctrl + .toEqual jasmine.any test.TestComponentBCtrl + + it 'should assign proper $inject array to service constructor', -> + + expect test.TestComponentBCtrl.$inject + .toEqual ['$scope', '$parse'] + + it 'should execute directive on element', -> + + expect element.hasClass 'test-component' + .toBe true + + expect $scope.name + .toBe 'FirstTestCtrl' + + expect $scope.ctrl.name + .toBe 'FAKE_CTRL_NAME' + + expect element.text() + .toBe $scope.name + $scope.ctrl.name + + + describe '@directive (case C : Auto finding Ctrl name)', -> + $scope = null + element = null + + beforeEach inject ($compile, $rootScope) -> + $scope = $rootScope.$new() + element = $compile('')($scope) + $rootScope.$digest() + + it 'should be defined', -> + + expect at.directive + .toEqual jasmine.any Function + + it 'should instantiate decorated class as new service', -> + + expect element + .toBeDefined() + + expect $scope.TestComponentCCtrl + .toEqual jasmine.any test.TestComponentCCtrl + + it 'should assign proper $inject array to service constructor', -> + + expect test.TestComponentCCtrl.$inject + .toEqual ['$scope', '$parse'] + + it 'should execute directive on element', -> + + expect element.hasClass 'test-component' + .toBe true + + expect $scope.name + .toBe 'FirstTestCtrl' + + expect $scope.TestComponentCCtrl.name + .toBe 'FAKE_CTRL_NAME' + + expect element.text() + .toBe $scope.name + $scope.TestComponentCCtrl.name + diff --git a/test/directive.ts b/test/directive.ts index a75f76d..ae8550e 100644 --- a/test/directive.ts +++ b/test/directive.ts @@ -4,6 +4,7 @@ module test { interface IFirstComponentScope extends angular.IScope { name: string; + ctrl: Object; } @directive('test', 'atTestComponent') @@ -46,4 +47,81 @@ module test { } + @directive('test', { + selector: 'atTestComponentb', + controllerAs: 'ctrl', + restrict: 'E' + }) + export class TestComponentBCtrl { + + public static link: angular.IDirectiveLinkFn = ( + scope: IFirstComponentScope, + element: angular.IAugmentedJQuery, + attrs: angular.IAttributes, + ctrl: TestComponentBCtrl + ) => { + ctrl.setCtrlName('FAKE_CTRL_NAME'); + }; + + public static template: angular.IDirectiveCompileFn = (tElement: angular.IAugmentedJQuery) => { + tElement.addClass('test-component'); + return '{{ name }}{{ ctrl.name }}'; + }; + + // And the rest are simple Ctrl instance members + public name: string; + + constructor( + @inject('$scope') $scope: IFirstComponentScope, + /* tslint:disable:variable-name */ + @inject('$parse') private $$parse: angular.IParseService + /* tslint:enable:variable-name */ + ) { + $scope.name = this.name = 'FirstTestCtrl'; + } + + public setCtrlName(name: string): void { + this.$$parse('name').assign(this, name); + } + + } + + @directive('test', { + selector: 'atTestComponentc', + restrict: 'E' + }) + export class TestComponentCCtrl { + + public static link: angular.IDirectiveLinkFn = ( + scope: IFirstComponentScope, + element: angular.IAugmentedJQuery, + attrs: angular.IAttributes, + ctrl: TestComponentBCtrl + ) => { + ctrl.setCtrlName('FAKE_CTRL_NAME'); + }; + + public static template: angular.IDirectiveCompileFn = (tElement: angular.IAugmentedJQuery) => { + tElement.addClass('test-component'); + return '{{ name }}{{ TestComponentCCtrl.name }}'; + }; + + // And the rest are simple Ctrl instance members + public name: string; + + constructor( + @inject('$scope') $scope: IFirstComponentScope, + /* tslint:disable:variable-name */ + @inject('$parse') private $$parse: angular.IParseService + /* tslint:enable:variable-name */ + ) { + $scope.name = this.name = 'FirstTestCtrl'; + } + + public setCtrlName(name: string): void { + this.$$parse('name').assign(this, name); + } + + } + } diff --git a/test/module.ts b/test/module.ts index 451c617..863ca3b 100644 --- a/test/module.ts +++ b/test/module.ts @@ -8,6 +8,7 @@ module test { export const directive: at.IDirectiveAnnotation = at.directive; export const classFactory: at.IClassFactoryAnnotation = at.classFactory; export const resource: at.IResourceAnnotation = at.resource; + export const decorator: at.IDecoratorAnnotation = at.decorator; angular.module('test', ['ngResource']); diff --git a/test/resource-spec.coffee b/test/resource-spec.coffee index d8d9d8b..0a43460 100644 --- a/test/resource-spec.coffee +++ b/test/resource-spec.coffee @@ -69,3 +69,13 @@ describe 'annotations:', -> .toBe 'application/json, text/plain, */* :: THE NAME :: 1001.1' + describe '@resource (without Resource name)', -> + TestResourceTwo = null + + beforeEach inject (_TestResourceTwo_) -> + TestResourceTwo = _TestResourceTwo_ + + it 'should prepare decorated resource class as new service', -> + + expect TestResourceTwo + .toBeDefined() \ No newline at end of file diff --git a/test/resource.ts b/test/resource.ts index 554a223..47bee53 100644 --- a/test/resource.ts +++ b/test/resource.ts @@ -38,4 +38,7 @@ module test { } + @resource('test') + export class TestResourceTwo {} + } diff --git a/test/service-spec.coffee b/test/service-spec.coffee index b62b458..4b06e05 100644 --- a/test/service-spec.coffee +++ b/test/service-spec.coffee @@ -34,3 +34,21 @@ describe 'annotations:', -> .toBe deps[dep] + describe '@service without Service name', -> + testServiceFour = null + + beforeEach inject (_TestServiceFour_) -> + testServiceFour = _TestServiceFour_ + + it 'should be defined', -> + + expect at.service + .toEqual jasmine.any Function + + it 'should instantiate decorated class as new service', -> + + expect testServiceFour + .toBeDefined() + + expect testServiceFour + .toEqual jasmine.any test.TestServiceFour \ No newline at end of file diff --git a/test/service.ts b/test/service.ts index 3ea74dd..df361c8 100644 --- a/test/service.ts +++ b/test/service.ts @@ -18,4 +18,7 @@ module test { } + @service('test') + export class TestServiceFour {} + }