PropertyFilter
Build a filter expression
This editor supplies an expression for the host to evaluate. Text values retain their casing and spaces.
{
"match": "all",
"tokens": [
{
"id": "site-west",
"field": "site",
"operator": "equals",
"value": "west"
}
]
}Choose a field, operator and value, then add a clause. Match all requires every clause; match any requires at least one. Your application evaluates the expression over its data.
import { useState } from "react";
import {
PropertyFilter,
type PropertyFilterField,
type PropertyFilterValue,
} from "@operator-ds/react";
const fields: readonly PropertyFilterField[] = [
{
key: "name",
label: "Name",
operators: [
{ key: "contains", label: "contains", requiresValue: true },
{ key: "empty", label: "is empty", requiresValue: false },
],
},
{
key: "site",
label: "Site",
operators: [{ key: "equals", label: "equals", requiresValue: true }],
options: [{ value: "west", label: "West campus" }],
},
];
function EquipmentFilters() {
const [value, setValue] = useState<PropertyFilterValue>({ match: "all", tokens: [] });
return <PropertyFilter fields={fields} value={value} onValueChange={setValue} />;
}
Field and operator identifiers use key. Each token contains a unique id, field, operator, and value. Text values retain their exact casing and spaces. Operators with requiresValue: false emit null. Supplied options create an enum selector; disabled options cannot be added.
Controlled updates and validation
onValueChange requests the complete next expression. Accept it through value. Declined additions keep the draft, and declined removal, clearing or match changes keep the displayed expression unchanged. When the application accepts the removal of the last clause or Clear filters, on the next render or after a server round trip, focus moves to Add filter, unless the operator has moved on in the meantime.
Enter in the Value text field adds the clause, with the same checks as Add filter, and keeps focus in the field; it never submits a form around the editor. Enter during input-method composition is left to the composition. With no fields to choose, Add filter is unavailable.
Incomplete drafts show an error under the controls, and the control to fix is marked invalid and described by it: the Field selector when nothing is chosen, the Value control when the value is missing or no longer available. Unknown fields, operators, enum values and incompatible values remain visible as invalid tags that can be removed. Use the same semantic validation at your application's input boundary:
import { getPropertyFilterTokenError } from "@operator-ds/react";
const error = getPropertyFilterTokenError(token, fields);
// A readable error string, or null for a semantically valid clause.
This helper checks one clause against supplied fields. It has no React in it, so a Server Action, a route handler or a Server Component can import it from the package root and call it. The host owns structural validation, unique token IDs, evaluation, persistence and recovery when a whole expression is invalid. A known enum option marked disabled remains valid in an existing token.
Disabled
Props
| PROP | TYPE | DEFAULT | REQUIRED |
|---|---|---|---|
fieldsReadonly PropertyFilterField[] with key, label, operators and optional value/label/disabled options. Operators have key, label and requiresValue. | array | — | yes |
valuePropertyFilterValue: match all or any, and readonly tokens containing id, field, operator and string or null value. | node | — | yes |
onValueChangeRequests the complete next value. The parent accepts it through value; declined updates keep tokens and addition drafts. | function | — | yes |
label | string | "Filters" | no |
disabled | boolean | false | no |
className | string | — | no |
Accessibility
- The editor and every control have accessible names; selectors use React Aria keyboard behavior.
- Invalid drafts show a readable error associated with Add filter and with the control at fault, which is marked invalid. Existing invalid clauses include their error in the tag label.
- Tags support arrow navigation and Delete/Backspace. Removing the final clause or clearing them returns focus to Add filter once the controlled update is accepted, however long it takes.
- Controls use visible focus indicators, minimum 24px targets, and labels at least 11px. Field, Operator and Value sit side by side when the editor itself gives each about 192 px, and stack at its full width when it is narrower, whatever the window size. Labels, values and draft errors wrap, and an unbroken token breaks anywhere.
- Native attributes reach the group:
id,data-*andaria-describedby; anaria-labelledbyis added to the editor's own label.
Manifest entry
<PropertyFilter fields={fields} value={filter} onValueChange={setFilter} label="Equipment filters" />View agent manifest
{
"description": "Controlled flat property-filter editor with text or enum values, all/any matching and removable, semantically validated clauses.",
"props": {
"fields": {
"type": "array",
"required": true,
"description": "Readonly PropertyFilterField[] with key, label, operators and optional value/label/disabled options. Operators have key, label and requiresValue."
},
"value": {
"type": "node",
"required": true,
"description": "PropertyFilterValue: match all or any, and readonly tokens containing id, field, operator and string or null value."
},
"onValueChange": {
"type": "function",
"required": true,
"description": "Requests the complete next value. The parent accepts it through value; declined updates keep tokens and addition drafts."
},
"label": {
"type": "string",
"default": "Filters"
},
"disabled": {
"type": "boolean",
"default": false
},
"className": {
"type": "string"
}
},
"composable_with": [
"TagGroup",
"DataGrid",
"CollectionState"
],
"never_inside": [
"Button"
],
"example": "<PropertyFilter fields={fields} value={filter} onValueChange={setFilter} label=\"Equipment filters\" />"
}