ng2-conventions-decorators

A set of minimal decorators for Angular2 that leverage established conventions to reduce boilerplate, improve maintainability, and improve type safety

Usage no npm install needed!

<script type="module">
  import ng2ConventionsDecorators from 'https://cdn.skypack.dev/ng2-conventions-decorators';
</script>

README

ng2-conventions-decorators

GitHub release npm npm npm Build Status A set of minimal decorators for Angular2 that leverage established conventions to reduce boilerplate, enforce consistent APIs, and leverage static type analysis.

Rationale

Angular 2 is very heavy on configuration, considerably heavier than AngularJS. For example, html element component directives should be given a kebab-case-name tag-name. This is both an official recommendation and a standard html convention. However, while AngularJS used a simple, easy to understand transformation to create these tag-names,

JavaScript

function someCustomElement() { ... }

angular.directive({ someCustomElement });

HTML

<some-custom-element></some-custom-element>

Angular 2 requires the tag-name be explicitly specified as a property of the component decorator factory's configuration object. This is just one of many examples but it clearly demonstrates the following issues:

  1. Violates DRY.
  2. Harder to maintain: You have to keep even more names in sync when refactoring.
  3. Verbose: Redundantly species configuration in strings.
  4. Error prone: Heavy use of optionality and simple strings fail to leverage strengths of Angular 2's foundational tools (e.g. TypeScript).
  5. Hard to teach: Recommended practices are now optional, but still expected, just optional. Angular 2 is opinionated, so there is no argument for flexibility or agnosticism.
  6. Harder to maintain: To know the tag name you need to use in html you have to read every component's definition or documentation.

Angular 2's AOT compilation process fails when a component contains private properties that are used in its markup. While this probably should not be the case at all it should at least be documented and enforced so that JIT mode and AOT mode have the same semantics and behavior. The included component decorator factory enforces this at the type level, providing an error when a component contains a private or protected property. This correctly gives you a static error in both JIT and AOT by communicating the intent missing from Angular 2's APIs.

@component(template) export class MyAwesomeComponent {
    prop = 'this is public';
} // OK
@component(template) export class MyAwesomeComponent {
    private prop = 'this is private';
} // Static Error

Installation

jspm

jspm i ng2-conventions-decorators

npm

npm i ng2-conventions-decorators --save

API

@pipe (decorator)

What?

A simple, and typechecked pipe decorator

How?

Use it just like @Pipe() except without the parenthesis and the redundant configuration object

@pipe export class LocalCurrencyPipe {
    transform(value: string) { ... }
}

is precisely equivalent to

@Pipe({ name: 'localCurrency' }) export class LocalCurrencyPipe {
    transform(value: string) { ... }
}

Furthermore

@pipe export class LocalCurrencyPipe {
    // forgot to implement transform
}

is a TypeScript compile error while

@Pipe({ name: 'localCurrency' }) export class LocalCurrencyPipe { 
    // forgot to implement transform
}

will fail at runtime.

Why?

  1. DRY
  2. Automatically creates camelCasedFunction name
  3. Convention over configuration
  4. Always pure, no confusing true defaulting boolean
  5. Ensures decorated class actually provides a transform(value) method at compile time
  6. When using TypeScript we both can and should take advantage of its semantic error checking
  7. No need for a decorator factory when a simple decorator is cleaner and more maintainable

@component (decorator factory)

What?

A minimal shorthand for the 90% case

How?

Use it just like @Component() to enjoy cleaner code and consistent selectors

@component(template, style) export class DynamicListViewComponent { }

is precisely equivalent to

@Component({
    template,
    styles: [style],
    selector: 'dynamic-list-view'
})
export class DynamicListViewComponent { }

Need additional configuration?

@component(template, style, {
    directives: [ListItem]
})
export class DynamicListViewComponent { }

Why?

  1. DRY
  2. Automatically create kebab-cased-element selector
  3. Convention over configuration
  4. Ensures standard selector naming conventions
  5. 99% of the time you only have one stylesheet per component

@input (decorator)

What?

A simple shorthand without an optional name argument

How?

Use it just like @Input() except without the parenthesis

@input initialItems = [];

is precisely equivalent to

@Input() initialItems = [];

which is precisely equivalent to

@Input('initialItems') initialItems = [];

Why?

  1. A shorter, cleaner, syntax that ensures standard naming conventions for input bindings
  2. Optional argument is 99.9% unused, so it should be a decorator, not a decorator factory

@output (decorator)

What?

A simple shorthand without an optional name argument

How?

Use it just like @Output() except without the parenthesis

@output itemAdded = new EventEmitter();

is precisely equivalent to

@Output() itemAdded = new EventEmitter();

which is precisely equivalent to

@Output('itemAdded') itemAdded = new EventEmitter();

Why?

  1. A shorter, cleaner, syntax that ensures standard naming conventions for output bindings
  2. Optional argument is 99.9% unused, so it should be a decorator, not a decorator factory

@injectable (decorator)

What?

A simple dependency injection annotation

How?

Use it just like @Injectable() except without the parenthesis

@injectable export class AccountProfileManager {
    constructor(
        readonly accountManager: AccountManager,
        readonly profileManager: ProfileManager
    ) { }
}

is precisely equivalent to

@Injectable() export class AccountProfileManager {
    constructor(
        readonly accountManager: AccountManager,
        readonly profileManager: ProfileManager
    ) { }
}

Why?

Since @Injectable from '@angular/core' takes no arguments, it should be a decorator, not a decorator factory

Implementation

As a quick look through the source will illustrate, all of the above are implemented by delegating to the underlying @angular/core so meaning that stay in sync and up to date.