A powerful and flexible query builder component for Vue 3 with Element Plus.
npm install @mvtcode/vue3-querybuildergit clone git@github.com:mvtcode/vue3-querybuilder.git<template>
<QueryBuilder v-model="query" :filters="filters" @update:modelValue="onQueryChange" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { QueryBuilder } from '@mvtcode/vue3-querybuilder'
import type { QueryBuilderRule, QueryBuilderGroup } from '@mvtcode/vue3-querybuilder'
import { FilterType, Operator } from '@mvtcode/vue3-querybuilder'
const query = ref<QueryBuilderRule | QueryBuilderGroup>({
type: 'rule',
field: 'name',
operator: Operator.EQUAL,
value: '',
})
const filters = [
{
field: 'name',
label: 'Name',
type: FilterType.STRING,
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.CONTAINS, Operator.NOT_CONTAINS],
},
{
field: 'age',
label: 'Age',
type: FilterType.NUMBER,
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.GREATER, Operator.LESS],
},
]
const onQueryChange = (newQuery: QueryBuilderRule | QueryBuilderGroup) => {
console.log('Query changed:', newQuery)
}
</script>enum FilterType {
STRING = 'string',
NUMBER = 'number',
BOOLEAN = 'boolean',
DATE = 'date',
}enum Operator {
EQUAL = 'equal',
NOT_EQUAL = 'not_equal',
CONTAINS = 'contains',
NOT_CONTAINS = 'not_contains',
BEGINS_WITH = 'begins_with',
ENDS_WITH = 'ends_with',
GREATER = 'greater',
GREATER_OR_EQUAL = 'greater_or_equal',
LESS = 'less',
LESS_OR_EQUAL = 'less_or_equal',
IN = 'in',
NOT_IN = 'not_in',
BETWEEN = 'between',
NOT_BETWEEN = 'not_between',
IS_EMPTY = 'is_empty',
IS_NOT_EMPTY = 'is_not_empty',
}| Prop | Type | Default | Description |
|---|---|---|---|
| modelValue | QueryBuilderRule | QueryBuilderGroup |
- | The current query value |
| filters | Filter[] |
[] |
Array of available filters |
| maxDepth | number |
0 |
Maximum depth of nested groups. Set to 0 for unlimited depth, 1 to disable nested groups, or any positive number to limit the nesting level |
| language | string |
'vi' |
Language for the component UI (supports 'en' and 'vi') |
| Event | Parameters | Description |
|---|---|---|
| update:modelValue | (value: QueryBuilderRule | QueryBuilderGroup) |
Emitted when the query value changes |
interface Filter {
field: string
label: string
type: 'string' | 'number' | 'boolean' | 'date'
operators: string[]
}
interface QueryBuilderRule {
type: 'rule'
field: string
operator: string
value: any
}
interface QueryBuilderGroup {
type: 'group'
condition: 'and' | 'or'
rules: (QueryBuilderRule | QueryBuilderGroup)[]
}Each filter can be configured with the following properties:
interface Filter {
field: string // Field name
label: string // Display label
type: 'string' | 'number' | 'boolean' | 'date' // Data type
operators: string[] // Allowed operators
input?: string // Input type ('select', 'radio', 'date', etc.)
values?: Array<{
// Values for select/radio
value: string
text: string
}>
validation?: {
// Validation rules
format?: string // Format for date (YYYY-MM-DD)
min?: number // Minimum value for number
max?: number // Maximum value for number
step?: number // Step for number
}
}{
field: 'name',
label: 'Name',
type: FilterType.STRING,
operators: [
Operator.EQUAL,
Operator.NOT_EQUAL,
Operator.CONTAINS,
Operator.NOT_CONTAINS,
Operator.BEGINS_WITH,
Operator.NOT_BEGINS_WITH,
Operator.ENDS_WITH,
Operator.NOT_ENDS_WITH,
Operator.IS_EMPTY,
Operator.IS_NOT_EMPTY,
],
}{
field: 'email',
label: 'Email',
type: FilterType.EMAIL,
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.CONTAINS, Operator.NOT_CONTAINS],
input: 'email',
}{
field: 'age',
label: 'Age',
type: FilterType.INTEGER,
validation: {
min: 0,
max: 100,
},
operators: [
Operator.EQUAL,
Operator.NOT_EQUAL,
Operator.GREATER,
Operator.GREATER_OR_EQUAL,
Operator.LESS,
Operator.LESS_OR_EQUAL,
Operator.BETWEEN,
Operator.NOT_BETWEEN,
],
}{
field: 'birthdate',
label: 'Birth Date',
type: FilterType.DATE,
input: 'date',
validation: {
format: 'YYYY-MM-DD',
},
operators: [
Operator.EQUAL,
Operator.NOT_EQUAL,
Operator.GREATER,
Operator.GREATER_OR_EQUAL,
Operator.LESS,
Operator.LESS_OR_EQUAL,
Operator.BETWEEN,
Operator.NOT_BETWEEN,
],
}{
field: 'active',
label: 'Active',
type: FilterType.BOOLEAN,
input: 'checkbox',
}{
field: 'status',
label: 'Status',
type: FilterType.STRING,
input: 'select',
value: 'pending', // Giá trị mặc định
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.IN, Operator.NOT_IN],
}Component supports the following operators:
equal: Equal (=)not_equal: Not Equal (≠)contains: Contains (⊃)not_contains: Not Contains (⊅)begins_with: Begins Withends_with: Ends Withgreater: Greater Than (>)greater_or_equal: Greater Than or Equal (≥)less: Less Than (<)less_or_equal: Less Than or Equal (≤)in: In Listnot_in: Not In Listbetween: Betweennot_between: Not Betweenis_empty: Is Emptyis_not_empty: Is Not Empty
The result is returned as an object with the following structure:
{
type: 'group',
condition: 'and' | 'or',
rules: [
{
type: 'rule',
field: string,
operator: string,
value: any
},
// or another group
{
type: 'group',
condition: 'and' | 'or',
rules: []
}
]
}import { toSQL } from '@mvtcode/vue3-querybuilder'
const rules = {
type: 'group',
condition: 'and',
rules: [
{
type: 'rule',
field: 'name',
operator: Operator.EQUAL,
value: 'John',
},
{
type: 'rule',
field: 'age',
operator: Operator.GREATER_OR_EQUAL,
value: 18,
},
],
}
const sqlWhere = toSQL(rules)
// Output: name = 'John' AND age >= 18import { toMongo } from '@mvtcode/vue3-querybuilder'
const rules = {
type: 'group',
condition: 'and',
rules: [
{
type: 'rule',
field: 'name',
operator: Operator.EQUAL,
value: 'John',
},
{
type: 'rule',
field: 'age',
operator: Operator.GREATER_OR_EQUAL,
value: 18,
},
],
}
const mongoQuery = toMongo(rules)
// Output: {
// $and: [
// { name: { $eq: 'John' } },
// { age: { $gte: 18 } }
// ]
// }import { fromSQL } from '@mvtcode/vue3-querybuilder'
const rules = fromSQL("name = 'John' AND age >= 18")
// Output: {
// type: 'group',
// condition: 'and',
// rules: [
// {
// type: 'rule',
// field: 'name',
// operator: Operator.EQUAL,
// value: 'John'
// },
// {
// type: 'rule',
// field: 'age',
// operator: Operator.GREATER_OR_EQUAL,
// value: 18
// }
// ]
// }import { fromMongo } from '@mvtcode/vue3-querybuilder'
const rules = fromMongo({
$and: [{ name: { $eq: 'John' } }, { age: { $gte: 18 } }],
})
// Output: {
// type: 'group',
// condition: 'and',
// rules: [
// {
// type: 'rule',
// field: 'name',
// operator: Operator.EQUAL,
// value: 'John'
// },
// {
// type: 'rule',
// field: 'age',
// operator: Operator.GREATER_OR_EQUAL,
// value: 18
// }
// ]
// }| QueryBuilder Operator | SQL Operator | MongoDB Operator |
|---|---|---|
| EQUAL | = | $eq |
| NOT_EQUAL | != | $ne |
| CONTAINS | LIKE | $regex |
| NOT_CONTAINS | NOT LIKE | $not |
| BEGINS_WITH | LIKE | $regex |
| ENDS_WITH | LIKE | $regex |
| GREATER | > | $gt |
| GREATER_OR_EQUAL | >= | $gte |
| LESS | < | $lt |
| LESS_OR_EQUAL | <= | $lte |
| IN | IN | $in |
| NOT_IN | NOT IN | $nin |
| BETWEEN | BETWEEN | $and |
| NOT_BETWEEN | NOT BETWEEN | $nor |
| IS_EMPTY | IS NULL | $exists: false |
| IS_NOT_EMPTY | IS NOT NULL | $exists: true |
Component cung cấp dynamic slots cho mỗi field với các props mở rộng:
<template>
<QueryBuilder v-model="query" :filters="filters">
<!-- Custom input cho email với validation -->
<template #email="{ rule, widthValueInput }">
<el-input
v-model="rule.value"
placeholder="Enter email"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
</template>
<!-- Custom input cho age với BETWEEN support -->
<template #age="{ isBetween, rule, widthValueInput }">
<el-input-number
v-if="!isBetween"
v-model="rule.value"
:min="0"
:max="100"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
<div v-else style="display: flex; align-items: center; gap: 10px">
<el-input-number
v-model="(rule.value as number[])[0]"
:min="0"
:max="100"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
<span>and</span>
<el-input-number
v-model="(rule.value as number[])[1]"
:min="0"
:max="100"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
</div>
</template>
<!-- Custom date picker với range support -->
<template #birthdate="{ rule, isBetween, widthValueInput }">
<el-date-picker
v-model="rule.value"
:type="isBetween ? 'daterange' : 'date'"
placeholder="Select date"
range-separator="To"
start-placeholder="Start date"
end-placeholder="End date"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
</template>
<!-- Custom checkbox cho boolean -->
<template #active="{ rule, widthValueInput }">
<el-checkbox v-model="rule.value" :style="{ width: `${widthValueInput}px` }"
>Active/Inactive</el-checkbox
>
</template>
<!-- Custom select với multiple support -->
<template #status="{ rule, widthValueInput }">
<el-select
v-model="rule.value"
placeholder="Select status"
:multiple="[Operator.IN, Operator.NOT_IN].includes(rule.operator)"
clearable
:style="{ width: `${widthValueInput}px` }"
>
<el-option label="Pending" value="pending" />
<el-option label="Completed" value="completed" />
</el-select>
</template>
</QueryBuilder>
</template>- FilterType.EMAIL: Tự động validation email
- FilterType.INTEGER: Hỗ trợ validation min/max và BETWEEN operators
- FilterType.DATE: Hỗ trợ date picker và date range
- FilterType.BOOLEAN: Hỗ trợ checkbox input
- FilterType.STRING: Hỗ trợ tất cả string operators
- BETWEEN/NOT_BETWEEN: Tự động chuyển đổi input thành range
- IN/NOT_IN: Tự động enable multiple selection
- IS_EMPTY/IS_NOT_EMPTY: Không cần input value
- BEGINS_WITH/ENDS_WITH: Hỗ trợ pattern matching
Tất cả slots đều nhận widthValueInput prop để control width của input:
<template #customField="{ rule, widthValueInput }">
<el-input v-model="rule.value" :style="{ width: `${widthValueInput}px` }" />
</template>| Name | Type | Description |
|---|---|---|
rule |
QueryBuilderRule |
Toàn bộ rule object với field, operator, value |
operator |
string |
Current operator của rule |
value |
any |
Current value của rule (alias cho rule.value) |
isBetween |
boolean |
Có phải BETWEEN/NOT_BETWEEN operator không |
widthValueInput |
number |
Width được tính toán cho value input |
index |
number |
Index của rule trong group |
Component tự động tạo slots dựa trên field names trong filters configuration. Ví dụ, nếu bạn có filter với field: 'name', bạn có thể sử dụng #name slot để customize input.
Mỗi slot nhận các props:
rule: Toàn bộ rule object với field, operator, valueoperator: Current operator được chọn cho rulevalue: Current value của rule (alias cho rule.value)isBetween: Boolean flag chỉ ra operator có phải BETWEEN hoặc NOT_BETWEEN khôngwidthValueInput: Width được tính toán cho value inputindex: Index của rule trong group
Khi isBetween là true, rule.value sẽ là array với 2 elements cho range values.
Với cấu hình filters trong ví dụ, bạn có thể tạo ra các query phức tạp như:
{
"condition": "AND",
"rules": [
{
"id": "uuid-1",
"field": "name",
"operator": "contains",
"value": "John"
},
{
"id": "uuid-2",
"field": "age",
"operator": "between",
"value": [18, 65]
},
{
"id": "uuid-3",
"field": "birthdate",
"operator": "greater",
"value": "1990-01-01"
},
{
"id": "uuid-4",
"field": "active",
"operator": "equal",
"value": true
},
{
"id": "uuid-5",
"field": "status",
"operator": "in",
"value": ["pending", "completed"]
}
]
}Query này sẽ được convert thành:
SQL:
name LIKE '%John%'
AND age BETWEEN 18 AND 65
AND birthdate > '1990-01-01'
AND active = true
AND status IN ('pending', 'completed')MongoDB:
{
$and: [
{ name: { $regex: 'John', $options: 'i' } },
{ age: { $gte: 18, $lte: 65 } },
{ birthdate: { $gt: '1990-01-01' } },
{ active: { $eq: true } },
{ status: { $in: ['pending', 'completed'] } },
]
}# Install dependencies
npm install
# Run development server
npm run dev
# Build for production
npm run build
# Run unit tests
npm run test:unit
# Lint and fix files
npm run lintĐây là ví dụ đầy đủ từ file App.vue với tất cả các loại filter và custom slots:
<template>
<div class="app">
<h1>Vue 3 QueryBuilder Demo</h1>
<QueryBuilder v-model="rules" :filters="filters">
<!-- Custom email input -->
<template #email="{ rule, widthValueInput }">
<el-input
v-model="rule.value"
placeholder="Enter email"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
</template>
<!-- Custom age input with between support -->
<template #age="{ isBetween, rule, widthValueInput }">
<el-input-number
v-if="!isBetween"
v-model="rule.value"
:min="0"
:max="100"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
<div v-else style="display: flex; align-items: center; gap: 10px">
<el-input-number
v-model="(rule.value as number[])[0]"
:min="0"
:max="100"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
<span>and</span>
<el-input-number
v-model="(rule.value as number[])[1]"
:min="0"
:max="100"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
</div>
</template>
<!-- Custom date picker -->
<template #birthdate="{ rule, isBetween, widthValueInput }">
<el-date-picker
v-model="rule.value"
:type="isBetween ? 'daterange' : 'date'"
placeholder="Select date"
range-separator="To"
start-placeholder="Start date"
end-placeholder="End date"
clearable
:style="{ width: `${widthValueInput}px` }"
/>
</template>
<!-- Custom checkbox for boolean -->
<template #active="{ rule, widthValueInput }">
<el-checkbox v-model="rule.value" :style="{ width: `${widthValueInput}px` }"
>Active/Inactive</el-checkbox
>
</template>
<!-- Custom select dropdown -->
<template #status="{ rule, widthValueInput }">
<el-select
v-model="rule.value"
placeholder="Select status"
:multiple="[Operator.IN, Operator.NOT_IN].includes(rule.operator)"
clearable
:style="{ width: `${widthValueInput}px` }"
>
<el-option label="Pending" value="pending" />
<el-option label="Completed" value="completed" />
</el-select>
</template>
</QueryBuilder>
<!-- Display current rules -->
<div class="rules-display">
<h3>Current Rules:</h3>
<pre>{{ JSON.stringify(rules, null, 2) }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import QueryBuilder from './components/QueryBuilder.vue'
import type { QueryBuilderGroup, QueryBuilderFilter } from './types/querybuilder'
import { FilterType, Operator } from './types/querybuilder'
const rules = ref<QueryBuilderGroup>({
condition: 'AND',
rules: [],
})
// Comprehensive filter configuration
const filters: QueryBuilderFilter[] = [
{
field: 'name',
label: 'Name',
type: FilterType.STRING,
operators: [
Operator.EQUAL,
Operator.NOT_EQUAL,
Operator.CONTAINS,
Operator.NOT_CONTAINS,
Operator.BEGINS_WITH,
Operator.NOT_BEGINS_WITH,
Operator.ENDS_WITH,
Operator.NOT_ENDS_WITH,
Operator.IS_EMPTY,
Operator.IS_NOT_EMPTY,
],
},
{
field: 'email',
label: 'Email',
type: FilterType.EMAIL,
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.CONTAINS, Operator.NOT_CONTAINS],
input: 'email',
},
{
field: 'age',
label: 'Age',
type: FilterType.INTEGER,
validation: {
min: 0,
max: 100,
},
operators: [
Operator.EQUAL,
Operator.NOT_EQUAL,
Operator.GREATER,
Operator.GREATER_OR_EQUAL,
Operator.LESS,
Operator.LESS_OR_EQUAL,
Operator.BETWEEN,
Operator.NOT_BETWEEN,
],
},
{
field: 'birthdate',
label: 'Birth Date',
type: FilterType.DATE,
input: 'date',
validation: {
format: 'YYYY-MM-DD',
},
operators: [
Operator.EQUAL,
Operator.NOT_EQUAL,
Operator.GREATER,
Operator.GREATER_OR_EQUAL,
Operator.LESS,
Operator.LESS_OR_EQUAL,
Operator.BETWEEN,
Operator.NOT_BETWEEN,
],
},
{
field: 'active',
label: 'Active',
type: FilterType.BOOLEAN,
input: 'checkbox',
},
{
field: 'status',
label: 'Status',
type: FilterType.STRING,
input: 'select',
value: 'pending', // Giá trị mặc định
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.IN, Operator.NOT_IN],
},
]
</script>
<style lang="scss" scoped>
pre {
padding: 20px;
background-color: #f0f0f0;
border-radius: 5px;
font-size: 14px;
line-height: 1.5;
color: #333;
}
</style><template>
<QueryBuilder
v-model="query"
:filters="filters"
:max-depth="2" <!-- Limit nesting to 2 levels -->
@update:modelValue="onQueryChange"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { QueryBuilder } from '@mvtcode/vue3-querybuilder'
import type { QueryBuilderRule, QueryBuilderGroup } from '@mvtcode/vue3-querybuilder'
import { FilterType, Operator } from '@mvtcode/vue3-querybuilder'
const query = ref<QueryBuilderRule | QueryBuilderGroup>({
type: 'group',
condition: 'AND',
rules: []
})
const filters = [
{
field: 'name',
label: 'Name',
type: FilterType.STRING,
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.CONTAINS, Operator.NOT_CONTAINS],
},
{
field: 'age',
label: 'Age',
type: FilterType.NUMBER,
operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.GREATER, Operator.LESS],
},
]
const onQueryChange = (newQuery: QueryBuilderRule | QueryBuilderGroup) => {
console.log('Query changed:', newQuery)
}
</script>maxDepth={0}: Unlimited nesting (default)maxDepth={1}: Disable nested groups completelymaxDepth={n}: Limit nesting to n levels (where n is a positive number)
MIT
Mạc Tân (Tanmv)
Email: tanmv@mpos.vn
FB: Mạc Tân
Telegram: @tanmac
Skype: trai_12a1
