Name: angular2-style-guide
Owner: Valor Software
Description: Community-driven set of best practices and style guidelines for Angular 2 application development
Forked from: mgechev/angular2-style-guide
Created: 2016-02-22 13:11:41.0
Updated: 2018-03-13 18:16:38.0
Pushed: 2016-02-21 19:50:37.0
Homepage: https://mgechev.github.io/angular2-style-guide/
Size: 160
Language: null
GitHub Committers
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
The purpose of the following style guide is to present a set of best practices and style guidelines for the development of Angular 2 applications. If you are looking for an opinionated style guide for syntax, conventions, and structuring Angular 2 applications, then you can step in!
Disclaimer: The document is in alpha version which means that some the guidelines will change and new will be added.
You are welcome to join the discussion of the best practices here.
The guidelines described below are based on:
Group files by the bounded context they belong to. When a context (directory, for insance) grows to contain more than 15 files, start to consider creating a separate context by-type for them. Your threshold may be different, so adjust as needed.
admin
??? home-dashboard.component.ts
??? home-dashboard.component.html
??? home-dashboard.component.css
??? home-dashboard.component.spec.ts
??? login.component.ts
??? login.component.spec.ts
??? admin.model.ts
??? user-management.service.ts
??? order-management.service.ts
shared
??? components
? ??? avatar.component.ts
? ??? avatar.component.html
? ??? login-form.component.ts
? ??? login-form.component.html
? ??? login-form.component.css
? ??? login-form.component.spec.ts
??? directives
? ??? form-validator.directive.ts
? ??? form-validator.directive.spec.ts
? ??? tooltip.directive.ts
? ??? tooltip.directive.spec.ts
??? services
??? authorization.service.ts
pipes
??? format-order-name.pipe.ts
??? format-order-name.pipe.spec.ts
shop
? components
??? edit-profile.component.ts
??? edit-profile.component.html
??? edit-profile.component.css
??? edit-profile.component.spec.ts
??? home.component.ts
??? home.component.spec.ts
??? home.component.html
??? register.component.ts
??? register.component.spec.ts
? models
??? shopping-cart.model.ts
??? shopping-item.model.ts
??? user.ts
? services
??? checkout.service.ts
Why?: The level of reusability of logic between bounded contexts should be low. On the other hand each code unit will belong to the bounded context it is associated with and will not pollute the directory structure.
Why?: Separation by bounded context will allow easier code reusability. In the general case a small, to medium applicaiton will contain a single bounded context, which will lead to a flat directory structure as the following:
components
??? edit-profile.component.ts
??? edit-profile.component.spec.ts
??? edit-profile.html
??? home.component.ts
??? home.component.spec.ts
??? home.html
??? register.component.ts
??? register.component.spec.ts
models
??? shopping-cart.ts
??? shopping-item.ts
??? user.ts
services
? checkout.service.ts
Why?: A developer can locate the code, identify what each file represents at a glance, the structure is flat as can be, and there is no repetitive nor redundant names.
Why?: When there are a lot of files (15+) locating them is easier with a consistent folder structures and more difficult in flat structures.
Define only a single code unit (component, directive, service, pipe, etc.) per file. If the unit uses other internal for the given module logic you can keep it in the same module, without exporting it.
Why?: The definitions will be easier to find simply by looking at the directory structure.
Why?: Do not export private APIs because you need to manage them and keep them consistent for the end users.
Name the files which contain component, directives, pipes and services definition with the corresponding suffix.
ontains the "home-dashboard" component
-dashboard.component.ts
ontains the ShoppingCart model. Notice that there is no suffix for the models.
ping-cart.ts
ontains the logic for the "checkout" service.
kout.service.ts
Keep the modules self-contained and coherent. Each module should have a single reason to change.
Why?: This way the modules will be more reusable, and thus composable.
Name the modules/directories with lower-kebab-case
.
Why?: Using lower case will not cause problem across platforms with different case sensitivity.
Why?: By using only English letters, numbers and dash (-
) the file names will be consistent across platforms.
Why?: Keeps consistency with AngularJS 1.x style guidelines.
ORRECT */
tip.directive.ts
.service.ts
Use attributes as selectors for your directives.
Why?: There could be many directives per element, which makes it more suitable to use attributes as opposed to elements.
Use lowerCamelCase
for naming the selectors of your directives.
Why?: Keeps the names of the properties defined in the controllers that are bound to the view consistent with the attribute names.
Why?: The Angular 2 HTML parser is case sensitive so lowerCamelCase
attributes are well supported.
Use custom prefix for the selector of your directives (for instance below is used the prefix sg
from Style Guide).
Why?: This way you will be able to prevent name collisions.
RONG */
ective({
ctor: '[tooltip]'
s BootstrapTooltipDirective {}
ective({
ctor: '[tooltip]'
s CustomTooltipDirective {}
ponent({
ctor: 'app',
late: `...`,
ctives: [CustomTooltip, BootstrapTooltip]
s AppComponent {}
s
ORRECT */
ective({
ctor: '[bsTooltip]'
s BootstrapTooltipDirective {}
ective({
ctor: '[myTooltip]'
s CustomTooltipDirective {}
ponent({
ctor: 'sg-app',
late: `...`,
ctives: [CustomTooltip, BootstrapTooltip]
s AppComponent {}
Name directives' controllers with Directive
suffix and components' controllers with Component
suffix. The name of any directive or component should be formed following the rule BasicDescription
+ Directive
or Component
.
ORRECT */
ective({
ctor: '[sgTooltip]`
s TooltipDirective {}
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent {}
Why?: When any code unit is imported the consumer will know how to use it based on its name, i.e. ButtonComponent
means that:
Use @HostListener
and @HostBinding
instead of the host
property of the @Directive
and @Component
decorators:
Why?: The name of the property, or method name associated to @HostBinding
or respectively @HostListener
should be modified only in a single place - in the directive's controller. In contrast if you use host
you need to modify both the property declaration inside the controller, and the metadata associated to the directive.
Why?: The metadata declaration attached to the directive is shorter and thus more readable.
RONG */
ective({
ctor: '[sgSample'],
: {
mouseenter)': 'onMouseEnter()',
ole': 'button'
s SampleDirective {
on;
useEnter() {...}
s
ORRECT */
ective({
ctor: '[sgSample]'
s SampleDirective {
tBinding('role') button;
tListener('mouseenter') onMouseEnter() {...}
Use element selectors for components.
Why?: There could be only a single component per element.
Why?: Components are the actual elements in our applications, compared to directives which only augment the elements.
Use kebab-case
for naming the element selectors of your components.
Why?: Keeps the element names consistent with the specification for Custom Elements.
RONG */
ponent({
ctor: '[sg-button]',
late: `...`
s ButtonComponent {}
s
ORRECT */
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent {}
Keep the components as simple and coherent as possible but not too simple.
Why?: Simple coherent components are easier to reason about, more reusable and composable.
Why?: Components which are too primitive may lead to scattering and harder management of the user interface of our applications.
Keep the components' templates as lean as possible and inline them inside of the @Component
decorator.
Why?: Keeping the components' templates short implies simple, composable components.
Why?: Keeping the template next to the component's controller makes it easier to lookup the component's structure and/or modify it.
Extract the more complex and bigger templates, longer than 15 lines of code, into a separate file and put them next to their controllers' definition.
Why?: In case a big and complex template is inlined in the component metadata it may shift the focus from the component's logic defined within the controller.
Locate the template of the component in the same directory where the component's logic resides in.
Use @Input
and @Output
instead of the inputs
and outputs
properties of the @Directive
and @Component
decorators:
Why?: The name of the property, or event name associated to @Input
or respectively @Output
should be modified only on a single place.
Why?: The metadata declaration attached to the directive is shorter and thus more readable.
RONG */
ponent({
ctor: 'sg-button',
late: `...`,
ts: [
abel'
uts: [
hange'
s ButtonComponent {
ge = new EventEmitter<any>();
l: string;
s
ORRECT */
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent {
put()
ge = new EventEmitter<any>();
ut()
l: string;
Do not rename inputs and outputs.
Why?: May lead to confusion when the output or the input properties of a given directive are named a given way but exported differently as a public API.
RONG */
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent {
put('changeEvent') change = new EventEmitter<any>();
ut('labelAttribute') label: string;
Need to be consumed the following way: *
*/
/ CORRECT /
@Component({
selector: 'sg-button',
template: ...
})
class ButtonComponent {
@Output() change = new EventEmitter
Need to be consumed the following way, which is much more straightforward: *
*/
Prefer inputs over the @Attribute
parameter decorator.
Why?: The input creates one-way binding which means that we can bind it to an expression and its value gets automatically updated. We can inject a property with @Attribute
to a controller's constructor and get its value a single time.
RONG */
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent {
l: string;
tructor(@Attribute('label') label) {
is.label = label;
s
ORRECT */
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent {
ut() label: string;
Detach components and directives which are not visible from the view in order to prevent the change detection running over them.
ORRECT */
ective({
ctor: '[pane]'
s Pane {
ut() title;
: EmbeddedViewRef;
tructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef) {}
reates and attaches the view
() {
is.view = this._viewContainer.createEmbeddedView(this._templateRef);
is._viewContainer.insert(this.view);
etaches the view container
() {
is._viewContainer.detach();
is._viewContainer.remove();
Do not use native elements injected to the controller's constructors with ElementRef
.
Why?: This way the application will get tightly coupled to the platform and thus won't be able to run independently from it. For instance, a web application injecting native DOM elements won't be able to run in WebWorker, nor be rendered on the server-side.
RONG */
ponent({
ctor: 'sg-text-field',
late: `<input type="text">`
s TextFieldComponent {
e: string;
tructor(el: ElementRef) {
.nativeElement.querySelector('input[type="text"]')
.onchange = () => this.value = el.value;
s
ORRECT */
rt {NgModel} from 'angular2/common';
ponent({
ctives: [NgModel],
ctor: 'sg-text-field',
late: `<input [(ngModel)]="value" type="text">`
s TextFieldComponent {
e: string;
Keep the component templates logicless.
Why?: Keeping the logic of the components in their controller, instead of template will bring a lot of benefits such as better testability, maintability, reusability.
Do not manipulate element referenced within the template.
Why?: This way the application will get tightly coupled to the platform and thus won't be able to run independently from it. For instance, a web application injecting native DOM elements won't be able to run in WebWorker, neither be rendered on the server-side.
RONG */
ponent({
ctor: 'sg-items-list',
late: `
nput type="text" #textInput>
utton (click)="add(textInput.value)">Add</button>
s ItemListComponent {
es: string[] = [];
val) {
is.values.push(val);
s
ORRECT */
rt {NgModel} from 'angular2/common';
ponent({
ctives: [NgModel],
ctor: 'sg-items-list',
late: `
nput type="text" [(ngModel)]="value">
utton (click)="add()">Add</button>
s ItemListComponent {
e: string;
es: string[] = [];
) {
is.values.push(this.value);
Always explicitly implement the interfaces associated with the used by given component life-cycle hooks.
Why?: In case the interface associated to given life-cycle hook is implemented one will get compile-time errors in case the hook is not implemented properly (for instance, the method name is misspelled).
RONG */
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent {
Init {
nsole.log('The component got initialized');
s
ORRECT */
rt {OnInit} from 'angular2/core';
ponent({
ctor: 'sg-button',
late: `...`
s ButtonComponent implements OnInit {
Init {
nsole.log('The component got initialized');
Limit the usage of forwardRef
.
Why?: The usage of forwardRef
indicates either a cyclic dependency or inconsistency in the services' declaration (i.e. the dependent service is declared before its dependency). In both cases there is usually a better approach to be used.
Do not declare global providers in the bootstrap
call, use a top-level component instead.
Why?: The providers registered in the bootstrap
method are meant for overriding existing providers rather than declaring dependencies.
Name pipes' controllers with Pipe
as suffix. The name of any pipe should be formed observing the BasicDescription + Pipe rule.
Always implement the PipeTransform
interface when building a Pipe.
Why? This ensures your custom Pipes will conform to the framework requirements should they change in the future.
Name your pipe name
property in camelCase.
RONG */
e({
: 'sg-transform-something'
s TransformSomething {
sform(input: any): any {
...
s
ORRECT */
e({
: 'sgTransformSomething'
s TransformSomethingPipe implements PipeTransform {
sform(input: any): any {
...
Use impure pipes only when they need to hold state.
Why?: The change detection can optimize pure pipes since they hold the referential transparency property.
Wherever stateful (impure) pipes are required, implement the OnDestroy
interface.
Why? It is likely that a stateful pipe may contain state that should be cleaned up when a binding is destroyed. For example, a subscription to a stream of data may need to be disposed, or an interval may need to be cleared.
Name the routes the same way the components associated with them are called.
Why?: This way there is only a single name associated to a given route. This avoids confusion in case a given route is called in a different way to the components associated with it.
Do not use expression for name of a control.
Why?: When the name of the control is dynamically generated it will be hard to reference it inside of the controller associated to the form, and may lead to confusion.
RONG */
m>
ut [ngControl]="foobar" type="text">
rm>
ponent(...)
s SampleComponent {
ar = 'foo';
tml
ORRECT */
m>
ut ngControl="foo" type="text">
rm>
ponent(...)
s SampleComponent {}
Follow the angular-cli publisher guide.
Why?: It will allow your library to be painlessly used with the official Angular 2 CLI tool.
Use Jasmine for unit testing.
Why?: It is well supported and popular in the JavaScript community.
Why?: It is widely popular in the Angular community. Jasmine is used for testing both AngularJS 1.x and Angular 2.
Why?: It has up-to-date type definitions so you can have great development experience using TypeScript.
Use Karma as test runner.
Place your test files side-by-side with your client code.
Why?: The tests are easier to find since they are always next to the code they are testing.
Why?: When you update source code it is easier to go update the tests at the same time.
Why?: The code may act as documentation of the tested component.
Name the test file the following way NAME_OF_THE_TESTED_UNIT.spec.EXT
:
ORRECT */
t.ts
t.spec.ts
Place integration tests and tests that cover multiple code units into a separate tests
directory in your project's root.
Use OnPush
change detection strategy for pure/dumb components that accepts as input immutable data.
Why?: Angular will optimize the performance of your application dramatically by not performing change detection over the entire sub tree with root the given pure component.
Use TSLint for linting your code.
Why?: In big teams, code following the same practices is easier to read and understand.
Be as explicit in the type definitions as possible (i.e. use any
as rarely as possible).
Why?: any
makes TypeScript behaves like a dynamic language. This way we gave up all the benefits we get from static typing such as better IDE/text editor support and compile-time type-checking.
Use “Façade modules” for exporting the public members of your application/library and hiding the implementation details.
xample */
rt {NgClass} from './directives/ng_class';
rt {NgFor} from './directives/ng_for';
rt {NgIf} from './directives/ng_if';
rt {NgStyle} from './directives/ng_style';
rt {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './directives/ng_switch';
rt * from './directives/observable_list_diff';
rt {CORE_DIRECTIVES} from './directives/core_directives';
Use the @Injectable()
decorator instead of explicitly declaring the dependencies using @Inject(TOKEN)
.
Why?: Using @Injectable()
the TypeScript compiler will emit the required type metadata, which makes the code using @Inject()
unnecessary verbose and less readable.
Use TSLint for linting your JavaScript and be sure to customize the TSLint options file and include in source control. See the TSLint docs for details on the options.
Why?: Provides a first alert prior to committing any code to source control.
Why?: Provides consistency across your team.
es": {
lass-name": true,
urly": false,
ofline": true,
ndent": "spaces",
ax-line-length": [true, 140],
ember-ordering": [true,
"public-before-private",
"static-before-instance",
"variables-before-functions"
o-arg": true,
o-construct": true,
o-duplicate-key": true,
o-duplicate-variable": true,
o-empty": true,
o-eval": true,
o-trailing-comma": true,
o-trailing-whitespace": true,
o-unused-expression": true,
o-unused-variable": true,
o-unreachable": true,
o-use-before-declare": true,
ne-line": [true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
uotemark": [true, "single"],
emicolon": true,
riple-equals": true,
ariable-name": false
Use the internal JavaScript DLS provided by Angular 2 for annotating constructor functions.
Why?: This syntax is simpler, more readable and closer to the original TypeScript version of the code.
Why?: The DLS uses function expressions which are closer to the non-hoisted ES2015 classes.
RONG */
tion Component() {
.
onent.annotations = [
ng.core.ComponentMetadata({
...
ng.router.RouteConfig([
...
s
ORRECT */
Component = ng.
onent({
...
teConfig([
...
ss({
nstructor: functions () {
//...
Use JSHint for linting your JavaScript and be sure to customize the JSHint options file and include in source control. See the JSHint docs for details on the options.
Why?: Provides a first alert prior to committing any code to source control.
Why?: Provides consistency across your team.
itwise": true,
amelcase": true,
urly": true,
qeqeq": true,
s3": false,
orin": true,
reeze": true,
mmed": true,
ndent": 2,
atedef": "nofunc",
ewcap": true,
oarg": true,
oempty": true,
onbsp": true,
onew": true,
lusplus": false,
uotmark": "single",
ndef": true,
nused": false,
trict": false,
axparams": 10,
axdepth": 5,
axstatements": 40,
axcomplexity": 8,
axlen": 140,
si": false,
oss": false,
ebug": false,
qnull": true,
snext": false,
vil": false,
xpr": false,
uncscope": false,
lobalstrict": false,
terator": false,
astsemic": false,
axbreak": false,
axcomma": false,
oopfunc": true,
axerr": false,
oz": false,
ultistr": false,
otypeof": false,
roto": false,
cripturl": false,
hadow": false,
ub": true,
upernew": false,
alidthis": false,
oyield": false,
rowser": true,
ode": true,
lobals": {
"angular": false,
"ng": false
Use JSCS for checking your coding styles your JavaScript and be sure to customize the JSCS options file and include in source control. See the JSCS docs for details on the options.
Why?: Provides a first alert prior to committing any code to source control.
Why?: Provides consistency across your team.
xcludeFiles": ["node_modules/**", "bower_components/**"],
equireCurlyBraces": [
"if",
"else",
"for",
"while",
"do",
"try",
"catch"
equireOperatorBeforeLineBreak": true,
equireCamelCaseOrUpperCaseIdentifiers": true,
aximumLineLength": {
"value": 140,
"allowComments": true,
"allowRegex": true
alidateIndentation": 2,
alidateQuoteMarks": "'",
isallowMultipleLineStrings": true,
isallowMixedSpacesAndTabs": true,
isallowTrailingWhitespace": true,
isallowSpaceAfterPrefixUnaryOperators": true,
isallowMultipleVarDecl": null,
equireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"return",
"try",
"catch"
equireSpaceBeforeBinaryOperators": [
"=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=",
"&=", "|=", "^=", "+=",
"+", "-", "*", "/", "%", "<<", ">>", ">>>", "&",
"|", "^", "&&", "||", "===", "==", ">=",
"<=", "<", ">", "!=", "!=="
equireSpaceAfterBinaryOperators": true,
equireSpacesInConditionalExpression": true,
equireSpaceBeforeBlockStatements": true,
equireLineFeedAtFileEnd": true,
isallowSpacesInsideObjectBrackets": "all",
isallowSpacesInsideArrayBrackets": "all",
isallowSpacesInsideParentheses": true,
sDoc": {
"checkAnnotations": true,
"checkParamNames": true,
"requireParamTypes": true,
"checkReturnTypes": true,
"checkTypes": true
isallowMultipleLineBreaks": true,
isallowCommaBeforeLineBreak": null,
isallowDanglingUnderscores": null,
isallowEmptyBlocks": null,
isallowTrailingComma": null,
equireCommaBeforeLineBreak": null,
equireDotNotation": null,
equireMultipleVarDecl": null,
equireParenthesesAroundIIFE": true
MIT