-
Notifications
You must be signed in to change notification settings - Fork 13.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(angular): use standalone components with routing #25589
Changes from all commits
504387a
92e9f8d
d12a867
27c722c
b64f522
29efb44
2713986
85f8192
428b41f
2baeb8f
39d8171
254a4d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a tech debt ticket for this? (Ditto for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm indifferent since this is an experimental API that could be ripped out completely if either Angular doesn't proceed with standalone as part of the official public API or if don't think it is a good architectural fit with Ionic. I think once we consider it public API, having tech debt to maintain it could make sense. But we also don't have many experimental features, so if we want to follow our existing process like everything else, I'm happy to create tickets 👍 |
||
*/ | ||
@Input() environmentInjector: EnvironmentInjector; | ||
|
||
@Output() stackEvents = new EventEmitter<any>(); | ||
// eslint-disable-next-line @angular-eslint/no-output-rename | ||
@Output('activate') activateEvents = new EventEmitter<any>(); | ||
|
@@ -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' + | ||
' <ion-router-outlet [environmentInjector]="environmentInjector"></ion-router-outlet>\n\n' + | ||
'Alternatively, if you are routing within ion-tabs:\n\n' + | ||
' <ion-tabs [environmentInjector]="environmentInjector"></ion-tabs>' | ||
); | ||
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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<ion-app> | ||
<ion-router-outlet [environmentInjector]="environmentInjector"></ion-router-outlet> | ||
</ion-app> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<ion-content> | ||
<p>This is a standalone component rendered from a route.</p> | ||
<ion-button routerLink="/">Return home</ion-button> | ||
</ion-content> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { } |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this change needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was to align with the type signature of the
EnvironmentInjector
type from@angular/core
. We are maintaining our own "version" of the types, since we cannot update our library dependencies to target Angular 14. When I originally ported this, I forgot to make theonDestroy
callback optional.Without this change, developers would receive a typescript language server warning in their IDE that the type of their
environmentInjector
does not match the type signature of what we are expecting to be passed in.