Skip to content

Commit 1a42bf6

Browse files
robin-drexlerlemonmadeigor10ksimontaisne
authored
Add a remote-dom to remote-ui adapter (#511)
* Initial legacy adaptors * Cleanup and documentation * Add remote root adaptor * Add fragment support * Add remote-ui example * E2E tests for legacy adapter * changeset * Delete legacy/remote * remove unused file * adjust remote ui example title * remove unused wait-on dep * remove superfluous ts references * Update packages/core/README.md * move remote-ui example into kitchen sink * Update changeset copy * Change the order of functions * Switch tree to Map * Get rid of negation in conditions * Lift out "update props" variables * Add slotWrapper * Update packages/core/source/legacy/host.ts * slotWrapper -> slotProps.wrapper * Add first set of tests * Update tests * Fix test imports * Add additional tests * Extract adapter into a separate package * Use adapter component mapping * ensure files are imported from source * renderRemoteUi -> renderLegacy * Merge imports * compat v0.1.0 --------- Co-authored-by: Chris Sauve <chris.sauve@shopify.com> Co-authored-by: Igor Kozlov <igor.kozlov@shopify.com> Co-authored-by: Simon Taisne <simon.taisne@shopify.com>
1 parent 6d42def commit 1a42bf6

File tree

25 files changed

+2077
-105
lines changed

25 files changed

+2077
-105
lines changed

.changeset/beige-windows-appear.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@remote-dom/compat': major
3+
---
4+
5+
Add a `adaptToLegacyRemoteChannel` helper that adapts a Remote DOM `RemoteConnection` object into a `remote-ui` `RemoteChannel`.
6+
7+
It allows to use a Remote DOM receiver class on the host, even if the remote environment is using `remote-ui`.

e2e/basic.e2e.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import {test, expect} from '@playwright/test';
77
['iframe', 'vue'],
88
['iframe', 'htm'],
99
['iframe', 'react'],
10+
['iframe', 'react-remote-ui'],
1011
['worker', 'vanilla'],
1112
['worker', 'preact'],
1213
['worker', 'svelte'],
1314
// ['worker', 'vue'],
1415
['worker', 'htm'],
1516
['worker', 'react'],
17+
['worker', 'react-remote-ui'],
1618
].forEach(([sandbox, example]) => {
1719
test(`basic modal interaction with ${sandbox} sandbox and ${example} example`, async ({
1820
page,
@@ -30,7 +32,8 @@ import {test, expect} from '@playwright/test';
3032
dialog.dismiss().catch(() => {});
3133
});
3234

35+
const dialogPromise = page.waitForEvent('dialog');
3336
await page.getByRole('button', {name: 'Close'}).click();
34-
await page.waitForEvent('dialog');
37+
await dialogPromise;
3538
});
3639
});

examples/kitchen-sink/app/host.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {ThreadIframe, ThreadWebWorker} from '@quilted/threads';
99
import type {SandboxAPI} from './types.ts';
1010
import {Button, Modal, Stack, Text, ControlPanel} from './host/components.tsx';
1111
import {createState} from './host/state.ts';
12+
import {adaptToLegacyRemoteChannel} from '@remote-dom/compat';
1213

1314
// We will put any remote elements we want to render in this root element.
1415
const uiRoot = document.querySelector('main')!;
@@ -59,27 +60,37 @@ const components = new Map([
5960

6061
const {receiver, example, sandbox} = createState(
6162
async ({receiver, example, sandbox}) => {
62-
if (sandbox === 'iframe') {
63-
await iframeSandbox.imports.render(receiver.connection, {
64-
sandbox,
65-
example,
66-
async alert(content) {
67-
console.log(
68-
`Alert API used by example ${example} in the iframe sandbox`,
69-
);
70-
window.alert(content);
63+
const api = {
64+
sandbox,
65+
example,
66+
async alert(content: string) {
67+
console.log(
68+
`Alert API used by example ${example} in the iframe sandbox`,
69+
);
70+
window.alert(content);
71+
},
72+
async closeModal() {
73+
document.querySelector('dialog')?.close();
74+
},
75+
};
76+
77+
const sandboxToUse = sandbox === 'iframe' ? iframeSandbox : workerSandbox;
78+
79+
if (example === 'react-remote-ui') {
80+
const remoteUiChannel = adaptToLegacyRemoteChannel(receiver.connection, {
81+
elements: {
82+
Text: 'ui-text',
83+
Button: 'ui-button',
84+
Stack: 'ui-stack',
85+
Modal: 'ui-modal',
7186
},
7287
});
88+
await sandboxToUse.imports.renderLegacy(remoteUiChannel, {
89+
...api,
90+
});
7391
} else {
74-
await workerSandbox.imports.render(receiver.connection, {
75-
sandbox,
76-
example,
77-
async alert(content) {
78-
console.log(
79-
`Alert API used by example ${example} in the worker sandbox`,
80-
);
81-
window.alert(content);
82-
},
92+
await sandboxToUse.imports.render(receiver.connection, {
93+
...api,
8394
});
8495
}
8596
},

examples/kitchen-sink/app/host/components.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ export function ControlPanel({
159159
<option value="svelte">Svelte</option>
160160
<option value="vue">Vue</option>
161161
<option value="htm">htm</option>
162+
<option value="react-remote-ui">React Remote UI</option>
162163
</Select>
163164
</section>
164165

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/** @jsxRuntime automatic */
2+
/** @jsxImportSource react */
3+
import {retain} from '@quilted/threads';
4+
5+
import {createRemoteReactComponent} from '@remote-ui/react';
6+
import {
7+
ButtonProperties,
8+
ModalProperties,
9+
RenderAPI,
10+
StackProperties,
11+
TextProperties,
12+
} from '../../types';
13+
import {useState} from 'react';
14+
import {createRoot, createRemoteRoot} from '@remote-ui/react';
15+
import {RemoteChannel} from '@remote-ui/core';
16+
17+
const Button = createRemoteReactComponent<
18+
'Button',
19+
ButtonProperties & {modal?: React.ReactNode}
20+
>('Button', {fragmentProps: ['modal']});
21+
22+
const Text = createRemoteReactComponent<'Text', TextProperties>('Text');
23+
const Stack = createRemoteReactComponent<'Stack', StackProperties>('Stack');
24+
const Modal = createRemoteReactComponent<
25+
'Modal',
26+
ModalProperties & {primaryAction?: React.ReactNode}
27+
>('Modal', {fragmentProps: ['primaryAction']});
28+
29+
export function renderUsingReactRemoteUI(
30+
channel: RemoteChannel,
31+
api: RenderAPI,
32+
) {
33+
retain(api);
34+
retain(channel);
35+
36+
const remoteRoot = createRemoteRoot(channel, {
37+
components: ['Button', 'Text', 'Stack', 'Modal'],
38+
});
39+
40+
createRoot(remoteRoot).render(<App api={api} />);
41+
remoteRoot.mount();
42+
}
43+
44+
function App({api}: {api: RenderAPI}) {
45+
return (
46+
<Stack spacing>
47+
<Text>
48+
Rendering example: <Text emphasis>{api.example}</Text>
49+
</Text>
50+
<Text>
51+
Rendering in sandbox: <Text emphasis>{api.sandbox}</Text>
52+
</Text>
53+
<Button modal={<CountModal {...api} />}>Open modal</Button>
54+
</Stack>
55+
);
56+
}
57+
58+
function CountModal({alert, closeModal}: RenderAPI) {
59+
const [count, setCount] = useState(0);
60+
61+
const primaryAction = (
62+
<Button
63+
onPress={() => {
64+
closeModal();
65+
}}
66+
>
67+
Close
68+
</Button>
69+
);
70+
71+
return (
72+
<Modal
73+
primaryAction={primaryAction}
74+
onClose={() => {
75+
if (count > 0) {
76+
alert(`You clicked ${count} times!`);
77+
}
78+
79+
setCount(0);
80+
}}
81+
>
82+
<Stack spacing>
83+
<Text>
84+
Click count: <Text emphasis>{count}</Text>
85+
</Text>
86+
<Button
87+
onPress={() => {
88+
setCount((count) => count + 1);
89+
}}
90+
>
91+
Click me!
92+
</Button>
93+
</Stack>
94+
</Modal>
95+
);
96+
}

examples/kitchen-sink/app/remote/examples/react.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const Button = createRemoteComponent('ui-button', ButtonElement, {
1919
onPress: {event: 'press'},
2020
},
2121
});
22+
2223
const Stack = createRemoteComponent('ui-stack', StackElement);
2324
const Modal = createRemoteComponent('ui-modal', ModalElement, {
2425
eventProps: {

examples/kitchen-sink/app/remote/iframe/sandbox.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {RemoteMutationObserver} from '@remote-dom/core/elements';
22
import {ThreadNestedIframe} from '@quilted/threads';
33

44
import '../elements.ts';
5-
import {render} from '../render.ts';
5+
import {render, renderLegacy} from '../render.ts';
66
import type {SandboxAPI} from '../../types.ts';
77

88
// We use the `@quilted/threads` library to create a “thread” for our iframe,
@@ -32,5 +32,8 @@ new ThreadNestedIframe<never, SandboxAPI>({
3232

3333
await render(root, api);
3434
},
35+
async renderLegacy(channel, api) {
36+
await renderLegacy(channel, api);
37+
},
3538
},
3639
});

examples/kitchen-sink/app/remote/render.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {RemoteChannel} from '@remote-ui/core';
12
import type {RenderAPI} from '../types.ts';
23

34
// Defines the custom elements available to render in the remote environment.
@@ -31,3 +32,10 @@ export async function render(root: Element, api: RenderAPI) {
3132
}
3233
}
3334
}
35+
36+
export async function renderLegacy(channel: RemoteChannel, api: RenderAPI) {
37+
const {renderUsingReactRemoteUI} = await import(
38+
'./examples/react-remote-ui.tsx'
39+
);
40+
return renderUsingReactRemoteUI(channel, api);
41+
}

examples/kitchen-sink/app/remote/worker/sandbox.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import '@remote-dom/react/polyfill';
44
import {ThreadWebWorker} from '@quilted/threads';
55

66
import '../elements.ts';
7-
import {render} from '../render.ts';
7+
import {render, renderLegacy} from '../render.ts';
88
import type {SandboxAPI} from '../../types.ts';
99

1010
// We use the `@quilted/threads` library to create a “thread” for our iframe,
@@ -27,5 +27,8 @@ new ThreadWebWorker<never, SandboxAPI>(self as any as Worker, {
2727

2828
await render(root, api);
2929
},
30+
async renderLegacy(channel, api) {
31+
await renderLegacy(channel, api);
32+
},
3033
},
3134
});

examples/kitchen-sink/app/types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {RemoteConnection} from '@remote-dom/core';
2+
import {RemoteChannel} from '@remote-ui/core';
23

34
/**
45
* Describes the technology used to sandbox the “remote” code, so that it does
@@ -25,7 +26,8 @@ export type RenderExample =
2526
| 'preact'
2627
| 'react'
2728
| 'svelte'
28-
| 'vue';
29+
| 'vue'
30+
| 'react-remote-ui';
2931

3032
/**
3133
* The object that the “host” page will pass to the “remote” environment. This
@@ -49,13 +51,19 @@ export interface RenderAPI {
4951
* alert.
5052
*/
5153
alert(content: string): Promise<void>;
54+
55+
/**
56+
* Closes the modal.
57+
*/
58+
closeModal(): void;
5259
}
5360

5461
/**
5562
*
5663
*/
5764
export interface SandboxAPI {
5865
render(connection: RemoteConnection, api: RenderAPI): Promise<unknown>;
66+
renderLegacy(channel: RemoteChannel, api: RenderAPI): Promise<unknown>;
5967
}
6068

6169
// These property and method types will be used by both the host and remote environments.

0 commit comments

Comments
 (0)