mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 11:41:20 +08:00
feat(angular): use standalone components with routing (#25589)
Resolves #25404
This commit is contained in:
@ -30,5 +30,5 @@ export abstract class EnvironmentInjector implements Injector {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract onDestroy(callback: () => void): void;
|
||||
abstract onDestroy?(callback: () => void): void;
|
||||
}
|
||||
|
@ -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<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);
|
||||
|
@ -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: ` <ng-content select="[slot=top]"></ng-content>
|
||||
<div class="tabs-inner">
|
||||
<ion-router-outlet #outlet tabs="true" (stackEvents)="onPageSelected($event)"></ion-router-outlet>
|
||||
<ion-router-outlet
|
||||
#outlet
|
||||
tabs="true"
|
||||
[environmentInjector]="environmentInjector"
|
||||
(stackEvents)="onPageSelected($event)"
|
||||
></ion-router-outlet>
|
||||
</div>
|
||||
<ng-content></ng-content>`,
|
||||
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 }>();
|
||||
|
||||
|
@ -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.');
|
||||
});
|
||||
});
|
3
angular/test/apps/ng14/src/app/app.component.html
Normal file
3
angular/test/apps/ng14/src/app/app.component.html
Normal file
@ -0,0 +1,3 @@
|
||||
<ion-app>
|
||||
<ion-router-outlet [environmentInjector]="environmentInjector"></ion-router-outlet>
|
||||
</ion-app>
|
11
angular/test/apps/ng14/src/app/app.component.ts
Normal file
11
angular/test/apps/ng14/src/app/app.component.ts
Normal file
@ -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) { }
|
||||
}
|
@ -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>
|
@ -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 { }
|
@ -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 { }
|
@ -1,7 +0,0 @@
|
||||
# Ionic Angular Test App
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run sync:build
|
||||
npm start
|
||||
```
|
@ -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: [
|
||||
|
21
angular/test/base/tsconfig.json
Normal file
21
angular/test/base/tsconfig.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -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}"
|
||||
|
35
angular/test/sync-e2e.sh
Executable file
35
angular/test/sync-e2e.sh
Executable file
@ -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}"
|
Reference in New Issue
Block a user