Skip to content
Forms, switch product

From 1.x to 2.0

In 2.0, the widgets your forms render come from GolemUI Components, which now also works on its own, in plain HTML or any framework. Getting there changed the stylesheets, the layouts and the markup of some widgets.

Your form definitions, stored JSON included, still render. Props that changed still work and are marked deprecated, so you can update them the next time you edit a form.

If you…Read
render formsEvery Forms app and Layouts
override GolemUI’s CSS, or select its markup in testsCSS and markup
write custom widgetsCustom widgets
build your own widget set on @golemui/coreCustom widget sets
pass a markdown parser in dependencies, in TypeScriptThe markdown parser
render forms on the server with @golemui/lit/ssrLit server rendering
import from @golemui/*/internalsPackages
  • Upgrade every @golemui/* package to 2.0 at once.
  • Import @golemui/gui-shared/forms.css after @golemui/gui-components/index.css.
  • Check your global CSS resets, which now override GolemUI’s styles.
  • Give every field a label.
  • In TypeScript, return SanitizedHtml from your markdown parser.
  • Add direction: 'row' to every grid that has no direction and should stay a row.
  • Replace flex layouts with grid, pixel gaps with gap steps, and check every grid that sets justify or align.
  • Update CSS and test selectors that target tabs, accordions, alerts, grids or error lists.
  • In custom widgets, stop setting flex from size, and listen for the gui-* events.
  • In your own widget set, rename the flex loader to grid, or add a grid loader.
  • Rename the @golemui/lit/ssr functions you import.

The packages are released together, and each one depends on the exact version of the others. Upgrade them all at once:

Terminal window
npm i @golemui/core@2 @golemui/gui-shared@2 @golemui/react@2 @golemui/gui-react@2 @golemui/gui-components@2

@golemui/gui-components/index.css now holds only the widgets’ styles. The styles of the form container, the repeater and the invalid submit button moved to @golemui/gui-shared/forms.css. Import it after the components stylesheet:

Before
@import '@golemui/gui-components/index.css';
After
@import '@golemui/gui-components/index.css';
@import '@golemui/gui-shared/forms.css';

In Angular, if you list the stylesheets in angular.json, add it to styles:

After
"styles": [
"node_modules/@golemui/gui-components/lib/styles/index.css",
"node_modules/@golemui/gui-shared/forms.css"
]

Without forms.css the form still works, but repeater rows lose their cards and the form loses its reset.

Every GolemUI rule is in a cascade layer:

@layer golemui.tokens, golemui.base, golemui.components, golemui.forms, golemui.theme;

CSS outside a layer beats layered CSS whatever its specificity, so an override no longer needs a selector as specific as GolemUI’s:

Before
.gui-form .gui-button button {
border-radius: 0;
}
After
gui-button button {
border-radius: 0;
}

Global resets win too. A plain button { … } or a { … } rule in your app used to lose to the widget styles, and now overrides them. To keep the 1.x result, put your resets in a layer, and declare the order at the top of your CSS, before the GolemUI stylesheets load:

After
@layer reset, golemui;
@layer reset {
button {
all: unset;
}
}

Token overrides work as before: set the --gui-* variables on :root or any ancestor.

A field takes its accessible name from its visible label. The generic names 1.x gave a field without a label, such as “Tags input”, are gone: such a field has no name for screen readers, and development builds log a warning. Give it a label:

Before
gui.inputs.tags('keywords');
After
gui.inputs.tags('keywords', { label: 'Keywords' });
Before
{ "kind": "input", "type": "tags", "path": "keywords" }
After
{ "kind": "input", "type": "tags", "path": "keywords", "label": "Keywords" }

This applies only to TypeScript apps that pass a markdown parser in dependencies.

The markdown and markdownText widgets insert the parser output as HTML without sanitizing it. To make that visible, parse now returns SanitizedHtml instead of string. A parser that returns a plain string no longer compiles.

Before
import snarkdown from 'snarkdown';
const dependencies = {
markdown: { parse: (md: string) => snarkdown(md) },
};
After
import type { SanitizedHtml } from '@golemui/gui-shared';
import snarkdown from 'snarkdown';
const dependencies = {
// `sanitizeHtml` is your HTML sanitizer.
markdown: {
parse: (md: string) => sanitizeHtml(snarkdown(md)) as SanitizedHtml,
},
};

When the markdown can come from users or stored data, sanitize the parser output. Snarkdown and Marked keep raw HTML and javascript: links. Micromark and markdown-it escape both with their default options. When all the markdown comes from your own source code, the cast alone is enough. Markdown Text shows a parser that sanitizes with DOMPurify.

2.0 replaces Flex and Grid with one Grid layout. In a row, it lines up the labels, controls and errors of the fields next to each other, so an error never moves the controls beside it.

In 1.x, a grid with no direction put its children in a row. In 2.0 it stacks them, as direction: 'column' does. Add direction: 'row' to keep the row, or use horizontalGrid:

Before
gui.layouts.grid([firstName, lastName]);
After
gui.layouts.horizontalGrid([firstName, lastName]);
Before
{ "kind": "layout", "type": "grid", "children": [] }
After
{
"kind": "layout",
"type": "grid",
"props": { "direction": "row" },
"children": []
}
PropValuesWhat it does
direction'column' (default), 'row'column stacks the children. row puts them on one line and splits the width by each child’s size.
columns1 to 12, 'auto'New. A grid that wraps. A number sets that many equal columns, and size is how many a child spans. 'auto' fits as many --gui-layout-column-min (12rem) columns as there is room for. Overrides direction.
gap'none', 'xs', 'sm', 'md' (default), 'lg', 'xl'The space between children, from the --gui-layout-gap-* tokens. Replaces pixel values.
justify'stretch' (default), 'start', 'center', 'end', 'space-between'New meaning. In a row, stretch splits the width by size. The other values keep each child at its natural width and place the children along the row: end puts buttons on the right.

Every grid stacks its children when the form is narrower than 480px.

These props still work: GolemUI maps them to the new ones. Replace them the next time you edit the form.

Use gap with a step. Pixels map to the nearest one: up to 2px is none, then 6px xs, 12px sm, 20px md, 28px lg, and anything larger xl.

Before
gui.layouts.horizontalGrid([firstName, lastName], { columnGap: 16, rowGap: 8 });
After
gui.layouts.horizontalGrid([firstName, lastName], { gap: 'md' });
Before
{ "direction": "row", "columnGap": 16, "rowGap": 8 }
After
{ "direction": "row", "gap": "md" }

Use justify. space-around and space-evenly map to space-between.

Before
gui.layouts.horizontalGrid([cancel, save], { align: 'end' });
After
gui.layouts.horizontalGrid([cancel, save], { justify: 'end' });
Before
{ "direction": "row", "align": "end" }
After
{ "direction": "row", "justify": "end" }

In 1.x, justify aligned the children across the row. Fields line up on their own now, so remove it. justify: 'stretch' works as before, but start, center and end now place the children along the row, at their natural width.

Before
gui.layouts.horizontalGrid([firstName, lastName], { justify: 'center' });
After
gui.layouts.horizontalGrid([firstName, lastName]);
Before
{ "direction": "row", "justify": "center" }
After
{ "direction": "row" }

Remove it. A row with autoFit: true, the old default, stays a row. A row with autoFit: false was a 12-column grid, which is now columns: 12.

Before
gui.layouts.horizontalGrid([city, zip], { autoFit: false });
After
gui.layouts.grid([city, zip], { columns: 12 });
Before
{ "direction": "row", "autoFit": false }
After
{ "columns": 12 }

A 1.x auto-fit row wrapped its children onto more lines when it ran out of room. A 2.0 row stays on one line until it stacks below 480px. For a grid that wraps, use columns: 'auto'.

The flex layout and the gui.layouts.flex, horizontalFlex and verticalFlex builders still work, and render as a grid. Move them to grid:

Before
gui.layouts.horizontalFlex([cancel, save], { gap: 8, align: 'end' });
gui.layouts.verticalFlex([name, email]);
After
gui.layouts.horizontalGrid([cancel, save], { gap: 'sm', justify: 'end' });
gui.layouts.grid([name, email]);
Before
{
"kind": "layout",
"type": "flex",
"props": { "direction": "row", "gap": 8, "align": "end" }
}
After
{
"kind": "layout",
"type": "grid",
"props": { "direction": "row", "gap": "sm", "justify": "end" }
}
1.x2.0
type: 'flex', direction: 'column' or nonetype: 'grid'
type: 'flex', direction: 'row'type: 'grid', direction: 'row'
row-reverse, column-reversenot supported: reorder the children instead
gap in pixelsgap, a step
alignjustify
justifynothing: fields line up on their own
gui.layouts.horizontalFlex(…)gui.layouts.horizontalGrid(…)
gui.layouts.flex(…), verticalFlex(…)gui.layouts.grid(…), verticalGrid(…)

row-reverse and column-reverse now render in source order. With them, screen readers and the keyboard went through the fields in a different order from the one on screen.

  • The form’s root layout is a grid stack instead of a flex column. Code that finds the root by its type must look for 'grid'.
  • Checkboxes, toggles and buttons in a row sit at the vertical centre of the row’s controls. The margin-top that lined up checkboxes and toggles with inputs is gone.

The widgets’ styles are no longer nested under .gui-form, and tabs, accordions and alerts render gui-* elements. If you override GolemUI’s CSS or select its markup in tests, the selectors to update are in GolemUI Components’ guide to 2.0: errors, grids, tabs, accordions and alerts, each with the 1.x and 2.0 selectors.

Two changes only concern forms:

  • The form is still a container named gui-form, so your @container gui-form queries keep working. GolemUI’s own styles query gui-container now.
  • Only submit buttons show the invalid state, with the gui-button--invalid class, styled in forms.css.

The grid cell around a widget applies its size now. Remove the code that sets an inline flex style from templateData.size:

Before
<div className="my-widget" style={{ flex: templateData.size }}>
After
<div className="my-widget">

In a row, the grid lines up each field’s label, control and errors with those of its neighbours. A custom widget joins in when its root has the gui-field class and its parts have the gui-label, gui-widget and gui-validator classes. GolemUI Components’ Grid page shows the anatomy.

If your widget renders gui-* elements, their API changed:

  • Every event starts with gui-: input and change are now gui-input and gui-change, blur is gui-blur, and so on. A listener for an old name never fires.
  • Multi-word attributes are kebab-case: autogrow is now auto-grow.
  • On gui-button, actionType is now type, and invalid is gone. In form definitions, actionType stays.
  • An element shows its errors as soon as you set them. Pass templateData.touched as touched to wait for the user, as the built-in widgets do.

GolemUI Components’ guide to 2.0 has each change with its code before and after, and every renamed event.

This section applies if you build your own widget set on @golemui/core, with your own widgetLoaders, instead of the gui widgets.

grid replaces flex as a reserved widget type. Core wraps every form whose form is an array in a grid layout with no props, so every widget set must provide a grid loader, next to repeater. Without one, the form can’t load its root: the console shows Widget "grid" could not be loaded, and nothing renders, on the server either.

Before
const widgetLoaders = {
flex: () => import('./layouts/stack').then((m) => m.Stack),
repeater: () => import('./layouts/repeater').then((m) => m.Repeater),
// …your widgets
};
After
const widgetLoaders = {
grid: () => import('./layouts/stack').then((m) => m.Stack),
repeater: () => import('./layouts/repeater').then((m) => m.Repeater),
// …your widgets
};
  • Your grid widget must stack its children when it gets no props.
  • Keep the flex loader too if your stored forms use flex layouts.
  • If your widget set has a DX adapter, its rootEntry must build a grid root too.
  • A widget set made with npx @golemui/schemas init now starts with a grid component schema instead of flex. In an existing one, add grid to the manifest in schemas.config.mjs, with its schema, and run npx @golemui/schemas generate again.

Five exports of @golemui/lit/ssr have new names:

1.x2.0
renderGuiHtmlrenderTemplate
renderGuiFormHtmlrenderForm
GuiSsrElementRendererRegisteredElementRenderer
stripShadowRootTemplates, stripFalseBooleanAttributescleanServerMarkup
Before
import { renderGuiHtml } from '@golemui/lit/ssr';
const markup = await renderGuiHtml(
html`<gui-form .config=${config}></gui-form>`,
);
After
import { renderTemplate } from '@golemui/lit/ssr';
const markup = await renderTemplate(
html`<gui-form .config=${config}></gui-form>`,
);
  • renderTemplate and renderForm take the same arguments as before, and RegisteredElementRenderer goes in the same place in elementRenderers.
  • cleanServerMarkup does the work of both strip functions in one pass, and also removes Lit’s hydration markers. Pass { keepMarkers: true } to keep them, as the 1.x functions did.
  • Each element the server rendered has a data-golemui-ssr attribute. resumeServerRenderedForm handles it, so only code that reads or snapshots the server markup sees the change.

Lit server rendering documents the whole API.

  • @golemui/gui-shared now depends on @golemui/gui-components, which no longer depends on the Forms packages.
  • @golemui/gui-components/internals no longer exports createIntersectionObserver or NO_AVAILABLE_TIMES_MESSAGE.
  • The value types, such as DateRange, Option, FileItem and UploadService, are now defined in @golemui/gui-components. @golemui/gui-shared still exports them, so your imports keep working.
  • safeDefine and cspStyleMap moved to a new package, @golemui/lit-utils. You don’t need to install it: @golemui/lit and @golemui/lit/internals still export both.

You don’t need these to upgrade, but they’re there:

  • A grid that wraps, with columns, gap steps and a justify that places buttons. See The grid props.
  • Translated built-in strings. configureMessages() from @golemui/gui-components translates the validation messages, accessible names and announcements the widgets produce. It works next to your form’s localization. See i18n in GolemUI Components.
  • A time label on the date-time calendar and picker, with timeLabel.
  • Keyboard tabs. The arrow keys, Home and End select a tab in every framework.
  • Right to left. A dir="rtl" on any ancestor applies the right-to-left styles.
  • A read-only select stays focusable, with aria-readonly, instead of being disabled.