diff --git a/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.html b/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.html new file mode 100644 index 0000000..344450d --- /dev/null +++ b/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.html @@ -0,0 +1,28 @@ + + + Blank + + + + + + + Blank + + + +
+ Ready to create an app? +

+ Start with Ionic + UI Components +

+
+ + Click me +
diff --git a/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.scss b/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.ts b/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.ts new file mode 100644 index 0000000..48376cb --- /dev/null +++ b/apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; + +@Component({ + selector: 'app-router-link', + templateUrl: 'router-link.page.html', + styleUrls: ['router-link.page.scss'], + standalone: true, + imports: [IonicModule], +}) +export class RouterLinkPage { + constructor() { } +} diff --git a/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.test.ts b/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.test.ts index 987bf09..b7c450d 100644 --- a/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.test.ts +++ b/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.test.ts @@ -166,6 +166,233 @@ describe("migrateComponents", () => { `), ); }); + + describe("hyperlinks", () => { + it("should detect and import routerLink used in the template", async () => { + const project = new Project({ useInMemoryFileSystem: true }); + const component = ` + import { Component } from "@angular/core"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true + }) + export class MyComponent { } + `; + + const componentSourceFile = project.createSourceFile( + "foo.component.ts", + dedent(component), + ); + + await migrateComponents(project, { dryRun: false }); + + expect(dedent(componentSourceFile.getText())).toBe( + dedent(` + import { Component } from "@angular/core"; + import { IonRouterLinkWithHref } from "@ionic/angular/standalone"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonRouterLinkWithHref] + }) + export class MyComponent { } + `) + ); + }); + + it("should detect and import routerAction used in the template", async () => { + const project = new Project({ useInMemoryFileSystem: true }); + const component = ` + import { Component } from "@angular/core"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true + }) + export class MyComponent { } + `; + + const componentSourceFile = project.createSourceFile( + "foo.component.ts", + dedent(component), + ); + + await migrateComponents(project, { dryRun: false }); + + expect(dedent(componentSourceFile.getText())).toBe( + dedent(` + import { Component } from "@angular/core"; + import { IonRouterLinkWithHref } from "@ionic/angular/standalone"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonRouterLinkWithHref] + }) + export class MyComponent { } + `) + ); + }); + + it("should detect and import routerDirection used in the template", async () => { + const project = new Project({ useInMemoryFileSystem: true }); + const component = ` + import { Component } from "@angular/core"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true + }) + export class MyComponent { } + `; + + const componentSourceFile = project.createSourceFile( + "foo.component.ts", + dedent(component), + ); + + await migrateComponents(project, { dryRun: false }); + + expect(dedent(componentSourceFile.getText())).toBe( + dedent(` + import { Component } from "@angular/core"; + import { IonRouterLinkWithHref } from "@ionic/angular/standalone"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonRouterLinkWithHref] + }) + export class MyComponent { } + `) + ); + }); + }); + + describe("Ionic components", () => { + it("should detect and import routerLink used in the template", async () => { + const project = new Project({ useInMemoryFileSystem: true }); + const component = ` + import { Component } from "@angular/core"; + import { IonicModule } from "@ionic/angular"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonicModule] + }) + export class MyComponent { } + `; + + const componentSourceFile = project.createSourceFile( + "foo.component.ts", + dedent(component), + ); + + await migrateComponents(project, { dryRun: false }); + + expect(dedent(componentSourceFile.getText())).toBe( + dedent(` + import { Component } from "@angular/core"; + import { IonRouterLink, IonButton } from "@ionic/angular/standalone"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonRouterLink, IonButton] + }) + export class MyComponent { } + `) + ); + }); + + it("should detect and import routerAction used in the template", async () => { + const project = new Project({ useInMemoryFileSystem: true }); + const component = ` + import { Component } from "@angular/core"; + import { IonicModule } from "@ionic/angular"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonicModule] + }) + export class MyComponent { } + `; + + const componentSourceFile = project.createSourceFile( + "foo.component.ts", + dedent(component), + ); + + await migrateComponents(project, { dryRun: false }); + + expect(dedent(componentSourceFile.getText())).toBe( + dedent(` + import { Component } from "@angular/core"; + import { IonRouterLink, IonButton } from "@ionic/angular/standalone"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonRouterLink, IonButton] + }) + export class MyComponent { } + `) + ); + }); + + it("should detect and import routerDirection used in the template", async () => { + const project = new Project({ useInMemoryFileSystem: true }); + const component = ` + import { Component } from "@angular/core"; + import { IonicModule } from "@ionic/angular"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonicModule] + }) + export class MyComponent { } + `; + + const componentSourceFile = project.createSourceFile( + "foo.component.ts", + dedent(component), + ); + + await migrateComponents(project, { dryRun: false }); + + expect(dedent(componentSourceFile.getText())).toBe( + dedent(` + import { Component } from "@angular/core"; + import { IonRouterLink, IonButton } from "@ionic/angular/standalone"; + + @Component({ + selector: 'my-component', + template: 'Home', + standalone: true, + imports: [IonRouterLink, IonButton] + }) + export class MyComponent { } + `) + ); + }); + }) + }); describe("single component angular modules", () => { diff --git a/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.ts b/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.ts index 5fce514..85879d1 100644 --- a/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.ts +++ b/packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.ts @@ -39,7 +39,7 @@ export const migrateComponents = async ( if (sourceFile.getFilePath().endsWith(".html")) { const htmlAsString = sourceFile.getFullText(); - const { skippedIconsHtml, ionIcons, ionicComponents } = + const { skippedIconsHtml, ionIcons, ionicComponents, hasRouterLink, hasRouterLinkWithHref } = detectIonicComponentsAndIcons(htmlAsString, sourceFile.getFilePath()); if (ionicComponents.length > 0 || ionIcons.length > 0) { @@ -52,6 +52,8 @@ export const migrateComponents = async ( ionicComponents, ionIcons, skippedIconsHtml, + hasRouterLink, + hasRouterLinkWithHref, cliOptions, ); @@ -61,7 +63,7 @@ export const migrateComponents = async ( } else if (sourceFile.getFilePath().endsWith(".ts")) { const templateAsString = getComponentTemplateAsString(sourceFile); if (templateAsString) { - const { skippedIconsHtml, ionIcons, ionicComponents } = + const { skippedIconsHtml, ionIcons, ionicComponents, hasRouterLink, hasRouterLinkWithHref } = detectIonicComponentsAndIcons( templateAsString, sourceFile.getFilePath(), @@ -72,6 +74,8 @@ export const migrateComponents = async ( ionicComponents, ionIcons, skippedIconsHtml, + hasRouterLink, + hasRouterLinkWithHref, cliOptions, ); @@ -88,6 +92,8 @@ async function migrateAngularComponentClass( ionicComponents: string[], ionIcons: string[], skippedIconsHtml: string[], + hasRouterLink: boolean, + hasRouterLinkWithHref: boolean, cliOptions: CliOptions, ) { let ngModuleSourceFile: SourceFile | undefined; @@ -113,6 +119,16 @@ async function migrateAngularComponentClass( ); } + if (hasRouterLink) { + addImportToClass(sourceFile, "IonRouterLink", "@ionic/angular/standalone"); + addImportToComponentDecorator(sourceFile, "IonRouterLink"); + } + + if (hasRouterLinkWithHref) { + addImportToClass(sourceFile, "IonRouterLinkWithHref", "@ionic/angular/standalone"); + addImportToComponentDecorator(sourceFile, "IonRouterLinkWithHref"); + } + for (const ionicComponent of ionicComponents) { if (isAngularComponentStandalone(sourceFile)) { const componentClassName = kebabCaseToPascalCase(ionicComponent); @@ -205,12 +221,23 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) { const ionIcons: string[] = []; const skippedIconsHtml: string[] = []; + let hasRouterLinkWithHref = false; + let hasRouterLink = false; + const recursivelyFindIonicComponents = (node: any) => { if (node.type === "Element$1") { if (IONIC_COMPONENTS.includes(node.name)) { if (!ionicComponents.includes(node.name)) { ionicComponents.push(node.name); } + + const routerLink = node.attributes.find( + (a: any) => a.name === 'routerLink' || a.name == 'routerDirection' || a.name === 'routerAction' + ) !== undefined; + + if (!hasRouterLink && routerLink) { + hasRouterLink = true; + } } if (node.name === "ion-icon") { @@ -253,6 +280,16 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) { } } + if (node.name === 'a') { + const routerLinkWithHref = node.attributes.find( + (a: any) => a.name === 'routerLink' || a.name == 'routerDirection' || a.name === 'routerAction' + ) !== undefined; + + if (!hasRouterLinkWithHref && routerLinkWithHref) { + hasRouterLinkWithHref = true; + } + } + if (node.children.length > 0) { for (const childNode of node.children) { recursivelyFindIonicComponents(childNode); @@ -269,6 +306,8 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) { ionicComponents, ionIcons, skippedIconsHtml, + hasRouterLinkWithHref, + hasRouterLink, }; }