├── .editorconfig ├── .gitignore ├── .npmignore ├── README.md ├── config ├── index.js ├── jest │ ├── fileMock.js │ └── styleMock.js ├── server.js └── webpack │ ├── _chunkPlugin.js │ ├── _externals.js │ ├── _extractCSS.js │ ├── _htmlPlugin.js │ ├── _loaders.dev.js │ ├── _loaders.prod.js │ ├── _plugins.js │ ├── _plugins.prod.js │ ├── _postCSS.js │ ├── _preLoaders.js │ ├── _resolve.js │ ├── docs.dev.js │ ├── docs.prod.js │ ├── lib.dev.js │ └── lib.prod.js ├── docs ├── app.cee4aa3055765d9d6528.js ├── index.html ├── style.css └── vendor.cee4aa3055765d9d6528.js ├── examples ├── components │ ├── App │ │ ├── index.jsx │ │ └── style.scss │ ├── Demo │ │ ├── index.jsx │ │ └── style.scss │ └── Editor │ │ ├── index.jsx │ │ └── style.scss ├── constants │ ├── accessor.json │ └── higherOrderRule.json ├── index.html └── index.jsx ├── package.json ├── src ├── components │ ├── Accessor │ │ ├── index.jsx │ │ └── style.scss │ ├── Any │ │ ├── index.jsx │ │ └── style.scss │ ├── HigherOrder │ │ ├── index.jsx │ │ └── style.scss │ ├── Input │ │ ├── index.jsx │ │ └── style.scss │ ├── Master │ │ ├── index.jsx │ │ └── style.scss │ └── SelectOperator │ │ └── index.jsx ├── index.jsx └── options.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | 5 | # Change these settings to your own preference 6 | indent_style = space 7 | indent_size = 2 8 | 9 | # We recommend you to keep these unchanged 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X - DS_Store 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Dependency Directory 9 | node_modules 10 | 11 | # Built Library 12 | dist 13 | 14 | # Coverage Reports 15 | coverage 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # OS X - DS_Store 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Dependency Directory 9 | node_modules 10 | 11 | # Coverage Reports 12 | coverage 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Json Logic 2 | 3 | [![NPM](https://nodei.co/npm/react-json-logic.png?downloads=true)](https://nodei.co/npm/react-json-logic/) 4 | 5 | Build and evaluate [JsonLogic](http://jsonlogic.com/) rules with React Components. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | # Yarn 11 | $ yarn add react-json-logic 12 | 13 | # NPM 14 | $ npm install react-json-logic --save 15 | ``` 16 | 17 | ## Usage 18 | There is a basic example on [examples folder](/examples/components/App.jsx), which is also live [here](https://altayaydemir.github.io/react-json-logic/). 19 | 20 | | Prop | Type | Default Value | Description | 21 | | :----: |:-------------:|:-----:| :------- | 22 | | **onChange*** | func | - | Onchange event, returns the latest structure of built json logic. | 23 | | **value** | object | { } | Initial value of the json logic expresison. | 24 | | **data** | object | { } | Data available for accessor fields. | 25 | 26 | ## Contribution 27 | This project has initialized from [react-component-starter](https://github.com/altayaydemir/react-component-starter), which contains detailed documentation about installation, development and build flow of the package. 28 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const config = { 4 | port: process.env.PORT || 8000, 5 | host: process.env.HOST || 'localhost', 6 | 7 | lib: { 8 | input: path.resolve(__dirname, '../src'), 9 | output: path.resolve(__dirname, '../dist'), 10 | }, 11 | 12 | docs: { 13 | input: path.resolve(__dirname, '../examples'), 14 | output: path.resolve(__dirname, '../docs'), 15 | }, 16 | }; 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /config/jest/fileMock.js: -------------------------------------------------------------------------------- 1 | // Return an empty string or other mock path to emulate the url that 2 | // webpack provides via the file-loader 3 | module.exports = ''; 4 | -------------------------------------------------------------------------------- /config/jest/styleMock.js: -------------------------------------------------------------------------------- 1 | // Returns an object to emulate css modules 2 | // https://facebook.github.io/jest/docs/tutorial-webpack.html#content 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /config/server.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const WebpackDevServer = require('webpack-dev-server'); 3 | const webpackConfig = require('../config/webpack/docs.dev'); 4 | const config = require('../config'); 5 | 6 | new WebpackDevServer(webpack(webpackConfig), { 7 | publicPath: webpackConfig.output.publicPath, 8 | hot: true, 9 | historyApiFallback: { 10 | index: webpackConfig.output.publicPath, 11 | }, 12 | stats: { 13 | colors: true, 14 | }, 15 | }).listen(config.port, config.host, (err) => { 16 | if (err) { 17 | console.log(err); 18 | } 19 | 20 | console.log(`Listening at ${config.host}:${config.port}`); 21 | }); 22 | -------------------------------------------------------------------------------- /config/webpack/_chunkPlugin.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = [ 4 | new webpack.optimize.CommonsChunkPlugin({ 5 | name: 'vendor', 6 | filename: '[name].[hash].js', 7 | minChunks: Infinity, 8 | }), 9 | ]; 10 | -------------------------------------------------------------------------------- /config/webpack/_externals.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | react: { 3 | root: 'React', 4 | commonjs2: 'react', 5 | commonjs: 'react', 6 | amd: 'react', 7 | }, 8 | 9 | 'react-dom': { 10 | root: 'ReactDOM', 11 | commonjs2: 'react-dom', 12 | commonjs: 'react-dom', 13 | amd: 'react-dom', 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /config/webpack/_extractCSS.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | 3 | module.exports = new ExtractTextPlugin('style.css', { allChunks: true }); 4 | -------------------------------------------------------------------------------- /config/webpack/_htmlPlugin.js: -------------------------------------------------------------------------------- 1 | const config = require('../'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = [ 6 | new HtmlWebpackPlugin({ 7 | template: path.join(config.docs.input, 'index.html'), 8 | filename: 'index.html', 9 | chunks: ['app', 'vendor'], 10 | }), 11 | ]; 12 | -------------------------------------------------------------------------------- /config/webpack/_loaders.dev.js: -------------------------------------------------------------------------------- 1 | const config = require('../'); 2 | const extractCSS = require('./_extractCSS'); 3 | 4 | module.exports = [ 5 | { 6 | test: /\.json$/, 7 | loader: 'json', 8 | }, 9 | { 10 | test: /\.jsx?$/, 11 | loader: 'babel', 12 | include: [ 13 | config.lib.input, 14 | config.docs.input, 15 | ], 16 | }, 17 | { 18 | test: /\.scss/, 19 | loader: extractCSS.extract( 20 | 'style', 21 | 'css-loader?modules=true&localIdentName=[folder]__[local]!postcss!sass?sourceMap' 22 | ), 23 | }, 24 | { 25 | test: /\.css/, 26 | loader: extractCSS.extract( 27 | 'style', 28 | 'css' 29 | ), 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /config/webpack/_loaders.prod.js: -------------------------------------------------------------------------------- 1 | const config = require('../'); 2 | const extractCSS = require('./_extractCSS'); 3 | 4 | module.exports = [ 5 | { 6 | test: /\.json$/, 7 | loader: 'json', 8 | }, 9 | { 10 | test: /\.jsx?$/, 11 | loader: 'babel', 12 | include: [ 13 | config.lib.input, 14 | config.docs.input, 15 | ], 16 | }, 17 | { 18 | test: /\.scss/, 19 | loader: extractCSS.extract( 20 | 'style', 21 | 'css-loader?modules=true&localIdentName=[folder]__[local]!postcss!sass' 22 | ), 23 | }, 24 | { 25 | test: /\.css/, 26 | loader: extractCSS.extract( 27 | 'style', 28 | 'css' 29 | ), 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /config/webpack/_plugins.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const extractCSS = require('./_extractCSS'); 3 | 4 | module.exports = [ 5 | extractCSS, 6 | 7 | new webpack.DefinePlugin({ 8 | 'process.env': { 9 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 10 | }, 11 | }), 12 | ]; 13 | -------------------------------------------------------------------------------- /config/webpack/_plugins.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = [ 4 | new webpack.optimize.UglifyJsPlugin({ 5 | minimize: true, 6 | comments: false, 7 | compress: { 8 | warnings: false, 9 | }, 10 | }), 11 | 12 | new webpack.optimize.OccurenceOrderPlugin(), 13 | new webpack.optimize.DedupePlugin(), 14 | ]; 15 | -------------------------------------------------------------------------------- /config/webpack/_postCSS.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ([ 2 | require('autoprefixer'), 3 | ]); 4 | -------------------------------------------------------------------------------- /config/webpack/_preLoaders.js: -------------------------------------------------------------------------------- 1 | const config = require('../'); 2 | 3 | module.exports = [ 4 | { 5 | test: /\.jsx?$/, 6 | loader: 'eslint-loader', 7 | exclude: [ 8 | /node_modules/, 9 | config.lib.output, 10 | config.docs.output, 11 | ], 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /config/webpack/_resolve.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extensions: ['', '.js', '.jsx'], 3 | }; 4 | -------------------------------------------------------------------------------- /config/webpack/docs.dev.js: -------------------------------------------------------------------------------- 1 | // Core 2 | const webpack = require('webpack'); 3 | 4 | // Constants & Configs 5 | const config = require('../'); 6 | const plugins = require('./_plugins'); 7 | const loaders = require('./_loaders.dev'); 8 | const preLoaders = require('./_preLoaders'); 9 | const resolve = require('./_resolve'); 10 | const htmlPlugin = require('./_htmlPlugin'); 11 | const postcss = require('./_postCSS'); 12 | 13 | // Main Config for Lib & Docs Development 14 | module.exports = { 15 | resolve, 16 | 17 | entry: { 18 | app: [ 19 | `webpack-dev-server/client?http://${config.host}:${config.port}`, 20 | 'webpack/hot/only-dev-server', 21 | 'react-hot-loader/patch', 22 | 'babel-polyfill', 23 | './examples/index.jsx', 24 | ], 25 | }, 26 | 27 | output: { 28 | path: config.docs.output, 29 | filename: 'index.js', 30 | publicPath: '/', 31 | }, 32 | 33 | module: { 34 | preLoaders, 35 | loaders, 36 | }, 37 | 38 | postcss, 39 | 40 | plugins: plugins.concat(htmlPlugin, [ 41 | new webpack.HotModuleReplacementPlugin(), 42 | new webpack.NoErrorsPlugin(), 43 | ]), 44 | }; 45 | -------------------------------------------------------------------------------- /config/webpack/docs.prod.js: -------------------------------------------------------------------------------- 1 | // Constants & Configs 2 | const config = require('../'); 3 | const plugins = require('./_plugins'); 4 | const productionPlugins = require('./_plugins.prod'); 5 | const chunkPlugin = require('./_chunkPlugin'); 6 | const loaders = require('./_loaders.prod'); 7 | const preLoaders = require('./_preLoaders'); 8 | const resolve = require('./_resolve'); 9 | const htmlPlugin = require('./_htmlPlugin'); 10 | const postcss = require('./_postCSS'); 11 | 12 | // Main Config for Lib & Docs Development 13 | module.exports = { 14 | debug: false, 15 | devtool: false, 16 | resolve, 17 | 18 | entry: { 19 | app: [ 20 | './examples/index.jsx', 21 | ], 22 | vendor: [ 23 | 'babel-polyfill', 24 | 'react', 25 | 'react-dom', 26 | ], 27 | }, 28 | 29 | output: { 30 | path: config.docs.output, 31 | filename: '[name].[hash].js', 32 | publicPath: './', 33 | }, 34 | 35 | module: { 36 | preLoaders, 37 | loaders, 38 | }, 39 | 40 | postcss, 41 | 42 | plugins: plugins.concat( 43 | productionPlugins, 44 | htmlPlugin, 45 | chunkPlugin 46 | ), 47 | }; 48 | -------------------------------------------------------------------------------- /config/webpack/lib.dev.js: -------------------------------------------------------------------------------- 1 | // Constants & Configs 2 | const config = require('../'); 3 | const name = require('../../package.json').name; 4 | const plugins = require('./_plugins'); 5 | const loaders = require('./_loaders.dev'); 6 | const preLoaders = require('./_preLoaders'); 7 | const resolve = require('./_resolve'); 8 | const externals = require('./_externals'); 9 | const postcss = require('./_postCSS'); 10 | 11 | // Main Config for Lib & Docs Development 12 | module.exports = { 13 | resolve, 14 | 15 | entry: './src/index.jsx', 16 | 17 | output: { 18 | path: config.lib.output, 19 | filename: 'index.js', 20 | libraryTarget: 'commonjs2', 21 | library: name, 22 | }, 23 | 24 | module: { 25 | preLoaders, 26 | loaders, 27 | }, 28 | 29 | postcss, 30 | 31 | externals, 32 | 33 | plugins, 34 | }; 35 | -------------------------------------------------------------------------------- /config/webpack/lib.prod.js: -------------------------------------------------------------------------------- 1 | // Constants & Configs 2 | const config = require('../'); 3 | const name = require('../../package.json').name; 4 | const plugins = require('./_plugins'); 5 | const productionPlugins = require('./_plugins.prod'); 6 | const loaders = require('./_loaders.prod'); 7 | const preLoaders = require('./_preLoaders'); 8 | const resolve = require('./_resolve'); 9 | const externals = require('./_externals'); 10 | const postcss = require('./_postCSS'); 11 | 12 | // Main Config for Lib & Docs Development 13 | module.exports = { 14 | debug: false, 15 | devtool: 'module-source-map', 16 | 17 | resolve, 18 | 19 | entry: './src/index.jsx', 20 | 21 | output: { 22 | path: config.lib.output, 23 | filename: 'index.js', 24 | libraryTarget: 'commonjs2', 25 | library: name, 26 | }, 27 | 28 | module: { 29 | preLoaders, 30 | loaders, 31 | }, 32 | 33 | postcss, 34 | 35 | externals, 36 | 37 | plugins: plugins.concat(productionPlugins), 38 | }; 39 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Json Logic 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body{margin:0;padding:0}.App__App{font-family:Helvetica,Arial,sans-serif}.App__Header{background:#222;color:#fff;padding:30px;text-align:center;margin-bottom:20px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.App__Header img{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:10px;margin-left:10px}.App__Header .App__ReactLogo{height:80px}.App__Header .App__JsonLogicLogo{height:65px}.App__Header .App__Baby{height:150px}.App__Content{width:720px;margin-left:auto;margin-right:auto}.Select{position:relative}.Select,.Select div,.Select input,.Select span{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.Select.is-disabled>.Select-control{background-color:#f9f9f9}.Select.is-disabled>.Select-control:hover{box-shadow:none}.Select.is-disabled .Select-arrow-zone{cursor:default;pointer-events:none;opacity:.35}.Select-control{background-color:#fff;border-color:#d9d9d9 #ccc #b3b3b3;border-radius:4px;border:1px solid #ccc;color:#333;cursor:default;display:table;border-spacing:0;border-collapse:separate;height:36px;outline:none;overflow:hidden;position:relative;width:100%}.Select-control:hover{box-shadow:0 1px 0 rgba(0,0,0,.06)}.Select-control .Select-input:focus{outline:none}.is-searchable.is-open>.Select-control{cursor:text}.is-open>.Select-control{border-bottom-right-radius:0;border-bottom-left-radius:0;background:#fff;border-color:#b3b3b3 #ccc #d9d9d9}.is-open>.Select-control>.Select-arrow{border-color:transparent transparent #999;border-width:0 5px 5px}.is-searchable.is-focused:not(.is-open)>.Select-control{cursor:text}.is-focused:not(.is-open)>.Select-control{border-color:#007eff;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 0 3px rgba(0,126,255,.1)}.Select--single>.Select-control .Select-value,.Select-placeholder{bottom:0;color:#aaa;left:0;line-height:34px;padding-left:10px;padding-right:10px;position:absolute;right:0;top:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value .Select-value-label,.has-value.Select--single>.Select-control .Select-value .Select-value-label{color:#333}.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label,.has-value.Select--single>.Select-control .Select-value a.Select-value-label{cursor:pointer;text-decoration:none}.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label:focus,.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label:hover,.has-value.Select--single>.Select-control .Select-value a.Select-value-label:focus,.has-value.Select--single>.Select-control .Select-value a.Select-value-label:hover{color:#007eff;outline:none;text-decoration:underline}.Select-input{height:34px;padding-left:10px;padding-right:10px;vertical-align:middle}.Select-input>input{width:100%;background:none transparent;border:0 none;box-shadow:none;cursor:default;display:inline-block;font-family:inherit;font-size:inherit;margin:0;outline:none;line-height:14px;padding:8px 0 12px;-webkit-appearance:none}.is-focused .Select-input>input{cursor:text}.has-value.is-pseudo-focused .Select-input{opacity:0}.Select-control:not(.is-searchable)>.Select-input{outline:none}.Select-loading-zone{cursor:pointer;display:table-cell;text-align:center}.Select-loading,.Select-loading-zone{position:relative;vertical-align:middle;width:16px}.Select-loading{-webkit-animation:Select-animation-spin .4s infinite linear;-o-animation:Select-animation-spin .4s infinite linear;animation:Select-animation-spin .4s infinite linear;height:16px;box-sizing:border-box;border-radius:50%;border:2px solid #ccc;border-right-color:#333;display:inline-block}.Select-clear-zone{-webkit-animation:Select-animation-fadeIn .2s;-o-animation:Select-animation-fadeIn .2s;animation:Select-animation-fadeIn .2s;color:#999;cursor:pointer;display:table-cell;position:relative;text-align:center;vertical-align:middle;width:17px}.Select-clear-zone:hover{color:#d0021b}.Select-clear{display:inline-block;font-size:18px;line-height:1}.Select--multi .Select-clear-zone{width:17px}.Select-arrow-zone{cursor:pointer;display:table-cell;position:relative;text-align:center;vertical-align:middle;width:25px;padding-right:5px}.Select-arrow{border-color:#999 transparent transparent;border-style:solid;border-width:5px 5px 2.5px;display:inline-block;height:0;width:0}.is-open .Select-arrow,.Select-arrow-zone:hover>.Select-arrow{border-top-color:#666}.Select--multi .Select-multi-value-wrapper{display:inline-block}.Select .Select-aria-only{display:inline-block;height:1px;width:1px;margin:-1px;clip:rect(0,0,0,0);overflow:hidden}@-webkit-keyframes Select-animation-fadeIn{0%{opacity:0}to{opacity:1}}@keyframes Select-animation-fadeIn{0%{opacity:0}to{opacity:1}}.Select-menu-outer{border-bottom-right-radius:4px;border-bottom-left-radius:4px;background-color:#fff;border:1px solid #ccc;border-top-color:#e6e6e6;box-shadow:0 1px 0 rgba(0,0,0,.06);box-sizing:border-box;margin-top:-1px;max-height:200px;position:absolute;top:100%;width:100%;z-index:1;-webkit-overflow-scrolling:touch}.Select-menu{max-height:198px;overflow-y:auto}.Select-option{box-sizing:border-box;background-color:#fff;color:#666;cursor:pointer;display:block;padding:8px 10px}.Select-option:last-child{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.Select-option.is-selected{background-color:#f5faff;background-color:rgba(0,126,255,.04);color:#333}.Select-option.is-focused{background-color:#ebf5ff;background-color:rgba(0,126,255,.08);color:#333}.Select-option.is-disabled{color:#ccc;cursor:default}.Select-noresults{box-sizing:border-box;color:#999;cursor:default;display:block;padding:8px 10px}.Select--multi .Select-input{vertical-align:middle;margin-left:10px;padding:0}.Select--multi.has-value .Select-input{margin-left:5px}.Select--multi .Select-value{background-color:#ebf5ff;background-color:rgba(0,126,255,.08);border-radius:2px;border:1px solid #c2e0ff;border:1px solid rgba(0,126,255,.24);color:#007eff;display:inline-block;font-size:.9em;line-height:1.4;margin-left:5px;margin-top:5px;vertical-align:top}.Select--multi .Select-value-icon,.Select--multi .Select-value-label{display:inline-block;vertical-align:middle}.Select--multi .Select-value-label{border-bottom-right-radius:2px;border-top-right-radius:2px;cursor:default;padding:2px 5px}.Select--multi a.Select-value-label{color:#007eff;cursor:pointer;text-decoration:none}.Select--multi a.Select-value-label:hover{text-decoration:underline}.Select--multi .Select-value-icon{cursor:pointer;border-bottom-left-radius:2px;border-top-left-radius:2px;border-right:1px solid #c2e0ff;border-right:1px solid rgba(0,126,255,.24);padding:1px 5px 3px}.Select--multi .Select-value-icon:focus,.Select--multi .Select-value-icon:hover{background-color:#d8eafd;background-color:rgba(0,113,230,.08);color:#0071e6}.Select--multi .Select-value-icon:active{background-color:#c2e0ff;background-color:rgba(0,126,255,.24)}.Select--multi.is-disabled .Select-value{background-color:#fcfcfc;border:1px solid #e3e3e3;color:#333}.Select--multi.is-disabled .Select-value-icon{cursor:not-allowed;border-right:1px solid #e3e3e3}.Select--multi.is-disabled .Select-value-icon:active,.Select--multi.is-disabled .Select-value-icon:focus,.Select--multi.is-disabled .Select-value-icon:hover{background-color:#fcfcfc}@keyframes Select-animation-spin{to{transform:rotate(1turn)}}@-webkit-keyframes Select-animation-spin{to{-webkit-transform:rotate(1turn)}}.Any__ChildrenControlButton{border:1px solid #ccc;background:#fff;box-shadow:none}.Any__ChildrenControlButton:hover{background:#e6e6e6}.Any__ChildrenControlButton:focus{outline:none}.Input__InputWrapper,.Input__SelectWrapper{display:inline-block}.Input__SelectWrapper{font-weight:700}.Input__InputWrapper{position:relative}.Input__InputWrapper input{position:absolute;height:22px;border-radius:2px;top:-26px;border:1px solid #ccc;padding:1px 10px}.Accessor__IteratorWrapper,.Accessor__SelectWrapper{display:inline-block}.HigherOrder__Wrapper{position:relative}.HigherOrder__FatArrow{position:absolute;font-weight:700;top:4px;left:4px}.HigherOrder__Child{padding-left:25px}.Master__Wrapper .SelectWrapper{display:inline-block}.Master__Wrapper .Select{font-size:12px;width:150px;display:inline-block}.Master__Wrapper .is-focused:not(.is-open)>.Select-control,.Master__Wrapper .is-focused>.Select-control{border-color:#151515;box-shadow:none}.Master__Wrapper .Select-placeholder{line-height:24px!important}.Master__Wrapper .Select-control{height:24px;border-radius:2px}.Master__Wrapper .Select-value{line-height:24px!important}.Master__Wrapper .Select-input{height:24px}.Master__Wrapper .Select-arrow-zone{padding-top:2px}.Master__Wrapper .Select-menu-outer{z-index:10}.Master__Wrapper .LogicInputWrapper{display:inline-block;width:150px}.Editor__Editor{border:1px solid #ddd;margin-bottom:10px}.Demo__Wrapper{border:1px solid #ddd;border-radius:2px;padding:10px 20px 20px;margin:15px auto 75px;font-size:14px}.Demo__Wrapper h3{text-align:center;margin:0 0 15px;font-size:18px}.Demo__Wrapper hr{opacity:.3;margin:20px auto}.Demo__Wrapper code{display:block;margin-bottom:20px} -------------------------------------------------------------------------------- /examples/components/App/index.jsx: -------------------------------------------------------------------------------- 1 | // Core 2 | import React from 'react'; 3 | 4 | // UI 5 | import GitHubForkRibbon from 'react-github-fork-ribbon'; 6 | import style from './style.scss'; 7 | 8 | // Examples 9 | import Demo from '../Demo'; 10 | 11 | // Predefined Rules & Accessor Data 12 | import accessor from '../../constants/accessor.json'; 13 | import higherOrderRule from '../../constants/higherOrderRule.json'; 14 | 15 | const App = () => ( 16 |
17 | 23 | Fork me on GitHub 24 | 25 | 26 |
27 | react 32 | 33 | jsonlogic 38 |
39 | 40 |
41 | 44 | 45 | 51 | 52 | 56 | 57 | 62 | 63 | 68 |
69 |
70 | ); 71 | 72 | export default App; 73 | -------------------------------------------------------------------------------- /examples/components/App/style.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | } 7 | 8 | .App { 9 | font-family: 'Helvetica', 'Arial', sans-serif; 10 | } 11 | 12 | .Header { 13 | background: #222222; 14 | color: white; 15 | padding: 30px; 16 | text-align: center; 17 | margin-bottom: 20px; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | 22 | img { 23 | display: inline-flex; 24 | margin-right: 10px; 25 | margin-left: 10px; 26 | } 27 | 28 | .ReactLogo { 29 | height: 80px; 30 | } 31 | 32 | .JsonLogicLogo { 33 | height: 65px; 34 | } 35 | 36 | .Baby { 37 | height: 150px; 38 | } 39 | } 40 | 41 | .Content { 42 | width: 720px; 43 | margin-left: auto; 44 | margin-right: auto; 45 | } 46 | -------------------------------------------------------------------------------- /examples/components/Demo/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import ReactJsonLogic, { applyLogic } from '../../../dist'; 3 | import '../../../dist/style.css'; 4 | 5 | import Editor from '../Editor'; 6 | import style from './style.scss'; 7 | 8 | // PropTypes 9 | const { bool, string, object, oneOfType } = PropTypes; 10 | const propTypes = { 11 | title: string.isRequired, 12 | value: oneOfType([object, string]), 13 | data: object, 14 | async: bool, 15 | }; 16 | 17 | const defaultProps = { 18 | value: {}, 19 | data: {}, 20 | async: false, 21 | }; 22 | 23 | class Demo extends Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | value: props.value, 28 | data: JSON.stringify(props.data, null, '\t'), 29 | result: 'Not Evaluated', 30 | }; 31 | } 32 | 33 | onLoad = () => { 34 | const value = JSON.parse('{"==":[{"and":[{"==":["1","1"]},{"===":["0","0"]}]},"1"]}'); 35 | this.setState({ value }); 36 | } 37 | 38 | onFieldValueChange = value => this.setState({ value }) 39 | 40 | onAccessorDataChange = data => this.setState({ data }) 41 | 42 | onEvaluate = () => this.setState({ 43 | result: applyLogic(this.state.value, JSON.parse(this.state.data)), 44 | }) 45 | 46 | render() { 47 | const { async, title } = this.props; 48 | const { value, data, result } = this.state; 49 | 50 | return ( 51 |
52 |

53 | {title} 54 |

55 | 56 | {async && 57 | 63 | } 64 | 65 | 70 | 71 |
72 | 73 |

Built Logic

74 | 75 | { 78 | try { 79 | return this.onFieldValueChange(JSON.parse(e)); 80 | } catch (err) { 81 | return ''; 82 | } 83 | }} 84 | /> 85 | 86 |
87 | 88 |

Data for Accessor Fields (Must be JSON)

89 | 90 | 94 | 95 | 101 | 102 |
103 | 104 | Result: {JSON.stringify(result)} 105 | 106 |
107 | ); 108 | } 109 | } 110 | 111 | Demo.propTypes = propTypes; 112 | Demo.defaultProps = defaultProps; 113 | 114 | export default Demo; 115 | -------------------------------------------------------------------------------- /examples/components/Demo/style.scss: -------------------------------------------------------------------------------- 1 | .Wrapper { 2 | border: 1px solid #ddd; 3 | border-radius: 2px; 4 | 5 | padding: 10px 20px 20px 20px; 6 | margin: 15px auto 75px auto; 7 | 8 | font-size: 14px; 9 | 10 | h3 { 11 | text-align: center; 12 | margin: 0 0 15px 0; 13 | font-size: 18px; 14 | } 15 | 16 | hr { 17 | opacity: 0.3; 18 | margin: 20px auto; 19 | } 20 | 21 | code { 22 | display: block; 23 | margin-bottom: 20px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/components/Editor/index.jsx: -------------------------------------------------------------------------------- 1 | // Core 2 | import React, { PropTypes } from 'react'; 3 | 4 | // UI 5 | import 'brace'; 6 | import AceEditor from 'react-ace'; 7 | 8 | import 'brace/mode/javascript'; 9 | import 'brace/mode/json'; 10 | import 'brace/theme/github'; 11 | 12 | import style from './style.scss'; 13 | 14 | // PropTypes 15 | const { string, func } = PropTypes; 16 | const propTypes = { 17 | value: string.isRequired, 18 | onChange: func.isRequired, 19 | }; 20 | 21 | const Editor = ({ value, onChange }) => ( 22 |
23 | 35 |
36 | ); 37 | 38 | Editor.propTypes = propTypes; 39 | 40 | export default Editor; 41 | -------------------------------------------------------------------------------- /examples/components/Editor/style.scss: -------------------------------------------------------------------------------- 1 | .Editor { 2 | border: 1px solid #ddd; 3 | margin-bottom: 10px; 4 | } 5 | -------------------------------------------------------------------------------- /examples/constants/accessor.json: -------------------------------------------------------------------------------- 1 | { 2 | "activity": { 3 | "answers": [{ 4 | "activityCategoryKey": "activity_category_2292027234", 5 | "answeredAt": "2017-01-18T09:51:33Z", 6 | "choiceKey": null, 7 | "choiceSetSectionKey": null, 8 | "comboItemKey": null, 9 | "questionKey": "order_question_8645829225", 10 | "type": "EntryAnswer", 11 | "value": "0" 12 | }, 13 | { 14 | "activityCategoryKey": "activity_category_2292027234", 15 | "answeredAt": "2017-01-18T09:51:33Z", 16 | "choiceKey": null, 17 | "choiceSetSectionKey": null, 18 | "comboItemKey": null, 19 | "questionKey": "order_question_8645829225", 20 | "type": "EntryAnswer", 21 | "value": "1" 22 | }] 23 | }, 24 | "category": { 25 | "actionType": "questionnaire", 26 | "answers": [{ 27 | "activityCategoryKey": "activity_category_2292027234", 28 | "answeredAt": "2017-01-18T09:51:33Z", 29 | "choiceKey": null, 30 | "choiceSetSectionKey": null, 31 | "comboItemKey": null, 32 | "questionKey": "order_question_8645829225", 33 | "type": "EntryAnswer", 34 | "value": "0" 35 | }, 36 | { 37 | "activityCategoryKey": "activity_category_2292027234", 38 | "answeredAt": "2017-01-18T09:51:33Z", 39 | "choiceKey": null, 40 | "choiceSetSectionKey": null, 41 | "comboItemKey": null, 42 | "questionKey": "order_question_8645829225", 43 | "type": "EntryAnswer", 44 | "value": "1" 45 | }], 46 | "key": "activity_category_2292027234" 47 | }, 48 | "question": { 49 | "answers": [ 50 | { 51 | "activityCategoryKey": "activity_category_Z/HoK4MD", 52 | "answeredAt": "2017-01-18T09:34:58Z", 53 | "choiceKey": null, 54 | "choiceSetSectionKey": null, 55 | "comboItemKey": null, 56 | "questionKey": "single_entry_question_877244225", 57 | "type": "EntryAnswer", 58 | "value": "gyub" 59 | }, 60 | { 61 | "activityCategoryKey": "activity_category_2292027234", 62 | "answeredAt": "2017-01-18T09:51:41Z", 63 | "choiceKey": null, 64 | "choiceSetSectionKey": null, 65 | "comboItemKey": null, 66 | "questionKey": "ratio_question_8476053142", 67 | "type": "EntryAnswer", 68 | "value": "44" 69 | } 70 | ], 71 | "key": "order_question_8645829225", 72 | "type": "OrderQuestion" 73 | }, 74 | "route": { 75 | "title": "rtu" 76 | }, 77 | "section": { 78 | "answers": [ 79 | { 80 | "activityCategoryKey": "activity_category_Z/HoK4MD", 81 | "answeredAt": "2017-01-18T09:34:58Z", 82 | "choiceKey": null, 83 | "choiceSetSectionKey": null, 84 | "comboItemKey": null, 85 | "questionKey": "single_entry_question_877244225", 86 | "type": "EntryAnswer", 87 | "value": "gyub" 88 | }, 89 | { 90 | "activityCategoryKey": "activity_category_2292027234", 91 | "answeredAt": "2017-01-18T09:51:41Z", 92 | "choiceKey": null, 93 | "choiceSetSectionKey": null, 94 | "comboItemKey": null, 95 | "questionKey": "ratio_question_8476053142", 96 | "type": "EntryAnswer", 97 | "value": "44" 98 | } 99 | ], 100 | "key": "choice_set_section_1812539428", 101 | "required": false 102 | }, 103 | "store": { 104 | "address": "address", 105 | "city": "Istanbul", 106 | "code": "8373517", 107 | "country": "Turkey", 108 | "district": "Besiktas", 109 | "hint": "8373517 BERKA--INONU 1 address, Besiktas, Istanbul, state, Turkey", 110 | "latitude": 41.390248, 111 | "longitude": 28.588807, 112 | "name": "BERKA--INONU 1", 113 | "state": "state", 114 | "storeType": "", 115 | "properties": { 116 | "kemal": "oo", 117 | "key_123": "kkk" 118 | } 119 | }, 120 | "visit": { 121 | "answers": [{ 122 | "activityCategoryKey": "activity_category_Z/HoK4MD", 123 | "answeredAt": "2017-01-18T09:34:58Z", 124 | "choiceKey": null, 125 | "choiceSetSectionKey": null, 126 | "comboItemKey": null, 127 | "questionKey": "single_entry_question_877244225", 128 | "type": "EntryAnswer", 129 | "value": "gyub" 130 | }, 131 | { 132 | "activityCategoryKey": "activity_category_2292027234", 133 | "answeredAt": "2017-01-18T09:51:41Z", 134 | "choiceKey": null, 135 | "choiceSetSectionKey": null, 136 | "comboItemKey": null, 137 | "questionKey": "ratio_question_8476053142", 138 | "type": "EntryAnswer", 139 | "value": "44" 140 | }], 141 | "auditorId": "mehmet", 142 | "sealed": false, 143 | "status": 0, 144 | "storeCode": "8373517", 145 | "visitStatus": "SUCCEEDED" 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /examples/constants/higherOrderRule.json: -------------------------------------------------------------------------------- 1 | { 2 | "!": [ 3 | { 4 | "!": [ 5 | { 6 | "some": [ 7 | { 8 | "=>": [ 9 | { 10 | "and": [ 11 | { 12 | "==": [ 13 | "activity_category_597958312", 14 | { 15 | "var": [ 16 | "activityCategoryKey" 17 | ] 18 | } 19 | ] 20 | }, 21 | { 22 | "==": [ 23 | "single_choice_question_551228875", 24 | { 25 | "var": [ 26 | "questionKey" 27 | ] 28 | } 29 | ] 30 | }, 31 | { 32 | "==": [ 33 | "choice_825813027", 34 | { 35 | "var": [ 36 | "choiceKey" 37 | ] 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | ] 44 | }, 45 | { 46 | "var": [ 47 | "visit.answers.value" 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Json Logic 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer } from 'react-hot-loader'; 4 | import App from './components/App'; 5 | 6 | // Initial Render 7 | render( 8 | 9 | 10 | , 11 | document.getElementById('app'), 12 | ); 13 | 14 | // Hot Reload Config 15 | if (module.hot) { 16 | module.hot.accept('./components/App', () => { 17 | const NextApp = require('./components/App').default; 18 | 19 | render( 20 | 21 | 22 | , 23 | document.getElementById('app'), 24 | ); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-json-logic", 3 | "version": "2.1.0", 4 | "description": "Build and evaluate JsonLogic rules with React Components.", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "dev": "concurrently \"yarn-or-npm run start\" \"yarn-or-npm run start:docs\"", 8 | "start": "webpack --watch --config config/webpack/lib.dev.js", 9 | "start:docs": "node config/server.js", 10 | "build": "yarn-or-npm run build:lib && yarn-or-npm run build:docs", 11 | "build:lib": "rimraf dist && NODE_ENV=production webpack --config config/webpack/lib.prod.js", 12 | "build:docs": "rimraf docs && NODE_ENV=production webpack --config config/webpack/docs.prod.js", 13 | "test": "jest --coverage", 14 | "test:watch": "jest --watch" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "react-json-logic", 19 | "json-logic", 20 | "json-logic-js", 21 | "json-logic-builder", 22 | "json-logic-ui" 23 | ], 24 | "author": "Altay Aydemir ", 25 | "license": "MIT", 26 | "homepage": "http://altayaydemir.github.io/react-json-logic", 27 | "repository": { 28 | "url": "https://github.com/altayaydemir/react-json-logic.git", 29 | "type": "git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/altayaydemir/react-json-logic/issues" 33 | }, 34 | "peerDependencies": { 35 | "react": ">=15.0", 36 | "react-dom": ">=15.0" 37 | }, 38 | "dependencies": { 39 | "brace": "0.9.1", 40 | "json-logic-js-enhanced": "1.1.0", 41 | "react-ace": "4.1.1", 42 | "react-select": "1.0.0-rc.2" 43 | }, 44 | "devDependencies": { 45 | "autoprefixer": "6.7.1", 46 | "babel-cli": "6.22.2", 47 | "babel-core": "6.22.1", 48 | "babel-eslint": "7.1.1", 49 | "babel-loader": "6.2.10", 50 | "babel-plugin-transform-object-rest-spread": "6.22.0", 51 | "babel-preset-es2015": "6.22.0", 52 | "babel-preset-react": "6.22.0", 53 | "babel-preset-stage-0": "6.22.0", 54 | "babel-register": "6.22.0", 55 | "concurrently": "3.1.0", 56 | "css-loader": "0.26.1", 57 | "enzyme": "2.7.1", 58 | "eslint": "3.14.1", 59 | "eslint-config-airbnb": "14.0.0", 60 | "eslint-loader": "1.6.1", 61 | "eslint-plugin-import": "2.2.0", 62 | "eslint-plugin-jsx-a11y": "3.0.2", 63 | "eslint-plugin-react": "6.9.0", 64 | "extract-text-webpack-plugin": "1.0.1", 65 | "html-webpack-plugin": "2.28.0", 66 | "jest": "18.1.0", 67 | "json-loader": "0.5.4", 68 | "lodash.isequal": "4.5.0", 69 | "node-sass": "4.4.0", 70 | "postcss-loader": "1.2.2", 71 | "react": "15.4.2", 72 | "react-addons-test-utils": "15.4.2", 73 | "react-dom": "15.4.2", 74 | "react-github-fork-ribbon": "0.4.4", 75 | "react-hot-loader": "3.0.0-beta.6", 76 | "rimraf": "2.5.4", 77 | "sass-loader": "4.1.1", 78 | "style-loader": "0.13.1", 79 | "webpack": "1.14.0", 80 | "webpack-dev-server": "1.16.2", 81 | "yarn-or-npm": "2.0.3" 82 | }, 83 | "eslintConfig": { 84 | "parser": "babel-eslint", 85 | "extends": "airbnb", 86 | "env": { 87 | "browser": true, 88 | "jest": true 89 | }, 90 | "rules": { 91 | "global-require": 0, 92 | "import/no-extraneous-dependencies": 0, 93 | "import/imports-first": 0, 94 | "no-param-reassign": 0, 95 | "react/forbid-prop-types": 0 96 | } 97 | }, 98 | "babel": { 99 | "presets": [ 100 | "react", 101 | "es2015", 102 | "stage-0" 103 | ], 104 | "plugins": [ 105 | "react-hot-loader/babel", 106 | "transform-object-rest-spread" 107 | ] 108 | }, 109 | "jest": { 110 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.jsx?$", 111 | "moduleFileExtensions": [ 112 | "js", 113 | "json", 114 | "jsx", 115 | "node" 116 | ], 117 | "collectCoverageFrom": [ 118 | "**/src/**" 119 | ], 120 | "moduleNameMapper": { 121 | "^.+\\.(css|scss)$": "/config/jest/styleMock.js", 122 | "^.+\\.(jpe?g|png|gif|ttf|eot|svg|md)$": "/config/jest/fileMock.js" 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/components/Accessor/index.jsx: -------------------------------------------------------------------------------- 1 | // Core 2 | import React, { Component, PropTypes } from 'react'; 3 | import Select from 'react-select'; 4 | 5 | // Helpers 6 | import isEqual from 'lodash.isequal'; 7 | import style from './style.scss'; 8 | 9 | // PropTypes 10 | const { string, object, func } = PropTypes; 11 | const propTypes = { 12 | onChange: func.isRequired, 13 | value: string, 14 | data: object, // @TODO: make this required and check presence on mount, throw error if empty 15 | }; 16 | 17 | const defaultProps = { 18 | value: '', 19 | data: {}, 20 | }; 21 | 22 | class Accessor extends Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | value: props.value, 27 | }; 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | const { value } = this.state; 32 | 33 | if (!isEqual(value, nextProps.value)) { 34 | this.setState({ 35 | value: nextProps.value, 36 | }); 37 | } 38 | } 39 | 40 | onChange = (value, level) => { 41 | let values = this.state.value.split('.').slice(0, level); 42 | 43 | values[level] = value; 44 | values = values.join('.'); 45 | 46 | this.props.onChange(values); 47 | } 48 | 49 | renderSelector = (data, level) => { 50 | const { value } = this.state; 51 | const splittedValue = value.split('.'); 52 | const levelValue = splittedValue[level] || ''; 53 | 54 | let iterator = null; 55 | 56 | if (Array.isArray(data)) { 57 | iterator = Object.keys(data[0]); 58 | } else if (data !== null && typeof data === 'object') { 59 | iterator = Object.keys(data); 60 | } 61 | 62 | if (levelValue !== '' && !iterator.some(item => item === levelValue)) { 63 | iterator = [levelValue, ...iterator]; 64 | } 65 | 66 | if (iterator) { 67 | return ( 68 |
69 |
70 | this.onChange(e.value, level)} 74 | options={iterator.map(item => ({ 75 | label: item, 76 | value: item, 77 | }))} 78 | promptTextCreator={label => `Create option: ${label}`} 79 | /> 80 |
81 | 82 | {this.renderSelector(data[levelValue], level + 1)} 83 |
84 | ); 85 | } 86 | 87 | return null; 88 | } 89 | 90 | render() { 91 | return ( 92 |
93 | {this.renderSelector(this.props.data, 0)} 94 |
95 | ); 96 | } 97 | } 98 | 99 | Accessor.propTypes = propTypes; 100 | Accessor.defaultProps = defaultProps; 101 | 102 | export default Accessor; 103 | -------------------------------------------------------------------------------- /src/components/Accessor/style.scss: -------------------------------------------------------------------------------- 1 | .IteratorWrapper, .SelectWrapper { 2 | display: inline-block; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Any/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Any Field 3 | * 4 | * This field is like a blueprint for whole structure. It's used for recursive rendering of 5 | * nested JSON Logic expression. Has a dropdown for selecting the type of children fields and 6 | * contains its children fields as child components. 7 | * 8 | * - onChange: Returns the latest part of the expression rendered by this component. Used by its 9 | * parent (and grand parents) to complete structure, part by part. 10 | * 11 | * - parent: Parent field, used for the selection of available operators (see src/options.js for 12 | * more detailed explanation) and extracting its children's value recursively. 13 | * 14 | * - value: Value of the field, passed from its parent. 15 | * 16 | * - data: Data available for accessor fields, gained from the parrent and will be passed to the 17 | * children fields. 18 | */ 19 | 20 | // Core 21 | import React, { PropTypes, Component } from 'react'; 22 | 23 | // Helpers 24 | import isEqual from 'lodash.isequal'; 25 | 26 | // UI 27 | import SelectOperator from '../SelectOperator'; 28 | import style from './style.scss'; 29 | 30 | // Constants 31 | import { FIELD_TYPES, OPERATORS } from '../../options'; 32 | 33 | // PropTypes 34 | const { string, any, object, func } = PropTypes; 35 | const propTypes = { 36 | onChange: func.isRequired, 37 | parent: string.isRequired, 38 | value: any, 39 | data: object, 40 | }; 41 | 42 | const defaultProps = { 43 | value: {}, 44 | data: {}, 45 | }; 46 | 47 | class Any extends Component { 48 | constructor(props) { 49 | super(props); 50 | this.state = this.onInitializeState(props); 51 | } 52 | 53 | componentWillReceiveProps(nextProps) { 54 | if (!isEqual(this.props.value, nextProps.value)) { 55 | this.setState(this.onInitializeState(nextProps)); 56 | } 57 | } 58 | 59 | /** 60 | * Initializes component state with respect to props. 61 | * 62 | * This method will be called on initial render (from constructor) and cases like async loading, 63 | * which may end up with change in value prop. 64 | */ 65 | onInitializeState = (props) => { 66 | const value = props.value; 67 | let field = 'value'; 68 | let selectedOperator = null; 69 | let fields = []; 70 | 71 | if (typeof value === 'object' && Object.keys(value).length > 0) { 72 | const firstElem = Object.keys(value)[0]; 73 | 74 | field = OPERATORS.some(operator => 75 | operator.signature === firstElem || operator.label === firstElem, 76 | ) ? firstElem : 'value'; 77 | } else if (typeof value === 'object') { 78 | field = ''; 79 | } 80 | 81 | selectedOperator = OPERATORS.find(operator => 82 | operator.signature === field || operator.label === field, 83 | ); 84 | 85 | if (selectedOperator) { 86 | fields = selectedOperator.fields; 87 | } 88 | 89 | // Insert Extra Fields 90 | if ( 91 | typeof value === 'object' && 92 | Object.keys(value).length > 0 && 93 | selectedOperator.fieldCount.min <= fields.length && 94 | selectedOperator.fieldCount.max > fields.length && 95 | value[field].length > fields.length 96 | ) { 97 | const extraFieldCount = value[field].length - fields.length; 98 | for (let i = 1; i <= extraFieldCount; i += 1) { 99 | fields = [...fields, FIELD_TYPES.ANY]; 100 | } 101 | } 102 | 103 | return { field, value, selectedOperator, fields }; 104 | } 105 | 106 | /** 107 | * Resets the content and type of its children. 108 | */ 109 | onFieldChange = (field) => { 110 | let value = {}; 111 | 112 | if (field === 'value') { 113 | value = ''; 114 | } else { 115 | value[field] = []; 116 | } 117 | 118 | this.props.onChange(value); 119 | }; 120 | 121 | /** 122 | * Updates its own state and emits changes to the parrent. 123 | */ 124 | onChildValueChange = (childValue, index) => { 125 | const field = this.state.field; 126 | let value = this.state.value; 127 | 128 | if (field === 'value') { 129 | value = childValue; 130 | } else { 131 | value[field][index] = childValue; 132 | } 133 | 134 | this.props.onChange(value); 135 | } 136 | 137 | /** 138 | * Used for selection of available child components. 139 | * Removes accessor field from the list if props.data is empty 140 | * Return value of this method will be the entry point of Select component. 141 | */ 142 | getAvailableOperators = () => { 143 | const { parent, data } = this.props; 144 | 145 | let operators = OPERATORS.filter(operator => 146 | !operator.notAvailableUnder.some(item => item === parent), 147 | ); 148 | 149 | if (Object.keys(data).length === 0) { 150 | operators = operators.filter(operator => operator.signature !== 'var'); 151 | } 152 | 153 | return operators; 154 | } 155 | 156 | addField = () => { 157 | this.setState({ fields: [...this.state.fields, FIELD_TYPES.ANY] }); 158 | } 159 | 160 | removeField = (index) => { 161 | const { value, field, fields } = this.state; 162 | 163 | fields.splice(index, 1); 164 | value[field].splice(index, 1); 165 | 166 | this.setState({ fields, value }, () => this.props.onChange(value)); 167 | } 168 | 169 | /** 170 | * Renders child component by checking its type, initial value. 171 | * Also passes onChange action and data (which will be consumed by Accessor field) to the child. 172 | */ 173 | renderChild = (childField, index) => { 174 | const { field, value, selectedOperator, fields } = this.state; 175 | const parent = field; 176 | const parentValue = value; 177 | const isRemovable = fields.length > selectedOperator.fieldCount.min; 178 | 179 | let childValue = ''; 180 | 181 | if (parent !== 'value') { 182 | childValue = parentValue[parent][index]; 183 | } else { 184 | childValue = parentValue; 185 | } 186 | 187 | const ChildComponent = childField.default; 188 | 189 | return ( 190 |
191 | {isRemovable && 192 | 200 | } 201 | 202 | this.onChildValueChange(val, index)} 206 | data={this.props.data} 207 | /> 208 |
209 | ); 210 | } 211 | 212 | render() { 213 | const { field, fields, selectedOperator } = this.state; 214 | let canAddMoreChildren = false; 215 | 216 | if (selectedOperator) { 217 | canAddMoreChildren = fields.length < selectedOperator.fieldCount.max; 218 | } 219 | 220 | return ( 221 |
222 | 227 | 228 | {canAddMoreChildren && 229 | 237 | } 238 | 239 | {selectedOperator && 240 |
241 | {fields.map(this.renderChild)} 242 |
243 | } 244 |
245 | ); 246 | } 247 | } 248 | 249 | Any.propTypes = propTypes; 250 | Any.defaultProps = defaultProps; 251 | 252 | export default Any; 253 | -------------------------------------------------------------------------------- /src/components/Any/style.scss: -------------------------------------------------------------------------------- 1 | .ChildrenControlButton { 2 | border: 1px solid #ccc; 3 | background: white; 4 | box-shadow: none; 5 | 6 | &:hover { 7 | background: darken(white, 10%); 8 | } 9 | 10 | &:focus { 11 | outline: none; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/HigherOrder/index.jsx: -------------------------------------------------------------------------------- 1 | // Core 2 | import React, { PropTypes, Component } from 'react'; 3 | 4 | // Helpers 5 | import isEqual from 'lodash.isequal'; 6 | 7 | // UI 8 | import Any from '../Any'; 9 | import style from './style.scss'; 10 | 11 | // PropTypes 12 | const { any, func, object, string } = PropTypes; 13 | const propTypes = { 14 | data: object, 15 | parent: string.isRequired, 16 | value: any, 17 | onChange: func.isRequired, 18 | }; 19 | 20 | const defaultProps = { 21 | data: {}, 22 | value: '', 23 | }; 24 | 25 | class HigherOrder extends Component { 26 | constructor(props) { 27 | super(props); 28 | this.state = { 29 | value: props.value, 30 | }; 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | if (!isEqual(this.props.value, nextProps.value)) { 35 | this.setState({ 36 | value: nextProps.value, 37 | }); 38 | } 39 | } 40 | 41 | onChange = (value) => { 42 | const newValue = { 43 | '=>': [value], 44 | }; 45 | 46 | this.setState({ value: newValue }, () => this.props.onChange(newValue)); 47 | } 48 | 49 | render() { 50 | const { parent, data } = this.props; 51 | const { value } = this.state; 52 | const firstElemOfValue = value['=>'] ? value['=>'][0] : {}; 53 | 54 | return ( 55 |
56 |
57 | {'=>'} 58 |
59 | 60 |
61 | 67 |
68 |
69 | ); 70 | } 71 | } 72 | 73 | HigherOrder.propTypes = propTypes; 74 | HigherOrder.defaultProps = defaultProps; 75 | 76 | export default HigherOrder; 77 | -------------------------------------------------------------------------------- /src/components/HigherOrder/style.scss: -------------------------------------------------------------------------------- 1 | .Wrapper { 2 | position: relative; 3 | } 4 | 5 | .FatArrow { 6 | position: absolute; 7 | font-weight: bold; 8 | top: 4px; 9 | left: 4px; 10 | } 11 | 12 | .Child { 13 | padding-left: 25px; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Input/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Input Field 3 | * 4 | * This can be considered as a free entry field which contains logic about the type of its content. 5 | * 6 | * - onChange: Returns the latest part of the expression rendered by this component. Used by its 7 | * parent (and grand parents) to complete structure, part by part. 8 | * 9 | * - value: Value of the field, passed from its parent. 10 | * 11 | * - type: Type of the field, will be determined with respect to the `typeof` result of the value. 12 | */ 13 | 14 | // Core 15 | import React, { Component, PropTypes } from 'react'; 16 | import Select from 'react-select'; 17 | 18 | // Helpers 19 | import isEqual from 'lodash.isequal'; 20 | 21 | // UI 22 | import style from './style.scss'; 23 | 24 | // Constants 25 | const INPUT_TYPES = ['text', 'number']; // @TODO: Add Date! 26 | 27 | // Helpers 28 | const isNumeric = value => typeof value === 'number'; 29 | const getType = (value, defaultType) => { 30 | if (isNumeric(value)) { 31 | return 'number'; 32 | } 33 | 34 | return defaultType; 35 | }; 36 | 37 | // PropTypes 38 | const { any, string, func } = PropTypes; 39 | const propTypes = { 40 | name: string, 41 | onChange: func.isRequired, 42 | value: any, 43 | type: string, 44 | }; 45 | 46 | const defaultProps = { 47 | name: '', 48 | type: INPUT_TYPES[0], 49 | value: '', 50 | }; 51 | 52 | class Input extends Component { 53 | constructor(props) { 54 | super(props); 55 | this.state = { 56 | value: props.value, 57 | type: getType(props.value, props.type), 58 | }; 59 | } 60 | 61 | componentWillReceiveProps(nextProps) { 62 | const { value, type } = this.props; 63 | 64 | if (!isEqual(value, nextProps.value) || !isEqual(type, nextProps.type)) { 65 | this.setState({ 66 | value: nextProps.value, 67 | type: getType(nextProps.value, nextProps.type), 68 | }); 69 | } 70 | } 71 | 72 | onTypeChange = (e) => { 73 | const type = e.value; 74 | let { value } = this.state; 75 | 76 | if (type === 'number') { 77 | value = parseFloat(value); 78 | } else { 79 | value = value.toString(); 80 | } 81 | 82 | this.setState({ type }, () => this.props.onChange(value)); 83 | } 84 | 85 | onValueChange = (e) => { 86 | const { type } = this.state; 87 | let value = e.target.value; 88 | 89 | if (type === 'number') { 90 | value = parseFloat(value); 91 | } 92 | 93 | this.props.onChange(value); 94 | } 95 | 96 | render() { 97 | const { name } = this.props; 98 | const { type, value } = this.state; 99 | 100 | return ( 101 |
102 |
103 | 121 |
122 |
123 | ); 124 | } 125 | } 126 | 127 | Input.propTypes = propTypes; 128 | Input.defaultProps = defaultProps; 129 | 130 | export default Input; 131 | -------------------------------------------------------------------------------- /src/components/Input/style.scss: -------------------------------------------------------------------------------- 1 | .SelectWrapper, .InputWrapper { 2 | display: inline-block; 3 | } 4 | 5 | .SelectWrapper { 6 | font-weight: bold; 7 | } 8 | 9 | .InputWrapper { 10 | position: relative; 11 | 12 | input { 13 | position: absolute; 14 | height: 22px; 15 | border-radius: 2px; 16 | top: -26px; 17 | border: 1px solid #ccc; 18 | padding: 1px 10px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Master/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Master Field 3 | * 4 | * Renders one `Any` field and expects onChange, value and data props to consturct a logical 5 | * recursion for rendering the rest of the JSON Logic Expression. 6 | * 7 | * - onChange: Returns the latest expression. 8 | * - value: Initial value of the json logic expresison. 9 | * - data: Data available for accessor fields. 10 | */ 11 | 12 | // Core 13 | import React, { PropTypes, Component } from 'react'; 14 | 15 | // Helpers 16 | import isEqual from 'lodash.isequal'; 17 | 18 | // UI 19 | import Any from '../Any'; 20 | import style from './style.scss'; 21 | 22 | // PropTypes 23 | const { func, object, string, oneOfType } = PropTypes; 24 | const propTypes = { 25 | onChange: func.isRequired, 26 | value: object, 27 | data: oneOfType([object, string]), 28 | }; 29 | 30 | const defaultProps = { 31 | value: {}, 32 | data: {}, 33 | }; 34 | 35 | class JsonLogicBuilder extends Component { 36 | constructor(props) { 37 | super(props); 38 | this.state = { 39 | value: props.value, 40 | }; 41 | } 42 | 43 | componentWillReceiveProps(nextProps) { 44 | if (!isEqual(this.props.value, nextProps.value)) { 45 | this.setState({ value: nextProps.value }); 46 | } 47 | } 48 | 49 | onChange = value => this.setState({ value }, () => this.props.onChange(value)) 50 | 51 | parseData = (data) => { 52 | if (typeof data !== 'object') { 53 | try { 54 | return JSON.parse(data); 55 | } catch (e) { 56 | return {}; 57 | } 58 | } 59 | 60 | return data; 61 | } 62 | 63 | render() { 64 | return ( 65 |
66 | 72 |
73 | ); 74 | } 75 | } 76 | 77 | JsonLogicBuilder.propTypes = propTypes; 78 | JsonLogicBuilder.defaultProps = defaultProps; 79 | 80 | export default JsonLogicBuilder; 81 | -------------------------------------------------------------------------------- /src/components/Master/style.scss: -------------------------------------------------------------------------------- 1 | .Wrapper { 2 | $selectBorder: #151515; 3 | 4 | :global { 5 | .SelectWrapper { 6 | display: inline-block; 7 | } 8 | 9 | .Select { 10 | font-size: 12px; 11 | width: 150px; 12 | display: inline-block; 13 | } 14 | 15 | .is-focused > .Select-control { 16 | border-color: $selectBorder; 17 | box-shadow: none; 18 | } 19 | 20 | .is-focused:not(.is-open) > .Select-control { 21 | border-color: $selectBorder; 22 | box-shadow: none; 23 | } 24 | 25 | .Select-placeholder { 26 | line-height: 24px!important; 27 | } 28 | 29 | .Select-control { 30 | height: 24px; 31 | border-radius: 2px; 32 | } 33 | 34 | .Select-value { 35 | line-height: 24px!important; 36 | } 37 | 38 | .Select-input { 39 | height: 24px; 40 | } 41 | 42 | .Select-arrow-zone { 43 | padding-top: 2px; 44 | } 45 | 46 | .Select-menu-outer { 47 | z-index: 10; 48 | } 49 | 50 | .LogicInputWrapper { 51 | display: inline-block; 52 | width: 150px; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/SelectOperator/index.jsx: -------------------------------------------------------------------------------- 1 | // Core 2 | import React, { PropTypes } from 'react'; 3 | import Select from 'react-select'; 4 | 5 | // PropTypes 6 | const { string, array, func } = PropTypes; 7 | const propTypes = { 8 | value: string, 9 | options: array.isRequired, 10 | onChange: func.isRequired, 11 | }; 12 | 13 | const defaultProps = { 14 | value: '', 15 | }; 16 | 17 | const SelectOperator = ({ value, options, onChange }) => ( 18 |
19 |