Enhance Your Project with Angular 19 Download a free ebook!
21 Jan 2025
10 min

Angular Generators

The Angular team constantly introduces new features and tools to make development easier. One of these helpful tools is 'generators.’ These generators automatically update your code to the latest best practices, saving you time and effort.

This guide will show you how to use these generators effectively. We’ll cover each generator, including:

  • The command to run it: A simple instruction you’ll type into your terminal. 
  • The Angular version it works with: Ensuring compatibility with your project.
  • A breakdown of the command prompts: Understanding what each question means.
  • Code examples: Illustrating how the generator changes your code.

Let’s get started!

Control Flow

If your Angular project still uses the older structural directives like *ngIf, *ngFor, and ngSwitch, you can modernize your code by using their newer, more efficient counterparts: @if, @for, and @switch

Command:

ng g @angular/core:control-flow

Available from: Angular 17

When we execute this command it comes with the following prompts:

    • Which path in your project should be migrated?
      The default value is ./ which means it will migrate your entire application. We can also provide a relative path if we want to migrate a subset of our application.
  • Should the migration reformat your templates?

    If you choose 'Y’ (yes), the generator will migrate your code and automatically format (re-indent) your HTML templates for better readability. Selecting 'n’ (no) will migrate your code without any formatting.

While it’s generally recommended to format your code for better maintainability, the choice is yours.

Format Template source code: https://github.com/angular/angular/blob/main/packages/core/schematics/ng-generate/control-flow-migration/migration.ts#L69


https://github.com/angular/angular/blob/main/packages/core/schematics/ng-generate/control-flow-migration/util.ts#L730

Code Examples

If statement: Before 

<div *ngIf="isVisible; else elseBlock">
  Displayed when isVisible is true
</div>
<ng-template #elseBlock>
  <div>
    Displayed when isVisible is false
  </div>
</ng-template>

If statement: After

@if (isVisible) {
  <div>Displayed when isVisible is true</div>
} @else {
  <div>Displayed when isVisible is false</div>
}

———————–

Switch Statement: Before

<div [ngSwitch]="color">
  <div *ngSwitchCase="'red'">Red color selected</div>
  <div *ngSwitchCase="'blue'">Blue color selected</div>
  <div *ngSwitchCase="'green'">Green color selected</div>
  <div *ngSwitchDefault>Other color selected</div>
</div>

Switch Statement: After

@switch (color) {
    @case ('red') {
      <div>Red color selected</div>
    }
    @case ('blue') {
      <div>Blue color selected</div>
    }
    @case ('green') {
      <div>Green color selected</div>
    }
    @default {
      <div>Other color selected</div>
    }
  }

———————–

For Block: Before

<ng-container *ngIf="items.length; else emptyList">
  <div *ngFor="let item of items">
    {{ item }}
  </div>
</ng-container>

<ng-template #emptyList>
  The list is empty
</ng-template>

For Block: After

@if (items.length) {
  @for (item of items; track item) {
    <div>
      {{ item }}
    </div>
  }
} @else {
  The list is empty
}

We can always do some more tweaks to have a better code. We can change the migrated code to use the @empty block

For Block: Improved

@for (item of items; track item) {
  <div>
    {{ item }}
  </div>
} @empty {
  The list is empty
}

Inject Function

The inject function compared to the constructor-based injections has several advantages.

Some of them are:

  1. Better type safety:
    The inject function infers the type of the injected dependency more effectively
  2. Reusability:
    With the inject function we can create re-usable utility functions that they depend on injected dependencies
  3. Improved Inheritance:
    Using the injection function in a class inheritance, we do not have to pass the injected dependencies through the parent’s constructor.
  4. Modern and future proof:
    Using the inject function aligns your code with the latest best practices.

Command:

ng g @angular/core:inject

Available from: Angular 18

When we execute this command it comes with the following prompts:

    •  Which path in your project should be migrated?
      The default value is ./ which means it will migrate your entire application. We can also provide a relative path if we want to migrate a subset of our application.
  • Do you want to migrate abstract classes? Abstract classes are not migrated by default, because their parameters aren’t guaranteed to be injectable
    By default, abstract classes are not migrated because Angular cannot determine if their constructor arguments can be properly injected. Enabling this option for abstract classes may introduce compilation errors in some cases.

To understand this better, let’s see the following example:

base-http.service.ts

export abstract class BaseHttpService {
  abstract endpoint: string;

  abstract baseUrl: string;
  constructor(private http: HttpClient) {}

  private getFullUrl(): string {
    return `${this.baseUrl}/${this.endpoint}`;
  }

  get<T>(params?: HttpParams, headers?: HttpHeaders): Observable<T> {
    return this.http.get<T>(this.getFullUrl(), { params, headers });
  }
}

products.service.ts

export class ProductsService extends BaseHttpService {
  endpoint: string = 'products';
  baseUrl: string = 'https://api.com';

  constructor(http: HttpClient) {
    super(http);
  }
}

The BaseHttpService injects the HttpClient using constructor-based injection. Since ProductsService extends BaseHttpService, it must provide the HttpClient to the superclass through its constructor.

Angular generators analyze files individually and are not aware of the relationships between classes. Therefore, when the generator processes BaseHttpService, it will:

  • Detect the HttpClient injection in the constructor.
  • Migrate the HttpClient injection to use the inject(HttpClient) function.
  • Remove the constructor parameters since it now uses inject.

This operation can lead to compilation errors in cases like ProductsService, where the subclass relies on providing the HttpClient to the superclass.

Enabling this option for abstract classes might introduce compilation errors. However, these errors can help identify and address potential issues.

Do you want to clean up all constructors or keep them backwards compatible? Enabling this option will include an additional signature of `constructor(…args: unknown[]);` that will avoid errors for sub-classes, but will increase the amount of generated code by the migration
By default, this generator removes the constructor arguments, and if the constructor has no arguments, it will also remove the constructor completely. If we enable this flag, Angular will introduce a constructor(…args: unknown[]) for backward compatibility. Enabling this option solves the issues we might have with the abstract classes.

Let’s see what the BaseHttpService looks like if we enable this option:

export abstract class BaseHttpService {
  private http = inject(HttpClient);

  abstract endpoint: string;

  abstract baseUrl: string;

  /** Inserted by Angular inject() migration for backwards compatibility */
  constructor(...args: unknown[]);
  constructor() {}

  private getFullUrl(): string {
    return `${this.baseUrl}/${this.endpoint}`;
  }

  get<T>(params?: HttpParams, headers?: HttpHeaders): Observable<T> {
    return this.http.get<T>(this.getFullUrl(), { params, headers });
  }
}

As you see, the generator added two constructors for backward compatibility. 

This solves the compilation errors but introduces additional code.

Do you want optional inject calls to be non-nullable? Enable this option if you want the return type to be identical to `@Optional()`, at the expense of worse type safety

Angular returns null if the injection of the @Optional parameter fails. However, since the decorators cannot influence their type, there are many cases where the types in our application are wrong. 

export class AppComponent {
  constructor(@Inject(MY_TOKEN) @Optional() private readonly token: MyToken) {}

  private tokenHandler() {
    console.log(this.token.claims);
  }
}

The tokenHandler method logs the claims of the token. However, since MY_TOKEN is optional, the this.token property can be null. This code will result in a runtime error if this.token is null when attempting to access its claims.

To prevent this runtime error, the correct approach is to define the token property as token: MyToken | null. This will trigger a compilation error if you attempt to access the claims of a null token, forcing you to explicitly check if the token is truthy before accessing its properties.

private tokenHandler() {
    if (this.token) {
      console.log(this.token.claims);
    }
  }

The angular team is aware of these developer mistakes and coded the generators accordingly.

If we answer yes to this prompt, the code will become:

private readonly token = inject<MyToken>(MY_TOKEN, { optional: true })!;

Note the exclamation mark at the end. This makes the type non-nullable and won’t introduce any compilation error.

Lazy Loading Routes

The lazy loading routes allow the build process to split the application into smaller chunks which makes the initial page load faster.

Command:

ng g @angular/core:route-lazy-loading

Available from: Angular 18

When we execute this command it comes with the following prompts:

  •  Which path in your project should be migrated?

    The default value is ./ which means it will migrate your entire application. We can also provide a relative path if we want to migrate a subset of our application.

Code Examples

provideRouter: Before

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        component: ProductsComponent,
      },
    ]),
  ],
};

provideRouter: After

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        loadComponent: () =>
          import('./pages/products/products.component').then(
            (m) => m.ProductsComponent,
          ),
      },
    ]),
  ],
};

———————–

It also handles the components that are exposed as default.

provideRouter: Before

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        component: ProductsComponent,
      },
    ]),
  ],
};

provideRouter: After

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        loadComponent: () => import('./pages/products/products.component'),
      },
    ]),
  ],
};

The schematic will also migrate the RouterModule configuration. The migrated code will be based on RouterModule but will migrate the eagerly loaded routes into lazy loading routes. It is highly encouraged to migrate to a standalone application and run this migration again. 

Signal Inputs

This schematic migrates the Input decorators to signal inputs.

Command:

ng g @angular/core:signal-input-migration

Available from: Angular 19

When we execute this command it comes with the following prompts:

  •  Which path in your project should be migrated?
    The default value is ./ which means it will migrate your entire application. We can also provide a relative path if we want to migrate a subset of our application.
  • Do you want to migrate as much as possible, even if it may break your build?
    Component inputs are a crucial part of the component API and should be treated as a single source of truth.

    • Traditional Inputs: Previously, inputs defined with the @Input() decorator could be modified within the component, potentially leading to unexpected behavior.
    • Signal Inputs: The new Signal Input API enforces a best practice: input values should be treated as immutable within the component.

If you choose 'yes’ for this prompt, all inputs in your project will be migrated to the Signal Input API, regardless of how they are currently used. This may require adjustments in your component logic if you were previously modifying input values within the component.

Code Examples

Let’s use this component as an example and see how the code will be migrated if we answer ‘n’ (No) to the second prompt.

Before

@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user | json }}</p>
    <p>{{ isEnabled }}</p>
    <p>{{ isEdit }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  @Input() user: User | undefined = undefined;
  @Input({
    required: true,
    transform: booleanAttribute,
  })
  isEnabled: boolean = false;
  @Input() isEdit: boolean = false;

  methodThatEditsTheInput() {
    this.isEdit = false;
  }
}

We expect the migrated code to:

  • have the correct default value on the user input
  • have the isEnabled as required and also has the boolean transformation
  • not migrate the isEdit since its value is getting updated from a method
  • We also expect the HTML template to have the right usage

After

@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user() | json }}</p>
    <p>{{ isEnabled() }}</p>
    <p>{{ isEdit }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  readonly user = input<User>();
  readonly isEnabled = input.required<boolean, unknown>({ transform: booleanAttribute });
  @Input() isEdit: boolean = false;

  methodThatEditsTheInput() {
    this.isEdit = false;
  }
}

The signal always has value, and the signal inputs have the value undefined by default. So, we do not have to explicitly define the undefined value on the user input.

Let’s also address the two generic types of the isEnabled input. The first one (`boolean`) is the actual type of the input. As such, the type of the isEnabled is boolean.

The second generic type is the type of the provided value. Since this is a boolean type, the value could be a string of “true” or “false”. So, it would be safe also to replace `unknown` with `string`. However, angular at the time of migration safely uses the `unknown` since the type is not known and eventually will be narrowed down through type assertions or type guards.

Let’s now see how the code will become if we answer “Y” (Yes) to the prompt.

@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user() | json }}</p>
    <p>{{ isEnabled() }}</p>
    <p>{{ isEdit() }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  readonly user = input<User>();
  readonly isEnabled = input.required<boolean, unknown>({ transform: booleanAttribute });
  readonly isEdit = input<boolean>(false);

  methodThatEditsTheInput() {
    this.isEdit = false; 
  }
}

The isEdit input got migrated which led to a compilation error in the methodThatEditsTheInput method.

A best combination is to answer “n” (No) and provide the option –insert-todos

–insert-todos

The –insert-todos option will add TODO comments in our code for the occurrences that failed to migrate

ng g @angular/core:signal-input-migration --insert-todos
@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user() | json }}</p>
    <p>isEnabled: {{ isEnabled() }}</p>
    <p>{{ isEdit }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  readonly user = input<User>();

  readonly isEnabled = input.required<boolean, unknown>({
    transform: booleanAttribute,
  });

  // TODO: Skipped for migration because:
  //  Your application code writes to the input. This prevents migration.
  @Input() isEdit: boolean = false;

  methodThatEditsTheInput() {
    this.isEdit = false;
  }
}

Outputs

This migration replaces the Output decorators with the output function.

Command:

ng g @angular/core:output-migration

Available from: Angular 19

When we execute this command it comes with the following prompts:

  •  Which path in your project should be migrated?
    The default value is ./ which means it will migrate your entire application. We can also provide a relative path if we want to migrate a subset of our application.

Code Examples

Before

export class UserCardComponent {
  @Output('userChanged') userChange = new EventEmitter();

  methodThatDoesSomething() {
    this.userChange.emit();
  }
}

After

export class UserCardComponent {
  readonly userChange = output({ alias: 'userChanged' });

  methodThatDoesSomething() {
    this.userChange.emit();
  }
}

Signal Queries

This migration converts the @ViewChild @ViewChildren @ContentChild and @ContentChildren decorators to viewChild, viewChildren, contentChild, and contentChildren signal queries respectively.

Command:

ng g @angular/core:signal-queries-migration

Available from: Angular 19

When we execute this command it comes with the following prompts:

  •  Which path in your project should be migrated?
    The default value is ./ which means it will migrate your entire application. We can also provide a relative path if we want to migrate a subset of our application.
  • Do you want to migrate as much as possible, even if it may break your build?
    Since this schematic tries not to introduce compilation errors while migrating our code, if we answer “n” (No) to this prompt it won’t migrate occurrences that may fail or produce compilation errors. Examples are:

    • If we use the expression in a control flow (*ngIf, @if) angular will fail narrowing the type
      Example
@Component({
  selector: 'app-user-card',
  imports: [NgIf, NgTemplateOutlet],
  template: `
    <ng-container *ngIf="cardContentTemplate">
      <ng-templateOutlet [ngTemplateOutlet]="cardContentTemplate" />
    </ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  @ContentChild('cardContent', { read: TemplateRef }) cardContentTemplate:
    | TemplateRef<any>
    | undefined;
}
  • If we use the query in combination with @HostBinding, the migration will fail.
    Example
 @HostBinding('class.my-custom-class')
  @ContentChild('cardContent', { read: TemplateRef })
  cardContentTemplate: TemplateRef<any> | undefined;
  • If we use a setter to handle the query value
    Example
@ContentChild('cardContent', { read: TemplateRef })
set cardContentTemplateAsSet(value: TemplateRef<any> | undefined) {
  console.log('cardContentTemplateAsSet', value);
}

If we answer yes, it will try to migrate but please note that we should manually check the application works as intended since it might have some errors.
If we want to be on the safe side, we can answer “n” (No) to the prompt and provide the option –insert-todos.

ng g @angular/core:signal-queries-migration --insert-todos

Having this option, the migrator will add TODO comments to the occurrences that failed to migrate.

Let’s see some code examples of the migrated code

Code Examples
viewChild & viewChildren Before

@ViewChild(UserCardComponent)
  userCard!: UserCardComponent;

  @ViewChildren(UserCardComponent)
  userCards!: QueryList<UserCardComponent>;

viewChild & viewChildren After

readonly userCard = viewChild.required(UserCardComponent);
readonly userCards = viewChildren(UserCardComponent);

contentChild Before

 @HostBinding('class.my-custom-class')
 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateWithHostBinding: TemplateRef<any> | undefined;

 @ContentChild('cardContent', { read: TemplateRef })
 set cardContentTemplateAsSet(value: TemplateRef<any> | undefined) {
   console.log('cardContentTemplateAsSet', value);
 }

 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateUsedWithIfCondition: TemplateRef<any> | undefined;

 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplate!: TemplateRef<any>;

contentChild After

 // TODO: Skipped for migration because:
 //  This query is used in combination with `@HostBinding` and migrating would break.
 @HostBinding('class.my-custom-class')
 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateWithHostBinding: TemplateRef<any> | undefined;

 // TODO: Skipped for migration because:
 //  Accessor queries cannot be migrated as they are too complex.
 @ContentChild('cardContent', { read: TemplateRef })
 set cardContentTemplateAsSet(value: TemplateRef<any> | undefined) {
   console.log('cardContentTemplateAsSet', value);
 }

 // TODO: Skipped for migration because:
 //  This query is used in a control flow expression (e.g. `@if` or `*ngIf`)
 //  and migrating would break narrowing currently.
 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateUsedWithIfCondition: TemplateRef<any> | undefined;

 readonly cardContentTemplate = contentChild.required('cardContent', { read: TemplateRef });

The Angular team is doing a fantastic job! They’re constantly adding new features and making it easier for us to keep our projects up-to-date. To learn more about the migration tools available, visit this page: https://next.angular.dev/reference/migrations. This page will show you all the existing migrations and any new ones that are added in the future.

Thanks for reading!!

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.