Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"files.associations": {
"*.template": "ejs"
}
}
2 changes: 1 addition & 1 deletion apps/chat/src/main.single-spa.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NavigationStart, Router } from '@angular/router';
import { getSingleSpaExtraProviders, singleSpaAngular } from 'single-spa-angular';
import { getSingleSpaExtraProviders, singleSpaAngular } from '@single-spa-community/angular';

import { singleSpaPropsSubject } from './single-spa/single-spa-props';
import { bootstrapApplication, platformBrowser } from '@angular/platform-browser';
Expand Down
4 changes: 2 additions & 2 deletions apps/elements/src/main.single-spa.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { singleSpaAngularElements } from 'single-spa-angular/elements';
import { getSingleSpaExtraProviders } from 'single-spa-angular';
import { singleSpaAngularElements } from '@single-spa-community/angular/elements';
import { getSingleSpaExtraProviders } from '@single-spa-community/angular';

import { AppModule } from './app/app.module';

Expand Down
2 changes: 1 addition & 1 deletion apps/navbar/src/main.single-spa.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NavigationStart, Router } from '@angular/router';
import { bootstrapApplication, platformBrowser } from '@angular/platform-browser';
import { getSingleSpaExtraProviders, singleSpaAngular } from 'single-spa-angular';
import { getSingleSpaExtraProviders, singleSpaAngular } from '@single-spa-community/angular';

import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
Expand Down
4 changes: 3 additions & 1 deletion apps/parcel/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@let _mountRootParcel = mountRootParcel();
@let _customProps = customProps();
@if (_mountRootParcel) {
<parcel [config]="config" [mountParcel]="_mountRootParcel" [customProps]="customProps" />
<parcel [config]="config" [mountParcel]="_mountRootParcel" [customProps]="_customProps" />
<button (click)="updateCustomProps()" id="update-custom-props">Update custom props</button>
}
14 changes: 10 additions & 4 deletions apps/parcel/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { defer, shareReplay } from 'rxjs';
import { ParcelModule } from 'single-spa-angular/parcel';
import { ParcelModule } from '@single-spa-community/angular/parcel';

import { config } from './ReactWidget/ReactWidget';

Expand All @@ -16,15 +16,21 @@ const singleSpa$ = defer(() => System.import('single-spa')).pipe(
imports: [ParcelModule],
})
export class AppComponent {
config = config;
readonly config = config;
readonly mountRootParcel = signal<typeof import('single-spa').mountRootParcel | null>(null);
customProps = {
readonly customProps = signal<Record<string, unknown>>({
hello: 'Hola',
};
});

constructor() {
singleSpa$.pipe(takeUntilDestroyed()).subscribe(({ mountRootParcel }) => {
this.mountRootParcel.set(mountRootParcel);
});
}

updateCustomProps(): void {
this.customProps.set({
hello: 'Bonjour',
});
}
}
2 changes: 1 addition & 1 deletion apps/parcel/src/main.single-spa.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { bootstrapApplication, platformBrowser } from '@angular/platform-browser';
import { getSingleSpaExtraProviders, singleSpaAngular } from 'single-spa-angular';
import { getSingleSpaExtraProviders, singleSpaAngular } from '@single-spa-community/angular';

import { AppComponent } from './app/app.component';

Expand Down
16 changes: 8 additions & 8 deletions apps/root-config/src/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@

"@angular/elements": "http://localhost:4600/es2022/angular-elements.js",

"single-spa-angular/internals": "http://localhost:4600/es2022/single-spa-angular-internals.js",
"single-spa-angular": "http://localhost:4600/es2022/single-spa-angular.js",
"single-spa-angular/elements": "http://localhost:4600/es2022/single-spa-angular-elements.js",
"single-spa-angular/parcel": "http://localhost:4600/es2022/single-spa-angular-parcel.js"
"@single-spa-community/angular/internals": "http://localhost:4600/es2022/single-spa-community-angular-internals.js",
"@single-spa-community/angular": "http://localhost:4600/es2022/single-spa-community-angular.js",
"@single-spa-community/angular/elements": "http://localhost:4600/es2022/single-spa-community-angular-elements.js",
"@single-spa-community/angular/parcel": "http://localhost:4600/es2022/single-spa-community-angular-parcel.js"
}
}
</script>
Expand Down Expand Up @@ -87,10 +87,10 @@

"@angular/elements": "http://localhost:4600/es2022/angular-elements.min.js",

"single-spa-angular/internals": "http://localhost:4600/es2022/single-spa-angular-internals.min.js",
"single-spa-angular": "http://localhost:4600/es2022/single-spa-angular.min.js",
"single-spa-angular/elements": "http://localhost:4600/es2022/single-spa-angular-elements.min.js",
"single-spa-angular/parcel": "http://localhost:4600/es2022/single-spa-angular-parcel.min.js"
"@single-spa-community/angular/internals": "http://localhost:4600/es2022/single-spa-community-angular-internals.min.js",
"@single-spa-community/angular": "http://localhost:4600/es2022/single-spa-community-angular.min.js",
"@single-spa-community/angular/elements": "http://localhost:4600/es2022/single-spa-community-angular-elements.min.js",
"@single-spa-community/angular/parcel": "http://localhost:4600/es2022/single-spa-community-angular-parcel.min.js"
}
}
</script>
Expand Down
2 changes: 1 addition & 1 deletion apps/shop/src/main.single-spa.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NavigationStart, Router } from '@angular/router';
import { bootstrapApplication, platformBrowser } from '@angular/platform-browser';
import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
import { singleSpaAngular, getSingleSpaExtraProviders } from '@single-spa-community/angular';

import { loadMontserrat } from './fonts';
import { appConfig } from './app/app.config';
Expand Down
8 changes: 6 additions & 2 deletions cypress/integration/parcel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Cypress.Screenshot.defaults({
});

describe('Angular parcel', () => {
it('should navigate to /parcel and render the React widget and lazy component', () => {
it('should navigate to /parcel and render the React widget and lazy component AND update custom props', () => {
cy.visit('/parcel')
// GitHub Actions CI is not as fast as the local setup, there can be some network delays,
// timeout is used only for this purpose.
Expand All @@ -15,6 +15,10 @@ describe('Angular parcel', () => {
.invoke('attr', 'alt')
.should('eq', 'React logo')
.get('parcel-root parcel h1')
.contains('Hola world');
.contains('Hola world')
.get('#update-custom-props')
.click()
.get('parcel-root parcel h1')
.contains('Bonjour world');
});
});
8 changes: 2 additions & 6 deletions libs/single-spa-community-angular/internals/src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { DomElementGetter, BaseSingleSpaAngularOptions } from './types';

// This will be provided through Terser global definitions by Angular CLI. This will
// help to tree-shake away the code unneeded for production bundles.
declare const ngDevMode: boolean;

export function getContainerElementAndSetTemplate<T extends BaseSingleSpaAngularOptions>(
options: T,
props: any,
): HTMLElement {
const domElementGetter = chooseDomElementGetter(options, props);

if ((typeof ngDevMode === 'undefined' || ngDevMode) && !domElementGetter) {
if (!domElementGetter) {
throw Error(
`Cannot mount angular application '${
props.name || props.appName
Expand All @@ -26,7 +22,7 @@ export function getContainerElementAndSetTemplate<T extends BaseSingleSpaAngular
function getContainerElement(domElementGetter: DomElementGetter, props: any): never | HTMLElement {
const element = domElementGetter(props);

if ((typeof ngDevMode === 'undefined' || ngDevMode) && !element) {
if (!element) {
throw Error('domElementGetter did not return a valid dom element');
}

Expand Down
21 changes: 11 additions & 10 deletions libs/single-spa-community-angular/parcel/src/parcel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,10 @@ const enum Action {
Unmount = 'unmount',
}

// This will be provided through Terser global definitions by Angular CLI. This will
// help to tree-shake away the code unneeded for production bundles.
declare const ngDevMode: boolean;

@Component({
selector: 'parcel',
template: '',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class ParcelComponent {
readonly config = input<ParcelConfig | null>(null);
Expand All @@ -47,14 +42,19 @@ export class ParcelComponent {
const customProps = this.customProps();
untracked(() => {
this.scheduleTask(Action.Update, () => {
this.parcel?.update?.(customProps);
this.parcel?.update?.({
...customProps,
domElement: this.wrapper,
});
});
});
});

afterNextRender(() => {
this.scheduleTask(Action.Mount, () => {
if ((typeof ngDevMode === 'undefined' || ngDevMode) && this.mountParcel === null) {
const mountParcel = this.mountParcel();

if (mountParcel === null) {
throw new Error(
'single-spa-angular: the [mountParcel] binding is required when using the <parcel> component. You can either (1) import mountRootParcel from single-spa or (2) use the mountParcel prop provided to single-spa applications.',
);
Expand All @@ -69,8 +69,7 @@ export class ParcelComponent {
this.host.nativeElement.appendChild(this.wrapper);
}

const mountParcel = this.mountParcel();
this.parcel = mountParcel!(this.config()!, {
this.parcel = mountParcel(this.config()!, {
...this.customProps(),
domElement: this.wrapper,
});
Expand All @@ -88,7 +87,9 @@ export class ParcelComponent {
inject(DestroyRef).onDestroy(() => {
this.scheduleTask(Action.Unmount, () => {
if (this.parcel?.getStatus() === 'MOUNTED') {
return this.parcel.unmount();
return this.parcel.unmount().then(() => {
this.parcel = null;
});
}
});

Expand Down
28 changes: 19 additions & 9 deletions libs/single-spa-community-angular/src/extra-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
type LocationChangeEvent,
type LocationChangeListener,
} from '@angular/common';
import { asyncScheduler, Subject, timer } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
import { switchMap } from 'rxjs/operators';

import { runOutsideAngular } from './run-outside-angular';

declare const Zone: any;

Expand Down Expand Up @@ -39,13 +41,21 @@ export class SingleSpaPlatformLocation extends BrowserPlatformLocation {
// arrives. This is the key to avoiding infinite loops and race conditions during fast
// navigation: if the user navigates rapidly (e.g., hitting back/forward quickly),
// only the most recent popstate event will be processed. Earlier ones are discarded.
switchMap(state =>
// `timer(0, asyncScheduler)` defers execution to the next macrotask (via setTimeout).
// This gives single-spa time to finish its own synchronous URL/state updates before
// Angular's router reacts. Without this delay, Angular and single-spa could both
// attempt to modify history state simultaneously, causing conflicts or infinite
// navigation loops.
timer(0, asyncScheduler).pipe(map(() => state)),
switchMap(
state =>
// setTimeout defers execution to the next macrotask.
// This gives single-spa time to finish its own synchronous URL/state updates before
// Angular's router reacts. Without this delay, Angular and single-spa could both
// attempt to modify history state simultaneously, causing conflicts or infinite
// navigation loops.
new Observable<[LocationChangeListener, LocationChangeEvent]>(subscriber =>
runOutsideAngular(() => {
const timeoutId = setTimeout(() => {
subscriber.next(state);
});
return () => clearTimeout(timeoutId);
}),
),
),
)
.subscribe(([fn, event]) => {
Expand Down
11 changes: 0 additions & 11 deletions libs/single-spa-community-angular/src/prod-mode.ts

This file was deleted.

1 change: 0 additions & 1 deletion libs/single-spa-community-angular/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { enableProdMode } from './prod-mode';
export { singleSpaAngular } from './single-spa-angular';
export { getSingleSpaExtraProviders } from './extra-providers';
7 changes: 7 additions & 0 deletions libs/single-spa-community-angular/src/run-outside-angular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare const Zone: any;

export function runOutsideAngular<T>(fn: () => T): T {
return typeof Zone !== 'undefined' && typeof Zone?.root?.run === 'function'
? Zone.root.run(fn)
: fn();
}
Loading
Loading