Skip to main content
This guide covers Angular-specific patterns and best practices enforced by ESLint rules and team conventions.

Component Patterns

Component Class Suffix

All components must use the Component suffix (@angular-eslint/component-class-suffix):
// Good
@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html'
})
export class UserProfileComponent {}

// Bad
@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html'
})
export class UserProfile {}

Directive Class Suffix

All directives must use the Directive suffix (@angular-eslint/directive-class-suffix):
// Good
@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {}

// Bad
@Directive({
  selector: '[appHighlight]'
})
export class Highlight {}

Change Detection Strategy

Prefer OnPush change detection for better performance (@angular-eslint/prefer-on-push-component-change-detection):
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush  // Required
})
export class UserListComponent {}
Benefits:
  • Improved performance by reducing change detection cycles
  • Forces better data flow patterns
  • Explicit state management

Lifecycle Hooks

Implement Lifecycle Interfaces

Always implement the corresponding interface (@angular-eslint/use-lifecycle-interface):
// Good
import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({ ... })
export class UserComponent implements OnInit, OnDestroy {
  ngOnInit(): void {
    // Initialization logic
  }

  ngOnDestroy(): void {
    // Cleanup logic
  }
}

// Bad - missing interfaces
export class UserComponent {
  ngOnInit(): void {
    // Initialization logic
  }
}

Contextual Lifecycle

Use lifecycle hooks appropriately for their context (@angular-eslint/contextual-lifecycle):
  • Don’t use component hooks in directives
  • Don’t use directive hooks in components
  • Use the right hook for the right job

Input/Output Patterns

No Input/Output Metadata

Don’t use inputs and outputs metadata arrays (@angular-eslint/no-inputs-metadata-property, @angular-eslint/no-outputs-metadata-property):
// Good
@Component({ ... })
export class UserComponent {
  @Input() userId: string;
  @Output() userChanged = new EventEmitter<User>();
}

// Bad
@Component({
  inputs: ['userId'],
  outputs: ['userChanged']
})
export class UserComponent {
  userId: string;
  userChanged = new EventEmitter<User>();
}

No Output Rename

Don’t rename outputs (@angular-eslint/no-output-rename):
// Bad
@Output('onChange') userChanged = new EventEmitter();

// Good
@Output() userChanged = new EventEmitter();

Prefer Signals

Use Angular signals for reactive state (@angular-eslint/prefer-signals):
import { Component, signal, computed } from '@angular/core';

@Component({ ... })
export class CounterComponent {
  // Prefer signals over traditional properties
  count = signal(0);
  doubleCount = computed(() => this.count() * 2);

  increment(): void {
    this.count.update(c => c + 1);
  }
}

EventEmitter Type

Use EventEmitter with proper typing (@angular-eslint/prefer-output-emitter-ref):
// Good
@Output() userChanged = new EventEmitter<User>();

// Better with EventEmitterRef (Angular 19+)
import { output } from '@angular/core';
@Output() userChanged = output<User>();

Dependency Injection

Constructor Injection

Use constructor injection for services:
@Component({ ... })
export class UserComponent {
  constructor(
    private userService: UserService,
    private route: ActivatedRoute,
    private i18nService: I18nService
  ) {}
}

Modern inject() Function

While @angular-eslint/prefer-inject is currently disabled, the inject() function is available:
import { Component, inject } from '@angular/core';

@Component({ ... })
export class UserComponent {
  private userService = inject(UserService);
  private route = inject(ActivatedRoute);
}

RxJS Best Practices

Automatic Unsubscription

Use takeUntilDestroyed to automatically unsubscribe (rxjs-angular/prefer-takeuntil):
import { Component, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({ ... })
export class UserComponent implements OnInit {
  private destroy$ = takeUntilDestroyed();

  ngOnInit(): void {
    this.userService.getUsers()
      .pipe(this.destroy$)  // Automatically unsubscribes on destroy
      .subscribe(users => {
        this.users = users;
      });
  }
}

Subject Protection

Don’t expose raw Subject instances - only expose as Observable (rxjs/no-exposed-subjects):
// Good
export class UserService {
  private usersSubject = new Subject<User[]>();
  users$ = this.usersSubject.asObservable();  // Exposed as Observable
}

// Allowed - protected subjects
export class UserService {
  protected usersSubject = new Subject<User[]>();  // Protected is allowed
}

// Bad
export class UserService {
  usersSubject = new Subject<User[]>();  // Public subject exposed
}
All rules from eslint-plugin-rxjs recommended config are enabled, including:
  • No nested subscriptions
  • No subscribe in subscribe
  • Proper error handling
  • Unsubscribe from finite observables

Template Patterns

Button Type Attribute

All buttons must have an explicit type attribute (@angular-eslint/template/button-has-type):
<!-- Good -->
<button type="button" (click)="save()">Save</button>
<button type="submit">Submit Form</button>

<!-- Bad -->
<button (click)="save()">Save</button>

Inline Templates

Inline templates are processed automatically by ESLint (angular.processInlineTemplates):
@Component({
  selector: 'app-inline',
  template: `
    <button type="button">Click Me</button>
  `
})
export class InlineComponent {}

Custom Bitwarden Rules

Required Labels on Icons

Icon buttons require accessibility labels (@bitwarden/components/require-label-on-biticonbutton):
<!-- Good -->
<bit-icon-button icon="close" aria-label="Close dialog"></bit-icon-button>

<!-- Bad -->
<bit-icon-button icon="close"></bit-icon-button>

<!-- Exception - certain directives auto-label -->
<bit-icon-button icon="eye" bitPasswordInputToggle></bit-icon-button>

No BWI Class Usage

Avoid using bwi-* classes directly in templates (@bitwarden/components/no-bwi-class-usage):
<!-- Avoid (warning) -->
<i class="bwi bwi-lock"></i>

<!-- Prefer -->
<bit-icon icon="lock"></bit-icon>

No Icon Children in Buttons

Don’t nest icons inside bit-button components (@bitwarden/components/no-icon-children-in-bit-button):
<!-- Bad (warning) -->
<bit-button>
  <i class="bwi bwi-plus"></i> Add Item
</bit-button>

<!-- Good -->
<bit-button icon="plus">Add Item</bit-button>

Pipes

Implement PipeTransform

All pipes must implement the PipeTransform interface (currently disabled):
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
  transform(value: string, length: number): string {
    return value.length > length ? value.substring(0, length) + '...' : value;
  }
}

Standalone Components

While @angular-eslint/prefer-standalone is currently disabled, standalone components are the modern approach:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-user',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './user.component.html'
})
export class UserComponent {}

Admin Console Strict Rules

The libs/admin-console and admin-console app directories enforce stricter Angular rules:
  • @angular-eslint/no-empty-lifecycle-method: "error"
  • @angular-eslint/no-input-rename: "error"
  • @angular-eslint/no-output-native: "error"
  • @angular-eslint/no-output-on-prefix: "error"
  • @angular-eslint/use-pipe-transform-interface: "error"
See eslint.config.mjs:609-631 for the full configuration.

Next Steps

Build docs developers (and LLMs) love