abt-query-builder
v2.5.3
Published
A framework-agnostic, customizable query builder web component
Maintainers
Readme
Query Builder V2
A modern, framework-agnostic, and customizable query builder web component.
Features
- 🚀 Global & Framework Agnostic: Works everywhere (Vanilla JS, React, Vue, Angular).
- 📦 Zero Dependencies: Built with standard Web Components.
- 🎨 Modern Design: Clean UI with CSS Variables for easy customization.
- ⚡ Lightweight: Small bundle size.
- 🛡️ TypeScript: Written in TypeScript for stability.
- 🔍 Live Search: Async backend search with debouncing, caching, and hybrid modes.
Installation
CDN (Vanilla JS / HTML)
Simply include the script from a CDN (once published) or your local build:
<!-- Import the script -->
<script type="module" src="./dist/query-builder.es.js"></script>
<!-- Use the component -->
<abt-query-builder id="qb"></abt-query-builder>
<script>
const qb = document.getElementById('qb');
// Define fields
qb.fields = [
{ id: 'name', label: 'Name', type: 'string' },
{ id: 'age', label: 'Age', type: 'number' },
{ id: 'role', label: 'Role', type: 'select', values: { 'admin': 'Admin', 'user': 'User' } }
];
// Listen for changes
qb.addEventListener('change', (e) => {
console.log('Rules:', e.detail);
});
</script>React
Import the component to register it, then use it as a custom element.
import React, { useEffect, useRef } from 'react';
import 'path/to/abt-query-builder.es.js'; // Registers <abt-query-builder>
function App() {
const qbRef = useRef(null);
useEffect(() => {
if (qbRef.current) {
qbRef.current.fields = [
{ id: 'name', label: 'Name', type: 'string' }
];
const handleChange = (e) => {
console.log(e.detail);
};
qbRef.current.addEventListener('change', handleChange);
return () => {
qbRef.current?.removeEventListener('change', handleChange);
};
}
}, []);
return (
<div>
<h1>Query Builder</h1>
<abt-query-builder ref={qbRef}></abt-query-builder>
</div>
);
}Vue
<template>
<abt-query-builder ref="qb" @change="handleChange"></abt-query-builder>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import 'path/to/abt-query-builder.es.js';
const qb = ref(null);
const handleChange = (e) => {
console.log(e.detail);
};
onMounted(() => {
if (qb.value) {
qb.value.fields = [
{ id: 'name', label: 'Name', type: 'string' }
];
}
});
</script>Angular
- Allow custom elements in your module:
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA], // Important!
bootstrap: [AppComponent]
})
export class AppModule { }Import the script in
main.tsorangular.json.Use in template:
<abt-query-builder #qb (change)="handleChange($event)"></abt-query-builder>Supported Operators
The component supports various operators based on the field type:
- String:
equal,not_equal,contains,not_contains,starts_with,ends_with,is_empty,is_not_empty,in_array,not_in_array - Number:
equal,not_equal,greater,less,between,is_empty,is_not_empty,in_array,not_in_array - Date:
equal,not_equal,greater,less,between,is_empty,is_not_empty,in_array,not_in_array - Select:
equal,not_equal,in_array,not_in_array,is_empty,is_not_empty - Boolean:
equal
Special Operators
- Between: Displays two input fields ("From" and "To").
- In List / Not In List: Enables multi-value selection (Tags).
Multi-Value Support
When using in_array ("In List") or not_in_array ("Not In List") operators:
- Select Fields: You can select multiple options from the dropdown.
- Text/Number/Date Fields: Type a value (or pick a date) and press Enter to add it as a tag.
Live Search (Async Data)
For large datasets, you can configure select fields to fetch options dynamically from a backend API instead of loading all data upfront.
Basic Usage
qb.fields = [
{
id: 'product',
label: 'Product',
type: 'select',
search: {
onSearch: async (term, fieldId) => {
const res = await fetch(`/api/products?q=${term}`);
return res.json(); // { key: label, key2: label2, ... }
},
minChars: 2, // Minimum characters before search (default: 2)
debounceMs: 300 // Debounce delay in ms (default: 300)
}
}
];Search Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| onSearch | (term: string, fieldId: string) => Promise<Record<string, string>> | required | Async callback to fetch options |
| onResolveLabel | (key: string, fieldId: string) => Promise<string> | - | Resolve label for saved values |
| mode | 'local' \| 'remote' \| 'hybrid' | 'remote' | Search strategy |
| minChars | number | 2 | Min characters before triggering search |
| debounceMs | number | 300 | Debounce delay in milliseconds |
| cache | boolean \| number | true | Cache results (true = forever, number = TTL in ms) |
| hybridAlwaysFetch | boolean | false | Always fetch backend in hybrid mode, even if local results exist |
| hybridFetchThreshold | number | 1 | Minimum local results needed to skip backend fetch |
| placeholder | string | 'Type to search...' | Input placeholder |
| loadingText | string | 'Searching...' | Text shown while loading |
| noResultsText | string | 'No results found' | Text shown when no results |
Search Modes
remote: Always fetch from backendlocal: Only search in pre-loadedvalues(no API calls)hybrid: Search localvaluesfirst, fallback to backend based on config
Hybrid Mode Features
Results are always sorted by best match (exact matches first, then partial matches).
| Config | Behavior |
|--------|----------|
| Default | Uses local results if any exist, otherwise fetches backend |
| hybridAlwaysFetch: true | Always fetches backend and merges with local |
| hybridFetchThreshold: 5 | Fetches backend if fewer than 5 local results |
Hybrid Mode Example
Pre-load popular items and fetch rest from backend:
qb.fields = [
{
id: 'product',
label: 'Product',
type: 'select',
values: { 'popular1': 'Popular Item 1', 'popular2': 'Popular Item 2' },
search: {
onSearch: async (term, fieldId) => fetchProducts(term),
onResolveLabel: async (key, fieldId) => fetchProductLabel(key),
mode: 'hybrid',
hybridAlwaysFetch: true, // Always merge with backend results
cache: true
}
}
];Resolving Labels for Saved Rules
When loading saved rules with setRules(), use onResolveLabel to fetch labels for stored keys:
search: {
onSearch: async (term) => { /* ... */ },
onResolveLabel: async (key, fieldId) => {
const product = await fetch(`/api/products/${key}`);
return product.name; // Returns the label for the key
}
}Load Fields from API
For API-driven field configuration, use loadFieldsFromAPI() to fetch and auto-wire fields:
// Load fields from API endpoint
await qb.loadFieldsFromAPI('/api/query-builder/fields');API Response Format
[
{ "id": "name", "label": "Customer Name", "type": "string" },
{
"id": "status",
"label": "Status",
"type": "select",
"values": { "active": "Active", "inactive": "Inactive" }
},
{
"id": "category",
"label": "Category",
"type": "select",
"valuesEndpoint": "/api/categories"
},
{
"id": "product",
"label": "Product",
"type": "select",
"search": {
"endpoint": "/api/products/search",
"mode": "hybrid",
"minChars": 2
}
}
]Field Configuration Options
| Property | Description |
|----------|-------------|
| values | Static inline values for small lists |
| valuesEndpoint | URL to fetch values once on load (for medium lists) |
| search.endpoint | URL for live search (appends ?q=term&field=id) |
| search.resolveLabelEndpoint | URL to resolve single value labels |
Custom Mapper
Transform non-standard API responses:
await qb.loadFieldsFromAPI('/api/columns', {
baseUrl: 'https://api.example.com',
mapper: (col) => ({
id: col.column_name,
label: col.display_name,
type: col.data_type === 'varchar' ? 'string' : col.data_type,
values: col.options
})
});Custom Output Serialization
By default, the component outputs a specific JSON structure. You can customize this by setting the serializer property.
const qb = document.getElementById('qb');
qb.serializer = (group) => {
// Transform the internal group structure to your desired format
// 'group' contains { id, condition, rules: [] }
// Example transformation
return {
logic: group.condition,
filters: group.rules.map(rule => {
if ('condition' in rule) {
// It's a nested group, handle recursively if needed
return qb.serializer(rule);
}
// It's a rule
return {
field: rule.field,
op: rule.operator,
val: rule.value
};
})
};
};Custom Input Parsing (Import)
To load data from a custom structure back into the query builder, define a parser function and use the setRules method.
const qb = document.getElementById('qb');
qb.parser = (data) => {
// Transform your custom data structure back to the internal format
// Internal format: { id, condition, rules: [] }
// Example transformation (simplified)
const transform = (node) => {
if (node.filters) {
return {
id: Math.random().toString(36).substr(2, 9),
condition: node.logic,
rules: node.filters.map(transform)
};
}
return {
id: Math.random().toString(36).substr(2, 9),
field: node.field,
operator: node.op,
value: node.val
};
};
return transform(data);
};
// Load data
const myData = { logic: 'AND', filters: [...] };
qb.setRules(myData);Customization
You can customize the appearance using CSS variables:
query-builder {
--qb-primary-color: #6366f1;
--qb-danger-color: #ef4444;
--qb-radius: 8px;
--qb-font-family: 'Inter', sans-serif;
}Development
Install dependencies:
npm installStart dev server:
npm run devBuild for production:
npm run build
Publishing to NPM
- Login to NPM:
npm login - Build the project:
npm run build - Publish:
npm publish --access public
Using via CDN
Once published to NPM, you can use the component directly in the browser via a CDN like unpkg or jsdelivr.
Using unpkg:
<script src="https://unpkg.com/abt-query-builder@latest/dist/abt-query-builder.umd.js"></script>Using jsdelivr:
<script src="https://cdn.jsdelivr.net/npm/abt-query-builder@latest/dist/abt-query-builder.umd.js"></script>Example Usage:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Query Builder Demo</title>
<script src="https://unpkg.com/abt-query-builder@latest/dist/abt-query-builder.umd.js"></script>
</head>
<body>
<abt-query-builder></abt-query-builder>
<script>
const qb = document.querySelector('abt-query-builder');
qb.fields = [
{ id: 'name', label: 'Name', type: 'string' },
{ id: 'age', label: 'Age', type: 'number' }
];
</script>
</body>
</html>