ng2-conventions-decorators
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:
- Violates DRY.
- Harder to maintain: You have to keep even more names in sync when refactoring.
- Verbose: Redundantly species configuration in strings.
- Error prone: Heavy use of optionality and simple strings fail to leverage strengths of Angular 2's foundational tools (e.g. TypeScript).
- 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.
- 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';
}
@component(template) export class MyAwesomeComponent {
private prop = 'this is private';
}
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 {
}
is a TypeScript compile error while
@Pipe({ name: 'localCurrency' }) export class LocalCurrencyPipe {
}
will fail at runtime.
Why?
- DRY
- Automatically creates camelCasedFunction name
- Convention over configuration
- Always pure, no confusing
true
defaulting boolean - Ensures decorated class actually provides a
transform(value)
method at compile time - When using TypeScript we both can and should take advantage of its semantic error checking
- 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?
- DRY
- Automatically create kebab-cased-element selector
- Convention over configuration
- Ensures standard selector naming conventions
- 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?
- A shorter, cleaner, syntax that ensures standard naming conventions for input bindings
- 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?
- A shorter, cleaner, syntax that ensures standard naming conventions for output bindings
- 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.