Skip to content

Commit

Permalink
feat(react): add react turbo fragments support
Browse files Browse the repository at this point in the history
  • Loading branch information
tchak committed Apr 25, 2024
1 parent de607e8 commit ad642ea
Show file tree
Hide file tree
Showing 37 changed files with 1,782 additions and 2,520 deletions.
42 changes: 17 additions & 25 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@
"description": "An implementation of @hotwired/turbo based on remix and morphdom",
"license": "MIT",
"repository": "tchak/coldwired",
"keywords": [
"remix",
"router",
"turbo",
"stimulus"
],
"keywords": ["turbo", "stimulus"],
"scripts": {
"turbo": "turbo run test lint build",
"build": "turbo run build --force",
"test": "turbo run test",
"test:webkit": "turbo run test:webkit",
"test:firefox": "turbo run test:firefox",
"lint": "turbo run lint",
"clean": "turbo run clean",
"prepare": "pnpm run build",
Expand All @@ -22,33 +19,28 @@
"devDependencies": {
"@axodotdev/oranda": "^0.6.1",
"@changesets/cli": "^2.27.1",
"@remix-run/node": "^2.7.2",
"@testing-library/dom": "^9.3.4",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitest/browser": "^1.3.1",
"@vitest/ui": "^1.3.1",
"@testing-library/dom": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"@vitest/browser": "^1.5.2",
"@vitest/ui": "^1.5.2",
"c8": "^9.1.0",
"del-cli": "^5.1.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"jsdom": "^24.0.0",
"eslint": "^8.50.0",
"eslint-config-prettier": "^8.0",
"npm-run-all": "^4.1.5",
"playwright": "^1.41.2",
"playwright": "^1.43.1",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^4.12.0",
"turbo": "^1.12.4",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vitest": "^1.3.1",
"webdriverio": "^8.32.3"
"rollup": "^4.16.4",
"turbo": "^1.13.2",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vitest": "^1.5.2"
},
"engines": {
"node": ">=16"
},
"packageManager": "pnpm@8.6.9",
"packageManager": "pnpm@8.15.4",
"prettier": {
"singleQuote": true,
"printWidth": 100
Expand Down
42 changes: 18 additions & 24 deletions packages/actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
"name": "@coldwired/actions",
"description": "DOM manipulation actions based on morphdom",
"license": "MIT",
"files": [
"dist"
],
"files": ["dist"],
"main": "./dist/index.cjs.js",
"module": "./dist/index.es.js",
"types": "./dist/types/src/index.d.ts",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js"
}
},
"type": "module",
"version": "0.11.2",
"keywords": [
"turbo"
],
"keywords": ["turbo"],
"scripts": {
"build": "run-s clean build:*",
"build:vite": "vite build",
"build:tsc": "tsc --emitDeclarationOnly",
"dev": "vitest",
"test": "vitest run",
"test:webkit": "vitest run --browser.name=webkit",
"test:firefox": "vitest run --browser.name=firefox",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage",
"lint": "run-s lint:*",
Expand All @@ -31,39 +31,30 @@
"clean": "del dist coverage node_modules/.vite"
},
"dependencies": {
"@coldwired/react": "*",
"@coldwired/utils": "^0.11.1",
"morphdom": "^2.7.2",
"tiny-invariant": "^1.3.2"
"morphdom": "^2.7.2"
},
"engines": {
"node": ">=16"
},
"packageManager": "pnpm@8.6.9",
"packageManager": "pnpm@8.15.4",
"prettier": {
"singleQuote": true,
"printWidth": 100
},
"eslintConfig": {
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-redeclare": "off"
},
"overrides": [
{
"files": [
"vite.config.js",
"vitest.config.ts"
],
"files": ["vite.config.js", "vitest.config.ts"],
"env": {
"node": true
}
Expand All @@ -83,7 +74,10 @@
}
},
"devDependencies": {
"@hotwired/turbo": "^7.3.0",
"intersection-observer": "^0.12.2"
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tiny-invariant": "^1.3.2"
}
}
208 changes: 208 additions & 0 deletions packages/actions/src/actions-container.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { getByText, fireEvent, waitFor } from '@testing-library/dom';
import { StrictMode, useState } from 'react';

import { NAME_ATTRIBUTE, PROPS_ATTRIBUTE, Manifest, encodeProps } from '@coldwired/react';

import { Actions } from '.';

describe('@coldwired/actions', () => {
let actions: Actions;

const DEFAULT_TAG_NAME = 'turbo-fragment';
const Counter = ({ label }: { label?: string }) => {
const [count, setCount] = useState(0);
return (
<div>
<p>
{label ?? 'Count'}: {count}
</p>
<button onClick={() => setCount((count) => count + 1)}>Increment</button>
</div>
);
};
const manifest: Manifest = { Counter };

beforeEach(async () => {
actions?.disconnect();
actions = new Actions({
element: document.documentElement,
container: { loader: (name) => Promise.resolve(manifest[name]) },
});

document.body.innerHTML = `<div id="main"><${DEFAULT_TAG_NAME} id="frag-1" class="loading"><div class="title">Hello</div></${DEFAULT_TAG_NAME}></div><div id="root"></div>`;
actions.observe();
await actions.ready();
await actions.mount(document.getElementById('root')!, StrictMode);
});

function layout(content: string) {
return `<div id="main">${content}</div><div id="root"></div>`;
}

it('render with container', async () => {
expect(document.body.innerHTML).toEqual(
layout(
`<${DEFAULT_TAG_NAME} id="frag-1"><div class="title">Hello</div></${DEFAULT_TAG_NAME}>`,
),
);
expect(actions.container?.getCache().size).toEqual(1);

actions.update({
targets: '#main',
fragment: `<${DEFAULT_TAG_NAME} id="frag-1">Yolo</${DEFAULT_TAG_NAME}>`,
});
await actions.ready();

expect(document.body.innerHTML).toEqual(
layout(`<${DEFAULT_TAG_NAME} id="frag-1">Yolo</${DEFAULT_TAG_NAME}>`),
);

actions.replace({
targets: '#frag-1',
fragment: `<${DEFAULT_TAG_NAME} id="frag-1"><p>plop</p></${DEFAULT_TAG_NAME}>`,
});
await actions.ready();

expect(document.body.innerHTML).toEqual(
layout(`<${DEFAULT_TAG_NAME} id="frag-1"><p>plop</p></${DEFAULT_TAG_NAME}>`),
);

actions.update({
targets: '#frag-1',
fragment: `<react-component ${NAME_ATTRIBUTE}="Counter"></react-component>`,
});
await actions.ready();
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 0</p><button>Increment</button></div></${DEFAULT_TAG_NAME}>`,
),
);
});

fireEvent.click(getByText(document.body, 'Increment'));
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 1</p><button>Increment</button></div></${DEFAULT_TAG_NAME}>`,
),
);
});
fireEvent.click(getByText(document.body, 'Increment'));
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 2</p><button>Increment</button></div></${DEFAULT_TAG_NAME}>`,
),
);
});

actions.update({
targets: '#main',
fragment: `<${DEFAULT_TAG_NAME} id="frag-1"><react-component ${NAME_ATTRIBUTE}="Counter"></react-component></${DEFAULT_TAG_NAME}><${DEFAULT_TAG_NAME} id="frag-2">Test</${DEFAULT_TAG_NAME}>`,
});
await actions.ready();
expect(actions.container?.getCache().size).toEqual(2);
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 2</p><button>Increment</button></div></${DEFAULT_TAG_NAME}><${DEFAULT_TAG_NAME} id="frag-2">Test</${DEFAULT_TAG_NAME}>`,
),
);
});

actions.update({
targets: '#main',
fragment: `<${DEFAULT_TAG_NAME} id="frag-1"><react-component ${NAME_ATTRIBUTE}="Counter"></react-component></${DEFAULT_TAG_NAME}><${DEFAULT_TAG_NAME} id="frag-2">Test 23</${DEFAULT_TAG_NAME}>`,
});
await actions.ready();
expect(actions.container?.getCache().size).toEqual(2);
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 2</p><button>Increment</button></div></${DEFAULT_TAG_NAME}><${DEFAULT_TAG_NAME} id="frag-2">Test 23</${DEFAULT_TAG_NAME}>`,
),
);
});

actions.update({
targets: '#main',
fragment: `<${DEFAULT_TAG_NAME} id="frag-1"><react-component ${NAME_ATTRIBUTE}="Counter"></react-component></${DEFAULT_TAG_NAME}>`,
});
await actions.ready();
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 2</p><button>Increment</button></div></${DEFAULT_TAG_NAME}>`,
),
);
expect(actions.container?.getCache().size).toEqual(1);
});

actions.update({
targets: '#main',
fragment: `<input name="age" /><${DEFAULT_TAG_NAME} id="frag-1"><react-component ${NAME_ATTRIBUTE}="Counter"></react-component></${DEFAULT_TAG_NAME}>`,
});
await actions.ready();
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<input name="age"><${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 2</p><button>Increment</button></div></${DEFAULT_TAG_NAME}>`,
),
);
expect(actions.container?.getCache().size).toEqual(1);
});

actions.update({
targets: '#main',
fragment: `<section><input name="age" /></section><${DEFAULT_TAG_NAME} id="frag-1"><react-component ${NAME_ATTRIBUTE}="Counter"></react-component></${DEFAULT_TAG_NAME}>`,
});
await actions.ready();
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<section><input name="age"></section><${DEFAULT_TAG_NAME} id="frag-1"><div><p>Count: 2</p><button>Increment</button></div></${DEFAULT_TAG_NAME}>`,
),
);
expect(actions.container?.getCache().size).toEqual(1);
});

actions.update({
targets: '#main',
fragment: `<section><${DEFAULT_TAG_NAME} id="frag-1"><react-component ${NAME_ATTRIBUTE}="Counter" label="My Count"></react-component></${DEFAULT_TAG_NAME}></section>`,
});
await actions.ready();
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<section><${DEFAULT_TAG_NAME} id="frag-1"><div><p>My Count: 2</p><button>Increment</button></div></${DEFAULT_TAG_NAME}></section>`,
),
);
expect(actions.container?.getCache().size).toEqual(1);
});

fireEvent.click(getByText(document.body, 'Increment'));
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<section><${DEFAULT_TAG_NAME} id="frag-1"><div><p>My Count: 3</p><button>Increment</button></div></${DEFAULT_TAG_NAME}></section>`,
),
);
});

actions.update({
targets: '#main',
fragment: `<section><${DEFAULT_TAG_NAME} id="frag-1"><react-component ${NAME_ATTRIBUTE}="Counter" ${PROPS_ATTRIBUTE}="${encodeProps({ label: 'My New Count' })}"></react-component></${DEFAULT_TAG_NAME}></section>`,
});
await actions.ready();
await waitFor(() => {
expect(document.body.innerHTML).toEqual(
layout(
`<section><${DEFAULT_TAG_NAME} id="frag-1"><div><p>My New Count: 3</p><button>Increment</button></div></${DEFAULT_TAG_NAME}></section>`,
),
);
expect(actions.container?.getCache().size).toEqual(1);
});
});
});
2 changes: 1 addition & 1 deletion packages/actions/src/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('@coldwired/actions', () => {
await Promise.resolve();

actions.morph(from, '<div>World</div>');
expect(from.outerHTML).toEqual('<div>World</div>');
expect(from.outerHTML).toEqual('<div class="">World</div>');
});

it('should preserve removed classes', async () => {
Expand Down
Loading

0 comments on commit ad642ea

Please sign in to comment.