@x-filter/react
v0.2.0
Published
Headless React hooks for the X-Filter query builder — state management, view models, DSL editor, URL sync, presets, undo/redo, and keyboard navigation.
Maintainers
Readme
@x-filter/react
Headless React layer for @x-filter/core. This package owns React state,
view-model shaping, DSL editor state, and adapter contracts without rendering a
specific UI kit.
Install
pnpm add @x-filter/react @x-filter/core react react-domConcepts
useFilterBuildermanages controlled or uncontrolledFilterstate and wraps core mutations such asaddRule,updateRule,addGroup, andmoveRule.useFilterViewModelturns a coreFilterplusFieldSchema[]into UI-ready rule/group nodes with resolved field metadata, operator metadata, validation errors, and ARIA labels.useDslEditorkeeps a formatted DSL draft in sync with the current filter, exposes completions, parses on commit, and reports parse errors.useReorderContractexposes adapter-friendly reorder handlers used by DnD implementations.
Minimal Builder
import type { FieldSchema } from '@x-filter/core';
import { formatDSL } from '@x-filter/core';
import { useFilterBuilder, useFilterViewModel } from '@x-filter/react';
const schema: FieldSchema[] = [
{
name: 'status',
label: 'Status',
type: 'select',
defaultOperator: 'equals',
defaultValue: 'open',
operators: [{ name: 'equals', label: 'equals', arity: 'binary' }],
values: [
{ value: 'open', label: 'Open' },
{ value: 'closed', label: 'Closed' },
],
},
];
export function HeadlessFilter() {
const builder = useFilterBuilder({ schema });
const viewModel = useFilterViewModel({
filter: builder.filter,
schema: builder.schema,
});
return (
<section aria-label={viewModel.root.aria.label}>
<button
type="button"
onClick={() =>
builder.addRule(builder.filter.id, {
field: 'status',
operator: 'equals',
value: 'open',
})
}
>
Add status rule
</button>
<pre>{formatDSL(builder.filter)}</pre>
<pre>{JSON.stringify(viewModel.root, null, 2)}</pre>
</section>
);
}DSL Editor
import type { FieldSchema, Filter } from '@x-filter/core';
import { useDslEditor } from '@x-filter/react';
import { useState } from 'react';
export function DslTextarea({
filter,
schema,
onCommit,
}: {
filter: Filter;
schema: FieldSchema[];
onCommit: (filter: Filter) => void;
}) {
const [cursor, setCursor] = useState<number | undefined>();
const editor = useDslEditor({ filter, schema, onCommit, cursor });
return (
<div>
<textarea
aria-label="Filter DSL"
value={editor.draftDSL}
onChange={(event) => {
editor.setDraftDSL(event.target.value);
setCursor(event.target.selectionStart ?? event.target.value.length);
}}
onSelect={(event) =>
setCursor((event.target as HTMLTextAreaElement).selectionStart ?? editor.draftDSL.length)
}
/>
<button type="button" onClick={editor.commit}>
Apply DSL
</button>
{editor.parseError ? <p role="alert">{editor.parseError}</p> : null}
<ul>
{editor.completions.map((item) => (
<li key={`${item.kind}-${item.value}`}>{item.label}</li>
))}
</ul>
</div>
);
}Adapter Consumption
UI adapters such as @x-filter/antd and @x-filter/shadcn consume this layer by
calling:
useFilterBuilder({ schema, value, defaultValue, onChange })for state and core mutation handlers.useFilterViewModel({ filter, schema, errors })for renderable groups/rules.useDslEditorwhen adsleditor is enabled.useReorderContractwhen adndexperience is enabled.
Adapters should pass the same schema, value, and onChange contract through
to consumers, expose slots for replacing atomic pieces, and preserve the ARIA
metadata from the view model.
