Home / Docs / gn-vquery

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

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