hx components Common - bradwoo8621/hx GitHub Wiki
Shared patterns that apply to all Hx components: reactive binding, global configuration, styling convention, and event forwarding.
Every form-capable component accepts $model (a reactive HxObject<T>) and $field (a ModelPath<T> | HxDataPath) for two-way data binding.
const model = reactive({ user: { name: 'John', role: 'admin' } });
// Read from model, write changes back automatically
<HxInput $model={model} $field="user.name" />Values are read via ERO.getValue(model, field) and written via ERO.setValue(model, field, value). Changes trigger reactive listeners registered by the component.
The $model is automatically propagated to children via DOMUtils.interposeToChildren(). You only need to specify $model at the top-level container:
<HxPanel $model={form} title="Profile">
<HxInput $field="user.name" /> {/* $model inherited */}
<HxSelect $field="user.role" options={roles} /> {/* $model inherited */}
</HxPanel>Components supporting $disabled or $readonly accept a function that receives the model and returns a boolean:
<HxButton text="Save" $disabled={(m) => !m.dirty} />
<HxInput $model={form} $field="name" $readonly={(m) => m.locked} />Tabs support $visible for reactive show/hide:
{ mark: 'admin', header: 'Admin', body: <AdminTab />, $visible: (m) => m.isAdmin }Each component group exposes a configHx*() function. Defaults are read at render time (not captured in closures), so configuration applies globally and immediately.
import {
configHxCommon,
configHxCallout,
configHxButton,
configHxInput,
configHxFormatInput,
configHxSelect,
configHxTextarea,
configHxCheckbox,
configHxMCheckbox,
configHxMRadio,
configHxRadio,
configHxBadge,
configHxBox,
configHxFlex,
configHxGrid,
configHxLabel,
configHxSeparator,
configHxPanel,
configHxPagination,
configHxTabs,
configHxTable,
configHxDateTimePicker,
configHxUpload,
configHxOverlay,
configHxWithCheck,
configHxActions,
configHxButtonBar,
configHxWithPopup,
} from '@hx/components';configHxCommon() sets global format patterns for model value display:
configHxCommon({
datetimeValueFormat: 'y/m/dTh:n:s', // default: year-month-day Thour:min:sec
dateValueFormat: 'y/m/d', // default: year-month-day
timeValueFormat: 'h:n:s', // default: hour:min:sec
});Components use data-* attributes for styling — no CSS-in-JS. All design tokens live in src/styles/variables/, split per category and aggregated by variables/index.css. Component styles are organized as shared rules in src/styles/common/ plus per-component modules in src/styles/components/<name>/; each module sets the styling slot variables (--hx-*-this / --hx-*-this-default) that the shared rules consume, so a parent or consumer overrides a single aspect of a component by setting one slot rather than restyling its internals.
<button data-hx-button data-hx-color="primary" data-hx-variant="solid">...</button>Values like HxColor, HxSize, HxDirection etc. are string literal unions that map directly to CSS custom properties.
Every stylesheet is imported into the hx cascade layer: src/styles/index.css imports each file with layer(hx), and the built hx-components.css ships as a single @layer hx { ... } block. Unlayered application styles therefore always beat hx styles, regardless of selector specificity — overriding a component rule needs neither !important nor a stronger selector.
/* Unlayered rule, wins over any hx rule of the same property */
.my-button {
text-transform: none;
}Design tokens follow the same rule: declaring a token in unlayered CSS overrides the hx default. Component font tokens (--hx-button-font-family, --hx-input-font-family, ...) default to --hx-font-family, and each font-family declaration consumes its token as the whole font stack, so include a generic family (such as system-ui) in the token value when overriding it.
The library only resets what it owns. box-sizing: border-box and div { position: relative } are scoped to the [data-hx-root] and [data-hx-portal-root] trees, so the host page keeps its own box model.
Resetting html and body is driven by the data-hx-reset-styles attribute:
| Element | Attribute | Toggled by |
|---|---|---|
<html> |
data-hx-reset-styles |
HxContextProvider prop resetHtmlStyles
|
<body> |
data-hx-reset-styles |
HxContextProvider prop resetBodyStyles
|
Both default to true. Opt out per provider or globally:
<HxContextProvider resetHtmlStyles={false} resetBodyStyles={false}>
<App />
</HxContextProvider>configHxContext({ resetHtmlStyles: false, resetBodyStyles: false });All components forward native DOM events to the underlying HTML element. Events not consumed internally are passed through. Supported event categories per element type:
| Element | Events |
|---|---|
<button> |
onClick, onMouseDown/Up/Enter/Leave/Move/Over/Out, onKeyDown/Up/Press, onFocus, onBlur, onTouchStart/End/Move, onPointerDown/Up/Enter/Leave
|
<input> |
All of the above + onChange, onInput, onBeforeInput, onCompositionStart/End/Update
|
<textarea> |
Same as <input>
|
<div>, <span>
|
onClick, onMouseDown/Up/Enter/Leave/Move/Over/Out, onKeyDown/Up, onFocus, onBlur, onScroll, onTouchStart/End/Move, onPointerDown/Up/Enter/Leave
|
<svg> |
All standard SVG pointer/focus events |
The options system (HxSelectOptions) is shared by HxSelect, HxMCheckbox, and HxMRadio.
interface HxSelectOption {
value: unknown;
label: ReactNode;
disabled?: boolean;
[key: string]: unknown; // extensible
}type HxSelectOptions<T> =
| HxSelectOption[] // Static array
| Iterable<HxSelectOption> // Iterable
| ((search?: string) => Promise<HxSelectOption[]>) // Async fetch
| ((search?: string) => Iterable<HxSelectOption>); // Sync generatorUsage with all three consumers:
const opts = [
{ value: 'us', label: 'United States' },
{ value: 'cn', label: 'China' },
];
<HxSelect options={opts} />
<HxMCheckbox options={async (s) => fetchOptions(s)} />
<HxMRadio options={opts} />