npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

abt-query-builder

v2.5.3

Published

A framework-agnostic, customizable query builder web component

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

  1. 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 { }
  1. Import the script in main.ts or angular.json.

  2. 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 backend
  • local: Only search in pre-loaded values (no API calls)
  • hybrid: Search local values first, 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

  1. Install dependencies:

    npm install
  2. Start dev server:

    npm run dev
  3. Build for production:

    npm run build

Publishing to NPM

  1. Login to NPM:
    npm login
  2. Build the project:
    npm run build
  3. 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>