├── .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 | [](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 |

32 |
33 |

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 |
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 |
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 |
113 |
114 |
115 |
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 |
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 |
29 | );
30 |
31 | SelectOperator.propTypes = propTypes;
32 | SelectOperator.defaultProps = defaultProps;
33 |
34 | export default SelectOperator;
35 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | // Global Dependencies
2 | import 'react-select/dist/react-select.css';
3 | import jsonLogic from 'json-logic-js-enhanced';
4 |
5 | // Local
6 | import ReactJsonLogic from './components/Master';
7 |
8 | export const applyLogic = jsonLogic.apply;
9 |
10 | export default ReactJsonLogic;
11 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Field Types Available for Usage to Build JSON Logic Expressions
3 | *
4 | * ANY: Consists of dropdown and selects its children fields.
5 | * Input: Basic HTML input with type selection.
6 | * Accessor: Data accessor field ("var" operator in JSON Logic)
7 | */
8 | export const FIELD_TYPES = {
9 | ANY: require('./components/Any'),
10 | INPUT: require('./components/Input'),
11 | ACCESSOR: require('./components/Accessor'),
12 | HIGHER_ORDER: require('./components/HigherOrder'),
13 | };
14 |
15 | /**
16 | * Operators Available for Usage to Build JSON Logic Expressions.
17 | *
18 | * - signature
19 | * Signature of the operator.
20 | *
21 | * - label
22 | * Visible name of the operator.
23 | *
24 | * - fields
25 | * Child fields that operator's structure constists of.
26 | *
27 | * - notAvailableUnder
28 | * Array of operators that wont be able to render this operator as its child field.
29 | */
30 | export const OPERATORS = [
31 | {
32 | type: 'Value Field',
33 | signature: 'value',
34 | label: 'value',
35 | fields: [FIELD_TYPES.INPUT],
36 | notAvailableUnder: ['master', 'or', 'and'],
37 | fieldCount: {
38 | min: 1,
39 | max: 1,
40 | },
41 | },
42 | {
43 | type: 'Higher Order',
44 | signature: 'some',
45 | label: 'some',
46 | fields: [FIELD_TYPES.HIGHER_ORDER, FIELD_TYPES.ANY],
47 | notAvailableUnder: [],
48 | fieldCount: {
49 | min: 1,
50 | max: 10,
51 | },
52 | },
53 | {
54 | type: 'Higher Order',
55 | signature: 'every',
56 | label: 'every',
57 | fields: [FIELD_TYPES.HIGHER_ORDER, FIELD_TYPES.ANY],
58 | notAvailableUnder: [],
59 | fieldCount: {
60 | min: 1,
61 | max: 10,
62 | },
63 | },
64 | {
65 | type: 'Higher Order',
66 | signature: 'map',
67 | label: 'map',
68 | fields: [FIELD_TYPES.HIGHER_ORDER, FIELD_TYPES.ANY],
69 | notAvailableUnder: [],
70 | fieldCount: {
71 | min: 1,
72 | max: 10,
73 | },
74 | },
75 | {
76 | type: 'Higher Order',
77 | signature: 'filter',
78 | label: 'filter',
79 | fields: [FIELD_TYPES.HIGHER_ORDER, FIELD_TYPES.ANY],
80 | notAvailableUnder: [],
81 | fieldCount: {
82 | min: 1,
83 | max: 10,
84 | },
85 | },
86 | {
87 | type: 'Accessor',
88 | signature: 'var',
89 | label: 'accessor',
90 | fields: [FIELD_TYPES.ACCESSOR],
91 | notAvailableUnder: ['master'],
92 | fieldCount: {
93 | min: 1,
94 | max: 1,
95 | },
96 | },
97 | {
98 | type: 'Statement',
99 | signature: 'or',
100 | label: 'or',
101 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
102 | notAvailableUnder: [],
103 | fieldCount: {
104 | min: 2,
105 | max: 10,
106 | },
107 | },
108 | {
109 | type: 'Statement',
110 | signature: 'and',
111 | label: 'and',
112 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
113 | notAvailableUnder: [],
114 | fieldCount: {
115 | min: 2,
116 | max: 10,
117 | },
118 | },
119 | {
120 | type: 'Logical',
121 | signature: '===',
122 | label: '===',
123 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
124 | notAvailableUnder: [],
125 | fieldCount: {
126 | min: 2,
127 | max: 2,
128 | },
129 | },
130 | {
131 | type: 'Logical',
132 | signature: '==',
133 | label: '==',
134 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
135 | notAvailableUnder: [],
136 | fieldCount: {
137 | min: 2,
138 | max: 2,
139 | },
140 | },
141 | {
142 | type: 'Logical',
143 | signature: '!=',
144 | label: '!=',
145 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
146 | notAvailableUnder: [],
147 | fieldCount: {
148 | min: 2,
149 | max: 2,
150 | },
151 | },
152 | {
153 | type: 'Logical',
154 | signature: '!==',
155 | label: '!==',
156 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
157 | notAvailableUnder: [],
158 | fieldCount: {
159 | min: 2,
160 | max: 2,
161 | },
162 | },
163 | {
164 | type: 'Logical',
165 | signature: '!',
166 | label: '!',
167 | fields: [FIELD_TYPES.ANY],
168 | notAvailableUnder: [],
169 | fieldCount: {
170 | min: 1,
171 | max: 1,
172 | },
173 | },
174 | {
175 | type: 'Numeric',
176 | signature: '<=',
177 | label: '<=',
178 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
179 | notAvailableUnder: [],
180 | fieldCount: {
181 | min: 2,
182 | max: 2,
183 | },
184 | },
185 | {
186 | type: 'Numeric',
187 | signature: '>=',
188 | label: '>=',
189 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
190 | notAvailableUnder: [],
191 | fieldCount: {
192 | min: 2,
193 | max: 2,
194 | },
195 | },
196 | {
197 | type: 'Numeric',
198 | signature: '<',
199 | label: '<',
200 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
201 | notAvailableUnder: [],
202 | fieldCount: {
203 | min: 2,
204 | max: 2,
205 | },
206 | },
207 | {
208 | type: 'Numeric',
209 | signature: '>',
210 | label: '>',
211 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
212 | notAvailableUnder: [],
213 | fieldCount: {
214 | min: 2,
215 | max: 2,
216 | },
217 | },
218 | {
219 | type: 'Numeric',
220 | signature: 'Between',
221 | label: '<=',
222 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY, FIELD_TYPES.ANY],
223 | notAvailableUnder: [],
224 | fieldCount: {
225 | min: 3,
226 | max: 3,
227 | },
228 | },
229 | {
230 | type: 'Arithmetic',
231 | signature: '+',
232 | label: '+',
233 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
234 | notAvailableUnder: ['master'],
235 | fieldCount: {
236 | min: 1,
237 | max: 100,
238 | },
239 | },
240 | {
241 | type: 'Arithmetic',
242 | signature: '-',
243 | label: '-',
244 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
245 | notAvailableUnder: ['master'],
246 | fieldCount: {
247 | min: 1,
248 | max: 2,
249 | },
250 | },
251 | {
252 | type: 'Arithmetic',
253 | signature: '*',
254 | label: '*',
255 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
256 | notAvailableUnder: ['master'],
257 | fieldCount: {
258 | min: 2,
259 | max: 100,
260 | },
261 | },
262 | {
263 | type: 'Arithmetic',
264 | signature: '/',
265 | label: '/',
266 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
267 | notAvailableUnder: ['master'],
268 | fieldCount: {
269 | min: 2,
270 | max: 2,
271 | },
272 | },
273 | {
274 | type: 'Arithmetic',
275 | signature: '%',
276 | label: '%',
277 | fields: [FIELD_TYPES.ANY, FIELD_TYPES.ANY],
278 | notAvailableUnder: ['master'],
279 | fieldCount: {
280 | min: 2,
281 | max: 2,
282 | },
283 | },
284 | ];
285 |
--------------------------------------------------------------------------------