Search code examples
htmlangularcontrol-flowangular18angular-migration

Control flow migration Error on Angular 18: @switch can only have @case or @default as childre


I tried to migrate to control flow format in my Angular 18 projects in nx workspace. For most of the templates worked perfectly but for one html I'm getting the following error:

 [ngSwitch]: Error: Text node: "
        ◬" would result in invalid migrated @switch block structure. @switch can only have @case or @default as children.

What I did:

  1. run npx nx generate @angular/core:control-flow-migration
  2. Specify my path of component

Here is my full html:

<ng-container *ngIf="!!type; else DeprecatedStructure">
  <ng-container [ngSwitch]="type">
    <ng-container *ngFor="let type of buttonTypes">
      <button
        *ngSwitchCase="type"
        [style.display]="'flex'"
        [style.flexDirection]="iconPosition === 'right' ? 'row' : 'row-reverse'"
        [ngClass]="buttonClasses[type]"
        [disabled]="disabled"
        [type]="htmlType"
        [id]="id"
        [attr.data-test-id]="dataTestId"
        (click)="handleClick($event)">
        <ng-container [ngSwitch]="type">
          <ng-container *ngSwitchDefault>
            <span><ng-template [ngTemplateOutlet]="ButtonContentRef"></ng-template></span>
            <mat-icon
              *ngIf="!!icon"
              class="fl-icon"
              [ngClass]="{
                right: iconPosition === 'right',
                left: iconPosition === 'left'
              }">
              {{ icon }}
            </mat-icon>

            <mat-icon
              *ngIf="!!svgIcon && !icon"
              class="fl-icon"
              [svgIcon]="svgIcon"
              [ngClass]="{
                right: iconPosition === 'right',
                left: iconPosition === 'left'
              }">
            </mat-icon>

            <span *ngIf="loading" class="loading loading-sm"></span>
          </ng-container>

          <ng-container *ngSwitchCase="icon">
            <mat-icon class="fl-icon">{{ icon }}</mat-icon>
            <span *ngIf="loading" class="loading loading-sm"></span>
          </ng-container>
        </ng-container>
      </button>
    </ng-container>
  </ng-container>
</ng-container>

<ng-template #DeprecatedStructure>
  <button
    [class.btn]="!onlyIcon"
    [class.btn-primary]="!onlyIcon && !textButton && !linkButton && !cardButton"
    [class.btn-secondary]="secondary && !textButton && !linkButton && !cardButton"
    [class.btn-destructive]="destructive && !textButton && !linkButton && !cardButton"
    [class.btn-sm]="size === 'small'"
    [class.btn-lg]="size === 'large'"
    [class.btn-floating-primary]="onlyIcon"
    [class.btn-floating-secondary]="onlyIcon && !!secondary"
    [class.btn-floating-destructive]="onlyIcon && !!destructive"
    [class.btn-flat-primary]="textButton"
    [class.btn-flat-secondary]="textButton && secondary"
    [class.btn-flat-destructive]="textButton && destructive"
    [class.btn-plain-primary]="linkButton"
    [class.btn-plain-secondary]="linkButton && secondary"
    [class.btn-plain-destructive]="linkButton && destructive"
    [class.btn-card]="cardButton"
    [class.btn-loading]="loading"
    [ngClass]="className"
    [disabled]="!!disabled"
    [type]="htmlType"
    [id]="id"
    [attr.data-test-id]="dataTestId"
    (click)="handleClick($event)">
    <mat-icon *ngIf="icon && iconPosition !== 'right'" class="fl-icon" [ngClass]="iconClassName">{{ icon }}</mat-icon>
    <span *ngIf="!onlyIcon"><ng-template [ngTemplateOutlet]="ButtonContentRef"></ng-template></span>
    <mat-icon *ngIf="icon && iconPosition === 'right'" class="fl-icon right" [ngClass]="iconClassName">{{
      icon
    }}</mat-icon>
    <span *ngIf="loading" class="loading loading-sm"></span>
  </button>
</ng-template>

<ng-template #ButtonContentRef>
  <ng-content></ng-content>
</ng-template>

Solution

  • Try manually migrating the code, it's a script, it can't handle all edge case scenarios. We can migrate it, it would look something like below.

    default should always be at the bottom of the switch statement, I think it might be a bug. Please note this change I made which deviates from your code, I promoted the case statement to the top in the inner switch statement.

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      template: `
        @if(!!type; else DeprecatedStructure) {
          @switch(type) {
            @for(type of buttonTypes; track $index) {
              @case (type) {
                <button
                  [style.display]="'flex'"
                  [style.flexDirection]="iconPosition === 'right' ? 'row' : 'row-reverse'"
                  [ngClass]="buttonClasses[type]"
                  [disabled]="disabled"
                  [type]="htmlType"
                  [id]="id"
                  [attr.data-test-id]="dataTestId"
                  (click)="handleClick($event)">
                    @switch(type) {
                      @case (icon) {
                        <mat-icon class="fl-icon">{{ icon }}</mat-icon>
                        @if(loading) {
                          <span class="loading loading-sm"></span>
                        }
                      }
                      @default {
                        <span><ng-template [ngTemplateOutlet]="ButtonContentRef"></ng-template></span>
                        @if(icon) {
                          <mat-icon
                            class="fl-icon"
                            [ngClass]="{
                              right: iconPosition === 'right',
                              left: iconPosition === 'left'
                            }">
                            {{ icon }}
                          </mat-icon>
                        }
                        @if(svgIcon && !icon) {
                          <mat-icon
                            class="fl-icon"
                            [svgIcon]="svgIcon"
                            [ngClass]="{
                              right: iconPosition === 'right',
                              left: iconPosition === 'left'
                            }">
                          </mat-icon>
                        }
                        @if(loading) {
                          <span class="loading loading-sm"></span>
                        }
                      }
                    }
                </button>
              }
            }
          }
        }
        <ng-template #DeprecatedStructure>
          <button
            [class.btn]="!onlyIcon"
            [class.btn-primary]="!onlyIcon && !textButton && !linkButton && !cardButton"
            [class.btn-secondary]="secondary && !textButton && !linkButton && !cardButton"
            [class.btn-destructive]="destructive && !textButton && !linkButton && !cardButton"
            [class.btn-sm]="size === 'small'"
            [class.btn-lg]="size === 'large'"
            [class.btn-floating-primary]="onlyIcon"
            [class.btn-floating-secondary]="onlyIcon && !!secondary"
            [class.btn-floating-destructive]="onlyIcon && !!destructive"
            [class.btn-flat-primary]="textButton"
            [class.btn-flat-secondary]="textButton && secondary"
            [class.btn-flat-destructive]="textButton && destructive"
            [class.btn-plain-primary]="linkButton"
            [class.btn-plain-secondary]="linkButton && secondary"
            [class.btn-plain-destructive]="linkButton && destructive"
            [class.btn-card]="cardButton"
            [class.btn-loading]="loading"
            [ngClass]="className"
            [disabled]="!!disabled"
            [type]="htmlType"
            [id]="id"
            [attr.data-test-id]="dataTestId"
            (click)="handleClick($event)">
            @if(icon && iconPosition !== 'right') {
              <mat-icon class="fl-icon" [ngClass]="iconClassName">{{ icon }}</mat-icon>
            }
            @if(!onlyIcon) {
              <span><ng-template [ngTemplateOutlet]="ButtonContentRef"></ng-template></span>
            }
            @if(icon && iconPosition === 'right') {
              <mat-icon class="fl-icon right" [ngClass]="iconClassName">{{
                icon
              }}</mat-icon>
            }
            @if(loading) {
              <span class="loading loading-sm"></span>
            }
          </button>
        </ng-template>
    
        <ng-template #ButtonContentRef>
          <ng-content></ng-content>
        </ng-template>
      `,
    })
    export class App {
      name = 'Angular';
    }
    
    bootstrapApplication(App);