Skip to content

Creating an input widget

An input widget binds to a path in the form data, supports validation, and tracks touched state. The framework helpers (useInputWidget in React and Vue, InputWidgetAdapter in Angular and Lit) handle data binding for you — your widget calls a few methods, the engine does the rest. The example we’ll walk through is productRating — a star-based rating control.

The maxRating prop controls how many stars are displayed. The path ('product.rating') tells the form engine where to store the selected value (a number from 1 to maxRating). The validator adds built-in number validation — in this case, making the field required.

import { gui } from '@golemui/gui-shared';
gui.inputs.custom('productRating', 'product.rating', {
maxRating: 10,
validator: { type: 'number', required: true },
});
{
"kind": "input",
"type": "productRating",
"path": "product.rating",
"props": { "maxRating": 10 },
"validator": { "type": "number", "required": true }
}
MethodPurpose
onValueChanged(value)Update the form data at the widget’s path.
onBlur()Mark the field as touched and trigger blur-time validation.
templateData.errorsArray of validation error messages for this field.
templateData.touchedWhether the user has interacted with the field.
injectValidationIssues(issues)Programmatically inject custom validation errors.

Key concepts (React)

  • useInputWidget returns the typed value, errors, isTouched, and a merged templateData (your custom props + engine-managed values).
  • Call onValueChanged(newValue) from a click/change handler — the engine writes through to the form data and runs validation.
  • Call onBlur() when the user leaves the field. With validateOn: 'blur' (or 'eager'), this triggers validation; either way, it sets isTouched.
  • style={{ flex: templateData.size }} lets the input participate in flex parents — see Sizing custom widgets for the rationale.
ProductRating.tsx
import type { InputWidget, WithWidget } from '@golemui/core';
import { useInputWidget } from '@golemui/react';
interface ProductRatingProps {
maxRating: number;
}
export function ProductRating(widgetInstance: WithWidget) {
const widget = widgetInstance.widget as InputWidget<number>;
const { uid, value, errors, isTouched, templateData, onValueChanged, onBlur } =
useInputWidget<number, ProductRatingProps>(widget);
const maxRating = templateData.maxRating || 5;
return (
<div className="product-rating" style={{ flex: templateData.size }}>
<div className="product-rating__widget" id={uid}>
<div className="product-rating__stars" onBlur={onBlur} tabIndex={0}>
{Array.from({ length: maxRating }, (_, i) => i + 1).map((star) => (
<span
key={star}
role="button"
className={`product-rating__star ${
star <= (value || 0) ? 'product-rating__star--active' : ''
}`}
onClick={() => onValueChanged(star)}
>
{star <= (value || 0) ? '' : ''}
</span>
))}
</div>
{isTouched && errors.length > 0 && (
<div className="product-rating__errors">
{errors.map((error, i) => (
<span key={i} className="product-rating__error">{error}</span>
))}
</div>
)}
</div>
</div>
);
}

Try clicking the stars to select a rating, then click away to trigger validation: