Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/clever-glasses-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'example-kitchen-sink': patch
'@remote-dom/polyfill': patch
'@remote-dom/signals': patch
'@remote-dom/compat': patch
'@remote-dom/core': patch
---

Fix elements are inserted or removed at incorrect positions, leading to UI inconsistencies.
18 changes: 18 additions & 0 deletions e2e/mutations.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {expect, test} from '@playwright/test';

const testSet: Array<[string, string, string]> = [
['iframe', 'react-mutations-1', 'Data: 1\nData: 2\nData: 3\nData: 4'],
['iframe', 'react-mutations-2', 'Data: 1\nData: 2\nData: 3\nData: 4'],
['iframe', 'react-mutations-3', 'Data: 1\nData: 2'],
];

testSet.forEach(([sandbox, example, expectedText]) => {
test(`mutations are applied correctly with ${sandbox} sandbox and ${example} example`, async ({
page,
}) => {
await page.goto(`/?sandbox=${sandbox}&example=${example}`);
await expect(page.getByTestId('test-done')).toBeAttached();
const testStack = page.getByTestId('test-stack');
expect(await testStack.innerText()).toBe(expectedText);
});
});
8 changes: 8 additions & 0 deletions examples/kitchen-sink/app/host/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ export function Button({
}

export function Stack({
testId,
spacing,
children,
}: {children?: ComponentChildren} & StackProperties) {
return (
<div
data-testid={testId}
class={['Stack', spacing && 'Stack--spacing'].filter(Boolean).join(' ')}
>
{children}
Expand Down Expand Up @@ -160,6 +162,9 @@ export function ControlPanel({
<option value="vue">Vue</option>
<option value="htm">htm</option>
<option value="react-remote-ui">React Remote UI</option>
<option value="react-mutations-1">React Mutations 1</option>
<option value="react-mutations-2">React Mutations 2</option>
<option value="react-mutations-3">React Mutations 3</option>
</Select>
</section>

Expand Down Expand Up @@ -194,6 +199,9 @@ const EXAMPLE_FILE_NAMES = new Map<RenderExample, string>([
['htm', 'htm.ts'],
['preact', 'preact.tsx'],
['react', 'react.tsx'],
['react-mutations-1', 'react-mutations.tsx'],
['react-mutations-2', 'react-mutations.tsx'],
['react-mutations-3', 'react-mutations.tsx'],
['svelte', 'App.svelte'],
['vue', 'App.vue'],
]);
Expand Down
3 changes: 3 additions & 0 deletions examples/kitchen-sink/app/host/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const ALLOWED_EXAMPLE_VALUES = new Set<RenderExample>([
'react',
'svelte',
'vue',
'react-mutations-1',
'react-mutations-2',
'react-mutations-3',
]);

export function createState(
Expand Down
1 change: 1 addition & 0 deletions examples/kitchen-sink/app/remote/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const Modal = createRemoteElement<
export const Stack = createRemoteElement<StackProperties>({
properties: {
spacing: {type: Boolean},
testId: {type: String},
},
});

Expand Down
110 changes: 110 additions & 0 deletions examples/kitchen-sink/app/remote/examples/react-mutations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/** @jsxRuntime automatic */
/** @jsxImportSource react */

import {createRemoteComponent} from '@remote-dom/react';
import {createRoot} from 'react-dom/client';

import {useEffect, useState} from 'react';
import type {RenderAPI} from '../../types.ts';
import {Stack as StackElement, Text as TextElement} from '../elements.ts';
import {useRenders} from './utils/react-hooks.ts';

const Stack = createRemoteComponent('ui-stack', StackElement);
const Text = createRemoteComponent('ui-text', TextElement);

const data1 = <Text key="d1">Data: 1</Text>;
const data2 = <Text key="d2">Data: 2</Text>;
const data3 = <Text key="d3">Data: 3</Text>;
const data4 = <Text key="d4">Data: 4</Text>;
const done = <Stack key="done" testId="test-done" />;
const loading1 = <Text key="l1">Loading: 1</Text>;
const loading2 = <Text key="l2">Loading: 2</Text>;

const Example1 = () => {
const renders = useRenders(2);

return (
<Stack spacing testId="test-stack">
<>
{renders === 1 && loading1}
{renders === 2 && (
<>
{data1}
{data2}
</>
)}
</>
<>
{renders === 1 && loading2}
{renders === 2 && (
<>
{data3}
{data4}
</>
)}
</>
{renders === 2 && done}
</Stack>
);
};

const Example2 = () => {
const [loading, setLoading] = useState(true);

useEffect(() => {
setLoading(false);
}, [setLoading]);

return (
<Stack spacing testId="test-stack">
<>
{loading && loading1}
{!loading && (
<>
{data1}
{data2}
</>
)}
</>
<>
{loading && loading2}
{!loading && (
<>
{data3}
{data4}
</>
)}
</>
{!loading && done}
</Stack>
);
};

const Example3 = () => {
const renders = useRenders(2);

return (
<Stack spacing testId="test-stack">
{renders === 2 && data1}
{data2}
{renders === 1 && data3}
{renders === 2 && done}
</Stack>
);
};

function App({api}: {api: RenderAPI}) {
const {example} = api;

return example === 'react-mutations-1' ? (
<Example1 />
) : example === 'react-mutations-2' ? (
<Example2 />
) : example === 'react-mutations-3' ? (
<Example3 />
) : null;
}

export function renderUsingReact(root: Element, api: RenderAPI) {
createRoot(root).render(<App api={api} key={api.example} />);
}
19 changes: 19 additions & 0 deletions examples/kitchen-sink/app/remote/examples/utils/react-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {useEffect, useState} from 'react';

export const useRenders = (max: number) => {
const [renders, setRenders] = useState(1);

useEffect(() => {
if (renders >= max) {
return;
}

const timeout = setTimeout(() => {
setRenders((r) => r + 1);
}, 200);

return () => clearTimeout(timeout);
}, [setRenders, renders]);

return renders;
};
6 changes: 6 additions & 0 deletions examples/kitchen-sink/app/remote/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export async function render(root: Element, api: RenderAPI) {
const {renderUsingReact} = await import('./examples/react.tsx');
return renderUsingReact(root, api);
}
case 'react-mutations-1':
case 'react-mutations-2':
case 'react-mutations-3': {
const {renderUsingReact} = await import('./examples/react-mutations.tsx');
return renderUsingReact(root, api);
}
case 'svelte': {
const {renderUsingSvelte} = await import('./examples/svelte.ts');
return renderUsingSvelte(root, api);
Expand Down
8 changes: 7 additions & 1 deletion examples/kitchen-sink/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ export type RenderExample =
| 'htm'
| 'preact'
| 'react'
| 'react-mutations'
| 'svelte'
| 'vue'
| 'react-remote-ui';
| 'react-remote-ui'
| 'react-mutations-1'
| 'react-mutations-2'
| 'react-mutations-3';

/**
* The object that the “host” page will pass to the “remote” environment. This
Expand Down Expand Up @@ -123,4 +127,6 @@ export interface StackProperties {
* Whether children should have space between them.
*/
spacing?: boolean;

testId?: string;
}
Loading