Skip to content

Latest commit

 

History

History
572 lines (425 loc) · 20.6 KB

angular-guidelines.md

File metadata and controls

572 lines (425 loc) · 20.6 KB

MarsBased Angular Style Guide

We follow the official and opinionated Angular coding style guide as the primary source of best practices and conventions.

1. Do's and Don'ts

Add and follow the official MarsBased Angular ESLint configuration, where most of do's and dont's are already defined.

1.1. Logic in templates

Don't put logic in templates.

<!-- bad  (component's logic, not HTML) -->
<div *ngIf="user.active && !user.expired">
  ...
</div>


<!-- good -->
<div *ngIf="isUserVisible">
  ...
</div>
export class MyComponent {
  ...

  get isUserVisible(): boolean {
    return this.user.active && !this.user.expired;
  }
  ...
}

1.2. Use index.ts

Do use index.ts when you see the opportunity. The model directory of a module is a good candidate. It decreases the number and size of imports in components and services.

Export models from the /models directory:

export * from './user.model';
export * from './product.model';

Import them where you need them:

import { UserModel, ProductModel } from '../models';

1.3. Use CDK Virtual Scroll

Do use CDK Virtual Scroll when showing huge lists of elements. It improves performance drastically.

1.4. Type Lazy modules

Do Type lazy modules with async and Promise types.

{
  path: 'lazy-module',
  pathMatch: 'full',
  loadChildren: async (): Promise<LazyModule> =>
    (await import('./modules/lazy-module/lazy-module.module')).LazyModule,
}

1.5. Private methods

Do mark the visibility of private methods when are called only from inside the class.

1.6. Public methods

Do not mark the visibility of public methods, as it's by default.

// bad (public by default)
public isUserVisible(): boolean {
  return this.user.active && !this.user.expired;
}

// good
isUserVisible(): boolean {
  return this.user.active && !this.user.expired;
}

1.7. View Methods

Do not mark as private methods called from the component's view (public).

1.8. Lifecycle Hooks

  • Add hooks just after the class constructor.
  • Add their interfaces to the class.
  • Add hooks in the order they execute.
  • Encapsulate logic inside private methods and call them from the hooks.
export class MyComponent implements OnInit, OnDestroy {

  constructor() {  }

  ngOnInit(): void {
    this.initData();
  }

  ngOnDestroy(): void {
    this.freeResources();
  }

  private initData(): void {
    ...
  }

  private freeResources(): void {
    ...
  }
}

1.9. HTML Attributes

Do sort the HTML tag attributes.

  • With multiple attributes, one attribute per line.
  • With multiple attributes, the closing tag has to be written on the next line.
  • Sort attributes
    1. Structural
    2. Angular data binding properties and events.
    3. Dynamic Properties
    4. Animations
    5. Static and native HTML properties.
<!-- bad (hard to follow) -->
  <div 
    class="user__container"
    @fadeIn
    [user]="user"
    (refresh)="onUserRefresh()"
    [title]="user.name"
    *ngIf="isVisible"
  >
   ...
  </>

<!-- good -->
  <div 
    *ngIf="isVisible"
    [user]="user"
    (refresh)="onUserRefresh()"
    [title]="user.name"
    @fadeIn
    class="user__container"
  >
   ...
  </>

1.10. Empty Observables

Do prefer the EMPTY variable over the of operator when generating an empty observable.

// bad (Don't use `of()` operator:)
const checkActionDispatched = !isActionDispatched ? 
  dispatch(new Action()) : 
  of();

// good
import { EMPTY } from 'rxjs';

const checkActionDispatched = !isActionDispatched ? 
  dispatch(new Action()) : 
  EMPTY;

1.11. HostListener/HostBinding decorators versus host metadata

Do prefer the @HostListener and @HostBinding to the host property of the @Directive and @Component decorators. Refer to the Angular Style Guide for more details.

1.12. Class members order

Do sort class members as follow:

  1. Inputs/Outputs
  2. ViewChilds
  3. Public properties
  4. Private properties
  5. Class accessors
  6. Constructor
  7. Angular's lifecycle hooks
  8. Event methods
  9. Public methods
  10. Private methods
const MY_LIST = [{id: 1, value: 'option 1'}, {id: 2, value: 'option 2'}]
@Component({
   templateUrl: './my-component.component.html',
   selector: 'my-component',
})
export class MyComponent {
  @Input() categories: Category[];

  @Output() disassociate = new EventEmitter<Category>();

  @ViewChild(FormComponent) formCpm: FormComponent;

  user: User;
  category: Category;
  private selectedUser: User;
  private selectedCategory: Category;
  readonly comboList = MY_LIST;

  get isActive(): boolean {
    return this.user.active;
  }

  constructor() { }

  ngOnInit(): void {
    this.initData();
  }

  submit(): void {
    ...
  }

  private initData(): void {
    ...
  }
}

2. General project organization and architecture

Follow the Angular Style Guide to name files and directories. Additionally:

  • Routed modules under the /pages directory.
  • Page components under the routed module directory /pages/*/.
  • Regular components under the routed module directory /pages/*/components.
  • Standalone modules under the /modules directory.
  • State management files under the /state directory.
  • Helpers, utils and custom libraries under the /libs directory.

2.1. Project structure example

cypress/                              # end-to-end tests
src/app/
|- pages/                            # routed modules
  |- admin/                           # routed /admin module
    |- components/                    # admin's modules
      |- avatar/
      |- navbar/
    |- pages/                        # routed modules under /admin
      |- planets/                     # routed /admin/planets module
    |- models/
    |- services/
    |- state/
    |- admin-routing.module.ts        # admin route definition
    |- admin.component.html           # admin component page
    |- admin.component.spec.ts        # admin component page
    |- admin.component.ts             # admin component page
    |- admin.module.ts                # admin module
  |- auth/
|- modules/                           # standalone modules (configuration, utils)
|- shared/                            # shared modules used from child routes
|- state/                             # state files (store, query, state)
|- libs/                              # internal libraries directory
|- models/
|- services/
|- guards/
|- interceptors/
|- app-routing.module.ts              # root component page
|- app.component.html                 # root component page
|- app.component.spec.ts              # root component page
|- app.component.ts                   # root component page
|- app.module.ts                      # root module

3. Description of the most common patterns used to solve common problems

3.1. Lazy Pages

Lazy load every page always, placing routed modules under the /pages directory. This way, the architecture and the tree folder mirror the URL map presented to the user.

E.g. http://sample.com/admin/planets can be accessed from src/app/pages/admin/pages/planets.

Check the next guides for further information about lazy loading and feature modules:

3.2. API Services

We define a clear separation between API Services that retrieve data, usually from an API endpoint, from the rest of the services with business logic.

3.2.1. Why?

Better control of the data flow, what happens if a request fails? should I keep the current store? should I empty the store? are we writing code only for success responses? That depends on the context, if we couple API calls and other logic we lose control of the data flow. We'll find ourselves writing the same API calls with some modifications for different situations.

  • Reusable. We often call the same endpoint from multiple parts of the app for different purposes, if the API service is not coupled with extra code we can reuse it easily.
  • Flexible. We don't need to store everything we retrieve from the API, and sometimes, we want to avoid it (like large data sets or blobs). Also, we could save/process the data in different ways depending on the context. Sometimes we'll want to cache data from responses, sometimes not. When retrieving an entity, we don't want to save it always in the same store, it could be a selectedEntity, oldEntity, newEntity, originEntity, etc... you get the idea.
  • Independent. API services don't have dependencies. In short, I think this could help to avoid ambiguous rules when writing services, leading to a more uniform codebase.

3.2.2. How?

@Injectable()
export class UsersService {
  constructor(private http: HttpClient) {}

  getAll() {
    return this.http
      .get<User>(``)
      .map((response: ResponseModel<User>) => response.data);
  }
}

We name these services adding the api suffix users.api.service.ts

@Injectable()
export class UsersApiService { ... }

We can keep regular mappings inside this service, as they extract the key data we want, saving some code inside components.

When saving an entity to a store, we do it from a regular service, where we have our business logic.

@Injectable()
export class UsersAdminService {
  constructor(
    private usersApiService: UsersApiService,
    private usersStore: UsersStore
  ) {}

  getUsers() {
    return this.usersApiService
      .getAll()
      .pipe(tap((response) => this.usersStore.set(response)));
  }
}

3.3. Forms (Reactive Forms)

The next articles are a good source to understand how to implement reactive forms in Angular:

3.3.1. Simple FormBuilder sample

Inject FormBuilder service to define forms fields and validations inside a component class.

  import { FormBuilder } from '@angular/forms';

  ...

  form: FormGroup;

  ...

  constructor(private fb: FormBuilder) {}

  ...

Often, we create the form instance inside the constructor or when the OnInit hook executes:

private initForm(): void {
  this.form = this.fb.group({
    name: ['', Validators.required],
    phone: ['']
    password: ['', [Validators.required,  Validators.pattern('^[a-z]*$')]]
  });
}

After, we register the created form for the HTML container tag, and attach every form field with every input or control.

<form [formGroup]="form">
  <input formControlName="name" type="text" />
</form>

3.4. Smart and Dumb (Presentational) components

The only goal of a presentational component is to keep the UI logic isolated. It doesn't have access to injected business services. It communicates using @Input and @Output data flows.Presentational components communicate using Input and Output only.

A smart component, or smart container, injects business services and keeps a more complex state. Its goal is to orchestrate the communication and behaviour between data services and presentational components. Smart components communicate to the rest of the app using injected services, Input an Outputs.

Having a separation between smart and presentational components adds the advantages:

  • Easier to write and more focused tests.
  • Isolated UI state.
  • Better component reusability.

Check the Angular University article about smart and presentational components for a detailed explanation.

3.5. State Management (with Akita)

Akita offers, basically, two JS classes to interact with, the store for adding/modify objects and the query for consulting it, and that's all, no more Actions, Reducers nor Effects.

Akita's decoupled components: All Akita store management could be done (and it is fully recommended) throw a service so components are totally detached from it, an async method that interacts with the store remains as an async method for the component and could react to fulfilment if needed (not as flux-like store actions that are "flattened")

3.5.1. Global Entities

It's so recommended exposing globally all entities obtained from the backend and that's the perfect use-case for the entity store and entity query , both could be imported in a root provided (or shared module imported by) service, related with 💥 Angular API Services - Development and totally in the same page, could be something like:

|- app/ |- api/ |- users-api.service.ts |- stores/ |- users/ |- users.state-service.ts |- users.store.ts |- users.query.ts

  • users-api.service.ts: Expose methods for obtaining data from the backend.
  • users.store.ts: Defines users store. Docs
  • users.query.ts: Offers different methods for exposing store objects. Contrary to Akita recommendations, we think could be a better approach to create composed queries in users-state.service instead of here and access to the query object only throw the service. Docs users.state-service.ts: Acts as facade for any other service that want to interact with users entities, uses users-api.service to obtain users related data, fills/modifies store throw users.store and creates and exposes queries using users.query. Docs

3.5.2. Specific module related logic

To code the logic related to a specific feature, all states could be imported only in the related module. In this case, Entity store/query doesn't make sense so normal store and query objects could be used.

|- users-section-module |- state/ |- users-section-state.service.ts |- users-section.store.ts |- users-section.query.ts

  • users-section.store.ts: Defines users-section store. Docs
  • users-section.query.ts: Offers different methods for exposing store objects. Contrary to Akita recommendations, we think could be a better approach to create composed queries in users-section-state.service instead of here and access to the query object only throw the service. Docs
  • users-section-state.service.ts: Holds all users-section related logic, interacts with users-section-store for filling/modifying objects, with users-section.query to expose them on demand and with api related services (users.state-service.ts) for obtaining backend data and modify/consult related store. Docs

3.6. Testing

Favour end-to-end-testing over components test.

3.7. Memory Leaks

Consider and prevent memory leaks from subscriptions in components.

Read Dealing with memory leaks to understand better when this problem arises.

To avoid these leaks, we use the NgNeat Until Destroy lib that adds support for unsubscribing automatically from subscriptions when a component is destroyed.

import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';

@UntilDestroy()
@Component({})
export class InboxComponent {
  ngOnInit() {
    interval(1000)
      .pipe(untilDestroyed(this))
      .subscribe();
  }
}

The order of decorators is important, make sure to put @UntilDestroy() before the @Component() decorator.

3.8. I18N

For simplicity and ease of use, we don't use the translation services that Angular has in its core.

Use Transloco.

4. Libraries

5. Learning Resources

Our articles:

Other resources:

These are our reference resources. Still, a project often needs extra libraries. Before adding a new library, it must be approved by the project's tech lead.

Are you missing any resources or interesting articles on this list? Feel free to suggest additional useful resources by creating a PR to let us know!