diff --git a/html/deno.jsonc b/html/deno.jsonc index dce51b9b3..eb0066225 100644 --- a/html/deno.jsonc +++ b/html/deno.jsonc @@ -5,6 +5,10 @@ // JSDOM dependencies require env. "test": "deno test --allow-env" }, - "imports": { + "imports": {}, + "compilerOptions": { + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "h.fragment" } } diff --git a/html/src/index.ts b/html/src/index.ts index 0f26707fd..71aec7e0e 100644 --- a/html/src/index.ts +++ b/html/src/index.ts @@ -1,3 +1,3 @@ export { render, setEventSanitizer, setNodeSanitizer } from "./render.ts"; export { debug, setDebug } from "./logger.ts"; -export { Fragment, h, type VNode } from "./jsx.ts"; +export { h, type VNode } from "./jsx.ts"; diff --git a/html/src/jsx.ts b/html/src/jsx.ts index ee57b7779..046d47d00 100644 --- a/html/src/jsx.ts +++ b/html/src/jsx.ts @@ -1,15 +1,15 @@ import type { Cell, Stream } from "@commontools/runner"; -// declare global { -// namespace JSX { -// interface IntrinsicElements { -// [elemName: string]: any; -// } -// } -// } +declare global { + namespace JSX { + interface IntrinsicElements { + [elemName: string]: any; + } + } +} -export const Fragment = "Fragment"; +const FRAGMENT_ELEMENT = "common-fragment"; -export function h( +export const h = Object.assign(function h( name: string | ((...args: any[]) => any), props: { [key: string]: any } | null, ...children: Child[] @@ -27,7 +27,11 @@ export function h( children: children.flat(), }; } -} +}, { + fragment({ children }: { children: Child[] }) { + return h(FRAGMENT_ELEMENT, null, children); + }, +}); /** * Dynamic properties. Can either be string type (static) or a Mustache diff --git a/html/test/jsx.tsx b/html/test/jsx.tsx new file mode 100644 index 000000000..92e264a5a --- /dev/null +++ b/html/test/jsx.tsx @@ -0,0 +1,64 @@ +import { beforeEach, describe, it } from "@std/testing/bdd"; +import * as assert from "./assert.ts"; +import { h } from "../src/jsx.ts"; + +describe("jsx dom fragments support", () => { + it("dom fragments should work", () => { + const fragment = ( + <> +

Hello world

+ + ); + + assert.matchObject( + fragment, + +

Hello world

+
, + ); + }); + + it("dom fragments with multiple children", () => { + const fragment = ( + <> +

Grocery List

+ + + ); + + assert.matchObject( + fragment, + +

Grocery List

+ +
, + ); + }); + + it("fragments inside the element", () => { + const grocery = ( + <> +

Grocery List

+ + + ); + + assert.matchObject( +
{grocery}
, +
+ +

Grocery List

+ +
+
, + ); + }); +}); diff --git a/jumble/src/components/DitherCube.tsx b/jumble/src/components/DitherCube.tsx index 89314fff4..75bb27180 100644 --- a/jumble/src/components/DitherCube.tsx +++ b/jumble/src/components/DitherCube.tsx @@ -388,7 +388,6 @@ export const DitheredCube = ({ - {/* @ts-expect-error DitheringPass is properly extended but TS doesn't recognize it */} diff --git a/recipes/counter.tsx b/recipes/counter.tsx new file mode 100644 index 000000000..9008fa8d8 --- /dev/null +++ b/recipes/counter.tsx @@ -0,0 +1,46 @@ +// deno-lint-ignore-file jsx-no-useless-fragment +import { h } from "@commontools/html"; +import { + derive, + handler, + NAME, + recipe, + schema, + str, + UI, +} from "@commontools/builder"; + +// Different way to define the same schema, using 'schema' helper function, +// let's as leave off `as const satisfies JSONSchema`. +const model = schema({ + type: "object", + properties: { + value: { type: "number", default: 0, asCell: true }, + }, + default: { value: 0 }, +}); + +const increment = handler({}, model, (_, state) => { + state.value.set(state.value.get() + 1); +}); + +const decrement = handler({}, model, (_, state) => { + state.value.set(state.value.get() - 1); +}); + +export default recipe(model, model, (cell) => { + return { + [NAME]: str`Simple counter: ${derive(cell.value, String)}`, + [UI]: ( +
+ + {/* use html fragment to test that it works */} + <> + {cell.value} + + +
+ ), + value: cell.value, + }; +}); diff --git a/runner/src/runtime/local-build.ts b/runner/src/runtime/local-build.ts index 183a4f01a..0d4e86e34 100644 --- a/runner/src/runtime/local-build.ts +++ b/runner/src/runtime/local-build.ts @@ -138,7 +138,7 @@ export const tsToExports = async ( strict: true, jsx: ts.JsxEmit.React, jsxFactory: "h", - jsxFragmentFactory: "Fragment", + jsxFragmentFactory: "h.fragment", esModuleInterop: true, sourceMap: true, // Enable source map generation inlineSources: false, // Don't include original source in source maps diff --git a/ui/src/components/common-fragment.ts b/ui/src/components/common-fragment.ts new file mode 100644 index 000000000..7fc7dd126 --- /dev/null +++ b/ui/src/components/common-fragment.ts @@ -0,0 +1,13 @@ +export class CommonFragmentElement extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }) + // Add a slot to display the children + .appendChild(document.createElement("slot")); + + // Tell engine to ignore this element for layout purposes + this.style.display = 'contents'; + } +} + +customElements.define("common-fragment", CommonFragmentElement); diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 707900c7a..c5644a3b5 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -4,6 +4,7 @@ export * as CommonCharm from "./common-charm.ts"; export * as CommonDatatable from "./common-datatable.ts"; export * as CommonDict from "./common-dict.ts"; export * as CommonForm from "./common-form.ts"; +export * as CommonFragment from "./common-fragment.ts"; export * as CommonGoogleOauth from "./common-google-oauth.ts"; export * as CommonGrid from "./common-grid.ts"; export * as CommonHeroLayout from "./common-hero-layout.ts";