Skip to content

Commit

Permalink
feat(angular): use standalone components with routing (#25589)
Browse files Browse the repository at this point in the history
Resolves #25404
  • Loading branch information
sean-perkins committed Aug 30, 2022
1 parent 8a1b3c5 commit 3c84d48
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 23 deletions.
2 changes: 1 addition & 1 deletion angular/src/di/r3_injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ export abstract class EnvironmentInjector implements Injector {
/**
* @internal
*/
abstract onDestroy(callback: () => void): void;
abstract onDestroy?(callback: () => void): void;
}
41 changes: 29 additions & 12 deletions angular/src/directives/navigation/ion-router-outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
20 changes: 18 additions & 2 deletions angular/src/directives/navigation/ion-tabs.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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: [
Expand Down Expand Up @@ -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 }>();

Expand Down
9 changes: 9 additions & 0 deletions angular/test/apps/ng14/e2e/src/standalone-routing.spec.ts
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.');
});
});
3 changes: 3 additions & 0 deletions angular/test/apps/ng14/src/app/app.component.html
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>
11 changes: 11 additions & 0 deletions angular/test/apps/ng14/src/app/app.component.ts
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 { }
7 changes: 0 additions & 7 deletions angular/test/base/README.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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: [
Expand Down
21 changes: 21 additions & 0 deletions angular/test/base/tsconfig.json
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"
}
]
}
}
2 changes: 2 additions & 0 deletions angular/test/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 35 additions & 0 deletions angular/test/sync-e2e.sh
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}"

0 comments on commit 3c84d48

Please sign in to comment.