├── .eslintrc.cjs ├── .gitignore ├── README.md ├── components.json ├── index.html ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── react-querybuilder-shadcn-ui.png ├── src ├── App.jsx ├── assets │ └── react.svg ├── components │ ├── mode-toggle.tsx │ ├── react-querybuilder-shadcn-ui │ │ ├── ShadcnUiActionElement.tsx │ │ ├── ShadcnUiActionElementIcon.tsx │ │ ├── ShadcnUiDragHandle.tsx │ │ ├── ShadcnUiNotToggle.tsx │ │ ├── ShadcnUiValueEditor.tsx │ │ ├── ShadcnUiValueSelector.tsx │ │ ├── index.tsx │ │ ├── multiselect.tsx │ │ ├── styles.scss │ │ └── utils.tsx │ ├── theme-provider.tsx │ └── ui │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── switch.tsx │ │ └── textarea.tsx ├── index.css ├── lib │ └── utils.ts └── main.jsx ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## @react-querybuilder/shadcn-ui 2 | 3 | ![react-querybuilder-shadcn-ui](./react-querybuilder-shadcn-ui.png) 4 | 5 | Unofficial [react-querybuilder](https://npmjs.com/package/react-querybuilder) components for [shadcn/ui](https://ui.shadcn.com). 6 | 7 | ## Installation 8 | 9 | Copy and paste the [src/components/react-querybuilder-shadcn-ui](https://github.com/jide/react-querybuilder-shadcn-ui/tree/main/src/components/react-querybuilder-shadcn-ui) in your project. 10 | 11 | ## Usage 12 | 13 | To render shadcn-ui-compatible components in the query builder, wrap the `` element in ``. 14 | 15 | ```tsx 16 | import { QueryBuilderShadcnUi } from "@/components/react-querybuilder-shadcn-ui"; 17 | import { QueryBuilder, RuleGroupType } from "react-querybuilder"; 18 | 19 | const fields = [ 20 | { name: "firstName", label: "First Name" }, 21 | { name: "lastName", label: "Last Name" }, 22 | ]; 23 | 24 | const App = () => { 25 | const [query, setQuery] = useState({ 26 | combinator: "and", 27 | rules: [], 28 | }); 29 | 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | ``` 37 | 38 | ## Notes 39 | 40 | - Some additional styling may be necessary, see [src/components/react-querybuilder-shadcn-ui/styles.scss](https://github.com/jide/react-querybuilder-shadcn-ui/tree/main/src/components/react-querybuilder-shadcn-ui/styles.scss) 41 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-querybuilder-shadcn-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-checkbox": "^1.0.4", 14 | "@radix-ui/react-dropdown-menu": "^2.0.6", 15 | "@radix-ui/react-label": "^2.0.2", 16 | "@radix-ui/react-radio-group": "^1.1.3", 17 | "@radix-ui/react-select": "^2.0.0", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@radix-ui/react-switch": "^1.0.3", 20 | "@react-querybuilder/dnd": "^7.2.0", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.1", 23 | "lucide-react": "^0.372.0", 24 | "react": "^18.2.0", 25 | "react-dnd": "^16.0.1", 26 | "react-dnd-html5-backend": "^16.0.1", 27 | "react-dom": "^18.2.0", 28 | "react-querybuilder": "^7.2.0", 29 | "tailwind-merge": "^2.3.0", 30 | "tailwindcss-animate": "^1.0.7" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20.12.7", 34 | "@types/react": "^18.2.66", 35 | "@types/react-dom": "^18.2.22", 36 | "@vitejs/plugin-react": "^4.2.1", 37 | "autoprefixer": "^10.4.19", 38 | "eslint": "^8.57.0", 39 | "eslint-plugin-react": "^7.34.1", 40 | "eslint-plugin-react-hooks": "^4.6.0", 41 | "eslint-plugin-react-refresh": "^0.4.6", 42 | "postcss": "^8.4.38", 43 | "sass": "^1.75.0", 44 | "tailwindcss": "^3.4.3", 45 | "vite": "^5.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /react-querybuilder-shadcn-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jide/react-querybuilder-shadcn-ui/e141446f701135f6e020bc2a6c4a8915401d60ae/react-querybuilder-shadcn-ui.png -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { QueryBuilder } from "react-querybuilder"; 2 | import { QueryBuilderDnD } from "@react-querybuilder/dnd"; 3 | import * as ReactDnD from "react-dnd"; 4 | import * as ReactDndHtml5Backend from "react-dnd-html5-backend"; 5 | import { ThemeProvider } from "@/components/theme-provider"; 6 | import { QueryBuilderShadcnUi } from "@/components/react-querybuilder-shadcn-ui"; 7 | import { ModeToggle } from "@/components/mode-toggle"; 8 | 9 | const values = [ 10 | { name: "option1", label: "Option 1" }, 11 | { name: "option2", label: "Option 2" }, 12 | { name: "option3", label: "Option 3" }, 13 | { name: "option4", label: "Option 4" }, 14 | ]; 15 | 16 | const fields = [ 17 | { name: "text", label: "text", inputType: "text" }, 18 | { name: "select", label: "select", valueEditorType: "select", values }, 19 | { name: "checkbox", label: "checkbox", valueEditorType: "checkbox" }, 20 | { name: "radio", label: "radio", valueEditorType: "radio", values }, 21 | { name: "textarea", label: "textarea", valueEditorType: "textarea" }, 22 | { 23 | name: "multiselect", 24 | label: "multiselect", 25 | valueEditorType: "multiselect", 26 | values, 27 | }, 28 | { name: "date", label: "date", inputType: "date" }, 29 | { 30 | name: "datetime-local", 31 | label: "datetime-local", 32 | inputType: "datetime-local", 33 | }, 34 | { name: "time", label: "time", inputType: "time" }, 35 | { name: "field", label: "field", valueSources: ["field", "value"] }, 36 | ]; 37 | export const operators = [ 38 | { name: "=", label: "=" }, 39 | { name: "in", label: "in" }, 40 | { name: "between", label: "between" }, 41 | ]; 42 | export const defaultQuery = { 43 | combinator: "and", 44 | rules: [ 45 | { field: "text", operator: "=", value: "" }, 46 | { field: "select", operator: "=", value: "option2" }, 47 | { field: "checkbox", operator: "=", value: true }, 48 | { field: "switch", operator: "=", value: true }, 49 | { field: "radio", operator: "=", value: "option2" }, 50 | { field: "textarea", operator: "=", value: "" }, 51 | { field: "multiselect", operator: "in", value: ["option1", "option2"] }, 52 | { field: "date", operator: "=", value: "" }, 53 | { field: "datetime-local", operator: "=", value: "" }, 54 | { field: "time", operator: "=", value: "" }, 55 | { field: "text", operator: "between", value: "A,Z" }, 56 | { field: "select", operator: "between", value: "option2,option4" }, 57 | { field: "field", operator: "=", value: "text", valueSource: "field" }, 58 | ], 59 | }; 60 | export const NullComponent = () => null; 61 | 62 | export default function App() { 63 | return ( 64 | 65 |
66 | 67 |
68 | 69 | 70 | 76 | 77 | 78 |
79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from "@/components/ui/dropdown-menu"; 10 | import { useTheme } from "@/components/theme-provider"; 11 | 12 | export function ModeToggle() { 13 | const { setTheme } = useTheme(); 14 | 15 | return ( 16 | 17 | 18 | 23 | 24 | 25 | setTheme("light")}> 26 | Light 27 | 28 | setTheme("dark")}> 29 | Dark 30 | 31 | setTheme("system")}> 32 | System 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/react-querybuilder-shadcn-ui/ShadcnUiActionElement.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import type { ComponentPropsWithoutRef } from "react"; 3 | import type { ActionWithRulesProps } from "react-querybuilder"; 4 | 5 | export type ShadcnUiActionProps = ActionWithRulesProps & 6 | ComponentPropsWithoutRef; 7 | 8 | export const ShadcnUiActionElement = ({ 9 | className, 10 | handleOnClick, 11 | label, 12 | title, 13 | disabled, 14 | disabledTranslation, 15 | // Props that should not be in extraProps 16 | testID: _testID, 17 | rules: _rules, 18 | level: _level, 19 | path: _path, 20 | context: _context, 21 | validation: _validation, 22 | ruleOrGroup: _ruleOrGroup, 23 | schema: _schema, 24 | ...extraProps 25 | }: ShadcnUiActionProps) => ( 26 | 36 | ); 37 | 38 | ShadcnUiActionElement.displayName = "ShadcnUiActionElement"; 39 | -------------------------------------------------------------------------------- /src/components/react-querybuilder-shadcn-ui/ShadcnUiActionElementIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import type { ComponentPropsWithoutRef } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | import type { ActionWithRulesProps } from "react-querybuilder"; 5 | 6 | export type ShadcnUiActionProps = ActionWithRulesProps & 7 | ComponentPropsWithoutRef; 8 | 9 | export const ShadcnUiActionElementIcon = ({ 10 | className, 11 | handleOnClick, 12 | label, 13 | title, 14 | disabled, 15 | disabledTranslation, 16 | // Props that should not be in extraProps 17 | testID: _testID, 18 | rules: _rules, 19 | level: _level, 20 | path: _path, 21 | context: _context, 22 | validation: _validation, 23 | ruleOrGroup: _ruleOrGroup, 24 | schema: _schema, 25 | ...extraProps 26 | }: ShadcnUiActionProps) => ( 27 | 38 | ); 39 | 40 | ShadcnUiActionElementIcon.displayName = "ShadcnUiActionElementIcon"; 41 | -------------------------------------------------------------------------------- /src/components/react-querybuilder-shadcn-ui/ShadcnUiDragHandle.tsx: -------------------------------------------------------------------------------- 1 | import { GripVertical } from "lucide-react"; 2 | import type { ComponentPropsWithRef } from "react"; 3 | import { forwardRef } from "react"; 4 | import type { DragHandleProps } from "react-querybuilder"; 5 | 6 | export type ShadcnUiDragHandleProps = DragHandleProps & 7 | ComponentPropsWithRef<"span">; 8 | 9 | export const ShadcnUiDragHandle = forwardRef< 10 | HTMLSpanElement, 11 | ShadcnUiDragHandleProps 12 | >(({ className, title }, dragRef) => ( 13 | 14 | 15 | 16 | )); 17 | 18 | ShadcnUiDragHandle.displayName = "ShadcnUiDragHandle"; 19 | -------------------------------------------------------------------------------- /src/components/react-querybuilder-shadcn-ui/ShadcnUiNotToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from "@/components/ui/switch"; 2 | import type { ComponentPropsWithoutRef } from "react"; 3 | import type { NotToggleProps } from "react-querybuilder"; 4 | 5 | export type ChakraNotToggleProps = NotToggleProps & 6 | ComponentPropsWithoutRef; 7 | 8 | export const ShadcnUiNotToggle = ({ 9 | className, 10 | handleOnChange, 11 | checked, 12 | disabled, 13 | label, 14 | }: ChakraNotToggleProps) => { 15 | return ( 16 |
17 | 23 | {label} 24 |
25 | ); 26 | }; 27 | 28 | ShadcnUiNotToggle.displayName = "ShadcnUiNotToggle"; 29 | -------------------------------------------------------------------------------- /src/components/react-querybuilder-shadcn-ui/ShadcnUiValueEditor.tsx: -------------------------------------------------------------------------------- 1 | import type { ValueEditorProps } from "react-querybuilder"; 2 | import { cn } from "@/lib/utils"; 3 | import { Checkbox } from "@/components/ui/checkbox"; 4 | import { Input } from "@/components/ui/input"; 5 | import { Label } from "@/components/ui/label"; 6 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 7 | import { Switch } from "@/components/ui/switch"; 8 | import { Textarea } from "@/components/ui/textarea"; 9 | import { 10 | getFirstOption, 11 | standardClassnames, 12 | useValueEditor, 13 | } from "react-querybuilder"; 14 | 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | export type ShadcnUiValueEditorProps = ValueEditorProps & { 17 | extraProps?: Record; 18 | }; 19 | 20 | export const ShadcnUiValueEditor = (allProps: ShadcnUiValueEditorProps) => { 21 | const { 22 | fieldData, 23 | operator, 24 | value, 25 | handleOnChange, 26 | title, 27 | className, 28 | type, 29 | inputType, 30 | values = [], 31 | listsAsArrays, 32 | parseNumbers, 33 | separator, 34 | valueSource: _vs, 35 | testID, 36 | disabled, 37 | selectorComponent: SelectorComponent = allProps.schema.controls 38 | .valueSelector, 39 | extraProps, 40 | ...props 41 | } = allProps; 42 | 43 | const { valueAsArray, multiValueHandler } = useValueEditor({ 44 | handleOnChange, 45 | inputType, 46 | operator, 47 | value, 48 | type, 49 | listsAsArrays, 50 | parseNumbers, 51 | values, 52 | }); 53 | 54 | if (operator === "null" || operator === "notNull") { 55 | return null; 56 | } 57 | 58 | const placeHolderText = fieldData?.placeholder ?? ""; 59 | const inputTypeCoerced = ["in", "notIn"].includes(operator) 60 | ? "text" 61 | : inputType || "text"; 62 | 63 | if ( 64 | (operator === "between" || operator === "notBetween") && 65 | (type === "select" || type === "text") 66 | ) { 67 | const editors = ["from", "to"].map((key, i) => { 68 | if (type === "text") { 69 | return ( 70 | multiValueHandler(e.target.value, i)} 78 | {...extraProps} 79 | /> 80 | ); 81 | } 82 | return ( 83 | multiValueHandler(v, i)} 88 | disabled={disabled} 89 | value={valueAsArray[i] ?? getFirstOption(values)} 90 | options={values} 91 | listsAsArrays={listsAsArrays} 92 | /> 93 | ); 94 | }); 95 | return ( 96 | 101 | {editors[0]} 102 | {separator} 103 | {editors[1]} 104 | 105 | ); 106 | } 107 | 108 | switch (type) { 109 | case "select": 110 | return ( 111 | 120 | ); 121 | 122 | case "multiselect": 123 | return ( 124 | 134 | ); 135 | 136 | case "textarea": 137 | return ( 138 |