SearchForm
search-formBuilds collapsible query filters with reset and submit actions for data lists.
Usage
Basic usage
fields configuration driver; omit values/onChange to enter the internal uncontrolled state. Enter/query trigger onSearch.
<SearchForm
fields={[
{ name: "keyword", label: "Keywords", placeholder: "Order number / customer name" },
{ name: "status", label: "Status", type: "select", placeholder: "All",
options: [{ value: "pending", label: "Pending" }, { value: "done", label: "Completed" }] },
{ name: "owner", label: "Responsible Person", placeholder: "Name" },
]}
onSearch={(v) => console.log(v)}
onReset={() => {}}
/>Foldable + Spanning Columns
The field can be folded when it is filled with multiple rows (only the first row is displayed when collapsed by default); date-range uses colSpan to span two columns.
<SearchForm
fields={fields} // 6+ fields
collapsible // Enough fields to automatically enable "expand/collapse"
onSearch={(v) => console.log(v)}
/>Customize the number of columns and copywriting
columns controls the number of desktop columns, submitText / resetText covers the button copy.
<SearchForm
fields={fields}
columns={2}
submitText="Search"
resetText="Clear"
onSearch={(v) => console.log(v)}
/>Hierarchical fields: cascader and region
cascader takes an options tree such as an org chart or a category tree, while region ships with Chinese administrative divisions and loads them on demand. Both produce a path array as their value.
const fields: SearchField[] = [
{
name: "store",
label: "Store",
type: "cascader",
options: orgTree, // TreeNode[], same as the nodes of Cascader
changeOnSelect: true, // allow filtering down to only a region or a city
},
{ name: "area", label: "Region", type: "region" },
];
<SearchForm fields={fields} columns={2} onSearch={(v) => console.log(v)} />When to use
Use SearchForm for the filter area above an admin list. Declare query fields with fields; the component arranges them in a fixed-column grid, collapses overflow to one row, and supplies Search and Reset actions. Unlike Form and ProForm, which submit business records, SearchForm emits filter parameters through onSearch. It is commonly used as ProTable's search area.
Import
import { SearchForm, planLayout, canCollapse, totalSpan } from "@hulianui/ui"Props
| Name | Type | Default | Description |
|---|---|---|---|
| fields* | SearchField[] | - | Definitions for the filter fields. |
| values | Record<string, unknown> | - | Controlled values; omit for internal state. |
| columns | number | 3 | Number of columns at desktop widths. |
| gap | number | 4 | Row and column gap (× 0.25rem). |
| collapsible | boolean | true | Enables one-row collapse when fields exceed one row; has no effect otherwise. |
| defaultCollapsed | boolean | true | Initial collapsed state. |
| loading | boolean | false | Whether the Search button is in a loading state. |
| className | string | - | Additional class name for the root element. |
Events
| Event | Type | Description |
|---|---|---|
| onSearch* | (values: Record<string, unknown>) => void | Called with the current filter values when Search is clicked or Enter is pressed. |
| onChange | (values: Record<string, unknown>) => void | Called after any field changes; use it to write back controlled values. |
| onReset | (values: Record<string, unknown>) => void | Called after reset with each field restored to its default or type-specific empty value. |
Slots
| Slot | Type | Description |
|---|---|---|
| submitText | ReactNode | Primary button label (default "\u67e5\u8be2", meaning “Search”). |
| resetText | ReactNode | Reset button label (default "\u91cd\u7f6e", meaning “Reset”). |
SearchField is a discriminated union selected by type or render; omitting both creates an input field. Shared properties are name* (the value key), label*, placeholder?, colSpan? (defaults to 1 and is capped at columns), and defaultValue?. Supported variants are:
type?: "input"+inputType?: stringtype: "number"+min?/max?/step?(forwards native input constraints)type: "number-range"+ the same constraints (the value is a tuple)type: "select"+options: { value: string; label: ReactNode }[]type: "multi-select"+options(value isstring[])type: "remote-select"+fetcher(signature is the same as RemoteSelect) +resolveValue?+multiple?type: "cascader"+options: TreeNode[](thenodesof Cascader) +changeOnSelect?/showSearch?(the value is a path array)type: "region"+level?: 2 | 3+changeOnSelect?/showSearch?(Chinese administrative divisions are built in, so no options are needed; the value is an array of division codes)type: "date"/type: "date-range"type: "datetime"/type: "datetime-range"(nativedatetime-local)
The field type determines the value shape: every *-range value is a two-item tuple [start, end], with "" for an unfilled endpoint; multi-select and a multiple remote-select use string[]; cascader and region use a root-to-leaf path array, which is [] while nothing is selected; all other built-in fields use string.
After reset, each field returns to its defaultValue or its type-specific empty shape. Do not assume every field resets to an empty string.
render: (ctx: { name; value; onChange }) => ReactNode(escape hatch for a custom control)
Example
const fields: SearchField[] = [
{ name: "keyword", label: "Keywords", placeholder: "Order number / customer name" },
{ name: "status", label: "Status", type: "select", placeholder: "All",
options: [
{ value: "pending", label: "Pending" },
{ value: "done", label: "Completed" },
] },
{ name: "range", label: "Created", type: "date-range", colSpan: 2 },
];
<SearchForm
fields={fields}
values={values}
onChange={setValues}
onSearch={(v) => fetchList(v)}
onReset={(v) => fetchList(v)}
/>Usage guidelines
- Controlled usage requires
valuesandonChange; without writeback, fields cannot be edited. - Expand/Collapse appears only when total field span exceeds one row. With fewer fields it is suppressed automatically.
onResetreceives values after every field returns to itsdefaultValueor type-specific empty shape, not{}. Re-query with that callback value to retain default filters.- Query operators such as `LIKE`, `BETWEEN`, and `=` do not belong in SearchForm. They are part of the backend contract. Translate values into the API request shape inside
onSearch; encoding one backend's protocol in field configuration would couple the library to that service. datetimeanddatetime-rangeuse nativedatetime-local, so values are local-time strings without a timezone, such as"2026-07-29T14:30". Do not callnew Date(...).toISOString()blindly; in UTC+8 it shifts the value by eight hours without reporting an error. Convert explicitly inonSearchif the API requires ISO timestamps.cascaderandregionproduce a path array (root to leaf), and an unselected field is[], not"". When the backend only accepts the leaf id, takepath.at(-1)insideonSearch; enablechangeOnSelectwhen an intermediate level should be submittable.- The built-in administrative-division table behind
regionis about 137 KB, so that field type is loaded on demand: only a page that actually declarestype: "region"fetches that chunk. Until it arrives the control's slot is an equally tall placeholder, so the query area never jumps. regionreports division codes (["11","1101","110101"]) only. When the backend wants the name path, use therenderescape hatch with RegionCascader, whoseonChangeprovides the names as its second argument.- RemoteSelect's
onChangealso returns complete options, but SearchForm stores only its first value argument. Use therenderescape hatch when the raw row is required.
Related
Form · ModalForm / DrawerForm · ProForm · StepsForm · LoginForm · Field
Playground
<SearchForm
fields={fields}
values={values}
onChange={setValues}
onSearch={(v) => console.log(v)}
onReset={() => {}}
/>