mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
5.4 KiB
5.4 KiB
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.*])