Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ setX(v)` returns the setter's return value and throws "invalid cleanup value". U
- `createProjection(fn, seed, options?)` is a derived, **read-only** store with the same `'id'`
default key. It can be driven from a non-reactive external source (the manager's subscribe
callback) by bumping a version signal from that callback and reading the signal in `fn`; this is
what `createQueryBuilderState` uses.
what `createQueryBuilder` uses.
- `createStore`'s setter takes a **draft callback** (`setStore(draft => { draft.x = … })`). There is
no 1.x `setStore('key', value)` path-argument form; it throws `fn is not a function`.

Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Changed

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

## Headless usage

`createQueryBuilder` is the primitive `<QueryBuilder />` is built on, and it is public API. It takes
the same props, returns the query, the manager, the schema, the actions and the context value, and
renders nothing — so you can drive an entirely custom UI from it.

```tsx
import { For } from 'solid-js';
import { createQueryBuilder } from 'solid-querybuilder';

function CustomBuilder(props) {
const state = createQueryBuilder(props);

return (
<ul>
<For each={state.rootGroup.rules}>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{(r, i) => (
<li>
{r.field} {r.operator} {String(r.value)}
<button onClick={() => state.actions.onRuleRemove([i()])}>×</button>
</li>
)}
</For>
<button onClick={() => state.actions.onRuleAdd(state.schema.createRule(), [])}>+ rule</button>
</ul>
);
}
```

Read the query from `state.rootGroup` (the store mirror, reconciled by `id`, so `<For>` sees stable
identities) rather than `state.query` (a plain identity signal) whenever you are rendering it.
`state.manager` is the underlying [`QueryManager`](https://react-querybuilder.js.org); everything
else is derived from it.

## Styling

Two prebuilt stylesheets ship in `dist`: `query-builder.css` (full) and
Expand Down
5 changes: 3 additions & 2 deletions docs/differences-from-react-querybuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ signature — so no component needs a `widenedProps` re-widening cast.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { RuleGroupType, RuleType } from '@react-querybuilder/core';
import { QueryManager } from '@react-querybuilder/core';
import { createStore } from 'solid-js';
import { describe, expect, it, vi } from 'vitest';
import { flatQuery, testFields } from '../../test/support.js';
import type { QueryBuilderProps } from '../types/props.js';
import { createRuleActions } from './createRuleActions.js';
import { flatQuery, testFields } from '../test/support.js';
import { createRuleActions } from './actions.js';
import type { QueryBuilderProps } from './types/props.js';

const nested: RuleGroupType = {
id: 'root',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from '@react-querybuilder/core';
import { isRuleGroup } from '@react-querybuilder/core';
import { snapshot } from 'solid-js';
import type { QueryBuilderProps } from '../types/props.js';
import type { QueryBuilderProps } from './types/props.js';

/**
* The `onAdd*`/`onMove*`/`onGroup*`/`onRemove` props return `false` to cancel an operation, a
Expand Down
6 changes: 3 additions & 3 deletions packages/solid-querybuilder/src/components/QueryBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { rootPath } from '@react-querybuilder/core';
import type { JSX } from '@solidjs/web';
import { Dynamic } from '@solidjs/web';
import { QueryBuilderContext } from '../reactive/context.js';
import { createQueryBuilderState } from '../reactive/createQueryBuilderState.js';
import { createQueryBuilder } from '../reactive/createQueryBuilder.js';
import type { QueryBuilderProps } from '../types/props.js';
import { defaultControlElements } from './defaultControlElements.js';

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

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

return (
// Solid 2 removed `.Provider`. `state.context` is a getter object, so descendants read
Expand Down
2 changes: 1 addition & 1 deletion packages/solid-querybuilder/src/components/Rule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { RuleProps } from '../types/props.js';
*
* Port of React Query Builder's `Rule`, and, like it, a small wrapper: the controls themselves
* live in `RuleComponents`, and a rule whose field supports match modes renders `RuleSubQuery`
* instead — which needs its own `createQueryBuilderState`, and therefore its own component.
* instead — which needs its own `createQueryBuilder`, and therefore its own component.
*
* Element order and conditional rendering are the contract: read React's `Rule.tsx` as the spec.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/solid-querybuilder/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// This is a star export, so it loses every name the port declares explicitly below; that is the
// intended precedence (the port's `Schema`, `RuleProps`, etc. are deliberate deltas).
export * from '@react-querybuilder/core';
export * from './actions.js';
export * from './components/index.js';
export { Label } from './internal/Label.jsx';
export * from './reactive/index.js';
Expand Down
6 changes: 3 additions & 3 deletions packages/solid-querybuilder/src/internal/RuleSubQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isRuleGroup, prepareOptionList, rootPath } from '@react-querybuilder/co
import type { JSX } from '@solidjs/web';
import { createMemo, merge, untrack } from 'solid-js';
import { defaultControlElements } from '../components/defaultControlElements.js';
import { createQueryBuilderState } from '../reactive/createQueryBuilderState.js';
import { createQueryBuilder } from '../reactive/createQueryBuilder.js';
import { createRuleGroupState } from '../reactive/createRuleGroupState.js';
import type { RuleState } from '../reactive/createRuleState.js';
import type { QueryBuilderProps, RuleGroupProps, RuleProps } from '../types/props.js';
Expand All @@ -16,7 +16,7 @@ const defaultSubproperties: FullField[] = [{ name: '', value: '', label: '' }];
* builder for the rule's value.
*
* Port of React Query Builder's `RuleComponentsWithSubQuery`. It exists as its own component for
* the same reason React's does: the subquery needs its own `createQueryBuilderState`, which runs
* the same reason React's does: the subquery needs its own `createQueryBuilder`, which runs
* during component setup and therefore cannot live behind a `<Show>` inside `Rule`.
*
* It provides no new context, matching React and both prior ports — a replacement control
Expand Down Expand Up @@ -71,7 +71,7 @@ export const RuleSubQuery = (props: { ruleProps: RuleProps; parts: RuleState }):
},
}) as unknown as QueryBuilderProps<RuleGroupTypeAny>;

const subState = createQueryBuilderState(() => subProps as never, {
const subState = createQueryBuilder(() => subProps as never, {
defaultControls: defaultControlElements as never,
});

Expand Down
76 changes: 76 additions & 0 deletions packages/solid-querybuilder/src/reactive/context-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { FullField, FullOperator, GetOptionIdentifierType } from '@react-querybuilder/core';
import type { Accessor } from 'solid-js';
import type { QueryBuilderContextProps } from '../types/props.js';
import type { MergedQueryBuilderConfig } from './context.js';

/**
* Builds the value handed to `<QueryBuilderContext value={…}>` — the merged config, projected back
* into the context's own shape so nested builders inherit it.
*
* A getter object, not a memo returning a fresh object: a Solid context value is read once by
* descendants, so every field must be a getter or consumers freeze on the first value.
*/
export const createContextValue = <F extends FullField, O extends FullOperator>(
config: Accessor<MergedQueryBuilderConfig<F, GetOptionIdentifierType<O>>>
): QueryBuilderContextProps<F, GetOptionIdentifierType<O>> => ({
get controlElements() {
return config().controls;
},
get controlClassnames() {
return config().classNames;
},
get translations() {
return config().translations;
},
get debugMode() {
return config().debugMode;
},
get enableMountQueryChange() {
return config().enableMountQueryChange;
},
get showCombinatorsBetweenRules() {
return config().showCombinatorsBetweenRules;
},
get showNotToggle() {
return config().showNotToggle;
},
get showShiftActions() {
return config().showShiftActions;
},
get showUndoRedo() {
return config().showUndoRedo;
},
get showCloneButtons() {
return config().showCloneButtons;
},
get showLockButtons() {
return config().showLockButtons;
},
get showMuteButtons() {
return config().showMuteButtons;
},
get resetOnFieldChange() {
return config().resetOnFieldChange;
},
get resetOnOperatorChange() {
return config().resetOnOperatorChange;
},
get autoSelectField() {
return config().autoSelectField;
},
get autoSelectOperator() {
return config().autoSelectOperator;
},
get autoSelectValue() {
return config().autoSelectValue;
},
get addRuleToNewGroups() {
return config().addRuleToNewGroups;
},
get listsAsArrays() {
return config().listsAsArrays;
},
get suppressStandardClassnames() {
return config().suppressStandardClassnames;
},
});
Loading