├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .scripts ├── get_gh_pages_url.js ├── mocha_runner.js ├── prepublish.sh ├── publish_storybook.sh └── user │ ├── prepublish.sh │ └── pretest.js ├── .storybook ├── .babelrc ├── config.js ├── stories │ ├── index.css │ └── index.js └── webpack.config.js ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── blob └── builder.jpg ├── package.json ├── src ├── Condition.js ├── Rule.js ├── TwoWayQuerybuilder.js ├── helpers │ ├── ASTree.js │ ├── QueryParser.js │ ├── TreeHelper.js │ └── TreeNode.js ├── index.js └── tests │ └── index.js └── styles.css /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.json] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .publish/* 2 | .scripts/* 3 | .storybook/* 4 | dist/* 5 | node_modules/* 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | # We use _ to define private variables and methods in clases 5 | "no-underscore-dangle": 0, 6 | # This seems to be buggy we don't want eslint to check this 7 | "import/no-extraneous-dependencies": 0, 8 | # This is a depricated rule. So we turned off it. 9 | "react/require-extension": 0, 10 | # We can write JSX in anyfile we want. 11 | "react/jsx-filename-extension": 0, 12 | # We don't like this rule. 13 | "arrow-body-style": 0, 14 | # We don't like this rule. We write arrow functions only when we needed. 15 | "prefer-arrow-callback": 0, 16 | # We don't need to write function names always. 17 | "func-names": 0, 18 | # propTypes can be object 19 | "react/forbid-prop-types": 0, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Coverage tools 14 | lib-cov 15 | coverage 16 | coverage.html 17 | .cover* 18 | 19 | # Editor and other tmp files 20 | *.swp 21 | *.un~ 22 | *.iml 23 | *.ipr 24 | *.iws 25 | *.sublime-* 26 | .idea/ 27 | *.DS_Store 28 | 29 | # Coverage tools 30 | lib-cov 31 | coverage 32 | coverage.html 33 | .cover* 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .babelrc 3 | -------------------------------------------------------------------------------- /.scripts/get_gh_pages_url.js: -------------------------------------------------------------------------------- 1 | // IMPORTANT 2 | // --------- 3 | // This is an auto generated file with React CDK. 4 | // Do not modify this file. 5 | 6 | const parse = require('git-url-parse'); 7 | var ghUrl = process.argv[2]; 8 | const parsedUrl = parse(ghUrl); 9 | 10 | const ghPagesUrl = 'https://' + parsedUrl.owner + '.github.io/' + parsedUrl.name; 11 | console.log(ghPagesUrl); 12 | -------------------------------------------------------------------------------- /.scripts/mocha_runner.js: -------------------------------------------------------------------------------- 1 | // IMPORTANT 2 | // --------- 3 | // This is an auto generated file with React CDK. 4 | // Do not modify this file. 5 | // Use `.scripts/user/pretest.js instead`. 6 | 7 | process.env.NODE_ENV = 'development'; 8 | require('babel-core/register'); 9 | require('babel-polyfill'); 10 | 11 | // Add jsdom support, which is required for enzyme. 12 | var jsdom = require('jsdom').jsdom; 13 | 14 | var exposedProperties = ['window', 'navigator', 'document']; 15 | 16 | global.document = jsdom(''); 17 | global.window = document.defaultView; 18 | Object.keys(document.defaultView).forEach((property) => { 19 | if (typeof global[property] === 'undefined') { 20 | exposedProperties.push(property); 21 | global[property] = document.defaultView[property]; 22 | } 23 | }); 24 | 25 | global.navigator = { 26 | userAgent: 'node.js' 27 | }; 28 | 29 | process.on('unhandledRejection', function (error) { 30 | console.error('Unhandled Promise Rejection:'); 31 | console.error(error && error.stack || error); 32 | }); 33 | 34 | require('./user/pretest.js'); 35 | -------------------------------------------------------------------------------- /.scripts/prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # IMPORTANT 4 | # --------- 5 | # This is an auto generated file with React CDK. 6 | # Do not modify this file. 7 | # Use `.scripts/user/prepublish.sh instead`. 8 | 9 | echo "=> Transpiling 'src' into ES5 ..." 10 | echo "" 11 | rm -rf ./dist 12 | NODE_ENV=production ./node_modules/.bin/babel --ignore tests,stories --plugins "transform-runtime" ./src --out-dir ./dist 13 | echo "" 14 | echo "=> Transpiling completed." 15 | 16 | . .scripts/user/prepublish.sh 17 | -------------------------------------------------------------------------------- /.scripts/publish_storybook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # IMPORTANT 4 | # --------- 5 | # This is an auto generated file with React CDK. 6 | # Do not modify this file. 7 | 8 | set -e # exit with nonzero exit code if anything fails 9 | 10 | # get GIT url 11 | 12 | GIT_URL=`git config --get remote.origin.url` 13 | if [[ $GIT_URL == "" ]]; then 14 | echo "This project is not configured with a remote git repo". 15 | exit 1 16 | fi 17 | 18 | # clear and re-create the out directory 19 | rm -rf .out || exit 0; 20 | mkdir .out; 21 | 22 | # run our compile script, discussed above 23 | build-storybook -o .out 24 | 25 | # go to the out directory and create a *new* Git repo 26 | cd .out 27 | git init 28 | 29 | # inside this git repo we'll pretend to be a new user 30 | git config user.name "GH Pages Bot" 31 | git config user.email "hello@ghbot.com" 32 | 33 | # The first and only commit to this new Git repo contains all the 34 | # files present with the commit message "Deploy to GitHub Pages". 35 | git add . 36 | git commit -m "Deploy Storybook to GitHub Pages" 37 | 38 | # Force push from the current repo's master branch to the remote 39 | # repo's gh-pages branch. (All previous history on the gh-pages branch 40 | # will be lost, since we are overwriting it.) We redirect any output to 41 | # /dev/null to hide any sensitive credential data that might otherwise be exposed. 42 | git push --force --quiet $GIT_URL master:gh-pages > /dev/null 2>&1 43 | cd .. 44 | rm -rf .out 45 | 46 | echo "" 47 | echo "=> Storybook deployed to: `node .scripts/get_gh_pages_url.js $GIT_URL`" 48 | -------------------------------------------------------------------------------- /.scripts/user/prepublish.sh: -------------------------------------------------------------------------------- 1 | # Use this file to your own code to run at NPM `prepublish` event. 2 | -------------------------------------------------------------------------------- /.scripts/user/pretest.js: -------------------------------------------------------------------------------- 1 | // Use this file to setup any test utilities. 2 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"] 3 | } -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | // IMPORTANT 2 | // --------- 3 | // This is an auto generated file with React CDK. 4 | // Do not modify this file. 5 | 6 | import { configure } from '@kadira/storybook'; 7 | 8 | function loadStories() { 9 | require('./stories/index.js'); 10 | } 11 | 12 | configure(loadStories, module); 13 | -------------------------------------------------------------------------------- /.storybook/stories/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Examples Stylesheet 3 | // ------------------- 4 | */ 5 | 6 | body { 7 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 8 | font-size: 14px; 9 | color: #333; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | a { 15 | color: #08c; 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | .container { 24 | margin-left: auto; 25 | margin-right: auto; 26 | max-width: 720px; 27 | padding: 1em; 28 | } 29 | 30 | .footer { 31 | margin-top: 50px; 32 | border-top: 1px solid #eee; 33 | padding: 20px 0; 34 | font-size: 12px; 35 | color: #999; 36 | } 37 | 38 | h1, h2, h3, h4, h5, h6 { 39 | color: #222; 40 | font-weight: 100; 41 | margin: 0.5em 0; 42 | } 43 | 44 | label { 45 | color: #999; 46 | display: inline-block; 47 | font-size: 0.85em; 48 | font-weight: bold; 49 | margin: 1em 0; 50 | text-transform: uppercase; 51 | } 52 | 53 | .hint { 54 | margin: 15px 0; 55 | font-style: italic; 56 | color: #999; 57 | } 58 | 59 | .customPrimaryBtn{ 60 | font-family: Roboto, sans-serif; 61 | -moz-osx-font-smoothing: grayscale; 62 | -webkit-font-smoothing: antialiased; 63 | font-size: 0.875rem; 64 | font-weight: 500; 65 | letter-spacing: 0.04em; 66 | line-height: 1.5rem; 67 | color: rgba(0, 0, 0, 0.87); 68 | color: var(--mdc-theme-text-primary-on-light, rgba(0, 0, 0, 0.87)); 69 | display: inline-block; 70 | position: relative; 71 | min-width: 64px; 72 | height: 36px; 73 | padding: 0 16px; 74 | border: none; 75 | border-radius: 2px; 76 | outline: none; 77 | background: transparent; 78 | font-size: 14px; 79 | font-weight: 500; 80 | line-height: 36px; 81 | text-align: center; 82 | text-transform: uppercase; 83 | vertical-align: middle; 84 | box-sizing: border-box; 85 | -webkit-appearance: none; 86 | -webkit-tap-highlight-color: transparent; 87 | box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); 88 | -webkit-transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); 89 | transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); 90 | will-change: box-shadow; 91 | min-width: 88px; 92 | margin-left: 1rem; 93 | cursor: pointer; 94 | background-color: #05bb1b; 95 | background-color: var(--mdc-theme-primary, #05bb1b); 96 | color: white; 97 | color: var(--mdc-theme-text-primary-on-primary, white); 98 | } 99 | 100 | .customDeleteBtn{ 101 | font-family: Roboto, sans-serif; 102 | -moz-osx-font-smoothing: grayscale; 103 | -webkit-font-smoothing: antialiased; 104 | font-size: 0.875rem; 105 | font-weight: 500; 106 | letter-spacing: 0.04em; 107 | line-height: 1.5rem; 108 | color: rgba(0, 0, 0, 0.87); 109 | color: var(--mdc-theme-text-primary-on-light, rgba(0, 0, 0, 0.87)); 110 | display: inline-block; 111 | position: relative; 112 | min-width: 64px; 113 | height: 36px; 114 | padding: 0 16px; 115 | border: none; 116 | border-radius: 2px; 117 | outline: none; 118 | background: transparent; 119 | font-size: 14px; 120 | font-weight: 500; 121 | line-height: 36px; 122 | text-align: center; 123 | text-transform: uppercase; 124 | vertical-align: middle; 125 | box-sizing: border-box; 126 | -webkit-appearance: none; 127 | -webkit-tap-highlight-color: transparent; 128 | box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); 129 | -webkit-transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); 130 | transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); 131 | will-change: box-shadow; 132 | min-width: 88px; 133 | margin-left: 1rem; 134 | cursor: pointer; 135 | background-color: #d81111; 136 | background-color: var(--mdc-theme-primary, #d81111); 137 | color: white; 138 | color: var(--mdc-theme-text-primary-on-primary, white); 139 | } 140 | -------------------------------------------------------------------------------- /.storybook/stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf, action } from '@kadira/storybook'; 3 | import TwoWayQuerybuilder from '../../src/TwoWayQuerybuilder'; 4 | import './index.css'; 5 | 6 | const config = { 7 | query: "((firstname >= 'Jack' AND lastName<'London') OR lastName<>'Smith')", 8 | }; 9 | 10 | const defaultFields = [ 11 | { name: 'firstName', operators: 'all', label: 'First Name', input: { type: 'text' } }, 12 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { type: 'text' } }, 13 | { name: 'age', operators: 'all', label: 'Age', input: { type: 'text' } }, 14 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { type: 'text' } }, 15 | ]; 16 | 17 | const validationFields = [ 18 | { name: 'firstName', operators: 'all', label: 'First Name', input: 19 | { type: 'text', errorText: 'Only letters allowed', pattern: new RegExp("[a-z]+", "gi") } }, 20 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { 21 | type: 'text', errorText: 'Only letters allowed', pattern: new RegExp("[a-z]+", "gi") } }, 22 | { name: 'age', operators: 'all', label: 'Age', input: { 23 | type: 'text', errorText: 'Only nubmers allowed', pattern: new RegExp('[0-9]+', 'gi') } }, 24 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { 25 | type: 'text', errorText: 'Only nubmers allowed', pattern: new RegExp('[0-9]+', 'gi') } 26 | }, 27 | ]; 28 | 29 | const changedFields = [ 30 | { name: 'firstName', operators: 'all', label: 'First Name', input: { type: 'text' } }, 31 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { 32 | type: 'select', 33 | options: [ 34 | { value: 'Smith', name: 'Smith' }, 35 | { value: 'London', name: 'London' }, 36 | ] } }, 37 | { name: 'age', operators: 'all', label: 'Age', 38 | input: { 39 | type: 'select', 40 | options: [ 41 | { value: '28', name: 'twenty eight' }, 42 | { value: '30', name: 'thirty' }, 43 | ] } }, 44 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { type: 'text' } }, 45 | ]; 46 | 47 | const styles = { 48 | primaryBtn: 'customPrimaryBtn', 49 | deleteBtn: 'customDeleteBtn', 50 | rule: 'rule', 51 | condition: 'condition', 52 | select: 'querySelect', 53 | input: 'queryInput', 54 | txtArea: 'queryText', 55 | }; 56 | 57 | const changedStyles = { 58 | styles, 59 | }; 60 | 61 | storiesOf('Query builder', module) 62 | .add('default view', () => ( 63 | 64 | )) 65 | .add('existing query', () => ( 66 | 67 | )) 68 | .add('changed input types', () => ( 69 | 70 | )) 71 | .add('custom styles', () => ( 72 | 73 | )) 74 | .add('validation', () => ( 75 | 76 | )); 77 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | // IMPORTANT 2 | // --------- 3 | // This is an auto generated file with React CDK. 4 | // Do not modify this file. 5 | // Use `.storybook/user/modify_webpack_config.js instead`. 6 | 7 | const path = require('path'); 8 | 9 | const config = { 10 | module: { 11 | loaders: [ 12 | { 13 | test: /\.css?$/, 14 | loaders: ['style', 'raw'], 15 | include: path.resolve(__dirname, '../'), 16 | }, 17 | { 18 | test: /\.json?$/, 19 | loaders: ['json'], 20 | include: path.resolve(__dirname, '../'), 21 | }, 22 | ], 23 | } 24 | }; 25 | 26 | module.exports = config; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vsicons.presets.angular": false 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to React Two Way Querybuilder Component 2 | 3 | We welcome your help to make this component better. This document will help to streamline the contributing process and save everyone's precious time. 4 | 5 | ## Development Setup 6 | 7 | This component has been setup with [React CDK](https://github.com/kadirahq/react-cdk). Refer [React CDK documentation](https://github.com/kadirahq/react-cdk)) to get started with the development. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Your Name. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Two Way Querybuilder 2 | 3 | A simple react component that lets you build queries dynamically on UI. Doesn't depend on any 3-d party libraries. 4 | 5 | ![image](https://github.com/Lefortov/react-two-way-querybuilder/blob/master/blob/builder.jpg) 6 | 7 | ## Installing 8 | 9 | ```bash 10 | npm i react-two-way-querybuilder --save 11 | ``` 12 | 13 | ## Using 14 | 15 | Two way query builder is flexible and configurable component with a set of possible options. 16 | 17 | Simple usage: 18 | 19 | ``` 20 | import React, { Component } from 'react'; 21 | import TwoWayQuerybuilder from 'react-two-way-querybuilder'; 22 | 23 | const fields = [ 24 | { name: 'firstName', operators: 'all', label: 'First Name', input: { type: 'text' } }, 25 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { type: 'text' } }, 26 | { name: 'age', operators: 'all', label: 'Age', input: { type: 'text' } }, 27 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { type: 'text' } }, 28 | ]; 29 | 30 | class App extends Component { 31 | 32 | handleChange(event) { 33 | console.log('query', event.query); 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default App; 44 | ``` 45 | 46 | ###Props: 47 | 48 | - **`fields`**: your fields used to build a query 49 | * name: name of the field that would be used in a query 50 | * label: how your field name would be shown in the dropdown 51 | * operators: remove this property or set to 'all' if you want to use all operators for this field, else you can limit them by passing the array of the allowed operators `['=', '<', '>']` 52 | * input: type of the input, possible options are: `text`, `textarea`, `select`. If you are using `select` input type pass options to the object in the following way: 53 | `input: {type: 'select', options: [{value: '1', name: 'one'}, {value: '2', name: 'two'}]}`. Also, it supports validation by passing `pattern` property with regexp pattern and 54 | `errorText` property for validation error message text. 55 | 56 | - **`onChange`**: pass here your function that will be called when data was changed 57 | - **`config`**: configuration object with possible options: 58 | * `query`: pass here prepared query, so UI will be built using it. 59 | * `operators`: array of operators, the default one is: 60 | ``` 61 | [ 62 | { operator: '=', label: '=' }, 63 | { operator: '<>', label: '<>' }, 64 | { operator: '<', label: '<' }, 65 | { operator: '>', label: '>' }, 66 | { operator: '>=', label: '>=' }, 67 | { operator: '<=', label: '<=' }, 68 | { operator: 'IS NULL', label: 'Null' }, 69 | { operator: 'IS NOT NULL', label: 'Not Null' }, 70 | { operator: 'IN', label: 'In' }, 71 | { operator: 'NOT IN', label: 'Not In' }, 72 | ] 73 | ``` 74 | * `combinators`: array of combinators, the default one is: 75 | ``` 76 | [ 77 | { combinator: 'AND', label: 'And' }, 78 | { combinator: 'OR', label: 'Or' }, 79 | { combinator: 'NOT', label: 'Not' }, 80 | ] 81 | ``` 82 | * `style`: use this object to redefine styles. Properties: 83 | * `primaryBtn`: used for primary button styles, 84 | * `deleteBtn`: delete button styles, 85 | * `rule`: rule styles, 86 | * `condition`: condition styles, 87 | * `select`: select styles, 88 | * `input`: input styles, 89 | * `txtArea`: text area styles :D 90 | * `error`: error message styling 91 | 92 | - **`buttonsText`**: text of the buttons, you can redefine it for multilanguage support or because you just want. By default used following text: 93 | * addRule: `'Add rule'`, 94 | * addGroup: `'Add group'`, 95 | * clear: `'Clear'`, 96 | * delete: `'Delete'` 97 | 98 | ## Samples 99 | 100 | Visit [DEMO](https://lefortov.github.io/react-two-way-querybuilder) storybook to take a look at basic usage cases: 101 | 102 | - **existing query**: 103 | ``` 104 | import React, { Component } from 'react'; 105 | import TwoWayQuerybuilder from 'react-two-way-querybuilder';; 106 | 107 | const fields = [ 108 | { name: 'firstName', operators: 'all', label: 'First Name', input: { type: 'text' } }, 109 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { type: 'text' } }, 110 | { name: 'age', operators: 'all', label: 'Age', input: { type: 'text' } }, 111 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { type: 'text' } }, 112 | ]; 113 | 114 | const config = { 115 | query: "((firstname='Jack' AND lastName='London') OR lastName='Smith')", 116 | }; 117 | 118 | class App extends Component { 119 | 120 | handleChange(event) { 121 | console.log('query', event.query); 122 | } 123 | 124 | render() { 125 | return ( 126 | 127 | ); 128 | } 129 | } 130 | 131 | export default App; 132 | ``` 133 | 134 | - **changed input types**: 135 | ``` 136 | import React, { Component } from 'react'; 137 | import TwoWayQuerybuilder from 'react-two-way-querybuilder';; 138 | 139 | const changedFields = [ 140 | { name: 'firstName', operators: 'all', label: 'First Name', input: { type: 'text' } }, 141 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { 142 | type: 'select', 143 | options: [ 144 | { value: 'Smith', name: 'Smith' }, 145 | { value: 'London', name: 'London' }, 146 | ] } }, 147 | { name: 'age', operators: 'all', label: 'Age', 148 | input: { 149 | type: 'select', 150 | options: [ 151 | { value: '28', name: 'twenty eight' }, 152 | { value: '30', name: 'thirty' }, 153 | ] } }, 154 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { type: 'text' } }, 155 | ]; 156 | 157 | class App extends Component { 158 | 159 | handleChange(event) { 160 | console.log('query', event.query); 161 | } 162 | 163 | render() { 164 | return ( 165 | 166 | ); 167 | } 168 | } 169 | 170 | export default App; 171 | ``` 172 | - **custom styles** 173 | ``` 174 | import React, { Component } from 'react'; 175 | import TwoWayQuerybuilder from 'react-two-way-querybuilder';; 176 | 177 | const fields = [ 178 | { name: 'firstName', operators: 'all', label: 'First Name', input: { type: 'text' } }, 179 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { type: 'text' } }, 180 | { name: 'age', operators: 'all', label: 'Age', input: { type: 'text' } }, 181 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { type: 'text' } }, 182 | ]; 183 | 184 | const styles = { 185 | primaryBtn: 'customPrimaryBtn', 186 | deleteBtn: 'customDeleteBtn', 187 | rule: 'rule', 188 | condition: 'condition', 189 | select: 'querySelect', 190 | input: 'queryInput', 191 | txtArea: 'queryText', 192 | }; 193 | 194 | const changedStyles = { 195 | styles, 196 | }; 197 | 198 | class App extends Component { 199 | 200 | handleChange(event) { 201 | console.log('query', event.query); 202 | } 203 | 204 | render() { 205 | return ( 206 | 207 | ); 208 | } 209 | } 210 | 211 | export default App; 212 | ``` 213 | - **validation** 214 | ``` 215 | import React, { Component } from 'react'; 216 | import TwoWayQuerybuilder from 'react-two-way-querybuilder';; 217 | 218 | const validationFields = [ 219 | { name: 'firstName', operators: 'all', label: 'First Name', input: { 220 | type: 'text', errorText: 'Only letters allowed', pattern: new RegExp("[a-z]+", "gi") } }, 221 | { name: 'lastName', operators: 'all', label: 'Last Name', input: { 222 | type: 'text', errorText: 'Only letters allowed', pattern: new RegExp("[a-z]+", "gi") } }, 223 | { name: 'age', operators: 'all', label: 'Age', input: { 224 | type: 'text', errorText: 'Only nubmers allowed', pattern: new RegExp('[0-9]+', 'gi') } }, 225 | { name: 'birthDate', operators: 'all', label: 'Birth date', input: { 226 | type: 'text', errorText: 'Only nubmers allowed', pattern: new RegExp('[0-9]+', 'gi') } 227 | }, 228 | ]; 229 | 230 | class App extends Component { 231 | 232 | handleChange(event) { 233 | console.log('query', event.query); 234 | } 235 | 236 | render() { 237 | return ( 238 | 239 | ); 240 | } 241 | } 242 | 243 | export default App; 244 | ``` 245 | 246 | ##License 247 | 248 | React-two-way-quierybuidler is MIT licensed 249 | -------------------------------------------------------------------------------- /blob/builder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lefortov/react-two-way-querybuilder/2aca623849002c7a22b3dbe87df6e77086c3266b/blob/builder.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-two-way-querybuilder", 3 | "version": "1.0.8", 4 | "description": "React Two Way Querybuilder Component", 5 | "author": "Vladimir Ostapenko", 6 | "homepage": "https://github.com/Lefortov/react-two-way-querybuilder", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Lefortov/react-two-way-querybuilder.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Lefortov/react-two-way-querybuilder/issues" 13 | }, 14 | "license": "MIT", 15 | "scripts": { 16 | "prepublish": ". ./.scripts/prepublish.sh", 17 | "lint": "eslint src", 18 | "lintfix": "eslint src --fix", 19 | "testonly": "mocha --require .scripts/mocha_runner src/**/tests/**/*.js", 20 | "test": "npm run lint && npm run testonly", 21 | "test-watch": "npm run testonly -- --watch --watch-extensions js", 22 | "storybook": "start-storybook -p 9010", 23 | "publish-storybook": "bash .scripts/publish_storybook.sh", 24 | "build-storybook": "build-storybook -c .storybook -o .out", 25 | "deploy-storybook": "storybook-to-ghpages" 26 | }, 27 | "devDependencies": { 28 | "@kadira/storybook": "^2.35.3", 29 | "@kadira/storybook-deployer": "^1.2.0", 30 | "babel-cli": "^6.14.0", 31 | "babel-core": "^6.14.0", 32 | "babel-eslint": "^6.1.2", 33 | "babel-loader": "^6.2.5", 34 | "babel-plugin-transform-runtime": "^6.15.0", 35 | "babel-polyfill": "^6.13.0", 36 | "babel-preset-es2015": "^6.5.0", 37 | "babel-preset-react": "^6.5.0", 38 | "babel-preset-react-app": "^0.2.1", 39 | "babel-preset-stage-2": "^6.5.0", 40 | "chai": "^3.5.0", 41 | "enzyme": "^2.2.0", 42 | "eslint": "^3.6.0", 43 | "eslint-config-airbnb": "^12.0.0", 44 | "eslint-plugin-import": "^1.16.0", 45 | "eslint-plugin-jsx-a11y": "^2.2.2", 46 | "eslint-plugin-react": "^6.3.0", 47 | "git-url-parse": "^6.0.1", 48 | "jsdom": "^9.5.0", 49 | "mocha": "^3.0.2", 50 | "raw-loader": "^0.5.1", 51 | "react": "^15.3.2", 52 | "react-addons-test-utils": "^15.3.2", 53 | "react-dom": "^15.3.2", 54 | "sinon": "^1.17.6" 55 | }, 56 | "peerDependencies": { 57 | "react": "^0.14.7 || ^15.0.0" 58 | }, 59 | "dependencies": { 60 | "babel-runtime": "^6.11.6", 61 | "prop-types": "^15.5.8" 62 | }, 63 | "main": "dist/index.js", 64 | "engines": { 65 | "npm": "^3.0.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Condition.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import TreeHelper from './helpers/TreeHelper'; 4 | import Rule from './Rule'; 5 | 6 | class Condition extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.treeHelper = new TreeHelper(this.props.data); 10 | this.node = this.treeHelper.getNodeByName(this.props.nodeName); 11 | this.state = { 12 | data: this.node, 13 | }; 14 | this.addRule = this.addRule.bind(this); 15 | this.addCondition = this.addCondition.bind(this); 16 | this.handleDelete = this.handleDelete.bind(this); 17 | this.handleChildUpdate = this.handleChildUpdate.bind(this); 18 | this.combinatorChange = this.combinatorChange.bind(this); 19 | this.styles = this.props.config.styles; 20 | } 21 | 22 | addRule() { 23 | const data = this.state.data; 24 | const nodeName = this.treeHelper.generateNodeName(this.state.data); 25 | data.rules.push({ 26 | field: this.props.fields[0].name, 27 | operator: this.props.config.operators[0].operator, 28 | value: '', 29 | nodeName }); 30 | this.setState({ data }); 31 | this.props.onChange(this.props.data); 32 | } 33 | 34 | addCondition() { 35 | const data = this.state.data; 36 | const nodeName = this.treeHelper.generateNodeName(this.state.data); 37 | data.rules.push({ 38 | combinator: this.props.config.combinators[0].combinator, 39 | nodeName, 40 | rules: [] }); 41 | this.setState({ data }); 42 | this.props.onChange(this.props.data); 43 | } 44 | 45 | handleDelete(nodeName) { 46 | this.treeHelper.removeNodeByName(nodeName); 47 | this.props.onChange(this.props.data); 48 | } 49 | 50 | handleChildUpdate() { 51 | const node = this.treeHelper.getNodeByName(this.props.nodeName); 52 | this.setState({ data: node }); 53 | this.props.onChange(this.props.data); 54 | } 55 | 56 | combinatorChange(event) { 57 | this.node.combinator = event.target.value; 58 | this.props.onChange(this.props.data); 59 | } 60 | 61 | render() { 62 | return ( 63 |
64 | 73 | 76 | 79 | {this.props.nodeName !== '1' 80 | ? 84 | : null} 85 | {this 86 | .state 87 | .data 88 | .rules 89 | .map((rule, index) => { 90 | if (rule.field) { 91 | return (); 100 | } else { 101 | return (); 109 | } 110 | })} 111 |
112 | ); 113 | } 114 | } 115 | 116 | Condition.propTypes = { 117 | buttonsText: PropTypes.object.isRequired, 118 | config: PropTypes.object.isRequired, 119 | data: PropTypes.object.isRequired, 120 | fields: PropTypes.array.isRequired, 121 | nodeName: PropTypes.string.isRequired, 122 | onChange: PropTypes.func, 123 | }; 124 | 125 | export default Condition; 126 | -------------------------------------------------------------------------------- /src/Rule.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import TreeHelper from './helpers/TreeHelper'; 4 | 5 | const defaultErrorMsg = 'Input value is not correct'; 6 | 7 | const isValueCorrect = (pattern, value) => { 8 | const newPattern = new RegExp(pattern); 9 | const match = newPattern.exec(value); 10 | return match === null; 11 | }; 12 | 13 | class Rule extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | this.getFieldByName = this.getFieldByName.bind(this); 17 | this.generateRuleObject = this.generateRuleObject.bind(this); 18 | this.onFieldChanged = this.onFieldChanged.bind(this); 19 | this.onOperatorChanged = this.onOperatorChanged.bind(this); 20 | this.onInputChanged = this.onInputChanged.bind(this); 21 | this.getInputTag = this.getInputTag.bind(this); 22 | this.handleDelete = this.handleDelete.bind(this); 23 | this.treeHelper = new TreeHelper(this.props.data); 24 | this.node = this.treeHelper.getNodeByName(this.props.nodeName); 25 | this.styles = this.props.styles; 26 | this.state = { 27 | currField: this.generateRuleObject(this.props.fields[0], this.node), 28 | validationError: false, 29 | }; 30 | } 31 | 32 | componentWillReceiveProps(nextProps) { 33 | this.node = this.treeHelper.getNodeByName(nextProps.nodeName); 34 | } 35 | 36 | onFieldChanged(event) { 37 | this.node.field = event.target.value; 38 | const field = this.getFieldByName(event.target.value); 39 | const rule = this.generateRuleObject(field, this.node); 40 | this.setState({ currField: rule }); 41 | this.props.onChange(); 42 | } 43 | 44 | onOperatorChanged(event) { 45 | this.node.operator = event.target.value; 46 | const field = this.getFieldByName(this.node.field); 47 | const rule = this.generateRuleObject(field, this.node); 48 | this.setState({ currField: rule }); 49 | this.props.onChange(); 50 | } 51 | 52 | onInputChanged(event) { 53 | const pattern = this.state.currField.input.pattern; 54 | if (pattern) { 55 | this.setState({ validationError: isValueCorrect(pattern, event.target.value) }); 56 | } 57 | this.node.value = event.target.value; 58 | const field = this.getFieldByName(this.node.field); 59 | const rule = this.generateRuleObject(field, this.node); 60 | this.setState({ currField: rule }); 61 | this.props.onChange(); 62 | } 63 | 64 | getFieldByName(name) { 65 | return this.props.fields.find(x => x.name === name); 66 | } 67 | 68 | getInputTag(inputType) { 69 | const errorText = this.state.currField.input.errorText; 70 | 71 | switch (inputType) { 72 | case 'textarea': return ( 73 |
74 |