hs-uix 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +720 -37
- package/dist/form.js +2 -2
- package/dist/form.mjs +2 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,81 +5,764 @@
|
|
|
5
5
|
|
|
6
6
|
Production-ready UI components for [HubSpot UI Extensions](https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/overview). Built entirely on HubSpot's native primitives — no custom HTML, no CSS, no iframes.
|
|
7
7
|
|
|
8
|
-
## Components
|
|
9
|
-
|
|
10
|
-
| Component | Description | Docs |
|
|
11
|
-
|-----------|-------------|------|
|
|
12
|
-
| **DataTable** | Filterable, sortable, paginated table with auto-sized columns, inline editing, row grouping, and more | [Full documentation](./packages/datatable/README.md) |
|
|
13
|
-
| **FormBuilder** | Declarative, config-driven form with validation, multi-step wizards, and 20+ field types | [Full documentation](./packages/form/README.md) |
|
|
14
|
-
|
|
15
8
|
## Install
|
|
16
9
|
|
|
17
10
|
```bash
|
|
18
11
|
npm install hs-uix
|
|
19
12
|
```
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
```jsx
|
|
15
|
+
import { DataTable } from "hs-uix/datatable";
|
|
16
|
+
import { FormBuilder } from "hs-uix/form";
|
|
17
|
+
|
|
18
|
+
// or import everything from the root
|
|
19
|
+
import { DataTable, FormBuilder } from "hs-uix";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires `react` >= 18.0.0 and `@hubspot/ui-extensions` >= 0.12.0 as peer dependencies (already present in any HubSpot UI Extensions project).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# DataTable
|
|
27
|
+
|
|
28
|
+
A drop-in table component for HubSpot UI Extensions. Define your columns, pass your data, and you get search, filtering, sorting, pagination, inline editing, row grouping, and auto-sized columns out of the box.
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
## Why DataTable?
|
|
33
|
+
|
|
34
|
+
If you've built tables with HubSpot's `Table`, `TableRow`, and `TableCell` primitives, you know the drill: wire up search, sorting, pagination, and filtering yourself, then spend an hour tweaking column widths that still look wrong. DataTable does all of that for you.
|
|
35
|
+
|
|
36
|
+
The column sizing alone is worth it. DataTable looks at your actual data (types, string lengths, unique values, whether a column has edit controls) and picks widths automatically. Booleans and dates get compact columns, text gets room, and editable columns are never too narrow for their inputs. You don't configure any widths unless you want to.
|
|
24
37
|
|
|
25
38
|
```jsx
|
|
26
39
|
import { DataTable } from "hs-uix/datatable";
|
|
27
40
|
|
|
41
|
+
const COLUMNS = [
|
|
42
|
+
{ field: "name", label: "Company", sortable: true, renderCell: (val) => val },
|
|
43
|
+
{ field: "status", label: "Status", renderCell: (val) => <StatusTag>{val}</StatusTag> },
|
|
44
|
+
{ field: "amount", label: "Amount", sortable: true, renderCell: (val) => formatCurrency(val) },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
<DataTable data={deals} columns={COLUMNS} searchFields={["name"]} pageSize={10} />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's a searchable, sortable, paginated table with auto-sized columns in 5 lines of config.
|
|
51
|
+
|
|
52
|
+
## DataTable Features
|
|
53
|
+
|
|
54
|
+
- Full-text search across any combination of fields, with optional fuzzy matching via Fuse.js
|
|
55
|
+
- Select, multi-select, and date range filters with configurable active badges and clear/reset controls
|
|
56
|
+
- Click-to-sort headers with three-state cycling (none, ascending, descending)
|
|
57
|
+
- Client-side or server-side pagination with configurable page size, visible page buttons, and First/Last navigation
|
|
58
|
+
- Collapsible row groups with per-column aggregation functions
|
|
59
|
+
- Row selection via checkboxes with client/server-aware "Select all" behavior
|
|
60
|
+
- Selection action bar with selected count, select/deselect all, and custom bulk action buttons
|
|
61
|
+
- Per-row actions via `rowActions` (static array or dynamic function)
|
|
62
|
+
- Two edit modes (discrete and inline/full-row) supporting 12 input types with per-column validation
|
|
63
|
+
- Auto-width column sizing based on data analysis, with manual overrides
|
|
64
|
+
- Text truncation helpers (`truncate: true` or `truncate: { maxLength }`)
|
|
65
|
+
- Customizable record label, row count display, and table appearance (`bordered`, `flush`, `scrollable`)
|
|
66
|
+
- Column-level footer for totals rows
|
|
67
|
+
- Works with `useAssociations` for live CRM data
|
|
68
|
+
- Server-side mode with loading/error states, search debounce, controlled state, and unified `onParamsChange` callback
|
|
69
|
+
|
|
70
|
+
## Filters and Footer Totals
|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
When more than 2 filters are defined, the first 2 appear inline and the rest are tucked behind a **Filters** button. Active filters display as removable chips with a "Clear all" option.
|
|
75
|
+
|
|
76
|
+
```jsx
|
|
77
|
+
const FILTERS = [
|
|
78
|
+
{
|
|
79
|
+
name: "status",
|
|
80
|
+
type: "select",
|
|
81
|
+
placeholder: "All statuses",
|
|
82
|
+
options: [
|
|
83
|
+
{ label: "Active", value: "active" },
|
|
84
|
+
{ label: "At Risk", value: "at-risk" },
|
|
85
|
+
{ label: "Churned", value: "churned" },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{ name: "closeDate", type: "dateRange", placeholder: "Close date" },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const COLUMNS = [
|
|
92
|
+
{ field: "company", label: "Company", sortable: true, footer: "Total",
|
|
93
|
+
renderCell: (val) => <Text format={{ fontWeight: "demibold" }}>{val}</Text> },
|
|
94
|
+
{ field: "amount", label: "Amount", sortable: true, align: "right",
|
|
95
|
+
footer: (rows) => formatCurrency(rows.reduce((sum, r) => sum + r.amount, 0)),
|
|
96
|
+
renderCell: (val) => formatCurrency(val) },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
<DataTable
|
|
100
|
+
data={DEALS}
|
|
101
|
+
columns={COLUMNS}
|
|
102
|
+
filters={FILTERS}
|
|
103
|
+
searchFields={["company"]}
|
|
104
|
+
pageSize={5}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Row Selection with Bulk Actions
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
const selectionActions = [
|
|
114
|
+
{ label: "Edit", icon: "edit", onClick: (ids) => console.log("Edit", ids) },
|
|
115
|
+
{ label: "Delete", icon: "delete", onClick: (ids) => console.log("Delete", ids) },
|
|
116
|
+
{ label: "Export", icon: "dataExport", onClick: (ids) => console.log("Export", ids) },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
<DataTable
|
|
120
|
+
data={COMPANIES}
|
|
121
|
+
columns={columns}
|
|
122
|
+
selectable={true}
|
|
123
|
+
rowIdField="id"
|
|
124
|
+
recordLabel={{ singular: "Company", plural: "Companies" }}
|
|
125
|
+
onSelectionChange={setSelected}
|
|
126
|
+
selectionActions={selectionActions}
|
|
127
|
+
searchFields={["name", "contact"]}
|
|
128
|
+
pageSize={10}
|
|
129
|
+
/>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Inline Editing
|
|
133
|
+
|
|
134
|
+

|
|
135
|
+
|
|
136
|
+
Two edit modes: **discrete** (click-to-edit, default) and **inline** (always-visible inputs).
|
|
137
|
+
|
|
138
|
+
```jsx
|
|
28
139
|
const columns = [
|
|
29
|
-
{ field: "
|
|
30
|
-
|
|
31
|
-
{ field: "
|
|
140
|
+
{ field: "company", label: "Company", editable: true, editType: "text",
|
|
141
|
+
renderCell: (val) => <Text format={{ fontWeight: "demibold" }}>{val}</Text> },
|
|
142
|
+
{ field: "status", label: "Status", editable: true, editType: "select",
|
|
143
|
+
editOptions: [
|
|
144
|
+
{ label: "Active", value: "active" },
|
|
145
|
+
{ label: "At Risk", value: "at-risk" },
|
|
146
|
+
],
|
|
147
|
+
renderCell: (val) => <StatusTag variant={STATUS_COLORS[val]}>{val}</StatusTag> },
|
|
148
|
+
{ field: "amount", label: "Amount", editable: true, editType: "currency",
|
|
149
|
+
renderCell: (val) => formatCurrency(val) },
|
|
32
150
|
];
|
|
33
151
|
|
|
34
|
-
<DataTable
|
|
152
|
+
<DataTable
|
|
153
|
+
data={data}
|
|
154
|
+
columns={columns}
|
|
155
|
+
rowIdField="id"
|
|
156
|
+
onRowEdit={(row, field, newValue) => handleEdit(row, field, newValue)}
|
|
157
|
+
/>
|
|
35
158
|
```
|
|
36
159
|
|
|
37
|
-
|
|
160
|
+
**Supported edit types:** `text`, `textarea`, `number`, `currency`, `stepper`, `select`, `multiselect`, `date`, `time`, `datetime`, `toggle`, `checkbox`
|
|
161
|
+
|
|
162
|
+
## Row Grouping
|
|
163
|
+
|
|
164
|
+

|
|
165
|
+
|
|
166
|
+
Collapsible groups with per-column aggregation functions:
|
|
167
|
+
|
|
168
|
+
```jsx
|
|
169
|
+
<DataTable
|
|
170
|
+
data={DEALS}
|
|
171
|
+
columns={COLUMNS}
|
|
172
|
+
groupBy={{
|
|
173
|
+
field: "segment",
|
|
174
|
+
label: (value, rows) => `${value} (${rows.length})`,
|
|
175
|
+
sort: "asc",
|
|
176
|
+
defaultExpanded: true,
|
|
177
|
+
aggregations: {
|
|
178
|
+
amount: (rows) => formatCurrency(rows.reduce((sum, r) => sum + r.amount, 0)),
|
|
179
|
+
},
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Full-Row Editing
|
|
185
|
+
|
|
186
|
+

|
|
187
|
+
|
|
188
|
+
Use `rowActions` with `editingRowId` for an Edit/Done flow:
|
|
189
|
+
|
|
190
|
+
```jsx
|
|
191
|
+
<DataTable
|
|
192
|
+
data={rows}
|
|
193
|
+
columns={columns}
|
|
194
|
+
rowIdField="id"
|
|
195
|
+
editingRowId={editingRowId}
|
|
196
|
+
onRowEdit={handleCommittedEdit}
|
|
197
|
+
rowActions={(row) => editingRowId === row.id
|
|
198
|
+
? [{ label: "Done", icon: "success", onClick: () => saveRow(row.id) }]
|
|
199
|
+
: [{ label: "Edit", icon: "edit", onClick: () => setEditingRowId(row.id) }]}
|
|
200
|
+
/>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Scrollable Wide Tables
|
|
204
|
+
|
|
205
|
+

|
|
206
|
+
|
|
207
|
+
```jsx
|
|
208
|
+
<DataTable data={data} columns={manyColumns} scrollable={true} pageSize={10} />
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## useAssociations
|
|
212
|
+
|
|
213
|
+

|
|
214
|
+
|
|
215
|
+
Connect live CRM data to a DataTable in two lines:
|
|
216
|
+
|
|
217
|
+
```jsx
|
|
218
|
+
import { useAssociations } from "@hubspot/ui-extensions/crm";
|
|
219
|
+
import { DataTable } from "hs-uix/datatable";
|
|
220
|
+
|
|
221
|
+
const { results, isLoading } = useAssociations({
|
|
222
|
+
toObjectType: "0-3",
|
|
223
|
+
properties: ["dealname", "dealstage", "amount", "closedate"],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const deals = results.map((a) => ({
|
|
227
|
+
id: a.toObjectId,
|
|
228
|
+
dealname: a.properties.dealname,
|
|
229
|
+
amount: Number(a.properties.amount) || 0,
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
<DataTable data={deals} columns={columns} searchFields={["dealname"]} />
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Server-Side Mode
|
|
236
|
+
|
|
237
|
+
For API-backed data or large datasets, use `serverSide={true}`. DataTable renders all the UI and fires callbacks — you handle the fetching.
|
|
238
|
+
|
|
239
|
+
```jsx
|
|
240
|
+
<DataTable
|
|
241
|
+
serverSide={true}
|
|
242
|
+
loading={loading}
|
|
243
|
+
error={error}
|
|
244
|
+
data={pageRows}
|
|
245
|
+
totalCount={totalCount}
|
|
246
|
+
columns={COLUMNS}
|
|
247
|
+
searchFields={["name", "email"]}
|
|
248
|
+
filters={FILTERS}
|
|
249
|
+
pageSize={25}
|
|
250
|
+
page={params.page}
|
|
251
|
+
searchDebounce={300}
|
|
252
|
+
onParamsChange={(p) => fetchData(p)}
|
|
253
|
+
/>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## DataTable Props
|
|
257
|
+
|
|
258
|
+
| Prop | Type | Default | Description |
|
|
259
|
+
|---|---|---|---|
|
|
260
|
+
| `data` | Array | *required* | Array of row objects |
|
|
261
|
+
| `columns` | Array | *required* | Column definitions |
|
|
262
|
+
| `renderRow` | `(row) => ReactNode` | — | Full row renderer (alternative to `renderCell`) |
|
|
263
|
+
| `searchFields` | string[] | `[]` | Fields to search across |
|
|
264
|
+
| `fuzzySearch` | boolean | `false` | Enable fuzzy matching via Fuse.js |
|
|
265
|
+
| `searchPlaceholder` | string | `"Search..."` | Placeholder text |
|
|
266
|
+
| `filters` | Array | `[]` | Filter configurations |
|
|
267
|
+
| `showFilterBadges` | boolean | `true` | Show active filter chips |
|
|
268
|
+
| `showClearFiltersButton` | boolean | `true` | Show "Clear all" button |
|
|
269
|
+
| `pageSize` | number | `10` | Rows per page |
|
|
270
|
+
| `maxVisiblePageButtons` | number | — | Max page buttons |
|
|
271
|
+
| `showButtonLabels` | boolean | `true` | Show First/Prev/Next/Last text |
|
|
272
|
+
| `showFirstLastButtons` | boolean | auto | Show First/Last buttons |
|
|
273
|
+
| `showRowCount` | boolean | `true` | Show record count text |
|
|
274
|
+
| `rowCountBold` | boolean | `false` | Bold the row count |
|
|
275
|
+
| `rowCountText` | `(shownOnPage, totalMatching) => string` | — | Custom row count formatter |
|
|
276
|
+
| `bordered` | boolean | `true` | Show table borders |
|
|
277
|
+
| `flush` | boolean | `true` | Remove bottom margin |
|
|
278
|
+
| `scrollable` | boolean | `false` | Allow horizontal scrolling |
|
|
279
|
+
| `defaultSort` | object | `{}` | Initial sort state |
|
|
280
|
+
| `groupBy` | object | — | Grouping config |
|
|
281
|
+
| `footer` | `(filteredData) => ReactNode` | — | Footer row renderer |
|
|
282
|
+
| `emptyTitle` | string | `"No results found"` | Empty state heading |
|
|
283
|
+
| `emptyMessage` | string | — | Empty state body |
|
|
284
|
+
| `recordLabel` | `{ singular, plural }` | `{ singular: "record", plural: "records" }` | Entity name |
|
|
285
|
+
| `selectable` | boolean | `false` | Enable row selection |
|
|
286
|
+
| `rowIdField` | string | `"id"` | Unique row identifier field |
|
|
287
|
+
| `selectedIds` | Array | — | Controlled selection |
|
|
288
|
+
| `onSelectionChange` | `(ids[]) => void` | — | Selection change callback |
|
|
289
|
+
| `onSelectAllRequest` | `(context) => void` | — | Server-side select all callback |
|
|
290
|
+
| `selectionActions` | Array | `[]` | Bulk action buttons |
|
|
291
|
+
| `selectionResetKey` | any | — | Reset key for uncontrolled selection |
|
|
292
|
+
| `rowActions` | Array \| Function | — | Per-row action buttons |
|
|
293
|
+
| `hideRowActionsWhenSelectionActive` | boolean | `false` | Hide row actions during selection |
|
|
294
|
+
| `editMode` | `"discrete"` \| `"inline"` | `"discrete"` | Edit mode |
|
|
295
|
+
| `editingRowId` | string \| number | — | Full-row edit target |
|
|
296
|
+
| `onRowEdit` | `(row, field, newValue) => void` | — | Edit commit callback |
|
|
297
|
+
| `onRowEditInput` | `(row, field, inputValue) => void` | — | Live input callback |
|
|
298
|
+
| `autoWidth` | boolean | `true` | Auto-compute column widths |
|
|
299
|
+
| `serverSide` | boolean | `false` | Enable server-side mode |
|
|
300
|
+
| `loading` | boolean | `false` | Show loading spinner |
|
|
301
|
+
| `error` | string \| boolean | — | Show error state |
|
|
302
|
+
| `totalCount` | number | — | Total records (server-side) |
|
|
303
|
+
| `page` | number | — | Current page (controlled) |
|
|
304
|
+
| `searchValue` | string | — | Controlled search term |
|
|
305
|
+
| `filterValues` | object | — | Controlled filter values |
|
|
306
|
+
| `sort` | object | — | Controlled sort state |
|
|
307
|
+
| `searchDebounce` | number | `0` | Debounce search callback (ms) |
|
|
308
|
+
| `onSearchChange` | `(term) => void` | — | Search callback |
|
|
309
|
+
| `onFilterChange` | `(filterValues) => void` | — | Filter callback |
|
|
310
|
+
| `onSortChange` | `(field, direction) => void` | — | Sort callback |
|
|
311
|
+
| `onPageChange` | `(page) => void` | — | Page callback |
|
|
312
|
+
| `onParamsChange` | `({ search, filters, sort, page }) => void` | — | Unified change callback |
|
|
313
|
+
|
|
314
|
+
### Column Definition
|
|
315
|
+
|
|
316
|
+
| Property | Type | Description |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| `field` | string | Key in the row object |
|
|
319
|
+
| `label` | string | Column header text |
|
|
320
|
+
| `sortable` | boolean | Enable sorting |
|
|
321
|
+
| `width` | `"min"` \| `"max"` \| `"auto"` \| `number` | Column width |
|
|
322
|
+
| `cellWidth` | `"min"` \| `"max"` \| `"auto"` | Cell-only width override |
|
|
323
|
+
| `align` | `"left"` \| `"center"` \| `"right"` | Text alignment |
|
|
324
|
+
| `renderCell` | `(value, row) => ReactNode` | Cell renderer |
|
|
325
|
+
| `truncate` | `true` \| `{ maxLength?: number }` | Text truncation with tooltip |
|
|
326
|
+
| `editable` | boolean | Enable inline editing |
|
|
327
|
+
| `editType` | string | Input type |
|
|
328
|
+
| `editOptions` | Array | Options for select/multiselect |
|
|
329
|
+
| `editValidate` | `(value, row) => true \| string` | Validation function |
|
|
330
|
+
| `editProps` | object | Pass-through props to edit input |
|
|
331
|
+
| `footer` | `string \| (rows) => ReactNode` | Footer cell content |
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
# FormBuilder
|
|
336
|
+
|
|
337
|
+
Declarative, config-driven forms for HubSpot UI Extensions. Define fields as data, get a complete form with validation, layout, multi-step wizards, and full HubSpot component integration.
|
|
338
|
+
|
|
339
|
+

|
|
38
340
|
|
|
39
341
|
```jsx
|
|
40
342
|
import { FormBuilder } from "hs-uix/form";
|
|
41
343
|
|
|
42
344
|
const fields = [
|
|
43
|
-
{ name: "
|
|
44
|
-
{ name: "
|
|
45
|
-
|
|
46
|
-
{ label: "User", value: "user" },
|
|
47
|
-
]},
|
|
345
|
+
{ name: "firstName", type: "text", label: "First name", required: true },
|
|
346
|
+
{ name: "lastName", type: "text", label: "Last name", required: true },
|
|
347
|
+
{ name: "email", type: "text", label: "Email", pattern: /^[^\s@]+@[^\s@]+$/, patternMessage: "Enter a valid email" },
|
|
48
348
|
];
|
|
49
349
|
|
|
50
|
-
<FormBuilder
|
|
350
|
+
<FormBuilder
|
|
351
|
+
columns={2}
|
|
352
|
+
fields={fields}
|
|
353
|
+
onSubmit={(values) => console.log(values)}
|
|
354
|
+
/>
|
|
51
355
|
```
|
|
52
356
|
|
|
53
|
-
|
|
357
|
+
## Field Types
|
|
358
|
+
|
|
359
|
+
Every field maps to a native HubSpot UI Extension component with full prop support:
|
|
360
|
+
|
|
361
|
+
| `type` | Component | Key Props |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| `text` | `Input` | `placeholder`, `onInput`, `onBlur` |
|
|
364
|
+
| `password` | `Input type="password"` | Same as text |
|
|
365
|
+
| `textarea` | `TextArea` | `rows`, `cols`, `resize`, `maxLength` |
|
|
366
|
+
| `number` | `NumberInput` | `min`, `max`, `precision`, `formatStyle` |
|
|
367
|
+
| `stepper` | `StepperInput` | `min`, `max`, `stepSize`, `precision` |
|
|
368
|
+
| `currency` | `CurrencyInput` | `currency` (ISO 4217), `min`, `max`, `precision` |
|
|
369
|
+
| `date` | `DateInput` | `format`, `min`, `max`, `timezone` |
|
|
370
|
+
| `time` | `TimeInput` | `interval`, `min`, `max`, `timezone` |
|
|
371
|
+
| `datetime` | `DateInput` + `TimeInput` | All date and time props |
|
|
372
|
+
| `select` | `Select` | `options`, `variant` |
|
|
373
|
+
| `multiselect` | `MultiSelect` | `options` |
|
|
374
|
+
| `toggle` | `Toggle` | `size`, `labelDisplay`, `textChecked`, `textUnchecked` |
|
|
375
|
+
| `checkbox` | `Checkbox` | `inline`, `variant` |
|
|
376
|
+
| `checkboxGroup` | `ToggleGroup checkboxList` | `options`, `inline`, `variant` |
|
|
377
|
+
| `radioGroup` | `ToggleGroup radioButtonList` | `options`, `inline`, `variant` |
|
|
378
|
+
| `display` | Custom render | Render-only, no form value |
|
|
379
|
+
| `repeater` | Sub-field rows | `fields`, `min`, `max` |
|
|
380
|
+
| `crmPropertyList` | `CrmPropertyList` | `properties`, `direction` |
|
|
381
|
+
| `crmAssociationPropertyList` | `CrmAssociationPropertyList` | `objectTypeId`, `properties`, `filters`, `sort` |
|
|
382
|
+
|
|
383
|
+
## Layout
|
|
384
|
+
|
|
385
|
+
FormBuilder provides four layout modes. Use `columns` or `columnWidth` to match HubSpot's standard look.
|
|
386
|
+
|
|
387
|
+
### Fixed Columns
|
|
54
388
|
|
|
55
389
|
```jsx
|
|
56
|
-
|
|
390
|
+
<FormBuilder columns={2} fields={fields} />
|
|
57
391
|
```
|
|
58
392
|
|
|
59
|
-
|
|
393
|
+
Use `colSpan` on individual fields to span multiple columns.
|
|
60
394
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
395
|
+
### Responsive (AutoGrid)
|
|
396
|
+
|
|
397
|
+
```jsx
|
|
398
|
+
<FormBuilder columnWidth={200} fields={fields} />
|
|
399
|
+
```
|
|
64
400
|
|
|
65
|
-
|
|
66
|
-
npm run build
|
|
401
|
+
Columns collapse automatically on narrow screens.
|
|
67
402
|
|
|
68
|
-
|
|
69
|
-
|
|
403
|
+
### Explicit Layout
|
|
404
|
+
|
|
405
|
+

|
|
406
|
+
|
|
407
|
+
```jsx
|
|
408
|
+
<FormBuilder
|
|
409
|
+
layout={[
|
|
410
|
+
["firstName", "lastName"], // 2 equal columns
|
|
411
|
+
["email"], // full width
|
|
412
|
+
["city", "state", "zip"], // 3 columns
|
|
413
|
+
]}
|
|
414
|
+
fields={fields}
|
|
415
|
+
/>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Weighted columns:
|
|
419
|
+
|
|
420
|
+
```jsx
|
|
421
|
+
layout={[
|
|
422
|
+
[{ field: "address", flex: 2 }, { field: "apt", flex: 1 }], // 2:1 ratio
|
|
423
|
+
]}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Layout priority:** `layout` > `columnWidth` > `columns` > legacy
|
|
427
|
+
|
|
428
|
+
## Validation
|
|
429
|
+
|
|
430
|
+
Built-in validators run in order, first failure wins:
|
|
431
|
+
|
|
432
|
+
```jsx
|
|
433
|
+
{
|
|
434
|
+
name: "email",
|
|
435
|
+
type: "text",
|
|
436
|
+
label: "Email",
|
|
437
|
+
required: true,
|
|
438
|
+
pattern: /^[^\s@]+@[^\s@]+$/,
|
|
439
|
+
patternMessage: "Enter a valid email",
|
|
440
|
+
minLength: 5,
|
|
441
|
+
maxLength: 100,
|
|
442
|
+
validators: [
|
|
443
|
+
(value) => value.endsWith("@example.com") ? true : "Use your company email",
|
|
444
|
+
],
|
|
445
|
+
validate: async (value, allValues, { signal }) => {
|
|
446
|
+
const exists = await checkEmailExists(value, { signal });
|
|
447
|
+
return exists ? "Email already in use" : true;
|
|
448
|
+
},
|
|
449
|
+
}
|
|
70
450
|
```
|
|
71
451
|
|
|
72
|
-
|
|
452
|
+
| Timing | Default | When |
|
|
453
|
+
|---|---|---|
|
|
454
|
+
| `validateOnChange` | `false` | Every keystroke |
|
|
455
|
+
| `validateOnBlur` | `true` | Field loses focus |
|
|
456
|
+
| `validateOnSubmit` | `true` | Submit attempt |
|
|
457
|
+
|
|
458
|
+
## Async Validation
|
|
459
|
+
|
|
460
|
+

|
|
73
461
|
|
|
462
|
+
Fields show a loading indicator while async validation runs. Pending requests are versioned and prior requests are aborted when supported (`signal`).
|
|
463
|
+
|
|
464
|
+
```jsx
|
|
465
|
+
{
|
|
466
|
+
name: "email",
|
|
467
|
+
type: "text",
|
|
468
|
+
label: "Email",
|
|
469
|
+
validate: async (value, allValues, { signal }) => {
|
|
470
|
+
const exists = await checkEmailExists(value, { signal });
|
|
471
|
+
return exists ? "Email already in use" : true;
|
|
472
|
+
},
|
|
473
|
+
validateDebounce: 500,
|
|
474
|
+
}
|
|
74
475
|
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
476
|
+
|
|
477
|
+
## Conditional Visibility & Dependent Properties
|
|
478
|
+
|
|
479
|
+

|
|
480
|
+
|
|
481
|
+
```jsx
|
|
482
|
+
const fields = [
|
|
483
|
+
{ name: "hasCompany", type: "toggle", label: "Has company?" },
|
|
484
|
+
{
|
|
485
|
+
name: "companyName",
|
|
486
|
+
type: "text",
|
|
487
|
+
label: "Company name",
|
|
488
|
+
visible: (values) => values.hasCompany === true,
|
|
489
|
+
},
|
|
490
|
+
];
|
|
81
491
|
```
|
|
82
492
|
|
|
493
|
+
Dependent fields are grouped in a HubSpot Tile container below their parent:
|
|
494
|
+
|
|
495
|
+
```jsx
|
|
496
|
+
{
|
|
497
|
+
name: "contractLength",
|
|
498
|
+
type: "number",
|
|
499
|
+
label: "Contract length (months)",
|
|
500
|
+
dependsOnConfig: {
|
|
501
|
+
field: "dealType",
|
|
502
|
+
display: "grouped",
|
|
503
|
+
label: "Contract details",
|
|
504
|
+
message: (parentLabel) => `These properties depend on ${parentLabel}`,
|
|
505
|
+
},
|
|
506
|
+
visible: (values) => values.dealType === "recurring",
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Multi-Step Wizard
|
|
511
|
+
|
|
512
|
+
```jsx
|
|
513
|
+
<FormBuilder
|
|
514
|
+
fields={allFields}
|
|
515
|
+
steps={[
|
|
516
|
+
{ title: "Contact Info", fields: ["firstName", "lastName", "email"] },
|
|
517
|
+
{ title: "Company", fields: ["company", "role"] },
|
|
518
|
+
{ title: "Review", render: ({ values, goBack }) => (
|
|
519
|
+
<ReviewPanel values={values} onEdit={goBack} />
|
|
520
|
+
)},
|
|
521
|
+
]}
|
|
522
|
+
showStepIndicator={true}
|
|
523
|
+
validateStepOnNext={true}
|
|
524
|
+
onSubmit={handleSubmit}
|
|
525
|
+
/>
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
## Repeater Fields
|
|
529
|
+
|
|
530
|
+

|
|
531
|
+
|
|
532
|
+
```jsx
|
|
533
|
+
{ name: "phones", type: "repeater", label: "Phone Numbers",
|
|
534
|
+
fields: [
|
|
535
|
+
{ name: "number", type: "text", label: "Number" },
|
|
536
|
+
{ name: "type", type: "select", label: "Type", options: PHONE_TYPES },
|
|
537
|
+
],
|
|
538
|
+
min: 1, max: 5 }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## Sections (Accordion Grouping)
|
|
542
|
+
|
|
543
|
+

|
|
544
|
+
|
|
545
|
+
```jsx
|
|
546
|
+
<FormBuilder
|
|
547
|
+
fields={fields}
|
|
548
|
+
sections={[
|
|
549
|
+
{ id: "basic", label: "Basic Info", fields: ["firstName", "lastName", "email"], defaultOpen: true },
|
|
550
|
+
{ id: "social", label: "Social Links", fields: ["facebook", "instagram"], defaultOpen: false },
|
|
551
|
+
]}
|
|
552
|
+
onSubmit={handleSubmit}
|
|
553
|
+
/>
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Custom Field Types
|
|
557
|
+
|
|
558
|
+

|
|
559
|
+
|
|
560
|
+
```jsx
|
|
561
|
+
<FormBuilder
|
|
562
|
+
fieldTypes={{
|
|
563
|
+
imageGallery: {
|
|
564
|
+
render: ({ value, onChange, error, field }) => (
|
|
565
|
+
<ImageGalleryInput urls={value} onUpdate={onChange} error={error} />
|
|
566
|
+
),
|
|
567
|
+
getEmptyValue: () => [],
|
|
568
|
+
isEmpty: (v) => v.length === 0,
|
|
569
|
+
},
|
|
570
|
+
}}
|
|
571
|
+
fields={[
|
|
572
|
+
{ name: "photos", type: "imageGallery", label: "Photos", required: true },
|
|
573
|
+
]}
|
|
574
|
+
/>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
## Display Options
|
|
578
|
+
|
|
579
|
+

|
|
580
|
+
|
|
581
|
+
## Read-Only Mode
|
|
582
|
+
|
|
583
|
+

|
|
584
|
+
|
|
585
|
+
```jsx
|
|
586
|
+
<FormBuilder
|
|
587
|
+
fields={fields}
|
|
588
|
+
readOnly={isPremiumAccount}
|
|
589
|
+
readOnlyMessage="This is a premium account. Editing is disabled."
|
|
590
|
+
onSubmit={handleSubmit}
|
|
591
|
+
/>
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## Controlled vs Uncontrolled
|
|
595
|
+
|
|
596
|
+
**Uncontrolled (default):**
|
|
597
|
+
|
|
598
|
+
```jsx
|
|
599
|
+
<FormBuilder
|
|
600
|
+
fields={fields}
|
|
601
|
+
initialValues={{ firstName: "John" }}
|
|
602
|
+
onSubmit={(values) => save(values)}
|
|
603
|
+
/>
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Controlled:**
|
|
607
|
+
|
|
608
|
+
```jsx
|
|
609
|
+
const [values, setValues] = useState({});
|
|
610
|
+
|
|
611
|
+
<FormBuilder
|
|
612
|
+
fields={fields}
|
|
613
|
+
values={values}
|
|
614
|
+
onChange={setValues}
|
|
615
|
+
onSubmit={(values) => save(values)}
|
|
616
|
+
/>
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
## Ref API
|
|
620
|
+
|
|
621
|
+
```jsx
|
|
622
|
+
const formRef = useRef();
|
|
623
|
+
|
|
624
|
+
<FormBuilder ref={formRef} fields={fields} onSubmit={save} />
|
|
625
|
+
|
|
626
|
+
formRef.current.submit(); // trigger validation + submit
|
|
627
|
+
formRef.current.validate(); // { valid: boolean, errors: {} }
|
|
628
|
+
formRef.current.reset(); // reset to initial values
|
|
629
|
+
formRef.current.getValues(); // current form values
|
|
630
|
+
formRef.current.isDirty(); // true if values changed
|
|
631
|
+
formRef.current.setFieldValue("email", "new@test.com"); // programmatic update
|
|
632
|
+
formRef.current.setFieldError("email", "Taken"); // programmatic error
|
|
633
|
+
formRef.current.setErrors({ email: "Exists", phone: "Invalid" }); // batch set
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Submit Lifecycle
|
|
637
|
+
|
|
638
|
+
```jsx
|
|
639
|
+
<FormBuilder
|
|
640
|
+
fields={fields}
|
|
641
|
+
transformValues={(values) => ({
|
|
642
|
+
...values,
|
|
643
|
+
fullName: `${values.firstName} ${values.lastName}`.trim(),
|
|
644
|
+
})}
|
|
645
|
+
onBeforeSubmit={async (values) => await showConfirmDialog()}
|
|
646
|
+
onSubmit={saveRecord}
|
|
647
|
+
onSubmitSuccess={(result, { reset }) => actions.addAlert({ type: "success", message: "Saved!" })}
|
|
648
|
+
onSubmitError={(err) => actions.addAlert({ type: "danger", message: err.message })}
|
|
649
|
+
resetOnSuccess={true}
|
|
650
|
+
/>
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
## Buttons
|
|
654
|
+
|
|
655
|
+
```jsx
|
|
656
|
+
<FormBuilder
|
|
657
|
+
fields={fields}
|
|
658
|
+
onSubmit={save}
|
|
659
|
+
labels={{ submit: "Save record", cancel: "Discard", back: "Previous", next: "Continue" }}
|
|
660
|
+
submitVariant="primary"
|
|
661
|
+
showCancel={true}
|
|
662
|
+
onCancel={() => actions.closeOverlay()}
|
|
663
|
+
loading={isSaving}
|
|
664
|
+
disabled={!canEdit}
|
|
665
|
+
/>
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
## FormBuilder Props
|
|
669
|
+
|
|
670
|
+
| Prop | Type | Default | Description |
|
|
671
|
+
|---|---|---|---|
|
|
672
|
+
| `fields` | `FormBuilderField[]` | required | Field definitions |
|
|
673
|
+
| `onSubmit` | `(values, { reset }) => void \| Promise` | required | Called on valid submit |
|
|
674
|
+
| `initialValues` | `Record<string, unknown>` | `{}` | Starting values (uncontrolled) |
|
|
675
|
+
| `values` | `Record<string, unknown>` | — | Controlled values |
|
|
676
|
+
| `onChange` | `(values) => void` | — | Change callback (controlled) |
|
|
677
|
+
| `errors` | `Record<string, string>` | — | Controlled validation errors |
|
|
678
|
+
| `onFieldChange` | `(name, value, allValues) => void` | — | Per-field change |
|
|
679
|
+
| `validateOnChange` | `boolean` | `false` | Validate on keystroke |
|
|
680
|
+
| `validateOnBlur` | `boolean` | `true` | Validate on blur |
|
|
681
|
+
| `validateOnSubmit` | `boolean` | `true` | Validate all before submit |
|
|
682
|
+
| `onValidationChange` | `(errors) => void` | — | Validation state callback |
|
|
683
|
+
| `steps` | `FormBuilderStep[]` | — | Enables multi-step mode |
|
|
684
|
+
| `step` | `number` | — | Controlled step (0-based) |
|
|
685
|
+
| `onStepChange` | `(step) => void` | — | Step change callback |
|
|
686
|
+
| `showStepIndicator` | `boolean` | `true` | Show StepIndicator |
|
|
687
|
+
| `validateStepOnNext` | `boolean` | `true` | Validate before Next |
|
|
688
|
+
| `submitVariant` | `"primary" \| "secondary"` | `"primary"` | Button variant |
|
|
689
|
+
| `showCancel` | `boolean` | `false` | Show cancel button |
|
|
690
|
+
| `onCancel` | `() => void` | — | Cancel callback |
|
|
691
|
+
| `submitPosition` | `"bottom" \| "none"` | `"bottom"` | Button placement |
|
|
692
|
+
| `loading` | `boolean` | — | Controlled loading state |
|
|
693
|
+
| `disabled` | `boolean` | `false` | Disable entire form |
|
|
694
|
+
| `labels` | `{ submit?, cancel?, back?, next? }` | — | Button label i18n object |
|
|
695
|
+
| `renderButtons` | `(context) => ReactNode` | — | Custom button-row renderer |
|
|
696
|
+
| `columns` | `number` | `1` | Fixed column count |
|
|
697
|
+
| `columnWidth` | `number` | — | AutoGrid responsive column width (px) |
|
|
698
|
+
| `layout` | `FormBuilderLayout` | — | Explicit row layout |
|
|
699
|
+
| `gap` | `string` | `"sm"` | Spacing between fields |
|
|
700
|
+
| `showRequiredIndicator` | `boolean` | `true` | Show * on required fields |
|
|
701
|
+
| `sections` | `FormBuilderSection[]` | — | Accordion field grouping |
|
|
702
|
+
| `fieldTypes` | `Record<string, FieldTypePlugin>` | — | Custom field type registry |
|
|
703
|
+
| `readOnly` | `boolean` | `false` | Lock all fields |
|
|
704
|
+
| `readOnlyMessage` | `string` | — | Warning alert in read-only mode |
|
|
705
|
+
| `alerts` | `{ addAlert?, errorTitle?, successTitle? }` | — | Grouped alert config |
|
|
706
|
+
| `error` | `string \| boolean` | — | Form-level error alert |
|
|
707
|
+
| `success` | `string` | — | Form-level success alert |
|
|
708
|
+
| `transformValues` | `(values) => values` | — | Reshape values before submit |
|
|
709
|
+
| `onBeforeSubmit` | `(values) => boolean \| Promise` | — | Intercept submit |
|
|
710
|
+
| `onSubmitSuccess` | `(result, helpers) => void` | — | Post-submit success |
|
|
711
|
+
| `onSubmitError` | `(error, helpers) => void` | — | Post-submit error |
|
|
712
|
+
| `resetOnSuccess` | `boolean` | `false` | Auto-reset after success |
|
|
713
|
+
| `autoSave` | `{ debounce?, onAutoSave }` | — | Debounced auto-save |
|
|
714
|
+
| `onDirtyChange` | `(isDirty) => void` | — | Dirty state callback |
|
|
715
|
+
| `ref` | `Ref<FormBuilderRef>` | — | Imperative ref |
|
|
716
|
+
|
|
717
|
+
### Field Props
|
|
718
|
+
|
|
719
|
+
| Prop | Type | Description |
|
|
720
|
+
|---|---|---|
|
|
721
|
+
| `name` | `string` | Unique field identifier |
|
|
722
|
+
| `type` | `FormBuilderFieldType` | Field type |
|
|
723
|
+
| `label` | `string` | Field label |
|
|
724
|
+
| `description` | `string` | Helper text |
|
|
725
|
+
| `placeholder` | `string` | Placeholder text |
|
|
726
|
+
| `tooltip` | `string` | Tooltip next to label |
|
|
727
|
+
| `required` | `boolean \| (values) => boolean` | Required validation |
|
|
728
|
+
| `readOnly` | `boolean` | Prevent editing |
|
|
729
|
+
| `disabled` | `boolean` | Disable this field |
|
|
730
|
+
| `defaultValue` | `unknown` | Default value |
|
|
731
|
+
| `colSpan` | `number` | Columns to span |
|
|
732
|
+
| `visible` | `(values) => boolean` | Conditional visibility |
|
|
733
|
+
| `dependsOnConfig` | `{ field, display?, label?, message? }` | Grouped dependent config |
|
|
734
|
+
| `validate` | `(value, allValues, context?) => true \| string \| Promise` | Custom validation |
|
|
735
|
+
| `validators` | `Array<Function>` | Additional custom validators |
|
|
736
|
+
| `validateDebounce` | `number` | Debounce async validation (ms) |
|
|
737
|
+
| `debounce` | `number` | Debounce onChange (ms) |
|
|
738
|
+
| `options` | `Option[] \| (values) => Option[]` | Dropdown/toggle options |
|
|
739
|
+
| `render` | `(props) => ReactNode` | Custom render escape hatch |
|
|
740
|
+
| `fieldProps` | `Record<string, unknown>` | Pass-through to HubSpot component |
|
|
741
|
+
| `onFieldChange` | `(value, allValues, helpers) => void` | Cross-field side effects |
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## Migrating from `@hs-uix/datatable` or `@hs-uix/form`
|
|
746
|
+
|
|
747
|
+
Both packages have been merged into `hs-uix`. Update your imports:
|
|
748
|
+
|
|
749
|
+
```diff
|
|
750
|
+
- import { DataTable } from "@hs-uix/datatable";
|
|
751
|
+
+ import { DataTable } from "hs-uix/datatable";
|
|
752
|
+
|
|
753
|
+
- import { FormBuilder } from "@hs-uix/form";
|
|
754
|
+
+ import { FormBuilder } from "hs-uix/form";
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Then remove the old packages:
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
npm uninstall @hs-uix/datatable @hs-uix/form
|
|
761
|
+
npm install hs-uix
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
83
766
|
## License
|
|
84
767
|
|
|
85
768
|
MIT
|
package/dist/form.js
CHANGED
|
@@ -945,14 +945,14 @@ var FormBuilder = (0, import_react.forwardRef)(function FormBuilder2(props, ref)
|
|
|
945
945
|
const handleFieldBlur = (0, import_react.useCallback)(
|
|
946
946
|
(name, value) => {
|
|
947
947
|
if (!validateOnBlur) return;
|
|
948
|
-
const resolvedValue = value != null ? value :
|
|
948
|
+
const resolvedValue = value != null ? value : formValuesRef.current[name];
|
|
949
949
|
const err = validateField(name, resolvedValue);
|
|
950
950
|
updateErrors({ [name]: err });
|
|
951
951
|
if (!err) {
|
|
952
952
|
triggerAsyncValidation(name, resolvedValue);
|
|
953
953
|
}
|
|
954
954
|
},
|
|
955
|
-
[validateOnBlur, validateField, updateErrors,
|
|
955
|
+
[validateOnBlur, validateField, updateErrors, triggerAsyncValidation]
|
|
956
956
|
);
|
|
957
957
|
const handleSubmit = (0, import_react.useCallback)(
|
|
958
958
|
async (e) => {
|
package/dist/form.mjs
CHANGED
|
@@ -949,14 +949,14 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
949
949
|
const handleFieldBlur = useCallback(
|
|
950
950
|
(name, value) => {
|
|
951
951
|
if (!validateOnBlur) return;
|
|
952
|
-
const resolvedValue = value != null ? value :
|
|
952
|
+
const resolvedValue = value != null ? value : formValuesRef.current[name];
|
|
953
953
|
const err = validateField(name, resolvedValue);
|
|
954
954
|
updateErrors({ [name]: err });
|
|
955
955
|
if (!err) {
|
|
956
956
|
triggerAsyncValidation(name, resolvedValue);
|
|
957
957
|
}
|
|
958
958
|
},
|
|
959
|
-
[validateOnBlur, validateField, updateErrors,
|
|
959
|
+
[validateOnBlur, validateField, updateErrors, triggerAsyncValidation]
|
|
960
960
|
);
|
|
961
961
|
const handleSubmit = useCallback(
|
|
962
962
|
async (e) => {
|
package/dist/index.js
CHANGED
|
@@ -1948,14 +1948,14 @@ var FormBuilder = (0, import_react2.forwardRef)(function FormBuilder2(props, ref
|
|
|
1948
1948
|
const handleFieldBlur = (0, import_react2.useCallback)(
|
|
1949
1949
|
(name, value) => {
|
|
1950
1950
|
if (!validateOnBlur) return;
|
|
1951
|
-
const resolvedValue = value != null ? value :
|
|
1951
|
+
const resolvedValue = value != null ? value : formValuesRef.current[name];
|
|
1952
1952
|
const err = validateField(name, resolvedValue);
|
|
1953
1953
|
updateErrors({ [name]: err });
|
|
1954
1954
|
if (!err) {
|
|
1955
1955
|
triggerAsyncValidation(name, resolvedValue);
|
|
1956
1956
|
}
|
|
1957
1957
|
},
|
|
1958
|
-
[validateOnBlur, validateField, updateErrors,
|
|
1958
|
+
[validateOnBlur, validateField, updateErrors, triggerAsyncValidation]
|
|
1959
1959
|
);
|
|
1960
1960
|
const handleSubmit = (0, import_react2.useCallback)(
|
|
1961
1961
|
async (e) => {
|
package/dist/index.mjs
CHANGED
|
@@ -1981,14 +1981,14 @@ var FormBuilder = forwardRef(function FormBuilder2(props, ref) {
|
|
|
1981
1981
|
const handleFieldBlur = useCallback2(
|
|
1982
1982
|
(name, value) => {
|
|
1983
1983
|
if (!validateOnBlur) return;
|
|
1984
|
-
const resolvedValue = value != null ? value :
|
|
1984
|
+
const resolvedValue = value != null ? value : formValuesRef.current[name];
|
|
1985
1985
|
const err = validateField(name, resolvedValue);
|
|
1986
1986
|
updateErrors({ [name]: err });
|
|
1987
1987
|
if (!err) {
|
|
1988
1988
|
triggerAsyncValidation(name, resolvedValue);
|
|
1989
1989
|
}
|
|
1990
1990
|
},
|
|
1991
|
-
[validateOnBlur, validateField, updateErrors,
|
|
1991
|
+
[validateOnBlur, validateField, updateErrors, triggerAsyncValidation]
|
|
1992
1992
|
);
|
|
1993
1993
|
const handleSubmit = useCallback2(
|
|
1994
1994
|
async (e) => {
|