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}
,
+ ,
+ );
+ });
+});
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";