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. 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.

Best practices

Always strive to write idiomatic up to date Angular code. Since Angular is a living framework, the best practices and recommendations may evolve over time. Stay informed about the latest changes and adapt your code accordingly.

Change detection

Use the OnPush change detection strategy. We will eventually enforce this project wide for performance reasons.

Control flow

Only use the new Angular control flow syntax (@if), do not use the older structural directives (*ngIf).

Dependency injection

Use the inject function to retrieve dependencies instead of constructor injection in Angular primitives (Components, Pipes, etc.). Do note that this only works in Angular contexts and you should still use constructor injection when writing code that is shared with non Angular clients.

Host bindings

Always prefer using the host property over @HostBinding and @HostListener. These decorators exist exclusively for backwards compatibility.

https://angular.dev/guide/components/host-elements#the-hostbinding-and-hostlistener-decorators

Standalone

Use standalone components, directives and pipes. NgModules can still be used for grouping multiple components but the inner components should be standalone.

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

Naming conventions (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:

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

and the following less conventional suffixes:

  • .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.

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

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

Reactivity (ADR-0003 & ADR-0028)

We make heavy use of reactive programming using Angular Signals & RxJS. Generally components should always derive their state reactively from services whenever possible.

Signals

Angular Signals is a new reactivity model introduced in Angular 16 and made stable in Angular 17. Signals provides a simple and intuitive way to manage state and reactivity in Angular applications.

However Signals are limited to Angular contexts only, and cannot be used in non Angular clients. Therefore we mostly limit our usage of Signals to components and presentational services only. Everywhere else we use RxJS.

// Example component which uses signals for a local state
@Component(
selector: "app-counter",
template: `
<h1>Current value of the counter {{counter()}}</h1>

<button (click)="increment()">Increment</button>
`,
})
export class Countcomponent {
protected counter = signal(0);

increment() {
this.counter.set(this.counter() + 1);
}
}

RxJS

RxJS is a powerful library for reactive programming using Observables. We use RxJS whenever we need interoperability with non Angular clients, or when we need more advanced operators not available in Signals.

// Example component which displays a list of folders
@Component({
selector: "app-folders-list",
template: `
<ul>
@for (folder of folders$ | async; track folder) {
<li>
{{ folder.name }}
</li>
}
</ul>
`,
})
export class FoldersListComponent {
private folderService = inject(FolderService);

protected folders$: Observable<FolderView[]> = this.folderService.folderViews$;
}

We have a couple of guidelines when writing RxJS code, which are enforced using the eslint-plugin-rxjs and the eslint-plugin-rxjs-angular. These rules are designed to assist in avoiding common RxJS pitfalls which can cause Observables to not be cleaned up, or behave unexpectedly.

Avoid subscriptions

Whenever possible we should avoid explicit subscriptions, and instead use the | async pipe in the templates. This will ensure that the subscription is cleaned up when the component is destroyed without any of the boilerplate.

To this end, we can use the .pipe operation along with the rxjs operators to modify the input observable into something we can display.

Consider the following example, it's quite easy to forget to unsubscribe from the observable, we also have a bit more boilerplate than we'd like.

private destroy$ = new Subject();
public transformed = [];

observable$
.pipe(takeUntil(this.destroy$))
.subscribe((v) => {
transformed = transform(v);
});

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}

// Template
@for (t of transformed) {
{{ t }}
}

Now instead consider the following example, in which we replaced the subscribe with | async.

transformed$ = observable$.pipe(map(transform));

// Template
@for (t of transformed$ | async; track t) {
{{ t }}
}

Unsubscribe using takeUntilDestroyed

Dangling subscriptions are a common cause of memory leaks. To avoid this we use the prefer-takeUntil rule. Which requires that any subscription is first piped through a takeUntilDestroyed operator.

The main benefit of the takeUntil pattern is that reviewers can at a quick glance verify the subscription is cleaned up.

constructor() {
// takeUntilDestroyed must be called from an injector context
this.observable$
.pipe(takeUntilDestroyed())
.subscribe(value => console.log);
}

When not called from an injector context, you can pass the DestroyRef as an argument.

constructor(private destroyRef: DestroyRef){}

ngOnInit() {
this.observable$
.pipe(takeUntilDestroyed(this.destroyRef))
// This subscription will automatically be cleaned up when the component is destroyed
.subscribe(value => console.log);
}

No async subscribes

Async subscriptions rarely work as you expect them. Rather than executing in sequence, there is a chance of them executing in parallel. Which can easily lead to unexpected behavior. To avoid this, async subscriptions are forbidden in our codebase, and you instead need to pick the right operation.

Some appropriate operators are:

  • switchMap: Cancels the previous operation making it appropriate for scenarios where we do not care about old results after a new input has been received.
  • concatMap: Runs the async operations in order, preventing parallel and out of order execution. Use this if we care about processing each event.
  • mergeMap: Please consider carefully if this is the right operator for your use case. mergeMap will flatten observables but not care about the order. If ordering is important use concatMap. If you only care about the latest value use switchMap.

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.dev/guide/forms#choosing-an-approach

Enum-likes (ADR-0025)

For general guidance on enum-likes, consult Avoid TypeScript Enums.

String-backed enum-likes

String-typed enum likes can be used as inputs of a component directly. Simply expose the enum-like property from your component:

// given:
const EnumLike = { Some: "some", Value: "value" };
type EnumLike = EnumLike[keyof typeof EnumLike];

// add the input:
@Component({ ... })
class YourComponent {
@Input() input: EnumLike = EnumLike.Some;

// ...
}

Composers can use the enum's string values directly:

<my-component input="value" />

Numeric enum-likes

Using numeric enum-likes in components should be avoided. If it is necessary, follow the same pattern as a string-backed enum.

Composers that need hard-coded enum-likes in their template should expose the data from their component:

import { EnumLike } from "...";

// add the input to your component:
@Component({ ... })
class TheirComponent {
protected readonly EnumLike = EnumLike;
}

And then bind the input in the template:

<my-component [input]='EnumLike.Value' />