Convert an Angular Component to Standalone with @if and @for Control Flow
A task guide for converting an existing Angular component to standalone, adopting the built-in @if and @for control flow syntax, and verifying the result without behavioral changes.
01 May 2026, 13:53 UTC

The problem: legacy module wiring and structural directives
Many Angular projects still declare components inside an NgModule and rely on *ngIf and *ngFor. That pattern works, but it keeps the component coupled to a module file and uses the older microsyntax for control flow. The goal here is to convert one existing component into a standalone component that declares its own dependencies and uses the built-in @if and @for blocks, without changing what the user sees.
Desired outcome
After this migration, the component:
- Sets
standalone: truein its@Componentmetadata and lists its own dependencies in animportsarray. - Is no longer declared in any
NgModule. - Uses
@ifand@forinstead of*ngIfand*ngFor. - Passes the same unit tests and renders the same UI as before.
Prerequisites and version assumptions
This procedure assumes:
- Angular v17 or newer. The built-in control flow syntax was introduced around that version and requires a compatible compiler.
- The Angular CLI is installed and you can run commands from the project root.
- The component is currently declared in an
NgModuleand its template uses structural directives that can be safely rewritten. - You have existing unit tests that cover conditional rendering and list iteration.
If the project is on an older version, upgrade first. The compiler will reject @if and @for in templates if the Angular version is too old.
Focused procedure
Step 1: Mark the component as standalone
Consider a component named app-user-list that displays a list of users and a loading indicator. Open the component file and add standalone: true to the @Component decorator. Add an imports array listing every directive or pipe the template uses. The built-in control flow blocks do not require CommonModule, but other directives such as ngClass or pipes like date still must be imported explicitly.
import { DatePipe } from '@angular/common';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
standalone: true,
imports: [DatePipe],
})
export class UserListComponent {
// ... existing inputs and logic
}
If you are creating a new component instead of converting an old one, the CLI can scaffold it directly:
ng generate component user-list --standalone
Run this in the project root. No elevated permissions are required. The CLI creates the component with standalone: true already set.
Step 2: Remove the component from the NgModule
Open the NgModule that currently declares the component and delete the component class from the declarations array. Leaving it in place causes a duplicate declaration error at build time.
@NgModule({
declarations: [
// Remove UserListComponent from here
],
imports: [CommonModule],
})
export class AppModule {}
Step 3: Rewrite structural directives to control flow blocks
Replace *ngIf with @if and *ngFor with @for. The new syntax uses a block container rather than an attribute. For lists, provide a track expression so Angular can identify items efficiently.
Before:
<div *ngIf="isLoading">Loading...</div>
<ul *ngIf="!isLoading">
<li *ngFor="let user of users">
{{ user.name }} - {{ user.lastLogin | date }}
</li>
</ul>
After:
@if (isLoading) {
<div>Loading...</div>
} @else {
<ul>
@for (user of users; track user.id) {
<li>
{{ user.name }} - {{ user.lastLogin | date }}
</li>
} @empty {
<li>No users found.</li>
}
</ul>
}
Note that @empty replaces the common pattern of a separate *ngIf checking for an empty array. If the original template had a separate "no results" block outside the list, fold it into the @empty block or keep a separate @if as needed.
Step 4: Update imports if the template uses other directives
Scan the template for any remaining Angular directives or pipes. If the template uses ngClass, ngModel, or a custom pipe, add those to the imports array. Missing imports produce template parse errors, not runtime errors, so the compiler catches them early.
Expected checks
Run these commands from the project root to confirm the migration did not break the build or behavior.
Compilation check
ng build
Expected result: the build completes with no errors. If the component is still declared in an NgModule, the compiler reports a duplicate declaration. If a directive or pipe is missing from imports, the compiler reports a template parse error.
Unit test check
ng test
Expected result: all existing specs pass. Because the goal is a behavior-preserving refactor, tests that cover conditional rendering and list iteration should not need changes. If a test fails, compare the rendered DOM before and after the template rewrite.
Manual browser check
ng serve
Open the application in a browser and navigate to the view that uses the component. Confirm that:
- Conditional sections appear and disappear exactly as before.
- List items render in the same order with the same content.
- Empty states appear when the data array is empty.
Lint check for legacy directives
ng lint
If your ESLint configuration includes Angular template rules, the linter flags any remaining *ngIf or *ngFor in the migrated file. Not every project has these rules enabled, so also search the template manually for the asterisk prefix.
Recovery options and rollback
Because this operation modifies source files, use version control to capture the current state before starting. If the migration fails, you can recover without losing work.
If compilation fails
Revert the template to the legacy *ngIf and *ngFor directives and re-add the component to the NgModule declarations. Remove standalone: true and the imports array. This restores the previous working state immediately.
If runtime behavior differs
Isolate the change by converting one control flow block at a time. For example, rewrite only the *ngIf to @if, rebuild, and verify. Then rewrite the *ngFor to @for and verify again. This makes it easier to identify which block introduced the regression.
Version control rollback
If the component is under Git and you have committed the pre-migration state, discard the working changes:
git checkout -- path/to/user-list.component.ts path/to/user-list.component.html
Replace the placeholders with the actual file paths. This is the fastest rollback when the migration cannot be completed in one sitting.
Limitations and edge cases
- Third-party libraries: Some older libraries still export
NgModule-only APIs. You may need to keep a wrapper component or continue importing the library module into a standalone component'simportsarray. - Explicit imports remain mandatory: Standalone does not mean automatic. Every directive and pipe used in the template must appear in the
importsarray. - Compiler version gate: The
@ifand@forsyntax is not available in Angular versions before v17. Attempting to use it in an older project produces a compiler error. - Track expressions: The
@forblock requires atrackexpression. Using the item object itself is allowed, but using a stable identifier such asuser.idreduces unnecessary DOM re-creation.
Final confirmation
Once ng build and ng test succeed, open the running application and interact with the component directly. Toggle the loading flag and adjust the data array to confirm that @if and @for render the same DOM as the previous directive-based template. Matching output and green tests are the practical signal that the migration is safe to commit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.