mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 02:33:46 +00:00
254 lines
5.4 KiB
Markdown
254 lines
5.4 KiB
Markdown
# Angular Migration Patterns Reference
|
|
|
|
## Table of Contents
|
|
|
|
- [Component Architecture](#component-architecture)
|
|
- [Dependency Injection](#dependency-injection)
|
|
- [Reactivity Patterns](#reactivity-patterns)
|
|
- [Template Syntax](#template-syntax)
|
|
- [Type Safety](#type-safety)
|
|
|
|
## 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.
|
|
|
|
```typescript
|
|
@Component({
|
|
selector: "app-user-profile",
|
|
imports: [CommonModule, ReactiveFormsModule, AsyncPipe],
|
|
templateUrl: "./user-profile.component.html",
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class UserProfileComponent {}
|
|
```
|
|
|
|
### Class Member Organization
|
|
|
|
```typescript
|
|
@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:**
|
|
|
|
```typescript
|
|
constructor(
|
|
private userService: UserService,
|
|
private route: ActivatedRoute
|
|
) {}
|
|
```
|
|
|
|
**After:**
|
|
|
|
```typescript
|
|
private userService = inject(UserService);
|
|
private route = inject(ActivatedRoute);
|
|
```
|
|
|
|
## Reactivity Patterns
|
|
|
|
### Signals for Component State (ADR-0027)
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
constructor() {
|
|
effect(() => {
|
|
const id = this.selectedId();
|
|
this.selectedItem.set(this.items().find(i => i.id === id) ?? null);
|
|
});
|
|
}
|
|
```
|
|
|
|
**✅ Good:**
|
|
|
|
```typescript
|
|
selectedItem = computed(() => this.items().find((i) => i.id === this.selectedId()) ?? null);
|
|
```
|
|
|
|
### Observables for Service Communication (ADR-0003)
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
protected users = toSignal(this.userService.users$, { initialValue: [] });
|
|
```
|
|
|
|
## Template Syntax
|
|
|
|
### New Control Flow
|
|
|
|
**Before:**
|
|
|
|
```html
|
|
<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:**
|
|
|
|
```html
|
|
@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:**
|
|
|
|
```html
|
|
<div [ngClass]="{ 'active': isActive(), 'disabled': isDisabled() }">
|
|
<div [ngStyle]="{ 'width.px': width(), 'height.px': height() }"></div>
|
|
</div>
|
|
```
|
|
|
|
**✅ Good:**
|
|
|
|
```html
|
|
<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:**
|
|
|
|
```typescript
|
|
enum CipherType {
|
|
Login = 1,
|
|
SecureNote = 2,
|
|
}
|
|
```
|
|
|
|
**After:**
|
|
|
|
```typescript
|
|
export const CipherType = Object.freeze({
|
|
Login: 1,
|
|
SecureNote: 2,
|
|
} as const);
|
|
export type CipherType = (typeof CipherType)[keyof typeof CipherType];
|
|
```
|
|
|
|
### Reactive Forms
|
|
|
|
```typescript
|
|
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 (use `computed()`)
|
|
- ❌ Using `ngClass`/`ngStyle` (use `[class.*]`/`[style.*]`)
|