Skip to content

mvtcode/vue3-querybuilder

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vue 3 QueryBuilder

A powerful and flexible query builder component for Vue 3 with Element Plus.

Installation

npm install @mvtcode/vue3-querybuilder

Repository

git clone git@github.com:mvtcode/vue3-querybuilder.git

Usage

<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>

Enums

FilterType

enum FilterType {
  STRING = 'string',
  NUMBER = 'number',
  BOOLEAN = 'boolean',
  DATE = 'date',
}

Operator

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',
}

Props

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')

Events

Event Parameters Description
update:modelValue (value: QueryBuilderRule | QueryBuilderGroup) Emitted when the query value changes

Types

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)[]
}

Filter Configuration

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
  }
}

Filter Types

1. Text (STRING) - Với tất cả operators

{
  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,
  ],
}

2. Email - Với validation tự động

{
  field: 'email',
  label: 'Email',
  type: FilterType.EMAIL,
  operators: [Operator.EQUAL, Operator.NOT_EQUAL, Operator.CONTAINS, Operator.NOT_CONTAINS],
  input: 'email',
}

3. Integer - Với validation và BETWEEN support

{
  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,
  ],
}

4. Date - Với date picker và range support

{
  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,
  ],
}

5. Boolean - Với checkbox

{
  field: 'active',
  label: 'Active',
  type: FilterType.BOOLEAN,
  input: 'checkbox',
}

6. Select (Dropdown) - Với multiple support

{
  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],
}

Operators

Component supports the following operators:

  • equal: Equal (=)
  • not_equal: Not Equal (≠)
  • contains: Contains (⊃)
  • not_contains: Not Contains (⊅)
  • begins_with: Begins With
  • ends_with: Ends With
  • greater: Greater Than (>)
  • greater_or_equal: Greater Than or Equal (≥)
  • less: Less Than (<)
  • less_or_equal: Less Than or Equal (≤)
  • in: In List
  • not_in: Not In List
  • between: Between
  • not_between: Not Between
  • is_empty: Is Empty
  • is_not_empty: Is Not Empty

Query Result

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: []
    }
  ]
}

Query Conversion

To SQL

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 >= 18

To MongoDB

import { 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 } }
//   ]
// }

From SQL

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
//     }
//   ]
// }

From MongoDB

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
//     }
//   ]
// }

Supported Operators Mapping

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

Tính năng mới và cải tiến

1. Custom Slots với Dynamic Props

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>

2. Enhanced Filter Types

  • 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

3. Advanced Operators Support

  • 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

4. Dynamic Width Control

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>

5. Enhanced Slot Props

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

Dynamic Slots

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, value
  • operator: Current operator được chọn cho rule
  • value: Current value của rule (alias cho rule.value)
  • isBetween: Boolean flag chỉ ra operator có phải BETWEEN hoặc NOT_BETWEEN không
  • widthValueInput: Width được tính toán cho value input
  • index: Index của rule trong group

Khi isBetween là true, rule.value sẽ là array với 2 elements cho range values.

Kết quả Query từ Demo

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'] } },
  ]
}

Development

# 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

Examples

Complete Demo Example

Đâ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>

Basic Usage with Max Depth

<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>

Max Depth Options

  • maxDepth={0}: Unlimited nesting (default)
  • maxDepth={1}: Disable nested groups completely
  • maxDepth={n}: Limit nesting to n levels (where n is a positive number)

License

MIT

Screenshot

Vue 3 QueryBuilder Screenshot

Author

Mạc Tân (Tanmv)

Email: tanmv@mpos.vn

FB: Mạc Tân

Telegram: @tanmac

Skype: trai_12a1

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published