f-ui
Components

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 QueryFilterf-ui QueryFilter
Staged submit (onFinish)onFinish — fires after Formily validation passes
onResetonReset — after form.reset()
defaultCollapsed / collapsed / onCollapseSame + collapsedRows; default defaultCollapsed={true}
span / responsive columnscolumns — number or { base, sm, md, lg, xl } grid
labelWidthlabelWidth — when orientation="horizontal"
submitter / optionRenderBuilt-in Search (primary) + Reset; optional expand toggle. Default actionsLayout="rowEnd" matches Pro’s last-cell submitter; newLine restores a full-width bottom bar
form / rulesFormily — FormField required, validator, reactions, or SchemaField
loading on submitloading — 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

AreaBehavior
Field authoring<FormField> (JSX) or <SchemaField> (JSON Schema) — same as Formily.
SubmissionStaged — Search or Enter calls form.submit(); onFinish receives values with empty fields pruned.
ValidationFormily 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").
LayoutVertical field grid by default (label above control); responsive columns via columns; optional collapse; expanded height capped by maxExpandedRows.
ActionsDefault in-grid last cell (rowEnd) with vertical Search / Reset / expand; opt into full-width newLine bar; expand/collapse when overflow.
External formPass form={createForm(...)} for effects, initial values, or shared state with other UI.
PairingStandalone — no list handle coupling; caller wires values to Table or any data source.

Interactions

EventBehavior
Type in a fieldUpdates Formily state only — no query.
Search / Enterform.submit() → validate all fields → onFinish(pruneEmpty(values)) on success.
Validation failureField errors shown (FormItem chrome + form-scope FormErrorSummary); collapsed fields expand; focus moves to the first invalid control; onFinish not called.
Resetform.reset() → empty strings on text fields → onReset callback.
Expand (+N) / CollapseToggle 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):

CapabilityOn Query Filter
required, validator, kind format rulesPer <FormField> — blocks Search when invalid
reactionsShow/hide/require fields based on other filter values
SchemaField + JSON SchemaBackend-driven filter definitions
form prop + createForm({ effects })Shared VM, onFieldValueChange, initial values
description / validatingDescriptionHelper 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.json
npx shadcn@latest add https://ui.isaacfei.com/api/plus/r/query-filter.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/query-filter.json
bunx shadcn@latest add https://ui.isaacfei.com/api/plus/r/query-filter.json

With 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

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.

mmddyyyy
mmddyyyy
"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>
  );
}

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.

Optional — at least 2 characters when filled

Optional — format checked on blur

Optional — must be zero or positive

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).

mmddyyyy
mmddyyyy
"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-1001Acme CorpConfirmed2,499.00
ORD-1002Globex IncShipped849.50
ORD-1003InitechDraft120.00
ORD-1004Umbrella LLCDelivered3,200.00
ORD-1005Soylent CorpCancelled45.99
ORD-1006Wayne EnterprisesConfirmed18,750.00
ORD-1007Stark IndustriesShipped5,600.00
ORD-1008OscorpDraft299.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

PropTypeDefaultDescription
childrenReactNode<FormField> and/or <SchemaField> filter fields.
formForm<T>internalExternal Formily form — use with SchemaField, effects, or shared state.
onFinish(values: Partial<T>) => void | Promise<void>After successful validation; empty fields pruned.
onReset() => voidFired after form.reset().
columnsnumber | { base?; sm?; md?; lg?; xl? }{ base: 1, md: 2, lg: 3 }Responsive field grid columns.
collapsedRowsnumber1Visible field rows when collapsed.
defaultCollapsedbooleantrueUncontrolled initial collapse when fields overflow.
collapsedbooleanControlled collapse.
onCollapse(collapsed: boolean) => voidToggle / validation-reveal callback (both modes).
loadingbooleanfalseDisables Search while a query is in flight.
orientation"vertical" | "horizontal" | "responsive""vertical"Label above control (default) or inline labels.
labelWidthstringHorizontal 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.
actionsDividerbooleantrueVertical divider between fields and in-grid actions.
maxExpandedRowsnumber3Max visible field rows while expanded before scrolling; 0 disables.
tQueryFilterTranslateFnPer-instance i18n overrides.
localestringLocale for built-in Search / Reset / expand labels.
classNamestringRoot 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).

On this page