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.
What to read
Section titled “What to read”| If you… | Read |
|---|---|
| render forms | Every Forms app and Layouts |
| override GolemUI’s CSS, or select its markup in tests | CSS and markup |
| write custom widgets | Custom widgets |
build your own widget set on @golemui/core | Custom widget sets |
pass a markdown parser in dependencies, in TypeScript | The markdown parser |
render forms on the server with @golemui/lit/ssr | Lit server rendering |
import from @golemui/*/internals | Packages |
Checklist
Section titled “Checklist”- Upgrade every
@golemui/*package to 2.0 at once. - Import
@golemui/gui-shared/forms.cssafter@golemui/gui-components/index.css. - Check your global CSS resets, which now override GolemUI’s styles.
- Give every field a
label. - In TypeScript, return
SanitizedHtmlfrom your markdown parser. - Add
direction: 'row'to everygridthat has nodirectionand should stay a row. - Replace
flexlayouts withgrid, pixel gaps with gap steps, and check every grid that setsjustifyoralign. - Update CSS and test selectors that target tabs, accordions, alerts, grids or error lists.
- In custom widgets, stop setting
flexfromsize, and listen for thegui-*events. - In your own widget set, rename the
flexloader togrid, or add agridloader. - Rename the
@golemui/lit/ssrfunctions you import.
Every Forms app
Section titled “Every Forms app”Upgrade every package
Section titled “Upgrade every package”The packages are released together, and each one depends on the exact version of the others. Upgrade them all at once:
npm i @golemui/core@2 @golemui/gui-shared@2 @golemui/react@2 @golemui/gui-react@2 @golemui/gui-components@2npm i @golemui/core@2 @golemui/gui-shared@2 @golemui/angular@2 @golemui/gui-angular@2 @golemui/gui-components@2npm i @golemui/core@2 @golemui/gui-shared@2 @golemui/lit@2 @golemui/gui-lit@2 @golemui/gui-components@2npm i @golemui/core@2 @golemui/gui-shared@2 @golemui/vue@2 @golemui/gui-vue@2 @golemui/gui-components@2npm i @golemui/core@2 @golemui/gui-shared@2 @golemui/lit@2 @golemui/gui-lit@2 @golemui/gui-components@2Import the forms stylesheet
Section titled “Import the forms stylesheet”@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:
@import '@golemui/gui-components/index.css';@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:
"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.
Your CSS now wins over GolemUI’s
Section titled “Your CSS now wins over GolemUI’s”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:
.gui-form .gui-button button { border-radius: 0;}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:
@layer reset, golemui;
@layer reset { button { all: unset; }}Token overrides work as before: set the --gui-* variables on :root or any ancestor.
Give every field a label
Section titled “Give every field a label”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:
gui.inputs.tags('keywords');gui.inputs.tags('keywords', { label: 'Keywords' });{ "kind": "input", "type": "tags", "path": "keywords" }{ "kind": "input", "type": "tags", "path": "keywords", "label": "Keywords" }The markdown parser returns SanitizedHtml
Section titled “The markdown parser returns SanitizedHtml”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.
import snarkdown from 'snarkdown';
const dependencies = { markdown: { parse: (md: string) => snarkdown(md) },};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.
Layouts
Section titled “Layouts”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.
A grid without direction is now a stack
Section titled “A grid without direction is now a stack”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:
gui.layouts.grid([firstName, lastName]);gui.layouts.horizontalGrid([firstName, lastName]);{ "kind": "layout", "type": "grid", "children": [] }{ "kind": "layout", "type": "grid", "props": { "direction": "row" }, "children": []}The grid props
Section titled “The grid props”| Prop | Values | What 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. |
columns | 1 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.
Deprecated grid props
Section titled “Deprecated grid props”These props still work: GolemUI maps them to the new ones. Replace them the next time you edit the form.
columnGap and rowGap
Section titled “columnGap and rowGap”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.
gui.layouts.horizontalGrid([firstName, lastName], { columnGap: 16, rowGap: 8 });gui.layouts.horizontalGrid([firstName, lastName], { gap: 'md' });{ "direction": "row", "columnGap": 16, "rowGap": 8 }{ "direction": "row", "gap": "md" }Use justify. space-around and space-evenly map to space-between.
gui.layouts.horizontalGrid([cancel, save], { align: 'end' });gui.layouts.horizontalGrid([cancel, save], { justify: 'end' });{ "direction": "row", "align": "end" }{ "direction": "row", "justify": "end" }The old justify
Section titled “The old justify”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.
gui.layouts.horizontalGrid([firstName, lastName], { justify: 'center' });gui.layouts.horizontalGrid([firstName, lastName]);{ "direction": "row", "justify": "center" }{ "direction": "row" }autoFit
Section titled “autoFit”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.
gui.layouts.horizontalGrid([city, zip], { autoFit: false });gui.layouts.grid([city, zip], { columns: 12 });{ "direction": "row", "autoFit": false }{ "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'.
Flex is deprecated
Section titled “Flex is deprecated”The flex layout and the gui.layouts.flex, horizontalFlex and verticalFlex builders still
work, and render as a grid. Move them to grid:
gui.layouts.horizontalFlex([cancel, save], { gap: 8, align: 'end' });gui.layouts.verticalFlex([name, email]);gui.layouts.horizontalGrid([cancel, save], { gap: 'sm', justify: 'end' });gui.layouts.grid([name, email]);{ "kind": "layout", "type": "flex", "props": { "direction": "row", "gap": 8, "align": "end" }}{ "kind": "layout", "type": "grid", "props": { "direction": "row", "gap": "sm", "justify": "end" }}| 1.x | 2.0 |
|---|---|
type: 'flex', direction: 'column' or none | type: 'grid' |
type: 'flex', direction: 'row' | type: 'grid', direction: 'row' |
row-reverse, column-reverse | not supported: reorder the children instead |
gap in pixels | gap, a step |
align | justify |
justify | nothing: 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.
Other layout changes
Section titled “Other layout changes”- The form’s root layout is a
gridstack instead of aflexcolumn. 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-topthat lined up checkboxes and toggles with inputs is gone.
CSS and markup
Section titled “CSS and markup”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-formqueries keep working. GolemUI’s own styles querygui-containernow. - Only submit buttons show the invalid state, with the
gui-button--invalidclass, styled informs.css.
Custom widgets
Section titled “Custom widgets”Stop setting flex from size
Section titled “Stop setting flex from size”The grid cell around a widget applies its size now. Remove the code that sets an inline flex
style from templateData.size:
<div className="my-widget" style={{ flex: templateData.size }}><div className="my-widget">host: { class: 'my-widget', '[style.flex]': 'this.adapter.templateData().size',},host: { class: 'my-widget',},override connectedCallback() { super.connectedCallback(); this.classList.add('my-widget'); this.style.flex = String(this.adapter.templateData.size ?? 1); this.subscriptions.push( this.adapter.templateDataChanged$.subscribe(() => { this.style.flex = String(this.adapter.templateData.size ?? 1); }), );}override connectedCallback() { super.connectedCallback(); this.classList.add('my-widget');}<div class="my-widget" :style="{ flex: templateData.size }"><div class="my-widget">override connectedCallback() { super.connectedCallback(); this.classList.add('my-widget'); this.style.flex = String(this.adapter.templateData.size ?? 1); this.subscriptions.push( this.adapter.templateDataChanged$.subscribe(() => { this.style.flex = String(this.adapter.templateData.size ?? 1); }), );}override connectedCallback() { super.connectedCallback(); this.classList.add('my-widget');}Line up with the fields beside it
Section titled “Line up with the fields beside it”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.
Widgets that render gui-* elements
Section titled “Widgets that render gui-* elements”If your widget renders gui-* elements, their API changed:
- Every event starts with
gui-:inputandchangeare nowgui-inputandgui-change,blurisgui-blur, and so on. A listener for an old name never fires. - Multi-word attributes are kebab-case:
autogrowis nowauto-grow. - On
gui-button,actionTypeis nowtype, andinvalidis gone. In form definitions,actionTypestays. - An element shows its
errorsas soon as you set them. PasstemplateData.touchedastouchedto 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.
Custom widget sets
Section titled “Custom widget sets”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.
const widgetLoaders = { flex: () => import('./layouts/stack').then((m) => m.Stack), repeater: () => import('./layouts/repeater').then((m) => m.Repeater), // …your widgets};const widgetLoaders = { grid: () => import('./layouts/stack').then((m) => m.Stack), repeater: () => import('./layouts/repeater').then((m) => m.Repeater), // …your widgets};- Your
gridwidget must stack its children when it gets no props. - Keep the
flexloader too if your stored forms useflexlayouts. - If your widget set has a DX adapter, its
rootEntrymust build agridroot too. - A widget set made with
npx @golemui/schemas initnow starts with agridcomponent schema instead offlex. In an existing one, addgridto the manifest inschemas.config.mjs, with its schema, and runnpx @golemui/schemas generateagain.
Lit server rendering
Section titled “Lit server rendering”Five exports of @golemui/lit/ssr have new names:
| 1.x | 2.0 |
|---|---|
renderGuiHtml | renderTemplate |
renderGuiFormHtml | renderForm |
GuiSsrElementRenderer | RegisteredElementRenderer |
stripShadowRootTemplates, stripFalseBooleanAttributes | cleanServerMarkup |
import { renderGuiHtml } from '@golemui/lit/ssr';
const markup = await renderGuiHtml( html`<gui-form .config=${config}></gui-form>`,);import { renderTemplate } from '@golemui/lit/ssr';
const markup = await renderTemplate( html`<gui-form .config=${config}></gui-form>`,);renderTemplateandrenderFormtake the same arguments as before, andRegisteredElementRenderergoes in the same place inelementRenderers.cleanServerMarkupdoes 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-ssrattribute.resumeServerRenderedFormhandles it, so only code that reads or snapshots the server markup sees the change.
Lit server rendering documents the whole API.
Packages
Section titled “Packages”@golemui/gui-sharednow depends on@golemui/gui-components, which no longer depends on the Forms packages.@golemui/gui-components/internalsno longer exportscreateIntersectionObserverorNO_AVAILABLE_TIMES_MESSAGE.- The value types, such as
DateRange,Option,FileItemandUploadService, are now defined in@golemui/gui-components.@golemui/gui-sharedstill exports them, so your imports keep working. safeDefineandcspStyleMapmoved to a new package,@golemui/lit-utils. You don’t need to install it:@golemui/litand@golemui/lit/internalsstill export both.
New in 2.0
Section titled “New in 2.0”You don’t need these to upgrade, but they’re there:
- A grid that wraps, with
columns, gap steps and ajustifythat places buttons. See The grid props. - Translated built-in strings.
configureMessages()from@golemui/gui-componentstranslates the validation messages, accessible names and announcements the widgets produce. It works next to your form’slocalization. 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.