Skip to content

Commit 44806ee

Browse files
authored
Merge pull request #10 from ionic-team/sp/router-link
feat: migrate routerLink and routerLinkHref directive usages Resolves #7
2 parents e9f4476 + 3d27184 commit 44806ee

File tree

5 files changed

+309
-2
lines changed

5 files changed

+309
-2
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<ion-header [translucent]="true">
2+
<ion-toolbar>
3+
<ion-title> Blank </ion-title>
4+
</ion-toolbar>
5+
</ion-header>
6+
7+
<ion-content [fullscreen]="true">
8+
<ion-header collapse="condense">
9+
<ion-toolbar>
10+
<ion-title size="large">Blank</ion-title>
11+
</ion-toolbar>
12+
</ion-header>
13+
14+
<div id="container">
15+
<strong>Ready to create an app?</strong>
16+
<p>
17+
Start with Ionic
18+
<a
19+
target="_blank"
20+
rel="noopener noreferrer"
21+
href="https://ionicframework.com/docs/components"
22+
>UI Components</a
23+
>
24+
</p>
25+
</div>
26+
27+
<a href="" routerDirection="forward">Click me</a>
28+
</ion-content>

apps/angular/ionic-angular-standalone/src/app/router-link/router-link.page.scss

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Component } from '@angular/core';
2+
import { IonicModule } from '@ionic/angular';
3+
4+
@Component({
5+
selector: 'app-router-link',
6+
templateUrl: 'router-link.page.html',
7+
styleUrls: ['router-link.page.scss'],
8+
standalone: true,
9+
imports: [IonicModule],
10+
})
11+
export class RouterLinkPage {
12+
constructor() { }
13+
}

packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.test.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,233 @@ describe("migrateComponents", () => {
166166
`),
167167
);
168168
});
169+
170+
describe("hyperlinks", () => {
171+
it("should detect and import routerLink used in the template", async () => {
172+
const project = new Project({ useInMemoryFileSystem: true });
173+
const component = `
174+
import { Component } from "@angular/core";
175+
176+
@Component({
177+
selector: 'my-component',
178+
template: '<a routerLink="/home">Home</a>',
179+
standalone: true
180+
})
181+
export class MyComponent { }
182+
`;
183+
184+
const componentSourceFile = project.createSourceFile(
185+
"foo.component.ts",
186+
dedent(component),
187+
);
188+
189+
await migrateComponents(project, { dryRun: false });
190+
191+
expect(dedent(componentSourceFile.getText())).toBe(
192+
dedent(`
193+
import { Component } from "@angular/core";
194+
import { IonRouterLinkWithHref } from "@ionic/angular/standalone";
195+
196+
@Component({
197+
selector: 'my-component',
198+
template: '<a routerLink="/home">Home</a>',
199+
standalone: true,
200+
imports: [IonRouterLinkWithHref]
201+
})
202+
export class MyComponent { }
203+
`)
204+
);
205+
});
206+
207+
it("should detect and import routerAction used in the template", async () => {
208+
const project = new Project({ useInMemoryFileSystem: true });
209+
const component = `
210+
import { Component } from "@angular/core";
211+
212+
@Component({
213+
selector: 'my-component',
214+
template: '<a routerAction="push">Home</a>',
215+
standalone: true
216+
})
217+
export class MyComponent { }
218+
`;
219+
220+
const componentSourceFile = project.createSourceFile(
221+
"foo.component.ts",
222+
dedent(component),
223+
);
224+
225+
await migrateComponents(project, { dryRun: false });
226+
227+
expect(dedent(componentSourceFile.getText())).toBe(
228+
dedent(`
229+
import { Component } from "@angular/core";
230+
import { IonRouterLinkWithHref } from "@ionic/angular/standalone";
231+
232+
@Component({
233+
selector: 'my-component',
234+
template: '<a routerAction="push">Home</a>',
235+
standalone: true,
236+
imports: [IonRouterLinkWithHref]
237+
})
238+
export class MyComponent { }
239+
`)
240+
);
241+
});
242+
243+
it("should detect and import routerDirection used in the template", async () => {
244+
const project = new Project({ useInMemoryFileSystem: true });
245+
const component = `
246+
import { Component } from "@angular/core";
247+
248+
@Component({
249+
selector: 'my-component',
250+
template: '<a routerDirection="forward">Home</a>',
251+
standalone: true
252+
})
253+
export class MyComponent { }
254+
`;
255+
256+
const componentSourceFile = project.createSourceFile(
257+
"foo.component.ts",
258+
dedent(component),
259+
);
260+
261+
await migrateComponents(project, { dryRun: false });
262+
263+
expect(dedent(componentSourceFile.getText())).toBe(
264+
dedent(`
265+
import { Component } from "@angular/core";
266+
import { IonRouterLinkWithHref } from "@ionic/angular/standalone";
267+
268+
@Component({
269+
selector: 'my-component',
270+
template: '<a routerDirection="forward">Home</a>',
271+
standalone: true,
272+
imports: [IonRouterLinkWithHref]
273+
})
274+
export class MyComponent { }
275+
`)
276+
);
277+
});
278+
});
279+
280+
describe("Ionic components", () => {
281+
it("should detect and import routerLink used in the template", async () => {
282+
const project = new Project({ useInMemoryFileSystem: true });
283+
const component = `
284+
import { Component } from "@angular/core";
285+
import { IonicModule } from "@ionic/angular";
286+
287+
@Component({
288+
selector: 'my-component',
289+
template: '<ion-button routerLink="/home">Home</ion-button>',
290+
standalone: true,
291+
imports: [IonicModule]
292+
})
293+
export class MyComponent { }
294+
`;
295+
296+
const componentSourceFile = project.createSourceFile(
297+
"foo.component.ts",
298+
dedent(component),
299+
);
300+
301+
await migrateComponents(project, { dryRun: false });
302+
303+
expect(dedent(componentSourceFile.getText())).toBe(
304+
dedent(`
305+
import { Component } from "@angular/core";
306+
import { IonRouterLink, IonButton } from "@ionic/angular/standalone";
307+
308+
@Component({
309+
selector: 'my-component',
310+
template: '<ion-button routerLink="/home">Home</ion-button>',
311+
standalone: true,
312+
imports: [IonRouterLink, IonButton]
313+
})
314+
export class MyComponent { }
315+
`)
316+
);
317+
});
318+
319+
it("should detect and import routerAction used in the template", async () => {
320+
const project = new Project({ useInMemoryFileSystem: true });
321+
const component = `
322+
import { Component } from "@angular/core";
323+
import { IonicModule } from "@ionic/angular";
324+
325+
@Component({
326+
selector: 'my-component',
327+
template: '<ion-button routerAction="push">Home</ion-button>',
328+
standalone: true,
329+
imports: [IonicModule]
330+
})
331+
export class MyComponent { }
332+
`;
333+
334+
const componentSourceFile = project.createSourceFile(
335+
"foo.component.ts",
336+
dedent(component),
337+
);
338+
339+
await migrateComponents(project, { dryRun: false });
340+
341+
expect(dedent(componentSourceFile.getText())).toBe(
342+
dedent(`
343+
import { Component } from "@angular/core";
344+
import { IonRouterLink, IonButton } from "@ionic/angular/standalone";
345+
346+
@Component({
347+
selector: 'my-component',
348+
template: '<ion-button routerAction="push">Home</ion-button>',
349+
standalone: true,
350+
imports: [IonRouterLink, IonButton]
351+
})
352+
export class MyComponent { }
353+
`)
354+
);
355+
});
356+
357+
it("should detect and import routerDirection used in the template", async () => {
358+
const project = new Project({ useInMemoryFileSystem: true });
359+
const component = `
360+
import { Component } from "@angular/core";
361+
import { IonicModule } from "@ionic/angular";
362+
363+
@Component({
364+
selector: 'my-component',
365+
template: '<ion-button routerDirection="forward">Home</ion-button>',
366+
standalone: true,
367+
imports: [IonicModule]
368+
})
369+
export class MyComponent { }
370+
`;
371+
372+
const componentSourceFile = project.createSourceFile(
373+
"foo.component.ts",
374+
dedent(component),
375+
);
376+
377+
await migrateComponents(project, { dryRun: false });
378+
379+
expect(dedent(componentSourceFile.getText())).toBe(
380+
dedent(`
381+
import { Component } from "@angular/core";
382+
import { IonRouterLink, IonButton } from "@ionic/angular/standalone";
383+
384+
@Component({
385+
selector: 'my-component',
386+
template: '<ion-button routerDirection="forward">Home</ion-button>',
387+
standalone: true,
388+
imports: [IonRouterLink, IonButton]
389+
})
390+
export class MyComponent { }
391+
`)
392+
);
393+
});
394+
})
395+
169396
});
170397

171398
describe("single component angular modules", () => {

packages/cli/src/angular/migrations/standalone/0002-import-standalone-component.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const migrateComponents = async (
3939
if (sourceFile.getFilePath().endsWith(".html")) {
4040
const htmlAsString = sourceFile.getFullText();
4141

42-
const { skippedIconsHtml, ionIcons, ionicComponents } =
42+
const { skippedIconsHtml, ionIcons, ionicComponents, hasRouterLink, hasRouterLinkWithHref } =
4343
detectIonicComponentsAndIcons(htmlAsString, sourceFile.getFilePath());
4444

4545
if (ionicComponents.length > 0 || ionIcons.length > 0) {
@@ -52,6 +52,8 @@ export const migrateComponents = async (
5252
ionicComponents,
5353
ionIcons,
5454
skippedIconsHtml,
55+
hasRouterLink,
56+
hasRouterLinkWithHref,
5557
cliOptions,
5658
);
5759

@@ -61,7 +63,7 @@ export const migrateComponents = async (
6163
} else if (sourceFile.getFilePath().endsWith(".ts")) {
6264
const templateAsString = getComponentTemplateAsString(sourceFile);
6365
if (templateAsString) {
64-
const { skippedIconsHtml, ionIcons, ionicComponents } =
66+
const { skippedIconsHtml, ionIcons, ionicComponents, hasRouterLink, hasRouterLinkWithHref } =
6567
detectIonicComponentsAndIcons(
6668
templateAsString,
6769
sourceFile.getFilePath(),
@@ -72,6 +74,8 @@ export const migrateComponents = async (
7274
ionicComponents,
7375
ionIcons,
7476
skippedIconsHtml,
77+
hasRouterLink,
78+
hasRouterLinkWithHref,
7579
cliOptions,
7680
);
7781

@@ -88,6 +92,8 @@ async function migrateAngularComponentClass(
8892
ionicComponents: string[],
8993
ionIcons: string[],
9094
skippedIconsHtml: string[],
95+
hasRouterLink: boolean,
96+
hasRouterLinkWithHref: boolean,
9197
cliOptions: CliOptions,
9298
) {
9399
let ngModuleSourceFile: SourceFile | undefined;
@@ -113,6 +119,16 @@ async function migrateAngularComponentClass(
113119
);
114120
}
115121

122+
if (hasRouterLink) {
123+
addImportToClass(sourceFile, "IonRouterLink", "@ionic/angular/standalone");
124+
addImportToComponentDecorator(sourceFile, "IonRouterLink");
125+
}
126+
127+
if (hasRouterLinkWithHref) {
128+
addImportToClass(sourceFile, "IonRouterLinkWithHref", "@ionic/angular/standalone");
129+
addImportToComponentDecorator(sourceFile, "IonRouterLinkWithHref");
130+
}
131+
116132
for (const ionicComponent of ionicComponents) {
117133
if (isAngularComponentStandalone(sourceFile)) {
118134
const componentClassName = kebabCaseToPascalCase(ionicComponent);
@@ -205,12 +221,23 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
205221
const ionIcons: string[] = [];
206222
const skippedIconsHtml: string[] = [];
207223

224+
let hasRouterLinkWithHref = false;
225+
let hasRouterLink = false;
226+
208227
const recursivelyFindIonicComponents = (node: any) => {
209228
if (node.type === "Element$1") {
210229
if (IONIC_COMPONENTS.includes(node.name)) {
211230
if (!ionicComponents.includes(node.name)) {
212231
ionicComponents.push(node.name);
213232
}
233+
234+
const routerLink = node.attributes.find(
235+
(a: any) => a.name === 'routerLink' || a.name == 'routerDirection' || a.name === 'routerAction'
236+
) !== undefined;
237+
238+
if (!hasRouterLink && routerLink) {
239+
hasRouterLink = true;
240+
}
214241
}
215242

216243
if (node.name === "ion-icon") {
@@ -253,6 +280,16 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
253280
}
254281
}
255282

283+
if (node.name === 'a') {
284+
const routerLinkWithHref = node.attributes.find(
285+
(a: any) => a.name === 'routerLink' || a.name == 'routerDirection' || a.name === 'routerAction'
286+
) !== undefined;
287+
288+
if (!hasRouterLinkWithHref && routerLinkWithHref) {
289+
hasRouterLinkWithHref = true;
290+
}
291+
}
292+
256293
if (node.children.length > 0) {
257294
for (const childNode of node.children) {
258295
recursivelyFindIonicComponents(childNode);
@@ -269,6 +306,8 @@ function detectIonicComponentsAndIcons(htmlAsString: string, filePath: string) {
269306
ionicComponents,
270307
ionIcons,
271308
skippedIconsHtml,
309+
hasRouterLinkWithHref,
310+
hasRouterLink,
272311
};
273312
}
274313

0 commit comments

Comments
 (0)