diff --git a/angular/src/di/r3_injector.ts b/angular/src/di/r3_injector.ts index 32f50d53568..dee7684e067 100644 --- a/angular/src/di/r3_injector.ts +++ b/angular/src/di/r3_injector.ts @@ -30,5 +30,5 @@ export abstract class EnvironmentInjector implements Injector { /** * @internal */ - abstract onDestroy(callback: () => void): void; + abstract onDestroy?(callback: () => void): void; } diff --git a/angular/src/directives/navigation/ion-router-outlet.ts b/angular/src/directives/navigation/ion-router-outlet.ts index 59927aa9409..01d02472410 100644 --- a/angular/src/directives/navigation/ion-router-outlet.ts +++ b/angular/src/directives/navigation/ion-router-outlet.ts @@ -14,6 +14,7 @@ import { Optional, Output, SkipSelf, + Input, } from '@angular/core'; import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET } from '@angular/router'; import { componentOnReady } from '@ionic/core'; @@ -55,6 +56,16 @@ export class IonRouterOutlet implements OnDestroy, OnInit { tabsPrefix: string | undefined; + /** + * @experimental + * + * The `EnvironmentInjector` provider instance from the parent component. + * Required for using standalone components with `ion-router-outlet`. + * + * Will be deprecated and removed when Angular 13 support is dropped. + */ + @Input() environmentInjector: EnvironmentInjector; + @Output() stackEvents = new EventEmitter(); // eslint-disable-next-line @angular-eslint/no-output-rename @Output('activate') activateEvents = new EventEmitter(); @@ -88,7 +99,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit { @Optional() @Attribute('tabs') tabs: string, private config: Config, private navCtrl: NavController, - @Optional() private environmentInjector: EnvironmentInjector, @Optional() private componentFactoryResolver: ComponentFactoryResolver, commonLocation: Location, elementRef: ElementRef, @@ -234,20 +244,24 @@ export class IonRouterOutlet implements OnDestroy, OnInit { } else { const snapshot = (activatedRoute as any)._futureSnapshot; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const component = snapshot.routeConfig!.component as any; - /** - * Angular 14 introduces a new `loadComponent` property to the route config, - * that assigns the component to load to the `component` property of - * the route snapshot. We can check for the presence of this property - * to determine if the route is using standalone components. - * - * TODO: FW-1631: Remove this check when supporting standalone components + * Angular 14 introduces a new `loadComponent` property to the route config. + * This function will assign a `component` property to the route snapshot. + * We check for the presence of this property to determine if the route is + * using standalone components. */ - if (component == null && snapshot.component) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (snapshot.routeConfig!.component == null && this.environmentInjector == null) { console.warn( - '[Ionic Warning]: Standalone components are not currently supported with ion-router-outlet. You can track this feature request at https://github.com/ionic-team/ionic-framework/issues/25404' + '[Ionic Warning]: You must supply an environmentInjector to use standalone components with routing:\n\n' + + 'In your component class, add:\n\n' + + ` import { EnvironmentInjector } from '@angular/core';\n` + + ' constructor(public environmentInjector: EnvironmentInjector) {}\n' + + '\n' + + 'In your router outlet template, add:\n\n' + + ' \n\n' + + 'Alternatively, if you are routing within ion-tabs:\n\n' + + ' ' ); return; } @@ -267,6 +281,9 @@ export class IonRouterOutlet implements OnDestroy, OnInit { */ resolverOrInjector = resolverOrInjector || this.componentFactoryResolver; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const component = snapshot.routeConfig!.component ?? snapshot.component; + if (resolverOrInjector && isComponentFactoryResolver(resolverOrInjector)) { // Backwards compatibility for Angular 13 and lower const factory = resolverOrInjector.resolveComponentFactory(component); diff --git a/angular/src/directives/navigation/ion-tabs.ts b/angular/src/directives/navigation/ion-tabs.ts index 33a163eefb3..6c844970f6e 100644 --- a/angular/src/directives/navigation/ion-tabs.ts +++ b/angular/src/directives/navigation/ion-tabs.ts @@ -1,5 +1,6 @@ -import { Component, ContentChild, EventEmitter, HostListener, Output, ViewChild } from '@angular/core'; +import { Component, ContentChild, EventEmitter, HostListener, Input, Output, ViewChild } from '@angular/core'; +import { EnvironmentInjector } from '../../di/r3_injector'; import { NavController } from '../../providers/nav-controller'; import { IonTabBar } from '../proxies'; @@ -10,7 +11,12 @@ import { StackEvent } from './stack-utils'; selector: 'ion-tabs', template: `
- +
`, styles: [ @@ -46,6 +52,16 @@ export class IonTabs { @ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet; @ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined; + /** + * @experimental + * + * The `EnvironmentInjector` provider instance from the parent component. + * Required for using standalone components with `ion-router-outlet`. + * + * Will be deprecated and removed when Angular 13 support is dropped. + */ + @Input() environmentInjector: EnvironmentInjector; + @Output() ionTabsWillChange = new EventEmitter<{ tab: string }>(); @Output() ionTabsDidChange = new EventEmitter<{ tab: string }>(); diff --git a/angular/test/apps/ng14/e2e/src/standalone-routing.spec.ts b/angular/test/apps/ng14/e2e/src/standalone-routing.spec.ts new file mode 100644 index 00000000000..e662959265f --- /dev/null +++ b/angular/test/apps/ng14/e2e/src/standalone-routing.spec.ts @@ -0,0 +1,9 @@ +describe('Routing with Standalone Components', () => { + beforeEach(() => { + cy.visit('/version-test/standalone'); + }); + + it('should render the component', () => { + cy.get('ion-content').contains('This is a standalone component rendered from a route.'); + }); +}); diff --git a/angular/test/apps/ng14/src/app/app.component.html b/angular/test/apps/ng14/src/app/app.component.html new file mode 100644 index 00000000000..fdfbfc0f6f5 --- /dev/null +++ b/angular/test/apps/ng14/src/app/app.component.html @@ -0,0 +1,3 @@ + + + diff --git a/angular/test/apps/ng14/src/app/app.component.ts b/angular/test/apps/ng14/src/app/app.component.ts new file mode 100644 index 00000000000..bd85755bb72 --- /dev/null +++ b/angular/test/apps/ng14/src/app/app.component.ts @@ -0,0 +1,11 @@ +import { Component, EnvironmentInjector } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + + constructor(public environmentInjector: EnvironmentInjector) { } +} diff --git a/angular/test/apps/ng14/src/app/version-test/standalone/standalone.component.html b/angular/test/apps/ng14/src/app/version-test/standalone/standalone.component.html new file mode 100644 index 00000000000..c5f470a160a --- /dev/null +++ b/angular/test/apps/ng14/src/app/version-test/standalone/standalone.component.html @@ -0,0 +1,4 @@ + +

This is a standalone component rendered from a route.

+ Return home +
diff --git a/angular/test/apps/ng14/src/app/version-test/standalone/standalone.component.ts b/angular/test/apps/ng14/src/app/version-test/standalone/standalone.component.ts new file mode 100644 index 00000000000..56f6f8a92b0 --- /dev/null +++ b/angular/test/apps/ng14/src/app/version-test/standalone/standalone.component.ts @@ -0,0 +1,12 @@ +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { IonicModule } from '@ionic/angular'; + +@Component({ + selector: 'app-standalone', + templateUrl: './standalone.component.html', + standalone: true, + imports: [IonicModule, RouterModule] +}) +export class StandaloneComponent { } diff --git a/angular/test/apps/ng14/src/app/version-test/version-test-routing.module.ts b/angular/test/apps/ng14/src/app/version-test/version-test-routing.module.ts new file mode 100644 index 00000000000..b7ef848b72c --- /dev/null +++ b/angular/test/apps/ng14/src/app/version-test/version-test-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: 'standalone', + loadComponent: () => import('./standalone/standalone.component').then(m => m.StandaloneComponent) + } + ]) + ], + exports: [RouterModule] +}) +export class VersionTestRoutingModule { } diff --git a/angular/test/base/README.md b/angular/test/base/README.md deleted file mode 100644 index 8d93d2cf3cc..00000000000 --- a/angular/test/base/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Ionic Angular Test App - -``` -npm install -npm run sync:build -npm start -``` diff --git a/angular/test/base/src/app/version-test/version-test-routing.module.ts b/angular/test/base/src/app/version-test/version-test-routing.module.ts index ea630abacfa..e64eac0b60e 100644 --- a/angular/test/base/src/app/version-test/version-test-routing.module.ts +++ b/angular/test/base/src/app/version-test/version-test-routing.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { VersionTestComponent } from "."; +import { VersionTestComponent } from "./version-test.component"; @NgModule({ imports: [ diff --git a/angular/test/base/tsconfig.json b/angular/test/base/tsconfig.json new file mode 100644 index 00000000000..f2be806b423 --- /dev/null +++ b/angular/test/base/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "importHelpers": true, + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "module": "es2020", + "target": "es2020", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "typeRoots": ["node_modules/@types"], + "lib": ["es2018", "dom"], + "plugins": [ + { + "name": "typescript-eslint-language-service" + } + ] + } +} diff --git a/angular/test/build.sh b/angular/test/build.sh index a646dc74ba8..d91b9cb1771 100755 --- a/angular/test/build.sh +++ b/angular/test/build.sh @@ -31,3 +31,5 @@ cp -R $FULL_BASE_DIR $BUILD_APP_DIR # Then we can copy the specific app which # will override any files in the base application. cp -R $FULL_APP_DIR $BUILD_APP_DIR + +echo "Copied test app files for ${APP_DIR}" diff --git a/angular/test/sync-e2e.sh b/angular/test/sync-e2e.sh new file mode 100755 index 00000000000..8bd09b982ff --- /dev/null +++ b/angular/test/sync-e2e.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# The directory where the source +# for each specific application is. +APPS_DIR="apps" + +# The directory where the +# base application logic is +BASE_DIR="base" +BUILD_DIR="build" + +# The specific application +# we are building +APP_DIR="${1}" + +# The full path to the specific application. +FULL_APP_DIR="${APPS_DIR}/${APP_DIR}/e2e" + +# The full path to the base application. +FULL_BASE_DIR="${BASE_DIR}/e2e" + +# The full path to the built application. +BUILD_APP_DIR="${BUILD_DIR}/${APP_DIR}/" + +# Make the build directory if it does not already exist. +mkdir -p $BUILD_DIR + +# First we need to copy the base application +cp -R $FULL_BASE_DIR $BUILD_APP_DIR + +# Then we can copy the specific app which +# will override any files in the base application. +cp -R $FULL_APP_DIR $BUILD_APP_DIR + +echo "Synced e2e tests for ${APP_DIR}"