gn-vquery
Framework-agnostic chip query model, dialect compilers (Advanced Query and REQL WHERE), and optional <gn-vquery-chip-bar> Custom Element.
Hosts (Angular, React, Vue, or vanilla) own UI chrome and backend execution — compile chips here, store the string on the data provider, reload the table/Gantt. No peer dependencies.
Supported contract: docs/SUPPORTED-API.md (Core / Advanced / Internal).
Compile matrix: docs/COMPILE-MATRIX.md.
License: proprietary — no use without a written commercial agreement (see LICENSE).
Low-level escape / CSV-split helpers: import { … } from '@gerald-neumann/gn-vquery/advanced'.
Why a separate package from gn-vtable
- Overview screens often turn native table search off and own a chip bar above the grid.
- Different products emit different filter languages from the same chip model.
- SQL / metamodel execution stays on the host backend. This library only builds and validates strings.
Install
npm install @gerald-neumann/gn-vquery
No peer dependencies. Node 18+.
Minimum example
Hard-coded field catalog — no server, no table host. Compiles chips to an Advanced Query string.
<gn-vquery-chip-bar aria-label="Query"></gn-vquery-chip-bar>
<pre id="out"></pre>
import {
registerGnVQueryChipBar,
chipsToAdvancedQuery,
type GnVQueryChipBarElement,
type VQueryFieldConfig,
} from '@gerald-neumann/gn-vquery';
import '@gerald-neumann/gn-vquery/styles/vquery-chip-bar.css';
import '@gerald-neumann/gn-vquery/styles/vquery-chip-bubbles.css';
registerGnVQueryChipBar();
const fields: VQueryFieldConfig[] = [
{ columnField: 'name', filterField: 'name', label: 'Name', type: 'text' },
{
columnField: 'status',
filterField: 'status',
label: 'Status',
type: 'enum',
options: [
{ value: 'OPEN', label: 'Open' },
{ value: 'DONE', label: 'Done' },
],
},
];
const bar = document.querySelector<GnVQueryChipBarElement>('gn-vquery-chip-bar')!;
bar.fields = fields;
bar.addEventListener('chips-change', (e) => {
const chips = (e as CustomEvent).detail;
document.querySelector('#out')!.textContent = chipsToAdvancedQuery(chips, fields);
});
Type a free-text term or pick Status → Open. Output looks like name~acme or status=OPEN.
Runnable copy of this sample (demo-minimum/):
.\start-minimum-sample.ps1
Opens http://localhost:4178 (full interactive demo stays on .\start.ps1 → :4177).
Dialects
import {
chipsToAdvancedQuery,
chipsToReqlWhere,
compileVQuery,
applyCompiledQueryToVTable,
applyCompiledQueryToVGantt,
} from '@gerald-neumann/gn-vquery';
const advanced = chipsToAdvancedQuery(chips, fields); // invoiceNumber~acme AND statusCode=DRAFT
const where = chipsToReqlWhere(chips, fields); // Title LIKE '%acme%' AND StatusName = 'Draft'
applyCompiledQueryToVTable(table, (q) => provider.setChipFilterQuery(q), advanced);
applyCompiledQueryToVGantt(gantt, (q) => provider.setChipFilterQuery(q), where);
Host reload types match @gerald-neumann/gn-vtable ^1.3.2 (reload({ countFirst, reloadMode })) and @gerald-neumann/gn-vgantt ^0.2.17 (reloadTable). Chip changes use { countFirst: true } (default). In-place row mutations on those hosts use { reloadMode: 'stale-while-revalidate' } — pass that as the fourth argument when you are not changing the filter set.
Dialect operator cheat sheet
| Intent | Chip | Advanced Query | REQL |
|---|---|---|---|
| Contains | ~ |
field~term |
LIKE '%term%' |
| Exact | = |
field=term |
field = 'term' |
| Not equal | = + negated |
field!=term |
field != 'term' |
| Compare | > >= < <= |
field>100 |
field > 100 |
CSV: text ~ → OR of per-token matches. Equality = CSV (text / enum / number) → OR of field=term (negated → AND of !=). Full matrix: COMPILE-MATRIX.md.
Negation: type ! / NOT, pick != / !~, or set chip.negated.
Empty edit dismiss (1.1.5+): click a committed chip to edit. Escape restores it. Enter, blur, or click outside with an empty value removes the chip. Backspace on an already-empty draft does the same as ×. Clearing the last character does not auto-delete until you commit.
Comparators (1.1.5+): number and date fields use = > >= < <= (and !=) as first-class chip.operator values — not text inside chip.value. Click the draft operator for a picker, or type >100 / >= 100. Advanced Query compiles timesheetId>100; REQL uses spaced form (TimesheetId > 100). Text fields can switch ~ / = (and negated). Omit operators on the catalog for type defaults, or set operators: ['='] to keep a column equality-only.
REQL helpers: mergeReqlWhere, stripReqlWhere, parseReql, validateReqlWhere, tokenizeReql.
parseReql splits SELECT/FROM/WHERE/… and checks quotes/parens. It does not compile to SQL — ReSpecT LogicalQueryCompiler (and MyERP QueryParserService) stay in those backends.
Chip bar UI
See Minimum example for a complete hard-coded host. Beyond that:
<gn-vquery-chip-bar> is a Custom Element (light DOM, --vquery-* tokens). Yin/yang incomplete-expression shapes are default-on for committed chips — no feature flag. Angular / React hosts wrap the element; the archived @gerald-neumann/ng-vquery prototype is not required for new work.
Field catalogs use options and optional loadOptions(): Promise<…> (no RxJS) for small eager pick-lists.
Server typeahead (suggestOptions, 1.1.8+): for large / unbounded catalogs (accounts, projects, invoices), supply a debounced async callback instead of shipping the full list. The chip bar cancels in-flight requests via AbortSignal, caps displayed rows, and commits a picked suggestion like an enum (value / label / =). Default is hybrid (free-text Enter still works); set suggestRequireMatch: true for enum-strict.
Multi-value typeahead (multiValue, 1.1.9+): set multiValue: true with suggestOptions (or enum) to accumulate several values before commit — like Gmail To: / GitHub labels, not a checkbox multi-pick. Confirm keys:
| Input | Behaviour |
|---|---|
, |
Accept highlighted / resolved term as a pending token; keep draft; reopen typeahead. Does not insert a literal comma. |
| Enter / click suggestion | Same as comma while accumulating; Enter on empty input (with ≥1 token) finishes one CSV field chip. |
| Backspace (empty input) | Pop last pending token; if none, existing field-clear behaviour. |
| Token × | Remove that pending token only. |
| Escape / draft Cancel × | Cancel the whole field draft (trailing × sits after the value input). |
Dropdown rows may show rich labels (25-00041 — Account); committed yang / pending tokens prefer short option.value. Compilers still see one kind: 'field' chip with CSV value.
bar.fields = [
{
columnField: 'customerId',
filterField: 'customerId',
label: 'Customer',
type: 'text',
suggestDebounceMs: 200, // optional; default 200
// suggestRequireMatch: true, // optional; default false = hybrid
suggestOptions: async (query, ctx) => {
const page = await http.get('/api/customers', {
params: { filter: query, pageSize: ctx?.limit ?? 40 },
signal: ctx?.signal,
});
return page.items.map((c) => ({ value: String(c.id), label: c.name }));
},
},
{
columnField: 'invoiceNumber',
filterField: 'invoiceNumber',
label: 'Invoice #',
type: 'text',
multiValue: true, // 1.1.9+ — token accumulate, then one CSV chip
suggestOptions: async (query, ctx) => {
const page = await http.get('/api/invoices', {
params: { filter: query, pageSize: ctx?.limit ?? 40 },
signal: ctx?.signal,
});
return page.items.map((inv) => ({
value: inv.number,
label: `${inv.number} — ${inv.accountName}`,
}));
},
},
];
Long committed yang values ellipsize; trailing chip × stays visible (title / aria-label hold the full string).
Keyboard / presets (1.1.3+): empty-draft Backspace removes the last user chip, then the last unlocked presetChips entry (preset-chip-remove). Locked presets (preset-chips-locked) are not removed by Backspace. Draft focus stays on the input after commit and Backspace-delete. Backspace on a focused editable chip focuses the draft and pops the last user chip. Tab/display presets should use structured chips (field + operator + value + label, or a parseable Field: value / Field.Facet: value label) so they render as split yin/yang like field chips; flat label-only kind: 'preset' is deprecated for UI.
Styling
The chip bar is light DOM (no Shadow DOM). Load the package CSS (or a host mirror that keeps the same class names), then override --vquery-* tokens and/or the stable vquery-chip* / vquery-chip-bar* selectors.
Tokens (defaults in vquery-chip-bar.css):
| Token | Controls | Default |
|---|---|---|
--vquery-border-radius |
Outer bar frame | 0.375rem |
--vquery-chip-radius |
Committed chips, draft shell, yin/yang halves | 999px (pill) |
--vquery-accent-color / --vquery-primary-color |
Focus ring, draft compose tint | teal / indigo |
--vquery-yin-color / --vquery-yang-color |
Field key vs value / free-text fills | accent / primary |
--vquery-border-color, --vquery-background, --vquery-muted-color, --vquery-warning-color, --vquery-tertiary-bg |
Chrome + logic chips | see package CSS |
Edgy chips — set --vquery-chip-radius (yin/yang asymmetric corners follow the same token):
:root {
--vquery-chip-radius: 0.15rem; /* or `0` for sharp */
--vquery-border-radius: 0.25rem; /* optional: match the bar frame */
}
Scope to one bar if needed: gn-vquery-chip-bar { --vquery-chip-radius: 0; }.
Deeper restyles — target kind / bubble classes (.vquery-chip--field, --text, --logic-and, --yin, --yang-right, --split, …). Suggest panel radii (.vquery-chip-bar__suggest) stay separate; override those selectors if you want them edgy too.
Incomplete expression bubbles (yin / yang)
| Expression | Shape |
|---|---|
joe (standalone free-text) |
free-text — yang color, underlined |
Draft OR joe (committed) |
left yin, right custom-right (yang half) |
A AND B (two fields) |
left yin, right yang-right |
Draft AND (waiting) |
Draft → yin |
OR joe (right without left) |
invalid |
Helpers remain available for custom renderers:
import {
canAppendBinaryLogic,
createCommittedChipElement,
fieldChipParts,
resolveVQueryChipBubbleShapes,
vqueryChipBubbleClass,
} from '@gerald-neumann/gn-vquery';
Workspace
npm test # vitest
npm run build # dist/
Local demo
Same entrypoint pattern as gn-vtable (start.ps1 / start-demo.ps1), but this package is compiler-only — the demo is a small Vite static page (no Angular, no API).
Windows:
.\start.ps1
# or: .\start-demo.ps1
Optional: .\start.ps1 -SkipInstall, .\start.ps1 -NoOpen.
Or:
npm install
npm start
Open http://localhost:4177 — interactive chips, CSV Advanced Query compile, and yin/yang incomplete bubbles.
Minimum hard-coded sample only: .\start-minimum-sample.ps1 → http://localhost:4178.
Publishing (maintainers)
Same GitHub Packages setup as gn-vtable: root .npmrc reads GITHUB_NPM_TOKEN, then:
$env:GITHUB_NPM_TOKEN = 'ghp_...'
npm run publish:lib
# or: .\scripts\publish-lib.ps1
See scripts/README.md.
Out of this package
- ReSpecT visual Advanced Query builder / Monaco editor (metamodel-specific)
- C# Logical Query / Advanced Query SQL compilers
- Framework wrappers beyond the Custom Element — Angular/React hosts wrap
<gn-vquery-chip-bar>