support passing no component to NgComponent + support loading indicators #2

Merged
laurin merged 1 commits from feat/loading-indictators-and-undefined-component-support into single-spa-only 4 months ago
  1. 19
      host-svelte/src/App.svelte
  2. 53
      ng-frontend/src/frontends/react/NgComponent.tsx
  3. 42
      ng-frontend/src/frontends/svelte5/NgComponent.svelte
  4. 66
      ng-frontend/src/frontends/vue/NgComponent.vue
  5. 4
      ng-frontend/src/utils/types.ts
  6. 4
      remote-vue/src/components/VueComponent2.vue

@ -48,7 +48,6 @@
<RouterTrace router={rootRouter}></RouterTrace>
<main class="min-h-screen">
<div class="mx-auto flex max-w-5xl flex-col gap-8 px-6 py-10">
<Route key="ex" path={/^\/ex/}>
<div class="bg-secondary">
@ -60,9 +59,15 @@
</header>
<section class="center grid gap-6 grid-cols-3">
<button class="btn" onclick={() => setMainApp("react1App")} >Set React1App as Main App</button>
<button class="btn" onclick={() => setMainApp("vue1App")} >Set Vue1App as Main App</button>
<button class="btn" onclick={() => resetMainApp()} >Reset Main App</button>
<button class="btn" onclick={() => setMainApp("react1App")}
>Set React1App as Main App</button
>
<button class="btn" onclick={() => setMainApp("vue1App")}
>Set Vue1App as Main App</button
>
<button class="btn" onclick={() => resetMainApp()}
>Reset Main App</button
>
</section>
<section class="center grid gap-6 md:grid-cols-2">
<div class="card shadow">
@ -88,7 +93,11 @@
<div class="center grid gap-6">
<section class="card shadow">
<NgComponent component={react1App} applicationId="react1App" />
<NgComponent
component={react1App}
applicationId="react1App"
loadingIndicator={"Loading React1 App..."}
/>
<NgComponent component={react1Switch} applicationId="react1Switch" />
</section>

@ -5,8 +5,16 @@ import {
ParentCallbacks,
} from "../../utils/types.ts";
import { ReactComponentContext } from "@ng-org/frontend/react/context";
import { useCallback, useContext, useEffect, useMemo } from "react";
import { JSX, useContext, useEffect, useMemo, useState } from "react";
import { deepSignal } from "@ng-org/alien-deepsignals";
import { ParcelConfig } from "single-spa";
type ReactNgComponentProps = ParcelProps & {
/** Props specific to single-spa-react/parcel */
reactParcelProps?: ParcelCompProps;
/** Component to show while the NgComponent is not loaded. E.g. a spinner. Default is empty. */
loadingIndicator?: JSX.Element;
};
/**
* React element to mount NG components.
@ -17,11 +25,10 @@ export default function NgComponent({
applicationId,
initialPathName,
component: componentFactory,
/** The props to be passed to the NG component. Those are reactive. */
props,
/** Specific props for single-spa-react */
reactParcelProps,
}: ParcelProps & { reactParcelProps?: ParcelCompProps }) {
loadingIndicator = <></>,
}: ReactNgComponentProps) {
const componentContext = useContext(ReactComponentContext);
if (!componentContext)
throw new Error(
@ -60,20 +67,32 @@ export default function NgComponent({
componentContext.parentCallbacks.unRegisterChild([applicationId]);
});
const lifecycles = useCallback(async () => {
let lifecycleHooks: any = await componentFactory;
if (typeof lifecycleHooks === "function") {
lifecycleHooks = await lifecycleHooks(componentFactoryParams);
}
if (typeof lifecycleHooks === "function") {
lifecycleHooks = await lifecycleHooks(componentFactoryParams);
}
const [resolvedLifecycles, setResolvedLifecycles] = useState<
ParcelConfig | undefined
>(undefined);
useEffect(() => {
if (!componentFactory) return;
Promise.try(async () => {
let lifecycleHooks: any = await componentFactory;
if (typeof lifecycleHooks === "function") {
lifecycleHooks = await lifecycleHooks(componentFactoryParams);
}
if (typeof lifecycleHooks === "function") {
lifecycleHooks = await lifecycleHooks(componentFactoryParams);
}
return {
name: applicationId,
...lifecycleHooks,
};
setResolvedLifecycles({
name: applicationId,
...lifecycleHooks,
});
});
}, [applicationId, componentFactory]);
return <Parcel {...props} {...reactParcelProps} config={lifecycles} />;
if (!resolvedLifecycles) return loadingIndicator;
return (
<Parcel {...props} {...reactParcelProps} config={resolvedLifecycles} />
);
}

@ -1,6 +1,7 @@
<script lang="ts">
import Parcel from "./Parcel.svelte";
import { getContext, onDestroy, onMount } from "svelte";
import type { SvelteComponent } from "svelte";
import { ngComponentContextKey } from "./ngComponentContext.ts";
import type {
ComponentFactoryParams,
@ -9,13 +10,20 @@
ParentCallbacks,
} from "../../utils/types.ts";
import { deepSignal } from "@ng-org/alien-deepsignals";
import type { SspaParcelConfigObject } from "@wjfe/single-spa-svelte";
type SvelteComponentProps = ParcelProps & {
/** Component to show while the NgComponent is not loaded. E.g. a spinner. Default is none. */
loadingIndicator?: SvelteComponent;
};
let {
applicationId,
props,
initialPathName,
component: componentFactory,
}: ParcelProps = $props();
loadingIndicator,
}: SvelteComponentProps = $props();
const componentContext = getContext<NgComponentContext | undefined>(
ngComponentContextKey,
@ -56,8 +64,12 @@
componentContext.parentCallbacks.unRegisterChild([applicationId]),
);
const lifecycles = $derived(
(async () => {
let lifecycles: SspaParcelConfigObject | undefined = $state(undefined);
$effect.pre(() => {
if (!componentFactory) return;
Promise.try(async () => {
let lifecycleHooks: any = await componentFactory;
if (typeof lifecycleHooks === "function") {
lifecycleHooks = await lifecycleHooks(componentFactoryParams);
@ -71,16 +83,18 @@
...lifecycleHooks,
};
return res;
})(),
);
lifecycles = res;
});
});
</script>
<Parcel
sspa={{
// Not sure why this is complaining..
// @ts-expect-error
config: lifecycles,
}}
{...props}
/>
{#if lifecycles}
<Parcel
sspa={{
config: lifecycles,
}}
{...props}
/>
{:else}
{loadingIndicator}
{/if}

@ -1,17 +1,23 @@
<script setup lang="ts">
import { inject, onUnmounted, ref } from "vue";
import { type Component, defineComponent, h, inject, onUnmounted, watch } from "vue";
import Parcel from "@ng-org/single-spa-vue/parcel";
import { ngComponentContextKey } from "./ngComponentContext.ts";
import type { ComponentFactoryParams, NgComponentContext, ParcelProps, ParentCallbacks } from "../../utils/types.ts";
import { mountRootParcel, type LifeCycles } from "single-spa";
import { mountRootParcel } from "single-spa";
import { deepSignal } from "@ng-org/alien-deepsignals";
type VueComponentProps = ParcelProps & {
/** Component to show while the NgComponent is not loaded. E.g. a spinner. Default is none. */
loadingIndicator?: Component;
};
const {
applicationId,
component: componentFactory,
initialPathName,
props,
} = defineProps<ParcelProps>();
props = {},
loadingIndicator
} = defineProps<VueComponentProps>();
const componentContext = inject<NgComponentContext>(ngComponentContextKey);
if (!componentContext) {
@ -45,10 +51,9 @@ onUnmounted(() => {
componentContext.parentCallbacks.unRegisterChild([applicationId]);
})
let resolvedConfig = ref<LifeCycles | undefined>(undefined);
(async () => {
if (resolvedConfig.value) return;
const loadLifecycles = async () => {
let lifecycleHooks: any = await componentFactory;
if (typeof lifecycleHooks === "function") {
lifecycleHooks = await lifecycleHooks(componentFactoryParams);
@ -57,13 +62,54 @@ let resolvedConfig = ref<LifeCycles | undefined>(undefined);
lifecycleHooks = await lifecycleHooks(componentFactoryParams);
}
resolvedConfig.value = {
return {
name: applicationId,
...lifecycleHooks,
};
})();
};
const LifecyclesWrapper = defineComponent({
async setup() {
if (!componentFactory) {
// wait until componentFactory becomes defined
await new Promise<void>((resolve) => {
const stop = watch(
() => componentFactory,
(v) => {
if (v) {
stop();
resolve();
}
},
);
});
}
const resolved = await loadLifecycles();
return () => {
return h(Parcel, {
config: resolved,
mountParcel: mountRootParcel,
parcelProps: props,
wrapWith: "div",
wrapClass: "",
wrapStyle: {},
});
};
},
});
</script>
<template>
<Parcel v-if="resolvedConfig" :config="resolvedConfig" :mountParcel="mountRootParcel" :parcelProps="props" />
<Suspense>
<template #default>
<LifecyclesWrapper />
</template>
<template #fallback>
<component v-if="loadingIndicator" :is="loadingIndicator" />
</template>
</Suspense>
</template>

@ -30,8 +30,10 @@ export interface ParcelProps {
| ComponentFactory
| Promise<ComponentFactory>
| (() => ComponentFactory)
| (() => Promise<ComponentFactory>);
| (() => Promise<ComponentFactory>)
| undefined;
/** The props to be passed to the component. They are reactive. */
props?: Record<string, any>;
/** The ID this application, which is used for routing purposes.*/
applicationId: string;

@ -2,13 +2,13 @@
import { NgComponent } from "@ng-org/frontend/vue";
import { loadApp } from "@ng-org/frontend";
const react1Switch = loadApp("n:g:example:vue", "component");
const react1Switch = loadApp("n:g:example:react1", "switch");
</script>
<template>
<div>
I'm a second component. Mounting a react switch.
I'm a second component. Mounting a react1 switch.
<NgComponent :component="react1Switch" :props="{ toggled: true }" :application-id="'react1SwitchInVue1'" />
</div>

Loading…
Cancel
Save