Skip to content

Commit e33b458

Browse files
authored
Merge pull request #2 from react-querybuilder:idiomaticity-2
Refactor query builder state
2 parents 2e95191 + dbe34bf commit e33b458

24 files changed

Lines changed: 1184 additions & 851 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ setX(v)` returns the setter's return value and throws "invalid cleanup value". U
185185
- `createProjection(fn, seed, options?)` is a derived, **read-only** store with the same `'id'`
186186
default key. It can be driven from a non-reactive external source (the manager's subscribe
187187
callback) by bumping a version signal from that callback and reading the signal in `fn`; this is
188-
what `createQueryBuilderState` uses.
188+
what `createQueryBuilder` uses.
189189
- `createStore`'s setter takes a **draft callback** (`setStore(draft => { draft.x = … })`). There is
190190
no 1.x `setStore('key', value)` path-argument form; it throws `fn is not a function`.
191191

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1616

1717
### Changed
1818

19+
- **Breaking:** `createQueryBuilderState` is renamed **`createQueryBuilder`** and is now documented
20+
as the package's headless entry point (see "Headless usage" in the README). It already was one —
21+
it returns query, tree, manager, schema, actions and context and renders nothing — so this is a
22+
naming change only. `CreateQueryBuilderStateOptions` is renamed `CreateQueryBuilderOptions`; the
23+
returned `QueryBuilderState` interface keeps its name. No alias is kept.
24+
- **Breaking (source path only):** `createRuleActions` moved from `src/reactive/` to `src/actions.ts`.
25+
It is the one module in the reactive layer with no reactive primitives — a pure `QueryManager`
26+
`QueryActions` adapter. The public barrel export is unchanged.
27+
- Internal: `createQueryBuilder.ts` (767 lines) is split along its existing `#region` seams into
28+
`manager-options.ts` (the option builders and `valuesEqual`), `manager-bridge.ts` (manager
29+
construction, query seeding, the version signals, the store projection, the subscription and the
30+
three effects), `schema.ts` (the option lists, the resolvers and the `Schema` getter object) and
31+
`context-value.ts`. Pure moves; the assembly file is ~200 lines and the new modules are internal.
1932
- Internal: ~20 `createMemo` calls that wrapped a single property read or a primitive-returning
2033
boolean expression are now plain closures. Solid props are already lazy getters, so those memos
2134
allocated a computation node to cache a property access. Memos that allocate an object, run a

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,40 @@ function App() {
8585
}
8686
```
8787

88+
## Headless usage
89+
90+
`createQueryBuilder` is the primitive `<QueryBuilder />` is built on, and it is public API. It takes
91+
the same props, returns the query, the manager, the schema, the actions and the context value, and
92+
renders nothing — so you can drive an entirely custom UI from it.
93+
94+
```tsx
95+
import { For } from 'solid-js';
96+
import { createQueryBuilder } from 'solid-querybuilder';
97+
98+
function CustomBuilder(props) {
99+
const state = createQueryBuilder(props);
100+
101+
return (
102+
<ul>
103+
<For each={state.rootGroup.rules}>
104+
{(r, i) => (
105+
<li>
106+
{r.field} {r.operator} {String(r.value)}
107+
<button onClick={() => state.actions.onRuleRemove([i()])}>×</button>
108+
</li>
109+
)}
110+
</For>
111+
<button onClick={() => state.actions.onRuleAdd(state.schema.createRule(), [])}>+ rule</button>
112+
</ul>
113+
);
114+
}
115+
```
116+
117+
Read the query from `state.rootGroup` (the store mirror, reconciled by `id`, so `<For>` sees stable
118+
identities) rather than `state.query` (a plain identity signal) whenever you are rendering it.
119+
`state.manager` is the underlying [`QueryManager`](https://react-querybuilder.js.org); everything
120+
else is derived from it.
121+
88122
## Styling
89123

90124
Two prebuilt stylesheets ship in `dist`: `query-builder.css` (full) and

docs/differences-from-react-querybuilder.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ signature — so no component needs a `widenedProps` re-widening cast.
144144

145145
React Query Builder's hooks (`useQueryBuilder`, `useRule`, `useRuleGroup`, `useValueEditor`, …) are
146146
not ported under those names. The Solid equivalents live in the `reactive/` layer and are exported:
147-
`createQueryBuilderState`, `createRuleState`, `createRuleGroupState`, `createRuleActions`,
148-
`createValueEditorReset`, and the `QueryBuilderContext` / `useQueryBuilderConfig` pair. The `create*`
147+
`createQueryBuilder`, `createRuleState`, `createRuleGroupState`, `createValueEditorReset`, and the
148+
`QueryBuilderContext` / `useQueryBuilderConfig` pair (`createRuleActions` is exported too, from
149+
`src/actions.ts` — it uses no reactive primitives). The `create*`
149150
naming disambiguates from core's own `createRule` / `createRuleGroup` / `createQueryActions`, which
150151
this package re-exports.
151152

packages/solid-querybuilder/src/reactive/createRuleActions.test.ts renamed to packages/solid-querybuilder/src/actions.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { RuleGroupType, RuleType } from '@react-querybuilder/core';
22
import { QueryManager } from '@react-querybuilder/core';
33
import { createStore } from 'solid-js';
44
import { describe, expect, it, vi } from 'vitest';
5-
import { flatQuery, testFields } from '../../test/support.js';
6-
import type { QueryBuilderProps } from '../types/props.js';
7-
import { createRuleActions } from './createRuleActions.js';
5+
import { flatQuery, testFields } from '../test/support.js';
6+
import { createRuleActions } from './actions.js';
7+
import type { QueryBuilderProps } from './types/props.js';
88

99
const nested: RuleGroupType = {
1010
id: 'root',

packages/solid-querybuilder/src/reactive/createRuleActions.ts renamed to packages/solid-querybuilder/src/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
} from '@react-querybuilder/core';
1111
import { isRuleGroup } from '@react-querybuilder/core';
1212
import { snapshot } from 'solid-js';
13-
import type { QueryBuilderProps } from '../types/props.js';
13+
import type { QueryBuilderProps } from './types/props.js';
1414

1515
/**
1616
* The `onAdd*`/`onMove*`/`onGroup*`/`onRemove` props return `false` to cancel an operation, a

packages/solid-querybuilder/src/components/QueryBuilder.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import { rootPath } from '@react-querybuilder/core';
99
import type { JSX } from '@solidjs/web';
1010
import { Dynamic } from '@solidjs/web';
1111
import { QueryBuilderContext } from '../reactive/context.js';
12-
import { createQueryBuilderState } from '../reactive/createQueryBuilderState.js';
12+
import { createQueryBuilder } from '../reactive/createQueryBuilder.js';
1313
import type { QueryBuilderProps } from '../types/props.js';
1414
import { defaultControlElements } from './defaultControlElements.js';
1515

1616
/**
1717
* The query builder.
1818
*
1919
* Port of React Query Builder's `QueryBuilder`/`QueryBuilderInternal`. All state lives in a
20-
* `QueryManager`; see `createQueryBuilderState`. The query can be driven three ways:
20+
* `QueryManager`; see `createQueryBuilder`. The query can be driven three ways:
2121
*
2222
* - `query` + `onQueryChange` — controlled.
2323
* - `defaultQuery` — uncontrolled.
@@ -38,7 +38,7 @@ export const QueryBuilder = <
3838
// read through for the lifetime of the component.
3939
const p = props as QueryBuilderProps<RuleGroupTypeAny, F, O, FullCombinator>;
4040

41-
const state = createQueryBuilderState<F, O>(p, { defaultControls: defaultControlElements });
41+
const state = createQueryBuilder<F, O>(p, { defaultControls: defaultControlElements });
4242

4343
return (
4444
// Solid 2 removed `.Provider`. `state.context` is a getter object, so descendants read

packages/solid-querybuilder/src/components/Rule.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { RuleProps } from '../types/props.js';
1111
*
1212
* Port of React Query Builder's `Rule`, and, like it, a small wrapper: the controls themselves
1313
* live in `RuleComponents`, and a rule whose field supports match modes renders `RuleSubQuery`
14-
* instead — which needs its own `createQueryBuilderState`, and therefore its own component.
14+
* instead — which needs its own `createQueryBuilder`, and therefore its own component.
1515
*
1616
* Element order and conditional rendering are the contract: read React's `Rule.tsx` as the spec.
1717
*/

packages/solid-querybuilder/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
// This is a star export, so it loses every name the port declares explicitly below; that is the
88
// intended precedence (the port's `Schema`, `RuleProps`, etc. are deliberate deltas).
99
export * from '@react-querybuilder/core';
10+
export * from './actions.js';
1011
export * from './components/index.js';
1112
export { Label } from './internal/Label.jsx';
1213
export * from './reactive/index.js';

packages/solid-querybuilder/src/internal/RuleSubQuery.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { isRuleGroup, prepareOptionList, rootPath } from '@react-querybuilder/co
33
import type { JSX } from '@solidjs/web';
44
import { createMemo, merge, untrack } from 'solid-js';
55
import { defaultControlElements } from '../components/defaultControlElements.js';
6-
import { createQueryBuilderState } from '../reactive/createQueryBuilderState.js';
6+
import { createQueryBuilder } from '../reactive/createQueryBuilder.js';
77
import { createRuleGroupState } from '../reactive/createRuleGroupState.js';
88
import type { RuleState } from '../reactive/createRuleState.js';
99
import type { QueryBuilderProps, RuleGroupProps, RuleProps } from '../types/props.js';
@@ -16,7 +16,7 @@ const defaultSubproperties: FullField[] = [{ name: '', value: '', label: '' }];
1616
* builder for the rule's value.
1717
*
1818
* Port of React Query Builder's `RuleComponentsWithSubQuery`. It exists as its own component for
19-
* the same reason React's does: the subquery needs its own `createQueryBuilderState`, which runs
19+
* the same reason React's does: the subquery needs its own `createQueryBuilder`, which runs
2020
* during component setup and therefore cannot live behind a `<Show>` inside `Rule`.
2121
*
2222
* It provides no new context, matching React and both prior ports — a replacement control
@@ -71,7 +71,7 @@ export const RuleSubQuery = (props: { ruleProps: RuleProps; parts: RuleState }):
7171
},
7272
}) as unknown as QueryBuilderProps<RuleGroupTypeAny>;
7373

74-
const subState = createQueryBuilderState(() => subProps as never, {
74+
const subState = createQueryBuilder(() => subProps as never, {
7575
defaultControls: defaultControlElements as never,
7676
});
7777

0 commit comments

Comments
 (0)