Ng-annotations is a small javascript library that helps to produce more structured angular applications using es6 classes and es7 decorators.
This library was build with webpack in mind but should works well with the other transpilers/javascript supersets like babel or typescript (with es7 and es6 advanced features)
npm install --save ng-annotations
bower install --save ng-annotations
all examples in this repo and below use the babel-core library as transpiler you're free to use any other if it supports the es7 decorator feature.
a configuration example is available in the webpack dev config
import {service, inject} from 'node_modules/ng-annotations';
@service()
@inject('$http')
export default class MyService {
controller($http) {
/*do something*/
}
}
a configuration example is available in the gruntfile
const {controller, inject} = ngAnnotations;
@controller('controllerName')
export default class theController {
controller() {
/*do something*/
}
}
All the examples below will show you the webpack way.
However, an implementation of the angular todolist with the basic es6 syntax is available in the example/es6 folder
all component annotations add 3 properties and 1 method to the given class
$type
: String. the component type (controller, config, service...). Used by theautodeclare
method.
$name
: String. the component name used by angular. Used by theautodeclare
method. Useful if you want to use the import system with the dependency injection system. With this method you'll avoid all hypothetical naming issues.
/*file1.js*/
import {service} from 'node_modules/ng-annotations';
@service()
export default class MyService {}
/*file2.js*/
import {controller, inject} from 'node_modules/ng-annotations';
// import {$name as myService} from './file1'; //before 0.1.6
import myService from './file1';
@controller()
@inject(myService)
export default class MyController {}
$component
: Object. the object/function used by angular, can be different than the original class (function wrap). Used by theautodeclare
method.
autodeclare
: Function(ngModule). declares the current component to angular. (replaces the traditionalangular.module('...').controller('name', fn)
)
the ngModule parameter can be a string (angular module name) or an angular module instance.
/*autodeclare way*/
import myService from './file1';
import myController from './file2';
var app = angular.module('app', []);
// useful if you wanna use the import system with the module dependency injection system.
export app.name;
[myService, myController].forEach(component => component.autodeclare(app));
/*alternative*/
import myService from './file1';
import myController from './file2';
var app = angular.module('app', []);
export app.name; // useful if you wanna use the import system with the module dependency injection system.
app.service(myService.$name, myService.$component);
app.controller(myController.$name, myController.$component);
/*without import*/
import {service} from 'node_modules/ng-annotations';
@service()
class MyService {}
MyService.autodeclare('moduleName');
The inject annotation replaces the classical array syntax for declare a dependency injection
Basically, it will feed the $inject property with the list of dependencies
- depsToInject: String|String[]|Component[]|Component. component(s) to inject
- ...otherDeps: (Optional) ...Strings.
import {inject, service} from 'node_modules/ng-annotations';
import myFactory from '../factory';
@service()
@inject('$http','$q',myFactory) // could be @inject(['$http','$q',myFactory])
export default class CommunicationService {
constructor(http, $q, factory) {
this.http = http;
this.promise = $q;
this.factory = factory;
}
do() {/*do something*/}
}
The implicit dependency injection syntax is also available but shouldn't be used because of minification issues.
import {inject, service} from 'node_modules/ng-annotations';
@service()
export default class CommunicationService {
constructor($http, $q) {
this.http = $http;
this.promise = $q;
}
do() {/*do something*/}
}
###@autobind
The autobind annotation gives the possibility to bind methods to its current context.
similar to object.method.bind(object)
import {service, inject, autobind} from 'node_modules/ng-annotations';
@service()
@inject('$timeout')
export default class CommunicationService {
constructor(timeout) {
this.timeout = timeout;
this.loop();
}
@autobind
loop() {
console.log('hello');
this.timeout(this.loop, 1000);
}
}
###@attach
The attach annotation provides a shortcut to bind references across components and keep them safe.
- source String|Component. source component
- "this" will target the current component
- path: (Optional) String. path toward the property
- split with dots.
obj.otherObj.myProperty
- split with dots.
####Usage:
// factories/user.js
import {factory, inject} from 'node_modules/ng-annotations';
@factory()
@inject('$http')
export default class User {
constructor() {
this.nested.property = 5;
}
connectedUsers = 0;
this.users = [];
load() {
this.$http.get('...').success(userlist => this.users = userlist)
}
}
// controller/user.js
import {inject,controller,attach} from 'node_modules/ng-annotations';
import UserFactory from '../factories/user.js';
@controller()
@inject(UserFactory)
class FooBarController {
@attach(UserFactory, 'users') // this.userlist will refers to UserFactory.users
userlist;
@attach(UserFactory, 'nested.property')
randomProperty;
@attach(UserFactory, 'load') // same as this.reload = factory.load.bind(factory);
reload;
clearUsers() {
this.users = []; // update the UserFactory.users property, the reference is kept.
}
}
binded target can be a function, a primitive or an object
The binding occurs after the constructor calling, so you can't use the property at this step. use the controller parameters instead.
the conceal decorator provides a way to declare the annotated properties as private
import {factory, inject, attach, conceal} from 'node_modules/ng-annotations';
@factory()
@inject('$http')
export default class UserFactory {
@conceal
@attach('$http')
$http
@conceal
datas = [];
constructor(timeout) {
this.datas = [1,2,3];
}
method() {
return this.privateMethod();
}
@conceal
privateMethod() {}
}
###@controller
declare the given class as an angular controller
- name: (Optional) String. angular controller name, by default the decorator will take the class name.
import {controller} from 'node_modules/ng-annotations';
@controller('HelloWorld')
export default class MyController {
prop = 0;
}
With this syntax you should always use the controllerAs option and forget $scope (except in certain cases like $watch or $on usages).
html
head
body
section(ng-controller="HelloWorld as HW") {{HW.prop}}
script(src="app.js")
###@service
declare the given class as an angular service
- name: (Optional) String. angular service name, by default the decorator will take the class name.
import {service} from 'node_modules/ng-annotations';
@service('OtherName')
export default class MyService {
method() { return 100 * Math.random()|0 }
}
###@provider
declare the given class as an angular provider
like the native angular provider you must implement a$get
.
- name: (Optional) String. angular provider name, by default the decorator will take the class name.
import {provider, inject} from 'node_modules/ng-annotations';
@provider()
export default class MyProvider {
@inject($http)
$get($http) {}
}
###@factory
declare the given class as an angular factory
- name: (Optional) String. angular factory name, by default the decorator will take the class name.
import {factory} from 'node_modules/ng-annotations';
@factory()
export default class MyFactory {
items;
constructor() {
this.items = [];
}
}
by default the decorator return an instance of the factory class to angular so the example above is similar to the following code
angular.module('...')
.factory('MyFactory', function() {
this.items = [];
return angular.extend(this);
})
You can change this behaviour by defining an
$expose
method
import {factory, autobind} from 'node_modules/ng-annotations';
@factory()
export default class MyFactory {
items;
@autobind
get() {
return this.items;
}
@autobind
load(list = []) {
this.items = list;
}
$expose() {
return {
load: this.load,
get: this.get
}
}
}
angular.module('...')
.factory('MyFactory', function() {
this.items = [];
this.get = function() { return this.items; }
this.load = function(list) { this.items = list || []; }
return {
get: this.get.bind(this),
load: this.load.bind(this)
}
})
###@directive
declare the given class as an angular directive
- name: (Optional) String. angular directive name, by default the decorator will take the class name.
import {directive} from 'node_modules/ng-annotations';
@directive('myDirective')
export default class MyDirective {
restrict = 'A';
scope = {};
link($scope, elem, attr) {
console.log('directive triggered');;
}
}
###@animation
declare the given class as an angular animation
- selector: String. css selector.
import {animation} from 'node_modules/ng-annotations';
@animation('.foobar')
export default class FoobarAnimation {
enter(elem, done) {
elem.css('opacity', 0);
/*do something*/
}
leave(elem, done) {
elem.css('opacity', 1);
/*do something*/
}
}
###@config
declare the given class as an angular config
import {config, inject} from 'node_modules/ng-annotations';
@config()
@inject('$routeProvider')
export default class FooBarConfiguration {
constructor(routeProvider) {
this.route = routeProvider;
this.setRoutes();
}
setRoutes() {
this.route.when('/xxx', { template: '...' })
}
}
###@run
declare the given class as an angular run
import {run, inject} from 'node_modules/ng-annotations';
@run()
@inject('myFactory')
export default class SomeRun {
constructor(myFactory) {
this.fact = myFactory;
this.initModel();
}
initModel() {
this.fact.load();
}
}
###@filter
declare the given class as an angular filter
- properties: (Optional) Object|String. angular filter properties. contains the name and the stateful attribute
- name: String. angular filter name, by default the decorator will take the class name.
- stateful: Boolean. default false
The decorated filter is slightly different than the original. to make it work you need to implement a
$filter
method. This is the method used by angular.
import {filter} from 'node_modules/ng-annotations';
@filter('capitalizeFilter')
export default class Capitalize {
toCapitalize(val) {
return val[0].toUpperCase() + val.slice(1);
}
$filter(val) {
return this.toCapitalize(val);
}
}
If you need to write a stateful filter, you must give a literal objet as parameter to the filter decorator
//inspired by https://docs.angularjs.org/guide/filter
import {filter, inject, attach} from 'node_modules/ng-annotations';
@filter({name:'decorate', stateful:true})
@inject('decoration')
export default Decorate {
@attach('decoration', 'symbol')
decorator;
$filter(input) {
return this.decorator + input + this.decorator;
}
}
###@decorator
provides a way to decorate an existing angular element
- name: String. angular element's name to decorate.
import {decorator, inject} from 'node_modules/ng-annotations';
@decorator('elementName')
@inject('$delegate')
export default class DecoratedElement {
constructor($delegate) {
/*decoration*/
}
}
by default the decorator return an instance of $delegate so the example above is similar to the following code
angular.module('...')
.config(function($provide) {
$provide.decorator('elementName', [
'$delegate',
($delegate) => {
/*decoration*/
return $delegate;
}
])
})
You can change this behaviour by defining an
$decorate
method
import {decorator, inject, attach} from 'node_modules/ng-annotations';
@decorator('elementName')
@inject('$delegate')
export default class DecoratedElement {
//@attach('$delegate')
//$delegate;
sayHello() {
console.log('hello world');
}
$decorate() {
//this.$delegate.sayHello = () => this.sayHello();
//return this.$delegate;
return this;
}
}
angular.module('...')
.config(function($provide) {
$provide.decorator('elementName', [
'$delegate',
($delegate) => {
return {
sayHello() {
console.log('hello world');
}
};
}
])
})
the Value and Constant components can't be replaced by a class.
In order to simplify their declaration two wrappers are available.
###constant
- name: String. constant name.
- value: Mix. constant value.
import {constant} from 'node_modules/ng-annotations';
export default constant('name', 'a constant');
###value
- name: String. value name.
- value: Mix. value value.
import {value} from 'node_modules/ng-annotations';
export default value('name', 'a value');
The composites decorators aren't simple angular component wrappers like above, they implement new concepts on top of Angular 1.
###@component
This decorator declares the given class as a controller and creates an associated directive
With the emergence of the new components oriented frameworks like react or angular 2, the application structures changed drastically. Most of the time, when we create a directive we want also create a controller with its own logic, however create 2 files for 2 lines of code each is a waste of time. The following decorator combine a controller and a directive in the way of the angular2's @component.
- options: (Mandatory) Object. component options.
- selector: (Mandatory) String. directive's name
- alias: (Optional) String. controllerAs option, defaults to the selector value
- type: (Optional) String. directive's restrict option, defaults to
E
- ioProps: (Optional) Object. the scope properties, all props are bind with the
=
operator (two way binding) - template: (Optional) Any. directive's template option
- templateUrl: (Optional) Any. directive's templateUrl option
- transclude: (Optional) Boolean. directive's transclude option
- lifecycle: (Optional) Object array of callbacks, the available hooks are
compile
,prelink
andpostlink
the component decorator injects a $ioProps property to the working class. It contains the scope properties
import {component, inject} from 'node_modules/ng-annotations';
@component({
selector: 'myComponent',
alias: 'MyCmp',
type: 'EA',
ioProps: {
name: 'cmpName'
},
template: `
<button ng-click="MyCmp.sayHello()">say hello</button>
`,
lifecycle: {
compile: () => { console.log('compile time'); },
prelink: () => { console.log('prelink time'); },
postlink: () => { console.log('postlink time'); }
}
})
@inject('$http')
export default class MyComponent {
sayHello() {
console.log(`Hello ${this.$ioProps.name}`);
}
}
npm install webpack-dev-server -g
npm install webpack
npm install
Build dist version: npm run build
Build es6 example: npm run es6
Start the dev server: npm run dev
then go to http://localhost:8080/webpack-dev-server/