Form Chunks
A JSON form definition does not have to live in a single file. A widget entry written as
{ "$ref": "./tabs/alert.form-chunk.json" } is a chunk reference: it names another JSON file that
holds the real widget. Before rendering, you call resolveChunkRefs. It fetches every reference,
puts the loaded widget in its place, and returns a plain form definition that <gui-form> accepts.
This is a JSON API feature. On the Programmatic API you split a form definition with ordinary ES module imports.
Why split a form
Section titled “Why split a form”A form definition grows with the form. A single file of a few thousand lines is hard to review, hard to diff, and slow to validate. Chunks let one file describe one section, so each file stays small enough to read on its own and a change to one section produces a one-file diff.
A tabbed form is the clearest case. The parent file declares the tabs layout and one chunk
reference per tab, and each tab lives in its own file. Adding a tab then means adding a file and one
line, instead of editing a file that holds every tab at once.
The parent file
Section titled “The parent file”The parent is an ordinary form definition. A chunk reference goes anywhere a widget can go:
{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "layout", "type": "tabs", "props": { "defaultOpen": "tabAlert", "tabs": [ { "label": "Alert Component", "uid": "tabAlert" }, { "label": "Button Component", "uid": "tabButton" } ] }, "children": [ { "$ref": "./tabs/alert.form-chunk.json" }, { "$ref": "./tabs/button.form-chunk.json" } ] } ]}$ref must be the object’s only key. An object that carries $ref plus any sibling key is treated
as a normal widget and is not resolved.
The chunk file
Section titled “The chunk file”A chunk file holds one widget object, not an array. It can declare its own $schema so an editor
validates it on its own:
{ "$schema": "https://golemui.com/schemas/gui/layout-widget.schema.json", "uid": "tabAlert", "kind": "layout", "type": "flex", "children": [ { "kind": "display", "type": "alert", "props": { "text": "Check your details", "level": "warning" } } ]}resolveChunkRefs strips $schema from every chunk it loads, because the form decoder does not
expect it. The parent file keeps its own $schema.
A chunk can contain chunk references of its own, and they are resolved too.
Resolving the references
Section titled “Resolving the references”resolveChunkRefs is exported from @golemui/gui-shared:
function resolveChunkRefs( formJson: Record<string, unknown>, baseUrl: string,): Promise<Record<string, unknown>>;It takes the parsed parent form and the URL the references are resolved against. It returns a new
object and does not modify the input. Fetch the form, resolve it, then pass the result as formDef:
import { resolveChunkRefs } from '@golemui/gui-shared';
export async function loadForm(path: string) { const baseUrl = new URL(path, window.location.href).href; const json = await fetch(baseUrl).then((response) => response.json()); return resolveChunkRefs(json, baseUrl);}The form definition now arrives after an await, so the component has to render nothing until it is there:
import { useEffect, useState } from 'react';import { GuiForm } from '@golemui/gui-react';import { loadForm } from './load-form';
export function MyForm() { const [formDef, setFormDef] = useState(undefined);
useEffect(() => { loadForm('/forms/settings.form.json').then(setFormDef); }, []);
if (!formDef) { return null; }
return <GuiForm config={{ formDef }} />;}import { Component } from '@angular/core';import { FormComponent } from '@golemui/gui-angular';import { loadForm } from './load-form';
@Component({ imports: [FormComponent], selector: 'app-my-form', template: ` @if (config) { <gui-form [config]="config"></gui-form> } `,})export class MyForm { protected config: { formDef: unknown } | undefined;
constructor() { this.loadConfig(); }
private async loadConfig() { const formDef = await loadForm('/forms/settings.form.json'); this.config = { formDef }; }}import { LitElement, html, nothing } from 'lit';import { customElement, state } from 'lit/decorators.js';import '@golemui/gui-lit';import { loadForm } from './load-form';
@customElement('my-form')export class MyForm extends LitElement { @state() config: { formDef: unknown } | undefined;
override createRenderRoot() { return this; }
override async connectedCallback() { super.connectedCallback(); const formDef = await loadForm('/forms/settings.form.json'); this.config = { formDef }; }
override render() { if (!this.config) { return nothing; } return html`<gui-form .config=${this.config}></gui-form>`; }}<script setup lang="ts">import { onMounted, ref } from 'vue';import { GuiForm } from '@golemui/gui-vue';import { loadForm } from './load-form';
const config = ref(undefined);
onMounted(async () => { const formDef = await loadForm('/forms/settings.form.json'); config.value = { formDef };});</script>
<template> <GuiForm v-if="config" :config="config" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';import { loadForm } from './load-form';
const form = document.getElementById('app-form');const formDef = await loadForm('/forms/settings.form.json');form.config = { formDef };How paths resolve
Section titled “How paths resolve”Every reference is resolved with new URL(ref, baseUrl), so it follows the same rules as a URL in a
web page. Relative paths (./tabs/alert.form-chunk.json), root-relative paths
(/forms/tabs/alert.form-chunk.json), and absolute URLs (https://cdn.example.com/alert.json) all
work.
Pass the form document’s own absolute URL as baseUrl. A loaded chunk then resolves its own
references against its own URL, not against the parent’s. A chunk directory can move as a unit
without rewriting the references inside it.
Serving the files
Section titled “Serving the files”The chunk files must be reachable over HTTP at the resolved URL, because the resolver fetches them.
This means import formDef from './my-form.json' does not work for a chunked form. A bundler
inlines the parent file and never sees the references. Put the parent form and its chunks in your
static asset directory instead, and load the parent with fetch.
Limits
Section titled “Limits”- Only the top-level
formarray andchildrenarrays are traversed. A reference insideprops, such as a repeater template, is not resolved. - JSON pointers are not supported. A
#/...fragment is ignored and the whole document is inlined. - There is no cache and no de-duplication. The same reference used twice causes two requests.
- There is no cycle detection. A chunk that refers back to its own parent recurses until the call stack overflows.
- Resolution is eager. Every reference is fetched before the form renders, siblings in parallel. A chunk is not fetched when its tab opens.
- One failed fetch rejects the whole call with
Failed to load form chunk <url>: <status>. There is no partial result, so handle the rejection where you callloadForm.
Validation
Section titled “Validation”A chunk reference is one of the accepted shapes of a form widget in the published schemas, so it
validates anywhere a widget can appear. The MCP tool json_validate_form_definition accepts one for
the same reason. A chunk file itself validates against the schema it names in its own $schema.
See also
Section titled “See also”- Widget Loaders - lazy-load the widget implementations themselves.
- Form Definition API - the full form definition reference.
- Widget Sets / JSON schemas - where
chunkRefis defined in the schema tree.