@causeeffect/foldkit-jsx
v0.0.2
Published
Classic JSX adapter for Foldkit's typed view builders
Readme
@causeeffect/foldkit-jsx
@causeeffect/foldkit-jsx adapts a Foldkit view's typed HtmlBuilder to a classic JSX factory. Foldkit remains responsible for HTML, event handlers, dispatch boundaries, and Submodels. The content mapper only translates JSX syntax into configurable factory calls.
Setup
In your application's directory, install the adapter and Foldkit's runtime dependencies:
npm install @causeeffect/foldkit-jsx [email protected] [email protected] @effect/[email protected]
npm install --save-dev @causeeffect/jsx-content-mapper vite@^8.1.4
npm install --save-dev --save-exact [email protected]This setup requires Node.js ^22.18.0 || >=24.11.0. TypeScript 7.1 content mappers
are experimental; stable TypeScript cannot check .gtsx files yet. The pinned
nightly above is the version used by the example and integration tests.
@effect/platform-browser supplies the browser services used by Foldkit's
application runtime.
Configure .gtsx
Create tsconfig.json in your application:
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "dom", "esnext.disposable"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["vite/client"]
},
"contentMappers": [
{
"package": "@causeeffect/jsx-content-mapper",
"extensions": [".gtsx"],
"options": {
"jsxRuntime": "classic",
"jsxFactory": "jsx.createElement",
"jsxFragmentFactory": "jsx.Fragment"
}
}
],
"include": ["src"]
}Create vite.config.mts with the same JSX options:
import { gtsx } from '@causeeffect/jsx-content-mapper/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
gtsx({
jsxRuntime: 'classic',
jsxFactory: 'jsx.createElement',
jsxFragmentFactory: 'jsx.Fragment',
}),
],
});Classic mode uses the factory expression in your file's scope. It does not inject a runtime import. Both TypeScript and Vite must use these classic options; the mapper's default generator runtime produces a different kind of element. The mapper guide documents the transformation and available options.
Add scripts to your application's package.json:
{
"type": "module",
"scripts": {
"dev": "vite",
"typecheck": "tsc --noEmit --runExternalCode",
"build": "npm run typecheck && vite build",
"preview": "vite preview"
}
}--runExternalCode allows TypeScript to start the mapper's Node process. Vite
transforms .gtsx for the browser, while the typecheck script checks the
original files. No jsx compiler option is required.
Views
Create jsx inside each view using the h argument supplied by Foldkit:
import { createJsx } from '@causeeffect/foldkit-jsx';
import type { Document, HtmlBuilder } from 'foldkit/html';
type Message = { readonly _tag: 'ClickedIncrement' };
type Model = { readonly count: number };
export function view(model: Model, h: HtmlBuilder<Message>): Document {
const jsx = createJsx(h);
return {
title: `Counter: ${model.count}`,
body: (
<main Class="counter">
<p>{model.count}</p>
<button OnClick={{ _tag: 'ClickedIncrement' }}>Increment</button>
</main>
),
};
}Intrinsic props use Foldkit's constructor names and parameter types directly.
Class="counter" calls h.Class('counter'), OnClick={message} calls
h.OnClick(message), and OnInput={(value) => message(value)} calls h.OnInput
with a callback whose value is contextually typed as string. Event Messages
must belong to the current view. Names keep Foldkit's exact casing, such as
Id, Role, AriaLabel, Value, Checked, and Style; lowercase class and
onClick are rejected.
The adapter derives prop types from HtmlBuilder<Message> and discovers the
corresponding constructors on the supplied builder. It contains no copied HTML
attribute table or event implementation. The generic content mapper preserves
the prop names in its classic factory calls and has no Foldkit dependency.
undefined props are omitted; boolean attribute values including false are
passed through. AllowDrop is a boolean switch for the zero-argument
h.AllowDrop() helper. The attributes prop remains an escape hatch for
childAttributes, required multi-argument helpers such as h.DataAttribute,
h.Attribute, and h.OnCutText, and optional controls such as h.OnClick
options. Its attributes are applied before direct props, using Foldkit's normal
attribute combination behavior.
key delegates to h.keyed(tag), while Key uses h.Key directly. Numbers and bigints become text; nested arrays and fragments flatten; null, undefined, and booleans render nothing. A fragment is a list of children, so a root Foldkit view should return an element or null. Void elements accept no children. A textarea uses Value={text} and rejects both children and InnerHTML.
Ordinary function components receive their props and a children array when JSX supplies children. Required props, declared child types, and the component's return type remain checked. Declare an optional children prop for components that render nested markup.
Submodels
Define a child with Foldkit's normal Submodel.defineView. Its .gtsx view creates its own adapter from its child builder:
import { Submodel } from 'foldkit';
import { createJsx } from '@causeeffect/foldkit-jsx';
export const childView = Submodel.defineView<ChildModel, ChildMessage>(
(model, h) => {
const jsx = createJsx(h);
return <button OnClick={childMessage}>{model.count}</button>;
},
);Embed that child through the parent's bound component:
const jsx = createJsx(h);
return (
<section>
<jsx.Submodel
slotId="counter"
model={model.counter}
view={childView}
toParentMessage={(message) => GotCounterMessage({ message })}
/>
</section>
);jsx.Submodel delegates directly to h.submodel. Foldkit infers the child's Model and Message from its branded view. Views that declare per-render inputs require viewInputs of that exact type; views without inputs reject that prop. Each embed site needs a distinct slotId. Update routing still uses Foldkit's Update.foldChild, as demonstrated by the example.
The adapter never stores a process-wide builder. Create it in each root or child view and pass builders to extracted helpers as Foldkit recommends.
Run your application
Keep Foldkit's normal Model, init, and update functions, and export your JSX View
from src/app.gtsx. Start the application from src/main.ts:
import { Runtime } from 'foldkit';
import { Model, init, update, view } from './app.gtsx';
const container = document.getElementById('root');
if (!container) throw new Error('The application container is missing.');
Runtime.run(Runtime.makeApplication({ Model, init, update, view, container }));Add the container and module entry point to your root index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Foldkit with GTSX</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Check the app and start Vite from your application directory:
npm run typecheck
npm run devOpen the URL Vite prints. Use npm run build to check and bundle the app, then
npm run preview to serve that build locally.
For complete Model, Message, update, and Submodel implementations, see the
runnable Foldkit example,
including its
application View
and
counter Submodel.
The
VS Code extension guide
explains how to enable hover types, completion, diagnostics, and navigation for
your .gtsx files.
