Angular 22+ Migration: $ARGUMENTS
If $ARGUMENTS is empty, ask the user for a component path or folder before proceeding.
Phase 1: Analyze
Read all files in the target path. For each component, directive, or service, scan for migration items below. If a folder is given, process all .ts and .html files within it.
What to migrate
Inputs / Outputs / Models:
@Input()→input()orinput.required()@Input()with setter →input()+linkedSignal()(preferred) oreffect()(only for DOM side effects)@Input()+@Output()pair (e.g.value/valueChange) →model()@Output()→output()- Boolean inputs: add
{ transform: booleanAttribute } - Number inputs: add
{ transform: numberAttribute }
Host bindings:
@HostBinding('class.x')→host: { '[class.x]': 'expr()' }@HostBinding('attr.x')→host: { '[attr.x]': 'expr()' }@HostBinding('style.x')→host: { '[style.x]': 'expr()' }@HostListener('event')→host: { '(event)': 'handler($event)' }
Queries:
@ViewChild()→viewChild()orviewChild.required()@ViewChildren()→viewChildren()@ContentChild()→contentChild()@ContentChildren()→contentChildren()
Template syntax:
*ngIf→@if*ngFor→@for(requirestrackexpression)*ngSwitch/*ngSwitchCase→@switch/@case
State management:
BehaviorSubjectused for local component state →signal()BehaviorSubject+combineLatest→computed()Subject<void>used only to trigger side effects →effect()effect()that only callssignal.set()for derived state →computed()orlinkedSignal- Redundant
markForCheck()after signal updates → remove - Object/array mutation in place +
signal.set()with same ref → immutable update withsignal.update()
CSS class building:
CssClassBuilder+@applyCssClass→computed()returning class string +host: { '[class]': '_cssClass()' }
Cleanup patterns:
- Custom
DestroyedService→DestroyRef+takeUntilDestroyed() ngOnDestroyonly fordestroy$.next()→ remove entirely after migration
Decorator cleanup:
- Remove
standalone: true(default since Angular 19) - Remove
allowSignalWritesoption ineffect()(the option no longer exists)
Imports:
- Deprecated
*Moduleclasses → individual component/directive imports
Member ordering (ESLint enforced):
- Decorated properties (any remaining
@Input,@Output,@ViewChild) - Signal inputs/outputs (
input(),output(),model()) - Public → Protected → Private fields
- Constructor
- Public → Protected → Private methods
What NOT to migrate
Do NOT convert these — they should stay as-is:
- RxJS for async operations: HTTP calls, WebSocket streams, timers, interval-based logic → keep as Observable
- BehaviorSubject with complex operators:
switchMap,debounceTime,distinctUntilChanged,combineLatestwith async sources → keep as RxJS - Plain properties with no reactive consumer: If nothing in template, host binding, computed, or effect reads it → keep as plain property, do not wrap in
signal() - Signals for internal bookkeeping: One-time flags, cached DOM measurements, counters with no reactive consumer → use plain property
Migration is optional for existing code
The migration targets are recommendations, not mandatory fixes. Present all findings but let the user decide scope. Existing @Input() / @Output() decorators are valid — only migrate them if the user wants to or if the code is being substantially reworked.
Phase 2: Present Plan
Output a structured migration plan:
## Migration Plan: [component name]
### Files affected
- path/to/component.ts
- path/to/component.html
- path/to/component.spec.ts
### Inputs / Outputs
| Current | Migration | Notes |
|---------|-----------|-------|
| `@Input() label: string` | `readonly label = input<string>('')` | |
| `@Input() disabled: boolean` | `readonly disabled = input(false, { transform: booleanAttribute })` | |
| `@Input() value` + `@Output() valueChange` | `readonly value = model<T>()` | Two-way binding |
### Host Bindings
| Current | Migration |
|---------|-----------|
| `@HostBinding('class.is-open')` | `host: { '[class.is-open]': 'isOpen()' }` |
### Template Changes
| Current | Migration |
|---------|-----------|
| `*ngIf="condition"` | `@if (condition) { ... }` |
| `*ngFor="let item of items"` | `@for (item of items; track item.id) { ... }` |
### State Management
| Current | Migration | Reason |
|---------|-----------|--------|
| `BehaviorSubject<boolean>` | `signal(false)` | Local state, no async consumers |
### Skipped (intentionally not migrated)
| Item | Reason |
|------|--------|
| `httpData$` Observable | Async HTTP stream |
### Test Updates
- [ ] Replace `component.input = value` with `fixture.componentRef.setInput('input', value)`
- [ ] Update module imports to individual components
- [ ] Verify existing tests still pass after migration
### Docs Updates
- [ ] Update examples if public API changed (input names, usage patterns)
- [ ] Ensure examples use individual imports, not deprecated modules
### Breaking Changes
- [ ] List any renamed/removed inputs or outputs
- [ ] Draft BREAKING CHANGE footer if needed
Stop here and wait for approval before proceeding.
Phase 3: Execute
After approval:
- Apply all migrations from the approved plan
- Format code:
yarn format - Run build:
nx run <library>:build - Run lint:
nx run <library>:lint - Run tests:
nx run <library>:test --testfile=<spec-file> --skip-nx-cache - Report results and any issues