Skip to content

Custom Widgets Overview

GolemUI ships with a rich set of built-in widgets, but real-world applications often need something tailor-made. Custom widgets let you plug your own widgets into the form engine while keeping all the benefits: data binding, validation, event handling, and state management.

Every widget in GolemUI has a kind that determines its role:

KindPurposeData bindingEventsImplementation guide
layoutContainers that arrange child widgetsNochangeLayout widget
inputControls that read and write form dataYes (path)change, filterInput widget
displayRead-only presentational contentNoNoneDisplay widget
actionButtons and triggers that fire eventsNoclickAction widget

To build custom widgets, GolemUI provides Hooks (React) and Adapters (Angular and Lit). These framework-specific helpers connect your widget to the form engine. Under the hood they interact with the FormContext — the central object that manages the widget registry, the reactive store, and the event stream. You don’t touch the FormContext directly; the hooks and adapters handle it for you.

Throughout this section, we build a Product Card Rating composed of four custom widgets — one for each kind:

  • LayoutproductCard: a card container with a title.
  • InputproductRating: a star-based rating control.
  • DisplayproductDescription: an image with a markdown description.
  • ActionproductShare: social sharing buttons that fire an event.

Here’s the full form definition we’re working toward:

import { gui } from '@golemui/gui-shared';
gui.layouts.custom('productCard', [
gui.displays.custom('productDescription', {
img: 'assets/product-image.png',
description: `### My Cool Product
My product is the best`,
}),
gui.inputs.custom('productRating', 'product.rating', {
maxRating: 10,
}),
gui.actions.custom('productShare', {
onClick: 'shareEvent',
}),
], { title: 'Product Card Name' });

And because custom widgets live side by side with built-in ones, you can easily extend the card — say, by adding a comment field and a submit button without writing any extra widgets:

import { gui } from '@golemui/gui-shared';
gui.layouts.custom('productCard', [
gui.displays.custom('productDescription', {
img: 'assets/product-image.png',
description: `### My Cool Product
My product is the best`,
}),
gui.inputs.custom('productRating', 'product.rating', {
maxRating: 10,
validator: { type: 'number', required: true },
}),
gui.inputs.textarea('product.comment', {
hint: 'Enter your comment (maximum 500 characters)',
placeholder: 'Let us know why you love our product',
counterMode: 'current',
validator: { type: 'string', maxLength: 500, required: true },
}),
gui.actions.custom('productShare', {
onClick: 'shareEvent',
}),
gui.actions.button({
label: 'Submit',
icon: 'save',
iconPosition: 'right',
onClick: 'submit',
}),
], { title: 'Product Card Name' });

Notice how textarea and button are built-in GolemUI widgets mixed right in with the custom productRating and productShare. That’s the power of the widget system — all widgets, custom or built-in, speak the same language.

Here’s what the extended card looks like with custom and built-in widgets working together:

Each kind has its own dedicated chapter with the full per-framework implementation, key concepts, and a live demo:

  1. Creating a layout widget — the productCard container.
  2. Creating an input widget — the productRating star control.
  3. Creating a display widget — the productDescription image+markdown card.
  4. Creating an action widget — the productShare social-buttons widget.
  5. Sending events with your widgets — emitting and receiving events, with detail payloads.

Before GolemUI can render your custom widgets, you need to register them in the widget loaders object. Widget loaders map a type string to a lazy-loaded widget. This is the same registry that built-in widgets use.

import * as Core from '@golemui/core';
export const customWidgetLoaders: Core.WidgetLoaders<
React.ComponentType<Core.WithWidget>
> = {
productCard: async () =>
(await import('./widgets/ProductCard')).ProductCard,
productRating: async () =>
(await import('./widgets/ProductRating')).ProductRating,
productDescription: async () =>
(await import('./widgets/ProductDescription')).ProductDescription,
productShare: async () =>
(await import('./widgets/ProductShare')).ProductShare,
};

The type key in the loader (productCard, productRating, etc.) must match the type field in your widget definitions. Widget loaders use dynamic import() so custom widgets are lazy-loaded — they’re only fetched when the form engine encounters them.

With all four widgets implemented and registered, here’s the complete form setup:

import * as Core from '@golemui/core';
import * as React from '@golemui/react';
import { gui } from '@golemui/gui-shared';
import { customWidgetLoaders } from './custom-widget-loaders';
const formDef = [
gui.layouts.custom('productCard', [
gui.displays.custom('productDescription', {
img: 'assets/product-image.png',
description: `### My Cool Product\nMy product is the best`,
}),
gui.inputs.custom('productRating', 'product.rating', {
maxRating: 10,
validator: { type: 'number', required: true },
}),
gui.actions.custom('productShare', {
onClick: 'shareEvent',
}),
], { title: 'Rate Our Product' }),
];
const formConfig = { widgetLoaders: customWidgetLoaders };
export function ProductPage() {
const SHARE_URLS: Record<string, (url: string) => string> = {
twitter: (url) => `https://x.com/intent/tweet?text=${encodeURIComponent('Check out this product!')}&url=${encodeURIComponent(url)}`,
facebook: (url) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
linkedin: (url) => `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
};
const handleFormEvent = (event: Core.FormEvent) => {
if (event.name === 'shareEvent') {
const network = event.detail as string;
const buildUrl = SHARE_URLS[network];
if (buildUrl) {
window.open(buildUrl(window.location.href), '_blank', 'noopener,noreferrer');
}
}
};
return (
<React.FormComponent
formDef={formDef}
formConfig={formConfig}
data={{}}
formEvent={handleFormEvent}
/>
);
}

Here’s the final result — the complete Product Card with all four custom widgets:

That’s it. Four custom widgets, three frameworks, one consistent API. The form engine handles data flow, validation, and event routing — your widgets just focus on rendering.