Query Filter
Form-style list search — staged submit, responsive grid, collapse, and full Formily validation on Search. Ant Design Pro QueryFilter pattern.
Plus Registry
Query Filter ships on the authenticated @f-ui-plus registry. Configure FUI_PLUS_REGISTRY_TOKEN as described in Installation — Plus Registry.
Query Filter is a staged search form for list pages: users fill label + control fields, then click Search to query — typing never hits the network. It follows the Ant Design Pro QueryFilter / Arco SearchForm pattern (grid layout, collapse, reset, submit).
By default, Search / Reset / Expand sit in an independent actions column beside the field grid (actionsLayout="rowEnd") with a vertical stack, a vertical divider, and justify-between (Search/Reset top, Expand bottom). Expanded fields are capped by maxExpandedRows so a long filter does not push the list off-screen.
Unlike a plain <form>, Query Filter is a thin shell over Form (Formily) — every field is a Formily Field. That means required, validator, reactions, SchemaField, and form effects work exactly as on a normal f-ui form; Search runs form.submit(), so invalid filters block onFinish until errors are fixed.
Query Filter shares the FormPanel container and FormLayoutProps layout API with the rest of the form family — consistent spacing, radius, and h-8 action buttons. Default actions sit in the grid actions cell (rowEnd); FormActions is used only when actionsLayout="newLine".
Pair it above Table or any list — wire onFinish to your fetch, adapter params, or client-side filter.
When To Use
- High-frequency filters at the top of a list — orders, users, tickets, audit logs.
- Form-style criteria without operators — each field is one control; your API maps values to predicates.
- You need validation before search — min length, email format, conditional required fields, async rules.
- Backend-driven filter definitions — render fields from JSON Schema via
SchemaField. - Use Table column-header filters instead when users need ad-hoc operator-level conditions (contains / between / not-in …).
Compared to Ant Design Pro QueryFilter
ProComponents QueryFilter | f-ui QueryFilter |
|---|---|
Staged submit (onFinish) | onFinish — fires after Formily validation passes |
onReset | onReset — after form.reset() |
defaultCollapsed / collapsed / onCollapse | Same + collapsedRows; default defaultCollapsed={true} |
span / responsive columns | columns — number or { base, sm, md, lg, xl } grid |
labelWidth | labelWidth — when orientation="horizontal" |
submitter / optionRender | Built-in Search (primary) + Reset; optional expand toggle. Default actionsLayout="rowEnd" matches Pro’s last-cell submitter; newLine restores a full-width bottom bar |
form / rules | Formily — FormField required, validator, reactions, or SchemaField |
loading on submit | loading — disables Search while your query runs |
| — (f-ui addition) | maxExpandedRows (default 3) — field area scrolls when expanded past the cap; Search stays outside the scrollport |
Query Filter does not sync to URL or render filter chips — it only emits values. Your page owns persistence and list wiring.
Features
| Area | Behavior |
|---|---|
| Field authoring | <FormField> (JSX) or <SchemaField> (JSON Schema) — same as Formily. |
| Submission | Staged — Search or Enter calls form.submit(); onFinish receives values with empty fields pruned. |
| Validation | Formily validators run on submit (revealErrors: "submit" via inner Form); failed validation expands hidden fields when needed and focuses the first invalid control (focusOnInvalid: "first-field"). |
| Layout | Vertical field grid by default (label above control); responsive columns via columns; optional collapse; expanded height capped by maxExpandedRows. |
| Actions | Default in-grid last cell (rowEnd) with vertical Search / Reset / expand; opt into full-width newLine bar; expand/collapse when overflow. |
| External form | Pass form={createForm(...)} for effects, initial values, or shared state with other UI. |
| Pairing | Standalone — no list handle coupling; caller wires values to Table or any data source. |
Interactions
| Event | Behavior |
|---|---|
| Type in a field | Updates Formily state only — no query. |
| Search / Enter | form.submit() → validate all fields → onFinish(pruneEmpty(values)) on success. |
| Validation failure | Field errors shown (FormItem chrome + form-scope FormErrorSummary); collapsed fields expand; focus moves to the first invalid control; onFinish not called. |
| Reset | form.reset() → empty strings on text fields → onReset callback. |
| Expand (+N) / Collapse | Toggle CSS-hidden overflow fields (does not clear values). |
loading={true} | Search button disabled until your async onFinish completes (you control loading). |
Formily on a search form
Query Filter is not a separate form engine. It renders:
QueryFilter
└── Form (Formily) ← form.submit(), revealErrors, vertical labels
├── field grid
│ ├── FormField / SchemaField ← validators, reactions, kinds
│ └── actions cell (rowEnd) ← Search → Reset → Expand
└── FormActions bar (newLine only)Use the same APIs documented on Form (Formily):
| Capability | On Query Filter |
|---|---|
required, validator, kind format rules | Per <FormField> — blocks Search when invalid |
reactions | Show/hide/require fields based on other filter values |
SchemaField + JSON Schema | Backend-driven filter definitions |
form prop + createForm({ effects }) | Shared VM, onFieldValueChange, initial values |
description / validatingDescription | Helper text under fields |
Empty values: onFinish runs pruneEmpty — "", null, undefined, and empty arrays are dropped so your API receives only active criteria.
Installing
Query Filter is a Plus component — install through the authenticated @f-ui-plus registry with FUI_PLUS_REGISTRY_TOKEN configured.
pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/query-filter.jsonnpx shadcn@latest add https://ui.isaacfei.com/api/plus/r/query-filter.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/query-filter.jsonbunx shadcn@latest add https://ui.isaacfei.com/api/plus/r/query-filter.jsonWith the @f-ui-plus namespace registered in components.json, shadcn add @f-ui-plus/query-filter also works and pulls formily automatically.
registryDependencies: @f-ui-plus/formily, button. Runtime: @formily/core, @formily/react, @formily/reactive, @formily/validator.
Usage
Derive select options from the same source as your table column variants so filter values match row data:
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
import { STATUS_VARIANTS } from "@/features/demo-orders/demo-data";
const STATUS_OPTIONS = Object.entries(STATUS_VARIANTS).map(([value, v]) => ({
label: v.label,
value,
}));
export function OrderFilters({ onSearch }: { onSearch: (values: Record<string, unknown>) => void }) {
return (
<QueryFilter
columns={{ base: 1, md: 2, lg: 3 }}
onFinish={onSearch}
onReset={() => onSearch({})}
>
<FormField kind="text" name="keyword" label="Keyword" />
<FormField
kind="select"
name="status"
label="Status"
componentProps={{ options: STATUS_OPTIONS, placeholder: "Any status" }}
/>
<FormField
kind="dateRange"
name="created"
label="Created"
/>
</QueryFilter>
);
}Examples
Basic Search
Two fields and Search — submitted values appear below after a successful query. Empty fields are omitted from onFinish.
"use client";
import { useState } from "react";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
export function QueryFilterMinimalDemo() {
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null);
return (
<div className="w-full space-y-4">
<QueryFilter onFinish={(values) => setSubmitted(values)}>
<FormField kind="text" name="keyword" label="Keyword" />
<FormField
kind="select"
name="status"
label="Status"
componentProps={{
options: [
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
}}
/>
</QueryFilter>
{submitted ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : null}
</div>
);
}Horizontal Layout (optional)
Pass orientation="horizontal" and labelWidth for Ant Pro–style inline labels. Default is vertical (label above control).
"use client";
import { useState } from "react";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
import { STATUS_VARIANTS } from "@/features/demo-orders/demo-data";
const STATUS_OPTIONS = Object.entries(STATUS_VARIANTS).map(([value, variant]) => ({
label: variant.label,
value,
}));
export function QueryFilterLabelWidthDemo() {
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null);
return (
<div className="w-full space-y-4">
<QueryFilter
orientation="horizontal"
labelWidth="5rem"
columns={{ base: 1, md: 2, lg: 3 }}
onFinish={setSubmitted}
>
<FormField kind="text" name="orderNumber" label="Order #" />
<FormField kind="text" name="customer" label="Customer" />
<FormField
kind="select"
name="status"
label="Status"
componentProps={{ options: STATUS_OPTIONS, placeholder: "Any" }}
/>
</QueryFilter>
{submitted ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : null}
</div>
);
}Collapse Overflow Fields
Six fields with collapsedRows={1} — default is already collapsed, so the demo omits defaultCollapsed. Expand (+N) reveals the rest without clearing values. Actions sit in an independent column (rowEnd) with Expand bottom-anchored.
"use client";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
export function QueryFilterCollapseDemo() {
return (
<QueryFilter
columns={{ base: 1, md: 2, lg: 3 }}
collapsedRows={1}
onFinish={() => {}}
>
<FormField kind="text" name="name" label="Name" />
<FormField kind="text" name="email" label="Email" />
<FormField kind="text" name="company" label="Company" />
<FormField kind="text" name="city" label="City" />
<FormField kind="text" name="country" label="Country" />
<FormField kind="text" name="tag" label="Tag" />
</QueryFilter>
);
}Bottom Action Row
Opt into the legacy full-width action bar with actionsLayout="newLine".
"use client";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
export function QueryFilterActionsNewlineDemo() {
return (
<QueryFilter
actionsLayout="newLine"
columns={{ base: 1, md: 2, lg: 3 }}
onFinish={() => {}}
>
<FormField kind="text" name="name" label="Name" />
<FormField kind="text" name="email" label="Email" />
<FormField kind="text" name="company" label="Company" />
</QueryFilter>
);
}Expanded Height Cap
Many fields with defaultCollapsed={false} and maxExpandedRows={3} — the field area scrolls; Search stays outside the scrollport.
"use client";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
const FIELDS = [
"Name", "Email", "Company", "City", "Country", "Tag",
"Owner", "Channel", "Sku", "Warehouse", "Priority", "Note",
] as const;
export function QueryFilterExpandedCapDemo() {
return (
<QueryFilter
columns={{ base: 1, md: 2, lg: 3 }}
collapsedRows={1}
defaultCollapsed={false}
maxExpandedRows={3}
onFinish={() => {}}
>
{FIELDS.map((label) => (
<FormField
key={label}
kind="text"
name={label.toLowerCase()}
label={label}
/>
))}
</QueryFilter>
);
}Field Kinds
Text, select, and date-range in one row. Select options use the same STATUS_VARIANTS labels as the Table order demos.
"use client";
import { useState } from "react";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
import { STATUS_VARIANTS } from "@/features/demo-orders/demo-data";
const STATUS_OPTIONS = Object.entries(STATUS_VARIANTS).map(([value, variant]) => ({
label: variant.label,
value,
}));
export function QueryFilterFieldKindsDemo() {
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null);
return (
<div className="w-full space-y-4">
<QueryFilter columns={{ base: 1, md: 3 }} onFinish={(v) => setSubmitted(v)}>
<FormField kind="text" name="keyword" label="Keyword" />
<FormField
kind="select"
name="status"
label="Status"
componentProps={{
options: STATUS_OPTIONS,
placeholder: "Any status",
}}
/>
<FormField kind="dateRange" name="created" label="Created" />
</QueryFilter>
{submitted ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : null}
</div>
);
}Validation Before Search
Formily validators gate Search: try keyword a or an invalid email — onFinish does not run until fields pass. The attempt counter only increments on successful submit.
Click Search with keyword a or email not-an-email — validation blocks onFinish. Fix the fields, then Search again.
Search attempts: 0 — only increments when validation passes and onFinish runs.
No successful query yet.
"use client";
import { useState } from "react";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
function minLengthWhenFilled(min: number, message: string) {
return (value: string | undefined) => {
const trimmed = String(value ?? "").trim();
if (!trimmed) return "";
return trimmed.length >= min ? "" : message;
};
}
export function QueryFilterValidationDemo() {
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null);
const [attempts, setAttempts] = useState(0);
return (
<div className="w-full space-y-4">
<p className="text-muted-foreground text-xs">
Click <strong>Search</strong> with keyword <code>a</code> or email{" "}
<code>not-an-email</code> — validation blocks <code>onFinish</code>. Fix the
fields, then Search again.
</p>
<QueryFilter
onFinish={(values) => {
setSubmitted(values);
setAttempts((n) => n + 1);
}}
onReset={() => {
setSubmitted(null);
setAttempts(0);
}}
>
<FormField
kind="text"
name="keyword"
label="Keyword"
description="Optional — at least 2 characters when filled"
validator={minLengthWhenFilled(2, "Enter at least 2 characters")}
/>
<FormField
kind="email"
name="customerEmail"
label="Customer email"
description="Optional — format checked on blur"
componentProps={{ placeholder: "buyer@example.com" }}
/>
<FormField
kind="currency"
name="minAmount"
label="Min amount"
description="Optional — must be zero or positive"
componentProps={{ currency: "USD", placeholder: "0.00" }}
validator={(value) => {
if (value === undefined || value === null || value === "") return "";
return Number(value) >= 0 ? "" : "Amount cannot be negative";
}}
/>
</QueryFilter>
<div className="text-muted-foreground space-y-2 text-xs">
<p>
Search attempts: <strong>{attempts}</strong> — only increments when validation
passes and <code>onFinish</code> runs.
</p>
{submitted ? (
<pre className="bg-muted overflow-auto rounded-md p-3">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : (
<p>No successful query yet.</p>
)}
</div>
</div>
);
}Cross-Field Reactions
Pick Cancelled to require a reason, or Shipped to show channel — reactions on FormField, identical to Form (Formily).
Pick Cancelled to reveal a required reason field, or Shipped to reveal channel — powered by Formily reactions on each FormField.
"use client";
import { useMemo, useState } from "react";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
import { STATUS_VARIANTS } from "@/features/demo-orders/demo-data";
const STATUS_OPTIONS = Object.entries(STATUS_VARIANTS).map(([value, variant]) => ({
label: variant.label,
value,
}));
const CHANNEL_OPTIONS = [
{ label: "Web", value: "web" },
{ label: "Retail", value: "retail" },
{ label: "Partner", value: "partner" },
];
export function QueryFilterReactionsDemo() {
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null);
const statusOptions = useMemo(() => STATUS_OPTIONS, []);
return (
<div className="w-full space-y-4">
<p className="text-muted-foreground text-xs">
Pick <strong>Cancelled</strong> to reveal a required reason field, or{" "}
<strong>Shipped</strong> to reveal channel — powered by Formily{" "}
<code>reactions</code> on each <code>FormField</code>.
</p>
<QueryFilter columns={{ base: 1, md: 2, lg: 3 }} onFinish={setSubmitted}>
<FormField kind="text" name="keyword" label="Keyword" />
<FormField
kind="select"
name="status"
label="Status"
componentProps={{ options: statusOptions, placeholder: "Any status" }}
/>
<FormField
kind="text"
name="cancelReason"
label="Cancel reason"
description="Required when status is Cancelled"
reactions={(field) => {
const isCancelled = field.query(".status").value() === "cancelled";
field.visible = isCancelled;
field.required = isCancelled;
}}
/>
<FormField
kind="select"
name="channel"
label="Channel"
description="Shown when status is Shipped"
componentProps={{ options: CHANNEL_OPTIONS, placeholder: "Any channel" }}
reactions={(field) => {
field.visible = field.query(".status").value() === "shipped";
}}
/>
</QueryFilter>
{submitted ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : null}
</div>
);
}JSON Schema Fields
Drive the filter from a schema object — useful when your BFF returns filter definitions. Pass form when you need a shared Formily instance.
Backend-driven filters: pass a JSON Schema to SchemaField inside QueryFilter — uses the same schema component registry as Form (Formily).
"use client";
import { useMemo, useState } from "react";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { createForm } from "@/components/f-ui/formily/internals/create-form";
import { SchemaField } from "@/components/f-ui/formily/schema-field";
import { STATUS_VARIANTS } from "@/features/demo-orders/demo-data";
const STATUS_OPTIONS = Object.entries(STATUS_VARIANTS).map(([value, variant]) => ({
label: variant.label,
value,
}));
const filterSchema = {
type: "object",
properties: {
keyword: {
type: "string",
title: "Keyword",
"x-decorator": "FormItem",
"x-component": "Input",
"x-component-props": { placeholder: "Order # or customer" },
},
status: {
type: "string",
title: "Status",
"x-decorator": "FormItem",
"x-component": "Select",
"x-component-props": { options: STATUS_OPTIONS, placeholder: "Any status" },
},
created: {
type: "string",
title: "Created",
"x-decorator": "FormItem",
"x-component": "DateRangePicker",
},
},
} as const;
interface FilterValues {
keyword?: string;
status?: string;
created?: [string, string];
}
export function QueryFilterSchemaDemo() {
const form = useMemo(() => createForm<FilterValues>(), []);
const [submitted, setSubmitted] = useState<Partial<FilterValues> | null>(null);
return (
<div className="w-full space-y-4">
<p className="text-muted-foreground text-xs">
Backend-driven filters: pass a JSON Schema to <code>SchemaField</code> inside{" "}
<code>QueryFilter</code> — uses the same schema component registry as Form (Formily).
</p>
<QueryFilter form={form} columns={{ base: 1, md: 3 }} onFinish={setSubmitted}>
<SchemaField schema={filterSchema} />
</QueryFilter>
{submitted ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(submitted, null, 2)}</code>
</pre>
) : null}
</div>
);
}With Table
QueryFilter above a Table; this demo filters in-memory rows client-side so the table visibly reacts after Search.
QueryFilter is independent of Table. Wire onFinish to your fetch or params — this demo filters in memory so you can see the table react after Search.
| ORD-1001 | Acme Corp | Confirmed | 2,499.00 |
| ORD-1002 | Globex Inc | Shipped | 849.50 |
| ORD-1003 | Initech | Draft | 120.00 |
| ORD-1004 | Umbrella LLC | Delivered | 3,200.00 |
| ORD-1005 | Soylent Corp | Cancelled | 45.99 |
| ORD-1006 | Wayne Enterprises | Confirmed | 18,750.00 |
| ORD-1007 | Stark Industries | Shipped | 5,600.00 |
| ORD-1008 | Oscorp | Draft | 299.99 |
"use client";
import { useMemo, useState } from "react";
import { useDataList } from "@/components/f-ui/data-list-internals/use-data-list";
import { isListInteractionLocked } from "@/components/f-ui/data-list-internals/lib/resolve-list-loading";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { Table } from "@/components/f-ui/table/table";
import { QueryFilter } from "@/components/f-ui/query-filter/query-filter";
import { FormField } from "@/components/f-ui/formily/form-field";
import { type Order, SEED_ORDERS, STATUS_VARIANTS } from "@/features/demo-orders/demo-data";
import { TableDemoQueryProvider } from "@/demos/table/samples";
import { f } from "@/components/f-ui/field-types/catalog";
const STATUS_OPTIONS = Object.entries(STATUS_VARIANTS).map(([value, variant]) => ({
label: variant.label,
value,
}));
function filterOrders(
orders: readonly Order[],
query: Record<string, unknown> | null,
): Order[] {
if (!query) return [...orders];
let result = [...orders];
const keyword = String(query.keyword ?? "").trim();
if (keyword) {
const lower = keyword.toLowerCase();
result = result.filter(
(row) =>
row.orderNumber.toLowerCase().includes(lower) ||
row.customer.toLowerCase().includes(lower),
);
}
const status = query.status;
if (status !== undefined && status !== null && status !== "") {
result = result.filter((row) => row.status === status);
}
return result;
}
const orderSchema = defineDataListSchema<Order>({
orderNumber: f.text({ label: "Order #", sortable: true }),
customer: f.text({ label: "Customer", sortable: true }),
status: f.enum({ render: "status", label: "Status", sortable: true, variants: STATUS_VARIANTS }),
amount: f.currency({ label: "Amount", currency: "USD", sortable: true }),
});
export function QueryFilterWithTableDemo() {
return (
<TableDemoQueryProvider>
<QueryFilterWithTableDemoInner />
</TableDemoQueryProvider>
);
}
function QueryFilterWithTableDemoInner() {
const [lastQuery, setLastQuery] = useState<Record<string, unknown> | null>(null);
const filteredData = useMemo(
() => filterOrders(SEED_ORDERS, lastQuery),
[lastQuery],
);
const handle = useDataList({
schema: orderSchema,
listCode: "docs-query-filter-with-table",
data: filteredData,
getRowId: (r) => r.id,
defaultPageSize: 8,
features: { pagination: { mode: "offset" } },
});
return (
<div className="space-y-3">
<p className="text-muted-foreground text-xs">
<code>QueryFilter</code> is independent of <code>Table</code>. Wire{" "}
<code>onFinish</code> to your fetch or params — this demo filters in memory so you can see
the table react after Search.
</p>
<QueryFilter
loading={isListInteractionLocked(handle.data.status)}
onFinish={(values) => {
setLastQuery(values);
handle.setParams({ page: 1 });
}}
onReset={() => {
setLastQuery(null);
handle.setParams({ page: 1 });
}}
>
<FormField kind="text" name="keyword" label="Keyword" />
<FormField
kind="select"
name="status"
label="Status"
componentProps={{ options: STATUS_OPTIONS }}
/>
</QueryFilter>
<Table dataList={handle} className="min-h-0" />
{lastQuery ? (
<pre className="bg-muted text-muted-foreground overflow-auto rounded-md p-3 text-xs">
<code>{JSON.stringify(lastQuery, null, 2)}</code>
</pre>
) : null}
</div>
);
}Composition
QueryFilter
├── Form (Formily)
│ ├── field grid (responsive columns)
│ │ ├── FormField / SchemaField children
│ │ └── actions cell (default rowEnd, vertical)
│ │ ├── Search (submit)
│ │ ├── Reset
│ │ └── expand / collapse (when overflow)
│ └── FormActions bar (actionsLayout="newLine" only)
│ ├── expand / collapse (when overflow)
│ ├── Reset
│ └── Search (submit)API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | <FormField> and/or <SchemaField> filter fields. |
form | Form<T> | internal | External Formily form — use with SchemaField, effects, or shared state. |
onFinish | (values: Partial<T>) => void | Promise<void> | — | After successful validation; empty fields pruned. |
onReset | () => void | — | Fired after form.reset(). |
columns | number | { base?; sm?; md?; lg?; xl? } | { base: 1, md: 2, lg: 3 } | Responsive field grid columns. |
collapsedRows | number | 1 | Visible field rows when collapsed. |
defaultCollapsed | boolean | true | Uncontrolled initial collapse when fields overflow. |
collapsed | boolean | — | Controlled collapse. |
onCollapse | (collapsed: boolean) => void | — | Toggle / validation-reveal callback (both modes). |
loading | boolean | false | Disables Search while a query is in flight. |
orientation | "vertical" | "horizontal" | "responsive" | "vertical" | Label above control (default) or inline labels. |
labelWidth | string | — | Horizontal label width when orientation="horizontal" (e.g. "7rem"). |
actionsLayout | "rowEnd" | "newLine" | "rowEnd" | Where Search / Reset / Expand live. |
actionsOrientation | "vertical" | "horizontal" | vertical when rowEnd (and ≥2 cols) | Button stack direction. |
actionsDivider | boolean | true | Vertical divider between fields and in-grid actions. |
maxExpandedRows | number | 3 | Max visible field rows while expanded before scrolling; 0 disables. |
t | QueryFilterTranslateFn | — | Per-instance i18n overrides. |
locale | string | — | Locale for built-in Search / Reset / expand labels. |
className | string | — | Root wrapper class. |
Breaking
Default collapse changed from expanded to collapsed (defaultCollapsed={true}). Set defaultCollapsed={false} to keep the old behavior. For Query List, use queryFilterDefaultCollapsed={false} / queryFilterCollapsed / queryFilterOnCollapse.
Layout Default
Actions live in an independent column beside the field grid (actionsLayout="rowEnd") with a vertical stack and divider. Use actionsLayout="newLine" to restore the full-width bottom bar, or actionsDivider={false} to hide the separator. Expanded fields are also capped by maxExpandedRows={3} — set 0 to disable. On Query List, pass queryFilterActionsLayout / queryFilterActionsOrientation / queryFilterActionsDivider / queryFilterMaxExpandedRows.
Formily field props
Query Filter children accept the full FormField surface — required, validator, reactions, kind, componentProps, etc. Validation and visibility behave the same as on a standalone Formily form; only the submit chrome differs (Search / Reset instead of a generic submit button).