GolemUI v1.5.0: server-side rendering and file uploads
GolemUI v1.5.0 is out. v1.4.0 made the core dramatically faster. Thanks to the one-way data flow we can in v1.5.0 take the form to the server.
Forms that arrive in the HTML
Section titled “Forms that arrive in the HTML”Until now a GolemUI form was born in the browser: the page loaded, the engine resolved the widget loaders, and the form appeared. With v1.5.0 the React, Angular, Lit and Vue adapters render on the server. The form markup arrives in the HTML, styled before any JavaScript runs, and the browser takes over without re-rendering what the server already sent.
Two pieces make this work. preloadFormWidgets resolves every widget loader once, before rendering, so the server can render a form synchronously and the client hydrates against the same widget set:
import { preloadFormWidgets } from '@golemui/core';import { widgetLoaders } from '@golemui/gui-react';import { renderToString } from 'react-dom/server';import { App } from './App';
export async function render() { await preloadFormWidgets({ widgetLoaders }); return renderToString(<App />);}And widget uids are now deterministic, including the uids of function widgets, so the server and the client produce the same markup and hydration has nothing to patch.
It works on your own Node server and through the meta-frameworks. Next.js, Analog, Astro and Nuxt each have a starter template and a guide, and the SSR overview explains what each framework emits and how to keep server and client in step.
We deployed to Vercel demo apps in case you want to check it out:
| Framework | Live URL |
|---|---|
| Next.js 15 + React 19 | https://golemui-ssr-nextjs.vercel.app |
| Nuxt 4 + Vue 3.5 | https://golemui-ssr-nuxt.vercel.app |
| Analog 2 + Angular 21 (zoneless) | https://golemui-ssr-analog.vercel.app |
| Astro 7 + Lit 3 | https://golemui-ssr-astro.vercel.app |
And as always, everything is Open Source, so check the GolemUI SSR repository to know more.
File Upload and Multi File Upload
Section titled “File Upload and Multi File Upload”Two new input widgets upload files. Both are a one-line input where the box is the drop target and holds the upload button, and while a file uploads the box itself becomes the progress bar. Once the upload finishes, fileUpload shows the file name with a remove button and multiFileUpload turns each file into a pill.
{ "kind": "input", "type": "fileUpload", "path": "cv", "label": "CV", "props": { "accept": ["application/pdf", ".docx"], "maxSize": 5242880 }, "validator": { "type": "file", "required": true }}GolemUI does not ship a transport. You inject an uploadService through the form’s dependencies with an upload(file, ctx) method that resolves with whatever your endpoint returned and an optional remove(item). The widget calls upload as soon as a file is picked, drives the progress bar from your onProgress calls, and hands you an AbortSignal for cancellations.
For example, given this upload service:
const uploadService = { async upload(file, { path, signal }) { const body = new FormData(); body.append('file', file); const response = await fetch(`/api/uploads?field=${path}`, { method: 'POST', body, signal, }); if (!response.ok) throw new Error('Upload rejected'); return response.json(); }, async remove(item) { await fetch(`/api/uploads/${item.data.key}`, { method: 'DELETE' }); },};You can inject it like this then in your form configuration:
import { uploadService } from './upload-service';
const config = { formDef: [gui.inputs.fileUpload('cv', { label: 'CV' })], formConfig: { dependencies: { uploadService }, },};What lands in the form data is never the File. It is a plain envelope, { id, name, size, type, status, data }, where data is your server’s response verbatim, so a submit payload carries exactly what your backend knows about the file. This signature allows us to do quick diff checks, something that would not be possible with a File.
Two validators go with it, file and files. Both default blockPendingUploads to true, so a form cannot be submitted while an upload is still running or has failed, and files adds minItems and maxItems.
A file that fails accept or maxSize, or that the server rejects, stays in the box with a validation message, and a server rejection gets a retry button. A failed file in the multi widget pauses the queue until it is retried or removed. And a draft restored while a file was still uploading is marked as interrupted, because the File only existed in the session that picked it, so the user knows to pick it again.
The demo above runs against a simulated service: nothing leaves the browser, and a file whose name contains fail is rejected so you can try the retry button. The File Upload and Multi File Upload pages have the props, the service contract and live demos for every option.
Upgrading
Section titled “Upgrading”There are no code changes for existing forms. Bump every @golemui/* package together, as always and enjoy SSR with GolemUI!