[READ-ONLY] Mirror of https://github.com/crutchcorn/causeeffect. JSX for Effect, with generator types preserved. causeeffect.website

Foldkit with .gtsx Views and Submodels #

A runnable Foldkit application using the TypeScript 7.1 content mapper and a separate, Foldkit-specific JSX adapter. Both the application View and reusable counter Submodel live in .gtsx files. Two instances have independent state and message boundaries; a parent action resets both through their child updates.

This is a private reference application in the CauseEffect repository, not a published npm package. To add Foldkit JSX to your own application, follow the adapter's npm setup.

Run the example #

With Node.js 24.11 or newer and PNPM 10 installed:

git clone https://github.com/crutchcorn/causeeffect.git
cd causeeffect
pnpm install
cd examples/foldkit
pnpm dev

Vite prints the local URL. The example pins Foldkit 0.165.0, Effect 4.0.0, and the TypeScript 7.1.0-dev.20261002.1 nightly.

The example's dev, build, typecheck, and test scripts build their workspace dependencies first, so they work from a clean checkout immediately after install.

pnpm typecheck
pnpm test
pnpm build

Run these commands from examples/foldkit. To serve the built application locally, run pnpm preview after pnpm build.

The typecheck runs the actual nightly compiler with --runExternalCode so it can start the content mapper. Vite uses the same transformation options for browser builds and Vitest. The Scene tests click the rendered counter controls and verify child message routing and parent resets without requiring a DOM. Four additional tests invoke TypeScript 7.1 against invalid .gtsx fixtures and verify source locations and contextual callback types for direct attributes, messages, Submodel configurations, and component props and children.

This example uses ordinary Vite reloads. Foldkit's optional @foldkit/vite-plugin can be added for Model preservation across source reloads.

Views #

Foldkit supplies each View's Message-typed h builder. Bind the adapter inside that View:

import { createJsx } from '@causeeffect/foldkit-jsx';

export const view = (model: Model, h: HtmlBuilder<Message>): Document => {
  const jsx = createJsx(h);

  return {
    title: `Count: ${model.count}`,
    body: (
      <button OnClick={Message.ClickedIncrement()} Class="increment">
        {model.count}
      </button>
    ),
  };
};

Write Foldkit helper names directly as JSX props. The adapter turns OnClick={message} into h.OnClick(message), Class="increment" into h.Class('increment'), and AriaLabel="Increment" into h.AriaLabel('Increment'). Prop names match the helpers on HtmlBuilder<Message> exactly, and their value types come from the helper signatures. The View's Message type checks the event values, and Foldkit dispatches them through that View's boundary.

Use attributes for helpers that require multiple arguments, such as h.OnCutText, h.DataAttribute, and h.Attribute, or event options:

<button attributes={[h.OnClick(message, { defaultAction: 'Prevent' })]}>
  Reset
</button>

JSX creates ordinary Foldkit Html, so root Views keep returning a Foldkit Document and child Views keep returning Html.

Submodels #

src/counter.gtsx uses Submodel.defineView<Model, Message, ViewInputs> unchanged. It builds its own createJsx(h) factory and owns its Model, Message union, and update function.

The parent embeds the branded child View with the adapter's component:

<jsx.Submodel
  slotId="left"
  model={model.left}
  view={Counter.view}
  viewInputs={{ label: 'Left counter' }}
  toParentMessage={(message) =>
    Message.GotCounterMessage({
      counterId: 'left',
      message,
    })
  }
/>

This delegates to h.submodel, retaining Foldkit's instance boundaries and Message typing. The parent routes wrappers into Update.foldChild; it does not write the counter's state directly. Parent-driven resets call Counter.reset through Update.foldChildStep and compose the two steps with Update.combine.

The example follows Foldkit's View, Submodel, and Runtime contracts.

Framework-neutral mapping #

The content mapper has no dependency on Foldkit. Both tsconfig.json and vite.config.mts select a conventional classic JSX transformation:

{
  "jsxRuntime": "classic",
  "jsxFactory": "jsx.createElement",
  "jsxFragmentFactory": "jsx.Fragment"
}

For example, <button OnClick={Message.ClickedReset()} Class="reset">Reset</button> maps to jsx.createElement('button', { OnClick: Message.ClickedReset(), Class: 'reset' }, 'Reset'). The lexical factory comes from @causeeffect/foldkit-jsx, which converts the props into Foldkit attributes. The mapper passes props through without knowing about Foldkit. Babel parses the JSX and the mapper retains source locations for compiler diagnostics.

Foldkit Views are pure synchronous functions, so this example uses ordinary Views rather than running generators during rendering. The mapper's default generator-preserving runtime remains available independently.