Skip to main content

Angular

At Bitwarden we use Angular as our client side framework. It's recommended to have a basic understanding of Angular before continuing reading, and when uncertain please refer to the official Angular Docs or the Getting started with Angular. We also make an effort at following the Angular Style Guide.

This document aims to cover the best practices we follow. Many of them are originally based on different ADRs, however while ADRs are good at describing the why, they provide a suboptimal reading experience.

Naming (ADR-0012)

We follow the Naming section from the Angular Style Guide. More specifically use dashes to septate words in the descriptive name, and dots to separate name from the type.

We use the following conventional suffixes suggested by Style-02-01:

  • service - Service (At Bitwarden this type denotes an abstract service)
  • component - Angular Components
  • pipe - Angular Pipe
  • module - Angular Module
  • directive - Angular Directive

At Bitwarden we also use a couple of more types:

  • .api - Api Model
  • .data - Data Model (used for serializing domain model)
  • .view - View Model (decrypted domain model)
  • .export - Export Model
  • .request - Api Request
  • .response - Api Response
  • .type - Type definition
  • .enum - Enum

The class names are expected to use the suffix as part of their class name as well. I.e. a service implementation will be named FolderService, a request model will be named FolderRequest.

Abstract & Default Implementations

The Bitwarden clients codebase serves multiple clients, one of which is node based and unable to utilize the Angular Dependency Injection. In order to make code useable in both Angular and non Angular based clients we generally use abstract classes to define interfaces. Ideally we would use interfaces but TypeScript interfaces exists at compile time only and therefore cannot be used to wire up dependency injection in JavaScript.

All consumers will use the abstract class as a parameter in their constructor which will be manually wired up in the CLI and use Angular dependency injection in Angular contexts. In the case a dependency is only used in the Angular client, the abstract class can be omitted and the implementation referenced directly using @Injectable.

To improve readability and avoid potential confusion caused by import aliases, we avoid naming implementations the same name as the abstract class. Depending on the class usage the following prefixes are allowed:

  • Default: Used for the default implementation of an abstract class.
  • Web, Browser, Desktop and Cli for platform specific implementations.

Organize by Feature (ADR-0011)

We strive to follow the Application structure and NgModules section from the Angular Style Guide.

The folder structure should be organized by feature, in a hierarchial manner. With features in turn being owned by a team. Below is a simplified folder structure which may diverge somewhat from the current structure.

In the example we have a single team, auth which has a single feature Emergency Access. The Emergency Access feature consists of a service, some components and a pipe. The feature is further broken down into a view feature which handles viewing another users vault.

The core and shared directories don't match a single team but is owned by the platform team. The core and shared modules are standard concepts in Angular, with core consisting of singleton services used throughout the application, and shared consisting of heavily reused components.

apps/web/src/app/
├─ core/ // Core services vital to the web app
| ├─ services/
| | ├─ web-platform-utils.service.ts
│ ├─ shared.module.ts
│ ├─ index.ts
├─ shared/ // Shared functionality usually owned by platform
│ ├─ feature/ // Feature module
│ ├─ shared.module.ts
│ ├─ index.ts
├─ auth/ // Auth team
│ ├─ shared/ // Generic components shared across the team
│ ├─ emergency-access/ // Feature module
│ │ ├─ access-type.pipe.ts
│ │ ├─ ea.module.ts
│ │ ├─ ea-routing.module.ts
│ │ ├─ ea.service.ts // Service encapsulating all business logic
│ │ ├─ ea.component.{ts,html)
│ │ ├─ dialogs/ // Dialogs used by the root component.
│ │ ├─ view/ // Logical group of components for viewing ea vault
│ │ ├─ index.ts
│ ├─ index.ts // Public interface that can be used by other teams
├─ app.component.ts
├─ app.module.ts

Observables (ADR-0003)

We are currently in the middle of a migration towards reactive data layer using RxJS. What this essentially means is that a component subscribes to a state service, e.g. FolderService and will continually get updates should the data ever change. This ensures that the components always stay up to date.

Previously we manually implemented an event system for sending basic messages which told other components to reload their state. This was error prone, and hard to maintain.

// Example component which displays a list of folders
@Component({
selector: "app-folders-list",
template: `
<ul>
<li *ngFor="let folder of folders$ | async">
{{ folder.name }}
</li>
</ul>
`,
})
export class FoldersListComponent {
folders$: Observable<FolderView[]>;

constructor(private folderService: FolderService) {}

ngOnInit() {
this.folders$ = this.folderService.folderViews$;
}
}

Reactive Forms (ADR-0001)

We almost exclusively use Angular Reactive forms instead of Template Driven forms. And the Bitwarden Component library is designed to integrate with Reactive forms.

Provide direct, explicit access to the underlying form's object model. Compared to template-driven forms, they are more robust: they're more scalable, reusable, and testable. If forms are a key part of your application, or you're already using reactive patterns for building your application, use reactive forms.

https://angular.io/guide/forms-overview#choosing-an-approach

Thin Components

Components should be thin and only contain the logic required to render the view. All other logic belongs to services. This way components that behave almost identical but looks quite different visually can avoid code duplication by sharing the same service. Services tends to be much easier to test than components as well.

Composition over Inheritance (ADR-0009)

Due to the multi client nature of Bitwarden, we have a lot of shared components with slightly different behavior client to client. Traditionally this was implemented using inheritance, however as the application continues to grow this has resulted in large components that are difficult to work with, as well as a multi level inheritance tree.

To avoid this, components should be broken up into logical standalone pieces. The different clients should use the the shared components by customizing the page level components.

plantuml

Understandably this is difficulty to properly implement when the different clients use different css frameworks, which until everything is properly migrated to Bootstrap means there will be a Login Component for each client that extends the generic Login Component. However it should only expose a client specific template.

The diagram below showcases how the reports share logic by extracting the list into a shared component. Instead of the organization component directly extending the base report page component. For more details, read the PR for the refactor.

plantuml