ng-decorate

0.0.18 • Public • Published

Description

ES7 decorators for Angular 1.x. Help you:

  • Get around Angular's dependency injection while using ES6 modules.
  • Make custom directive declarations very short and semantic.
  • Declare bindables directly on class properties.

Perfect for an ES7 / TypeScript application.

This readme assumes you're already using jspm and System, and have an ES6 transpilation workflow with babel or typescript. If not, here's a guide to get you started: [1].

Contents

Installation

In shell:

npm i --save-dev ng-decorate
# or 
jspm install npm:ng-decorate

In app:

import {Component, bindTwoWay} from 'ng-decorate';
 
@Component({
  selector: 'my-accordeon'
})
class X {
  @bindTwoWay length: number;
}

Directives

ng-decorate provides two directive shortcuts: custom element (@Component) and custom attribute (@Attribute).

@Component

Defines a custom element: a directive with a template and an isolated scope.

Simplest usage:

import {Component} from 'ng-decorate';
 
@Component({
  selector: 'my-element'
})
class X {}

With default settings, this expands to:

angular.module('myElement', ['ng']).directive('myElement', function() {
  return {
    restrict: 'E',
    scope: {},
    templateUrl: 'my-element/my-element.html',
    controller: X,
    controllerAs: 'self',
    bindToController: true
  };
});
class X {}

See defaults for customisation.

@Attribute

Defines a custom attribute.

Simplest usage:

import {Attribute} from 'ng-decorate';
 
@Attribute({
  selector: '[my-attribute]'
})
class X {}

With default settings, this expands to:

angular.module('myAttribute', ['ng']).directive('myAttribute', function() {
  return {
    restrict: 'A',
    scope: false,
    controller: X
  };
});
class X {}

See defaults for customisation.

Directive Options

This applies to both @Component and @Attribute.

Any passed options will be included into the resulting directive definition object, so you can use the standard directive API on top of ng-decorate-specific options. Example:

@Component({
  selector: 'my-element',
  scope: {myProperty: '='}
})

selector : string

Required. This is the selector string for the resulting directive. For @Attribute, you can optionally enclose it into brackets:

@Attribute({selector: '[my-attribute]'})

module : ng.IModule

Optional. The directive will be registered under the given angular module, no new module will be created, and other module options will be ignored:

@Component({
  module: angular.module('myApp'),
  selector: 'my-element'
})

I recommend using a single angular "module" for the entire application. Angular dependencies are kinda fake: all providers (services, etc.) are registered globally in the application injector, and dependencies "bleed through" to sister modules. Might as well admit this and keep it simple. The real dependency tree of your application is defined by ES6 modules. See defaults for setting up a default angular module.

moduleName : string

Optional. Dictates the name of the new angular "module" that will be created:

@Attribute({
  moduleName: 'app.myAttribute',
  selector: '[my-attribute]'
})

If omitted, defaults to the directive's name, as shown above. See defaults for how to set up an implicit module.

dependencies : string[]

Optional. Names of other angular "modules" the newly created module depends on. This is necessary when you depend on third party services that need to be dependency-injected (see inject below):

@Component({
  selector: 'my-link',
  dependencies: ['ui.router']
})

If omitted, defaults to ['ng'], as shown above.

inject : string[]

Deprecated, see @autoinject.

Optional. Names of angular services that will be dependency-injected and automatically assigned to the class's prototype:

@Component({
  selector: 'my-element',
  inject: ['$q']
})
class X {
  constructor() {
    console.log(this.$q)
  }
}

This lets you easily get hold of angular services while using ES6 modules for everything else. The magic happens during Angular's "run phase", before any directives are instantiated.

See the gotcha for the possible dependency injection issues. They can be easily avoided by using just one module.

injectStatic : string[]

Deprecated, see @autoinject.

Optional. Works exactly like inject, but assigns the injected services to the class as static properties.

@Component({
  selector: 'my-element',
  injectStatic: ['$http']
})
class X {
  constructor() {
    console.log(X.$http)
  }
}

Statics

These directive options can be included as static methods or properties:

template
templateUrl
compile
link
scope

Example:

@Attribute({
  selector: 'svg-icon'
})
class X {
  @autoinject $templateCache;
 
  static template($element) {
    var element = $element[0];
    var src = 'svg/' + element.getAttribute('icon') + '.svg';
    return X.$templateCache.get(src);
  }
}

Services

@Ambient

Dependency injection helper. It's a strict subset of other decorators in this library. Use it when you want to obtain Angular's services without creating any new directives or services.

import {Ambient, autoinject} from 'ng-decorate';
 
@Ambient
class X {
  @autoinject $q;
  @autoinject static $http;
 
  constructor() {
    console.log(this.$q)
    console.log(X.$http)
  }
}

(See @autoinject below.)

Just like with other class decorators, you can include module, moduleName and so on. For this particular decorator, arguments are optional.

@Service

Same as @Ambient but registers a new angular service with the given name. The serviceName option is mandatory. This is useful when you're porting a legacy application, where some parts still rely on Angular's DI.

Create a service:

@Service({
   serviceName: 'MySpecialClass'
})
class X {}

Get it in Angular's DI:

angular.module('app').run(['MySpecialClass', function(MySpecialClass) {
  /* ... */
}]);

@Controller

Same as @Ambient but also registers the class as an "old school" controller under the given name. Can optionally publish the class to the DI system, just like @Service.

Register the controller:

@Controller({
  controllerName: 'X'
})
class X {}
 
@Controller({
  controllerName: 'Y',
  serviceName: 'Y'
})
class Y {}

Use in a template:

<div ng-controller="X"></div>

This type of controller usage is outdated and generally not recommended. Use @Component instead. This decorator is provided to ease migration of legacy apps to ES6/7.

Service Options

This applies to @Ambient, @Service, and @Controller.

module : ng.IModule

moduleName : string[]

dependencies : string[]

See Directive Options.

Bindings

Directly annotate class properties to declare them as bindable. Perfect with TypeScript. When used with @Component or @Attribute, the annotations are grouped and converted into a scope: {/* ... */} declaration for the internal directive definition object.

Example:

import {Component, bindString, bindTwoWay} from 'ng-decorate';
 
@Component({
  selector: 'editable'
})
class X {
  @bindString label: string;
  @bindTwoWay value: string;
}

Expands to:

import {Component} from 'ng-decorate';
 
@Component({
  selector: 'editable',
  scope: {
    label: '@',
    value: '='
  }
})
class X {
  label: string;
  value: string;
}

This lets you declare bindings directly in the class body and makes them more semantic.

@bindString

@Component({
  selector: 'my-element'
})
class X {
  @bindString first: string;
  @bindString('last') second: string;
}

Expands to:

@Component({
  selector: 'my-element',
  scope: {
    first: '@',
    second: '@last'
  }
})
class X {
  first: string;
  second: string;
}

@bindTwoWay

@Component({
  selector: 'my-element'
})
class X {
  @bindTwoWay first: any;
  @bindTwoWay({collection: true, optional: true, key: 'last'})
  second: any;
}

Expands to:

@Component({
  selector: 'my-element',
  scope: {
    first: '=',
    second: '=*?last'
  }
})
class X {
  first: any;
  second: any;
}

@bindOneWay

This is a special feature of ng-decorate. It bridges the gap between Angular 2, where one-way binding is the default, and Angular 1.x, which doesn't support it natively.

Example with a hardcoded string:

<controlled-input value="'constant value'"></controlled-input>
@Component({
  selector: 'controlled-input'
})
class X {
  @bindOneWay value: any;
 
  constructor() {
    this.value = 123;         // has no effect
    console.log(this.value);  // prints 'constant value'
  }
}

@bindExpression

@Component({
  selector: 'my-element'
})
class X {
  @bindExpression first: Function;
  @bindExpression('last') second: Function;
}

Expands to:

@Component({
  selector: 'my-element',
  scope: {
    first: '&',
    second: '&last'
  }
})
class X {
  first: Function;
  second: Function;
}

@autoinject

Properties annotated with @autoinject are automatically assigned to the prototype (instance properties) or class (static properties). You don't need to inject them in the constructor.

Must be used with one of the class decorators, like @Component or @Ambient.

import {Component, autoinject} from 'ng-decorate';
 
@Component({
  selector: 'todo-list'
})
class X {
  @autoinject $q;
  @autoinject static $timeout;
 
  constructor() {
    console.log(this.$q);
    console.log(X.$timeout);
  }
}

Works great with TypeScript and property type annotations.

As an alternative, you can pass arrays of properties as inject and injectStatic to the class decorator (deprecated).

Note that this only works for global services. For contextual dependencies like $scope, use constructor injection:

class X {
  static $inject = ['$scope', '$element'];
  constructor($scope, $element) {
    /* ... */
  }
}

With TypeScript, use the private or public modifier to automatically assign the injected value to the instance:

class X {
  static $inject = ['$scope', '$element'];
  constructor(private $scope, private $element) {
    /* ... */
  }
}

defaults

The package is stateful. You can import its configuration object and mutate it to set global defaults.

Default configuration (with type annotations for the sake of clarity):

export const defaults = {
  // Default angular module. Supersedes module declarations.
  module: <ng.IModule>null,
  // Default module name.
  moduleName: <string>null,
  // Controller key. Other common variants include 'ctrl' and 'vm'.
  controllerAs: 'self',
  // Generates a template url from an element name. Another common variant:
  //   'components/elementName/elementName.html'.
  makeTemplateUrl(elementName: string): string {
    return elementName + '/' + elementName + '.html';
  }
};

Example configuration:

import {defaults} from 'ng-decorate';
 
defaults.module = angular.module('app');
defaults.controllerAs = 'vm';

I recommend setting your application's main, or only, module as the default (see below).

Gotcha

Each angular "module" created or reused by the decorators must be present in the dependency tree of your main module (the one that has been bootstrapped via ng-app or angular.bootstrap). If you let the decorators create new modules, you must add them to the dependency list of the main module.

To avoid this, use a single angular module for the entire application, setting it in defaults or explicitly passing it to decorators. There's nothing to gain by using fake Angular dependendencies that end up sharing the same global namespace.

Alternatives

ng-forward is an upcoming ng1->ng2 migration helper based on angular-decorators with input from a1atscript, angular2-now and ng-decorate.

Readme

Keywords

Package Sidebar

Install

npm i ng-decorate

Weekly Downloads

17

Version

0.0.18

License

none

Last publish

Collaborators

  • mitranim