
Angular Modernization
- 115 installs
- 13.5k repo stars
- Updated August 5, 2026
- bitwarden/clients
Modernize Bitwarden Angular client code—patterns, dependencies, and structure—while preserving security-sensitive UI flows across web, desktop, and browser extension surfaces.
About
Angular modernization skill from the bitwarden/clients repository for upgrading legacy Angular patterns, dependencies, and module structure. It helps agents refactor Bitwarden’s password-manager frontends—web, extension, and shared packages—toward current Angular standards while maintaining secure, consistent user experiences.
- Targets Bitwarden clients Angular codebase conventions
- Covers dependency upgrades and deprecated API migration
- Balances modernization with password-manager security UX
- Supports web, extension, and shared UI module patterns
- Reduces tech debt in long-lived Angular monorepo apps
Angular Modernization by the numbers
- 115 all-time installs (skills.sh)
- Ranked #1,020 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bitwarden/clients --skill angular-modernizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 115 |
|---|---|
| repo stars | ★ 13.5k |
| Last updated | August 5, 2026 |
| Repository | bitwarden/clients ↗ |
What it does
Modernize Bitwarden Angular client code—patterns, dependencies, and structure—while preserving security-sensitive UI flows across web, desktop, and browser extension surfaces.
Files
Angular Modernization
Transforms legacy Angular components to modern architecture using a two-step approach:
1. Automated migrations - Angular CLI schematics for standalone, control flow, and signals 2. Bitwarden patterns - ADR compliance, OnPush change detection, proper visibility, thin components
Workflow
Step 1: Run Angular CLI Migrations
⚠️ CRITICAL: ALWAYS use Angular CLI migrations when available. DO NOT manually migrate features that have CLI schematics.
Angular provides automated schematics that handle edge cases, update tests, and ensure correctness. Manual migration should ONLY be used for patterns not covered by CLI tools.
IMPORTANT:
- Always run the commands using
npx ng. - All the commands must be run on directories and NOT files. Use the
--pathoption to target directories. - Run migrations in order (some depend on others)
1. Standalone Components
npx ng generate @angular/core:standalone --path=<directory> --mode=convert-to-standaloneNgModule-based → standalone architecture
2. Control Flow Syntax
npx ng generate @angular/core:control-flow*ngIf, *ngFor, *ngSwitch → @if, @for, @switch
3. Signal Inputs
npx ng generate @angular/core:signal-input-migration@Input() → signal inputs
4. Signal Outputs
npx ng generate @angular/core:output-migration@Output() → signal outputs
5. Signal Queries
npx ng generate @angular/core:signal-queries-migration@ViewChild, @ContentChild, etc. → signal queries
6. inject() Function
npx ng generate @angular/core:inject-migrationConstructor injection → inject() function
7. Self-Closing Tag
npx ng generate @angular/core:self-closing-tagUpdates templates to self-closing syntax
8. Unused Imports
npx ng generate @angular/core:unused-importsRemoves unused imports
Step 2: Apply Bitwarden Patterns
See migration-patterns.md for detailed examples.
1. Add OnPush change detection 2. Apply visibility modifiers (protected for template access, private for internal) 3. Convert local component state to signals 4. Keep service observables (don't convert to signals) 5. Extract business logic to services 6. Organize class members correctly 7. Update tests for standalone
Step 3: Validate
- Fix linting and formatting using
npm run lint:fix - Run tests using
npm run test
If any errors occur, fix them accordingly.
Key Decisions
Signals vs Observables
- Signals - Component-local state only (ADR-0027)
- Observables - Service state and cross-component communication (ADR-0003)
- Use
toSignal()to bridge observables into signal-based components
Visibility
protected- Template-accessible membersprivate- Internal implementation
Other Rules
- Always add OnPush change detection
- No TypeScript enums (use const objects with type aliases per ADR-0025)
- No code regions (refactor instead)
- Thin components (business logic in services)
Validation Checklist
Before completing migration:
- [ ] OnPush change detection added
- [ ] Visibility modifiers applied (
protected/private) - [ ] Signals for component state, observables for service state
- [ ] Class members organized (see migration-patterns.md)
- [ ] Tests updated and passing
- [ ] No new TypeScript enums
- [ ] No code regions
References
Bitwarden ADRs
- ADR-0003: Observable Data Services
- ADR-0025: No TypeScript Enums
- ADR-0027: Angular Signals
- Bitwarden Angular Style Guide
Angular Resources
Angular Migration Patterns Reference
Table of Contents
Component Architecture
Standalone Components
Angular defaults to standalone components. Components should omit standalone: true, and any component specifying standalone: false SHALL be migrated to standalone.
@Component({
selector: "app-user-profile",
imports: [CommonModule, ReactiveFormsModule, AsyncPipe],
templateUrl: "./user-profile.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserProfileComponent {}Class Member Organization
@Component({...})
export class MyComponent {
// 1. Inputs (public)
@Input() data: string;
// 2. Outputs (public)
@Output() valueChange = new EventEmitter<string>();
// 3. ViewChild/ContentChild
@ViewChild('template') template: TemplateRef<any>;
// 4. Injected dependencies (private/protected)
private userService = inject(UserService);
protected dialogService = inject(DialogService);
// 5. Public properties
public formGroup: FormGroup;
// 6. Protected properties (template-accessible)
protected isLoading = signal(false);
protected items$ = this.itemService.items$;
// 7. Private properties
private cache = new Map();
// 8. Lifecycle hooks
ngOnInit() {}
// 9. Public methods
public save() {}
// 10. Protected methods (template-accessible)
protected handleClick() {}
// 11. Private methods
private processData() {}
}Dependency Injection
Modern inject() Function
Before:
constructor(
private userService: UserService,
private route: ActivatedRoute
) {}After:
private userService = inject(UserService);
private route = inject(ActivatedRoute);Reactivity Patterns
Signals for Component State (ADR-0027)
// Local state
protected selectedFolder = signal<Folder | null>(null);
protected isLoading = signal(false);
// Derived state
protected hasSelection = computed(() => this.selectedFolder() !== null);Prefer computed() Over effect()
Use computed() for derived values. Use effect() only for side effects (logging, analytics, DOM sync).
❌ Bad:
constructor() {
effect(() => {
const id = this.selectedId();
this.selectedItem.set(this.items().find(i => i.id === id) ?? null);
});
}✅ Good:
selectedItem = computed(() => this.items().find((i) => i.id === this.selectedId()) ?? null);Observables for Service Communication (ADR-0003)
// In component
protected folders$ = this.folderService.folders$;
// Template
// <div *ngFor="let folder of folders$ | async">
// For explicit subscriptions
constructor() {
this.userService.user$
.pipe(takeUntilDestroyed())
.subscribe(user => this.handleUser(user));
}Bridging Observables to Signals
Use toSignal() to convert service observables to signals in components. Keep service state as observables (ADR-0003).
Before:
private destroy$ = new Subject<void>();
users: User[] = [];
ngOnInit() {
this.userService.users$.pipe(takeUntil(this.destroy$))
.subscribe(users => this.users = users);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}After:
protected users = toSignal(this.userService.users$, { initialValue: [] });Template Syntax
New Control Flow
Before:
<div *ngIf="user$ | async as user; else loading">
<p *ngFor="let item of user.items">{{ item.name }}</p>
</div>
<ng-template #loading>Loading...</ng-template>After:
@if (user$ | async; as user) { @for (item of user.items; track item.id) {
<p>{{ item.name }}</p>
} } @else {
<p>Loading...</p>
}Prefer Class/Style Bindings Over ngClass/ngStyle
Use [class.*] and [style.*] bindings instead of ngClass/ngStyle.
❌ Bad:
<div [ngClass]="{ 'active': isActive(), 'disabled': isDisabled() }">
<div [ngStyle]="{ 'width.px': width(), 'height.px': height() }"></div>
</div>✅ Good:
<div [class.active]="isActive()" [class.disabled]="isDisabled()">
<div [style.width.px]="width()" [style.height.px]="height()"></div>
</div>Type Safety
No TypeScript Enums (ADR-0025)
Before:
enum CipherType {
Login = 1,
SecureNote = 2,
}After:
export const CipherType = Object.freeze({
Login: 1,
SecureNote: 2,
} as const);
export type CipherType = (typeof CipherType)[keyof typeof CipherType];Reactive Forms
protected formGroup = new FormGroup({
name: new FormControl('', { nonNullable: true }),
email: new FormControl<string>('', { validators: [Validators.email] }),
});Anti-Patterns to Avoid
- ❌ Manually refactoring when CLI migrations exist
- ❌ Manual subscriptions without
takeUntilDestroyed() - ❌ TypeScript enums (use const objects per ADR-0025)
- ❌ Mixing constructor injection with
inject() - ❌ Signals in services shared with non-Angular code (ADR-0003)
- ❌ Business logic in components
- ❌ Code regions
- ❌ Converting service observables to signals (ADR-0003)
- ❌ Using
effect()for derived state (usecomputed()) - ❌ Using
ngClass/ngStyle(use[class.*]/[style.*])