1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00
Files
browser/.claude/skills/angular-modernization/migration-patterns.md
Oscar Hinton c036ffd775 [PM-29235] Make a claude skill that modernizes angular code (#17445)
Experiment at creating a claude skill for modernizing Angular code.
2025-12-05 13:36:52 +01:00

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 (use computed())
  • Using ngClass/ngStyle (use [class.*]/[style.*])