Code Monkey home page Code Monkey logo

react-jsonschema-form-conditionals's Introduction

Build Status Coverage Status npm version

Form with conditionals

This project extends react-jsonschema-form with conditional logic, which allow to have more complicated logic expressed and controlled with JSON schema. This is primarily useful for complicated schemas with extended business logic, which are suspect to changes and need to be manageable and changeable without modifying running application.

If you need simple rule logic, that does not change a lot, you can use original mozilla project, by following examples like https://jsfiddle.net/69z2wepo/68259/

The project is done to be fully compatible with mozilla, without imposing additional limitations.

Features

Installation

Install react-jsonschema-form-conditionals by running:

npm install --s react-jsonschema-form-conditionals

Usage

The simplest example of using react-jsonschema-form-conditionals

import applyRules from 'react-jsonschema-form-conditionals';
import Engine from 'json-rules-engine-simplified';
import Form from "react-jsonschema-form";

...

const rules = [{
    ...
}];

let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);

ReactDOM.render(
  <FormWithConditionals .../>,
  document.querySelector('#app')
);

To show case uses for this library we'll be using simple registration schema example

import applyRules from 'react-jsonschema-form-conditionals';
import Form from "react-jsonschema-form";

let schema = {
  definitions: {
    hobby: {
      type: "object",
      properties: {
        name: { type: "string" },
        durationInMonth: { "type": "integer" },
      }
    }
  },
  title: "A registration form",
  description: "A simple form example.",
  type: "object",
  required: [
    "firstName",
    "lastName"
  ],
  properties: {
    firstName: {
      type: "string",
      title: "First name"
    },
    lastName: {
      type: "string",
      title: "Last name"
    },
    age: {
      type: "integer",
      title: "Age",
    },
    bio: {
      type: "string",
      title: "Bio",
    },
    country: {
      type: "string",
      title: "Country"
    },
    state: {
      type: "string",
      title: "State"
    },
    zip: {
      type: "string",
      title: "ZIP"
    },
    password: {
      type: "string",
      title: "Password",
      minLength: 3
    },
    telephone: {
      type: "string",
      title: "Telephone",
      minLength: 10
    },
    hobbies: {
        type: "array",
        items: { "$ref": "#/definitions/hobby" }
    }
  }
}

let rules = [{
    ...
}]

let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);

render((
  <FormWithConditionals />
), document.getElementById("app"));

Conditionals functionality is build using 2 things

Rules engine responsibility is to trigger events, action mechanism performs needed actions on the requests.

Rules engine

Project supports 2 rules engines out of the box:

In order to use either of those, you need to specify Engine in applyRules configuration.

For example:

To use Simplified Json Rules Engine, you can do following:

import applyRules from 'react-jsonschema-form-conditionals';
import Form from "react-jsonschema-form";

import Engine from 'json-rules-engine-simplified';

...

let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);

ReactDOM.render(
  <FormWithConditionals />,
  document.querySelector('#app')
);

To use Json Rules Engine, is almost the same:

import applyRules from 'react-jsonschema-form-conditionals';
import Engine from 'json-rules-engine';
import Form from "react-jsonschema-form";

// ...

let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);

ReactDOM.render(
  <FormWithConditionals />,
  document.querySelector('#app')
);

Extending rules engine

If non of the provided engines satisfies, your needs, you can implement your own Engine which should comply to following:

class Engine {
  constructor(rules, schema) {
  }
  addRule = (rule) => {
  }
  run = (formData) => {
    return Promise[Event]
  }
}

Original rules and schema is used as a parameter for a factory call, in order to be able to have additional functionality, such as rules to schema compliance validation, like it's done in Simplified Json Rules Engine](https://github.com/RxNT/json-rules-engine-simplified)

Schema action mechanism

Rules engine emits events, which are expected to have a type and params field, type is used to distinguish action that is needed, params are used as input for that action:

{
  "type": "remove",
  "params": {
    "field": "name"
  }
}

By default action mechanism defines a supported set of rules, which you can extend as needed:

  • remove removes a field or set of fields from schema and uiSchema
  • require makes a field or set of fields required

Remove action

If you want to remove a field, your configuration should look like this:

    {
      "conditions": { },
      "event": {
        "type": "remove",
        "params": {
          "field": "password"
        }
      }
    }

When condition is met, password will be removed from both schema and uiSchema.

In case you want to remove multiple fields name, password, rule should look like this:

    {
      "conditions": { },
      "event": {
        "type": "remove",
        "params": {
          "field": [ "name", "password" ]
        }
      }
    }

To remove nested schema properties, use json dot notation. e.g. For schema object:

  {
    "type": "object",
    "properties": {
      "someParentWrapper": {
        "type": "object",
        "properties": {
          "booleanValA": {
            "type": "boolean",
            "title": "Some boolean input"
          },
          "booleanValB": {
            "type": "boolean",
            "title": "Another boolean input"
          }
        }
      }
    }
  }

You can remove the nested booleanValA or booleanValB like so:

  {
    "conditions": { },
    "event": {
      "type": "remove",
      "params": {
        "field": "someParentWrapper.booleanValA"
      }
    }
  }

Require action

The same convention goes for require action

For a single field:

    {
      "conditions": { },
      "event": {
        "type": "require",
        "params": {
          "field": "password"
        }
      }
    }

For multiple fields:

    {
      "conditions": { },
      "event": {
        "type": "require",
        "params": {
          "field": [ "name", "password"]
        }
      }
    }

UiSchema actions

API defines a set of actions, that you can take on uiSchema, they cover most of the

  • uiAppend appends uiSchema specified in params with an original uiSchema
  • uiOverride replaces field in original uiSchema with fields in params, keeping unrelated entries
  • uiRepalce replaces whole uiSchema with a conf schema

To show case, let's take a simple schema

{
  "properties": {
    "lastName": { "type": "string" },
    "firstName": { "type": "string" },
    "nickName": { "type": "string"}
  }
}

and uiSchema

{
  "ui:order": ["firstName"],
  "lastName": {
    "classNames": "col-md-1",
  },
  "firstName": {
    "ui:disabled": false,
    "num": 23
  },
  "nickName": {
    "classNames": "col-md-12"
  }
}

with event params something like this

{
  "ui:order": [ "lastName" ],
  "lastName": {
    "classNames": "has-error"
  },
  "firstName" : {
    "classNames": "col-md-6",
    "ui:disabled": true,
    "num": 22
  }
}

And look at different results depend on the choosen action.

uiAppend

UiAppend can handle arrays and string, with fallback to uiOverride behavior for all other fields.

So the expected result uiSchema will be:

{
  "ui:order": ["firstName", "lastName"],
  "lastName": {
    "classNames": "col-md-1 has-error"
  },
  "firstName": {
    "classNames": "col-md-6",
    "ui:disabled": true,
    "num": 22
  },
  "nickName": {
    "classNames": "col-md-12"
  }
}

In this case it

  • added lastName to ui:order array,
  • appended has-error to classNames in lastName field
  • added classNames and enabled firstName
  • as for the num in firstName it just overrode it

This is useful for example if you want to add some additional markup in your code, without touching layout that you've defined.

uiOverride

uiOverride behaves similar to append, but instead of appending it completely replaces overlapping values

So the expected result uiSchema will be:

{
  "ui:order": [ "lastName" ],
  "lastName": {
    "classNames": "has-error"
  },
  "firstName": {
    "classNames": "col-md-6",
    "ui:disabled": true,
    "num": 22
  },
  "nickName": {
    "classNames": "col-md-12"
  }
}

In this case it

  • ui:order was replaced with configured value
  • className for the lastName was replaced with has-error
  • added classNames and enabled firstName
  • as for the num in firstName it just overrode it

uiReplace

uiReplace just replaces all fields in uiSchema with params fields, leaving unrelated fields untouched.

So the result uiSchema will be

{
  "ui:order": [ "lastName" ],
  "lastName": {
    "classNames": "has-error"
  },
  "firstName" : {
    "classNames": "col-md-6",
    "ui:disabled": true,
    "num": 22
  },
  "nickName": {
     "classNames": "col-md-12"
   }
}

Extension mechanism

You can extend existing actions list, by specifying extraActions on the form.

Let's say we need to introduce replaceClassNames action, that would just specify classNames col-md-4 for all fields except for ignored one. We also want to trigger it only when password is empty.

This is how we can do this:

import applyRules from 'react-jsonschema-form-conditionals';
import Engine from 'json-rules-engine-simplified';
import Form from "react-jsonschema-form";

...

const rules = [
    {
        conditons: {
            password: "empty"
        },
        event: {
            type: "replaceClassNames",
            params: {
                classNames: "col-md-4",
                ignore: [ "password" ]
            }
        }
    }
];


let extraActions = {
    replaceClassNames: function(params, schema, uiSchema, formData) {
        Object.keys(schema.properties).forEach((field) => {
            if (uiSchema[field] === undefined) {
                uiSchema[field] = {}
            }
            uiSchema[field].classNames = params.classNames;
        }
    }
};

let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine, extraActions)(Form);

ReactDOM.render(
  <FormWithConditionals/>,
  document.querySelector('#app')
);

Provided snippet does just that.

Extension with calculated values

In case you need to calculate value, based on other field values, you can also do that.

Let's say we want to have schema with a, b and sum fields

import applyRules from 'react-jsonschema-form-conditionals';
import Engine from 'json-rules-engine-simplified';
import Form from "react-jsonschema-form";

...

const rules = [
    {
        conditons: {
            a: { not: "empty" },
            b: { not: "empty" }
        },
        event: {
            type: "updateSum"
        }
    }
];


let extraActions = {
    updateSum: function(params, schema, uiSchema, formData) {
        formData.sum = formData.a + formData.b;
    }
};

let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine, extraActions)(Form);

ReactDOM.render(
  <FormWithConditionals/>,
  document.querySelector('#app')
);

This is how you can do that.

WARNING!!! You need to be careful with a rules order, when using calculated values. Put calculation rules at the top of your rules specification.

For example, let's say you want to mark sum field, if you have sum greater than 10. The rule would look something like this:

{
  "conditions": {
    "sum": { "greater" : 10 }
  },
  "event": {
    "type": "appendClass",
    "classNames": "has-success"
  }
}

But it will work only if you put it after updateSum rule, like this

[
    {
        "conditons": {
            "a": { "not": "empty" },
            "b": { "not": "empty" }
        },
        "event": {
            "type": "updateSum"
        }
    },
    {
      "conditions": {
        "sum": { "greater" : 10 }
      },
      "event": {
        "type": "appendClass",
        "classNames": "has-success"
      }
    }
];

Otherwise it will work with old sum values and therefor show incorrect value.

Rules order

Originally actions performed in sequence defined in the array. If you have interdependent rules, that you need to run in order you can specify order on a rule, so that it would be executed first. Rules are executed based on order from lowest to highest with rules without order executed last.

For example to make updateSum work regardless the order rules were added, you can do following:

[
    {
      "conditions": {
        "sum": { "greater" : 10 }
      },
      "order": 1,
      "event": {
        "type": "appendClass",
        "classNames": "has-success"
      }
    },
    {
        "conditons": {
            "a": { "not": "empty" },
            "b": { "not": "empty" }
        },
        "order": 0,
        "event": {
            "type": "updateSum"
        }
    }
]

Here although updateSum comes after appendClass, it will be executed first, since it has a lower order.

Action validation mechanism

All default actions are validated by default, checking that field exists in the schema, to save you some headaches. There are 2 levels of validation

  • propTypes validation, using FB prop-types package
  • explicit validation

You can define those validations in your actions as well, to improve actions usability.

All validation is disabled in production.

Prop types action validation

This is reuse of familiar prop-types validation used with React components, and it's used in the same way:

In case of require it can look like this:

require.propTypes = {
  field: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.arrayOf(PropTypes.string),
  ]).isRequired,
};

The rest is magic.

WARNING, the default behavior of prop-types is to send errors to console, which you need to have running in order to see them.

For our replaceClassNames action, it can look like this:

replaceClassNames.propTypes = {
  classNames: PropTypes.string.isRequired,
  ignore: PropTypes.arrayOf(PropTypes.string)
};

Explicit validation

In order to provide more granular validation, you can specify validate function on your action, that will receive params, schema and uiSchema so you could provide appropriate validation.

For example, validation for require can be done like this:

  require.validate = function({ field }, schema, uiSchema) {
    if (Array.isArray(field)) {
      field
        .filter(f => schema && schema.properties && schema.properties[f] === undefined)
        .forEach(f => console.error(`Field  "${f}" is missing from schema on "require"`));
    } else if (
      schema &&
      schema.properties &&
      schema.properties[field] === undefined
    ) {
      console.error(`Field  "${field}" is missing from schema on "require"`);
    }
  };

Validation is not mandatory, and will be done only if field is provided.

For our replaceClassNames action, it would look similar:

  replaceClassNames.validate = function({ ignore }, schema, uiSchema) {
    if (Array.isArray(field)) {
      ignore
        .filter(f => schema && schema.properties && schema.properties[f] === undefined)
        .forEach(f => console.error(`Field  "${f}" is missing from schema on "replaceClassNames"`));
    } else if (
      schema &&
      schema.properties &&
      schema.properties[ignore] === undefined
    ) {
      console.error(`Field  "${ignore}" is missing from schema on "replaceClassNames"`);
    }
  };

Listening to configuration changes

In order to listen for configuration changes you can specify onSchemaConfChange, which will be notified every time schema or uiSchema changes it's value.

let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine, extraActions)(Form);

ReactDOM.render(
  <FormWithConditionals onSchemaConfChange = {({ schema, uiSchema }) => { console.log("configuration changed") }}/>,
  document.querySelector('#app')
);

Contribute

  • Issue Tracker: github.com/RxNT/react-jsonschema-form-conditionals/issues
  • Source Code: github.com/RxNT/react-jsonschema-form-conditionals

Support

If you are having issues, please let us know.

License

The project is licensed under the Apache-2.0 license.

Migration

Migration to 0.4.x

The only significant change is signature of applyRules call. In 0.4.0 schema, uiSchema, rules, Engine and extraActions all consider to be constant that is why, they moved to applyRules call. This helps improve performance on large schemas.

react-jsonschema-form-conditionals's People

Contributors

cheeverm avatar mavarazy avatar mrclay avatar sandeepkesarkar avatar sneu012 avatar theluk avatar zboldyga avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-jsonschema-form-conditionals's Issues

Validate actions on creation

Currently action validation happens when we actually execute an action, this makes it very hard . We want validation to happen when Form is created, so we would prevent errors on initial phase.

Support installing this lib as a script

Is it possible to use this lib as a script, i.e. by adding a <script src="...."></script> tag to the page? If so, could you add instructions to the docs?

This would be useful both for applications that aren't using modules, and when creating JSFiddle, Plunker, etc. demos.

Add support for calculated columns

In RxNT case for example BMI is calculated based on Weight and Height of the person.
With current rules engine, there is no way to have this functionality.

formData after rules are applied

If I have a simple rule that conditionally removes a field, how can I get a version of formData that does not include that field?

Currently, { formData } in onChange and onSubmit contains all fields, including those which are conditionally removed (i.e. hidden). Is this by design, or a bug on my end?

If my explanation is unclear I can provide a repro.

Add appendClass action

Often times the action required is just to append some classNames to initial presentation, without replacing uiSchema completely.

Raise condition when rendering form

Few times, while rendering form with manager, and field overrides raise condition happened and setState was called within the render function, which is prohibited behavior.

Support action validation, based on prop-types

In order to minimize errors, during construction, we should support validation of actions defined in configuration, this can be done with prop-types, so we at least would know the form of the object is correct

JS Use

Is there a way to call the library with schema+data params and get a list of errors/required fields?

Optimize rendering on change

Currently a simple form change can trigger set of re render actions

  1. When formData changes in state
  2. When schema recalculated

We need to optimize it from performance and UX point of view, if nothing changed we don't need to trigger additional rerenderings

Apply remove on properties which contains "$ref" doesn't work

Hello @mavarazy,

Thanks for this useful project.

I'm trying to remove a schema property which contains "$ref" but the schema returned by the function fetchSchema (https://github.com/RxNT/react-jsonschema-form-conditionals/blob/master/src/utils.js#L40) is the "$ref" definition object.

So, the function doRemove (https://github.com/RxNT/react-jsonschema-form-conditionals/blob/master/src/actions/remove.js#L10), breaks.

You can reproduce the error by adding the test in remove.test.js (https://github.com/RxNT/react-jsonschema-form-conditionals/blob/master/test/actions/remove.test.js)

let origSchemaWithRef = {
  definitions: {
    address: {
      properties: {
        street: { type: "string" },
        zip: { type: "string" },
      },
    },
  },
  properties: {
    title: { type: "string" },
    firstName: { type: "string" },
    address: { $ref: "#/definitions/address" },
  },
};

let originUiSchemaWithRef = {
  title: {},
  firstName: {},
  address: {},
};

test("remove field which contains ref", () => {
  let schemaWithRef = deepcopy(origSchemaWithRef);
  let uiSchemaWithRef = deepcopy(originUiSchemaWithRef);
  remove({ field: ["address"] }, schemaWithRef, uiSchemaWithRef);

  let schemaWithoutAddress = {
    definitions: {
      address: {
        properties: {
          street: { type: "string" },
          zip: { type: "string" },
        },
      },
    },
    properties: {
      title: { type: "string" },
      firstName: { type: "string" },
    },
  };

  expect(schemaWithRef).toEqual(schemaWithoutAddress);

  let uiSchemaWithoutAddress = {
    title: {},
    firstName: {},
  };
  expect(uiSchemaWithRef).toEqual(uiSchemaWithoutAddress);

});

Tell me if you need more informations.

Switch from package deep-equals to react-jsonschema-form native implementation

Live testing showed, that deep-equal package does not compare functions in desired way, since react-jsonschema-form uses functions to customize view, in many cases deep-equals will show content changed, although it did not.
It's better to fallback to react-jsonschema-form native implementation in lib/utils package, with
import { deepEquals } from "react-jsonschema-form/lib/utils";

Make Actions mechanism ad-hoc extensible

Current scope of actions defined for RxNT might not be enough for the open source community, they should have a way to extend it with additional actions as needed.

paging merge

Sandeep expressed a need for us to be able to show/hide tabs based on conditions. In order to accomplish this we might want your conditional module to have the ability to act on the ui:tabID property I've added in the pagination project (e.g. have a condition effect all fields with a specific ui:tabID value.

I see currently that your rules are defined on a per-field basis. Maybe we can add a mechanism for defining a rule for a ui:tabID?

We're also going to run into a similar situation with having to show/hide groups of fields - in the healthcare context it seems that it's usually going to be whole sections of a form that show/hide based on the input of some field. In this situation we'll have a similar issue - we can explicitly define a rule for every component in the group, but it might make more sense to have a concise handle that acts on a whole group of components.

I think the groups will just be Widgets, as defined in the Mozilla project. So we may want conditional rules to apply to Widgets as well... We may want to defer thinking about the Widgets until we get a little further though.

What do you think?

conditions documentation?

This is a note for a few weeks from now... We can come back to this during the knowledge transfer sprint.

In each object specified in the rules array, it looks like we need to specify a conditions property and an event property. There are examples in our documentation of how to use the event property, but it seems like we're deferring to documentation for other projects for how to use conditions.

I think for the sake of the main client (RxNT), we might want to make it simpler for them to quickly understand how to use the tool. Maybe we give documentation for a default engine choice (whichever engine RxNT is using)?

I'm thinking that if it's not dead simple, they aren't going to continue to use the tool. I don't think Thomas & Sandeep have time to look thru code; they'll probably want the READMEs to be fully comprehensive.

Support boolean logic for multiple fields

#4 is enough for simple cases of comparison related for a single field, but in case we need to interact with multiple fields, it's not enough.

For example, we want to hide password field, when firstName is missing or nickName is admin.
We can express this like this:

let rules = {
    password: {
        action: "remove",
        when: { "or" : [ { firstName: "empty"} , { nickName: "admin" } ] },
    }
}

Exception when the root object is not an object

Steps to reproduce

  1. Open the playground.
  2. Use a very simple schema (with empty UI and Rules):
{
  "type": "number"
}
  1. Put a number in Data.
  2. Erase the number from the input field.

Outcome

The input field now contains [object Object].

The following errors are logged:

Warning: Failed prop type: Invalid prop `formData` supplied to `NumberField`.
    in NumberField (created by SchemaField)
    in SchemaField (created by Form)
    in form (created by Form)
    in Form (created by FormWithConditionals)
    in FormWithConditionals (created by App)
    in div (created by App)
    in div (created by App)
    in App (created by ResultForm)
    in div (created by ResultForm)
    in ResultForm (created by ResultForm)
    in ResultForm

Warning: Failed prop type: Invalid prop `formData` supplied to `StringField`.
    in StringField (created by NumberField)
    in NumberField (created by SchemaField)
    in div (created by DefaultTemplate)
    in DefaultTemplate (created by SchemaField)
    in SchemaField (created by Form)
    in form (created by Form)
    in Form (created by FormWithConditionals)
    in FormWithConditionals (created by App)
    in div (created by App)
    in div (created by App)
    in App (created by ResultForm)
    in div (created by ResultForm)
    in ResultForm (created by ResultForm)
    in ResultForm

Warning: Failed prop type: Invalid prop `value` supplied to `TextWidget`.
    in TextWidget (created by StatelessComponent)
    in Unknown (created by StringField)
    in StringField (created by NumberField)
    in NumberField (created by SchemaField)
    in div (created by DefaultTemplate)
    in DefaultTemplate (created by SchemaField)
    in SchemaField (created by Form)
    in form (created by Form)
    in Form (created by FormWithConditionals)
    in FormWithConditionals (created by App)
    in div (created by App)
    in div (created by App)
    in App (created by ResultForm)
    in div (created by ResultForm)
    in ResultForm (created by ResultForm)
    in ResultForm

Support array of events for rule conditions

We currently trigger a single event per rule, we need to support arrays of events to allow complicated triggers on the same condition.

For example, if we want to remove a field and change class name for the field under the same condition, currently we would need to do following:

[{
    {
      conditions: {
        firstName: "empty",
      },
      event: {
        type: "remove",
        params: { field: "email" }, 
     },
    },
   {
      conditions: {
        firstName: "empty",
      },
      event: {
        type: "uiAppend",
        params: { lastName: { classNames: "danger" } },       
     },
    }
}]

While what we want to do is this:

{
    conditions: {
      firstName: "empty",
    },
    event: [{
      type: "remove",
      params: { field: "email" },
    }, {
      type: "uiAppend",
      params: { lastName: { classNames: "danger" } },
    }]
  }

Remove "hidden" elements of schema from form data

Given this schema

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "nameHider": {
      "type": "boolean"
    }
  },
  "required": ["name", "nameHider"]
} 

And the following rule which hides the name field if nameHider is true

  const rules = [{
    conditions: {
      nameHider: {is: true}
    },
    event: {
      type: "remove",
      params: {
        field: "name"
      }
    }
  }];

If the following actions are performed

  1. Set the name field to "Bob"
  2. Set the nameHider field to true

I would expect the form data to be:

{"nameHider": true}

But instead it is:

{"nameHider": true, "name": "Bob"}

I guess I could implement this as a custom action, but IMO this ought to be performed by the remove action.

Support rules order

Following #34
When using calculated rules they need to be run first, before changing any other formData dependent values. We can do that by explicitly ordering and putting calculation rules at the head of the least, but this is not always the case that you know your order ahead, giving an explicit way to define order would be a useful feature for such kind of issues.

     {
        conditons: {
            a: { not: "empty" },
            b: { not: "empty" }
        },
        order: 0,
        event: {
            type: "updateSum"
        }
    }

Action fails on embedded fields

Currently the only fields, that can be verified are on the first level, so no embedded fields are supported at the moment.

Switch to new rules definition format

Rules need to be split into 2 fields action to perform and conditional when to perform specified action, like this

let rules = {
  telephone: {
      action: "remove",
      when: {
        age: {
          greater: 5,
          less : 70,
        }
      }
  }
}

This helps readability of the rule engine and improves maitainability

The Engine should run again if the formData changes as the result of a rule

I have a rule that populates a set of fields, which should fire another rule

handleChange in applyRules.js

handleChange = state => {
      let { formData } = state;
      runRules(formData, this.props).then(conf => {
        this.updateState(formData, conf);
        if (this.props.onChange) {
          state = Object.assign({}, state, conf);
          this.props.onChange(state);
        }
      });
    };

could be something like this

handleChange = state => {
  let { formData } = state;
  runRules(formData, this.props).then(conf => {
    this.updateState(formData, conf);
    if (this.props.onChange) {
      if(!deepEqual(formData, conf.formData  )) {
        this.handleChange({ formData: conf.formData })
      } else {
        state = Object.assign({}, state, conf);
        this.props.onChange(state);
      }
    }
  });
};

Sample rules

[
  {
    "title": "Rule #2",
    "description": "This hides Address, Email, Gender and the Password fields until First Name and Last Name have a value",
    "conditions": {
      "and": [
        {
          "or": [
            {
              "registration.firstName": "empty"
            },
            {
              "registration.lastName": "empty"
            }
          ]
        }
      ]
    },
    "event": {
      "type": "remove",
      "params": {
        "field": [
          "address",
          "registration.gender",
          "registration.email",
          "registration.password",
          "registration.confirmPassword"
        ]
      }
    }
  },
  {
    "title": "Rule #3",
    "description": "prefills firstName",
    "conditions": {
      "and": [
        {
          "registration.firstName": {
            "_equals": "Barry"
          }
        }
      ]
    },
    "event": {
      "params": {
        "field": [
          "registration.lastName"
        ],
        "options": {
          "registration.lastName": "White",
          "registration.gender": "Male"
        }
      },
      "type": "populate"
    }
  }
]

At the moment a second Higher Order Component needs to be written with the following to achieve this,


    componentWillReceiveProps (nextProps) {
      if(!deepEqual(nextProps.formData,this.props.formData)){
        // Rules do not run again when formData changes as a result from a rule
        this.onChange({formData: nextProps.formData})
      }

Add more extensive uiSchema actions

While using in internal project current set of actions for uiSchema proved to be insufficient, since many actions require more, then one modification to uiSchema object.

Proposed solution is to deprecate appendClass and replaceUi and replace them with a different set of actions uiAppend, uiOverride and uiReplace, which would take a subset of uiSchema as configuration.

uiAppend will append specific configurations to original uiSchema, only if provided string was not already part of uiSchema configuration.
uiOverride will replace specific configurations of original uiSchema
uiReplace will replace all configurations in original uiSchema

This should improve library usability.

Support multi action for the same field

Library should support more, than one action on specified field, so that you can twist logic as needed

let rules = {
  telephone: [
      {
          action: "remove",
          when: { age: or: [ { less : 5 }, { greater: 70 } ] }
       },
       {
          action: "require",
          when: { age: { greater: 5, less : 70 } }
       }
  ]
}

In this example telephone is either removed from the form in case age is less than 5 and greater than 70, or required otherwise

Support boolean logic on a field comparison level

In order to express complicated logic (like ranges or value in), we need to extend used predicate library, by adding support for basic boolean operations - NOT, AND and OR.

let rules = {
  telephone: {
      action: "remove",
      when: {
        age: {
          not: {
              greater: 5,
              less : 70,
          }
        }
      }
  }
}

Keep focus when data changes on a changed element

Currently behavior is inconsistent - focus might be lost from time to time, when you edit data in an object, if rules are run in the background. We need to make it more consistent for a smoother user experience.

is removal of fields reversible?

It seems like using the decorator approach, if we remove an item from the schema object in our wrapper class, then pass that filtered schema down to the mozilla form, we will have lost our component. If we use this approach, we might then need to have some separate state object that represents deleted schema items?

Or maybe instead of deleting the form items we just add/remove a CSS class that hides the component's visibility?

Form fields lose focus when input value changes

I'm trying to render a FormWithConditionals and a FormModelInspector. The latter is a very simple component that shows the current formData.

screen shot 2018-02-01 at 17 50 32

The source code is given below:

import React from 'react';
import PropTypes from 'prop-types';
import Engine from "json-rules-engine-simplified";
import Form from "react-jsonschema-form";
import applyRules from "react-jsonschema-form-conditionals";

function FormModelInspector (props) {

  return (
    <div>
      <div className="checkbox">
        <label>
          <input type="checkbox" onChange={props.onChange} checked={props.showModel}/>
          Show Form Model
        </label>
      </div>
      {
        props.showModel && <pre>{JSON.stringify(props.formData, null, 2)}</pre>
      }
    </div>
  )
}

class ConditionalForm extends React.Component {

  constructor (props) {
    super(props);
    this.state = {
      formData: {},
      showModel: true
    };
    this.handleFormDataChange = this.handleFormDataChange.bind(this);
    this.handleShowModelChange = this.handleShowModelChange.bind(this);
  }

  handleShowModelChange (event) {
    this.setState({showModel: event.target.checked});
  }

  handleFormDataChange ({formData}) {
    this.setState({formData});
  }

  render () {
    const schema = {
      type: "object",
      title: "User form",
      properties: {
        nameHider: {
          type: 'boolean',
          title: 'Hide name'
        },
        name: {
          type: 'string',
          title: 'Name'
        }
      }
    };

    const uiSchema = {};

    const rules = [{
      conditions: {
        nameHider: {is: true}
      },
      event: {
        type: "remove",
        params: {
          field: "name"
        }
      }
    }];

    const FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);

    return (
      <div className="row">
        <div className="col-md-6">
          <FormWithConditionals schema={schema}
                uiSchema={uiSchema}
                formData={this.state.formData}
                onChange={this.handleFormDataChange}
                noHtml5Validate={true}>
          </FormWithConditionals>
        </div>
        <div className="col-md-6">
          <FormModelInspector formData={this.state.formData}
                              showModel={this.state.showModel}
                              onChange={this.handleShowModelChange}/>
        </div>
      </div>
    );
  }
}

ConditionalForm.propTypes = {
  schema: PropTypes.object.isRequired,
  uiSchema: PropTypes.object.isRequired,
  rules: PropTypes.array.isRequired
};

ConditionalForm.defaultProps = {
  uiSchema: {},
  rules: []
};

As you can see in this demo, every time I change a field's value, the field loses focus. I suspect the cause of the problem is react-jsonschema-form-conditionals, because if I replace <FormWithConditionals> with <Form>, the problem does not occur.

If I remove the handler onChange={this.handleFormDataChange} the input field no longer loses focus when it's value changes (but removing this handler breaks the FormModelInspector).

Apologies for not providing a JSFiddle example instead of pasting all the code into the issue, but I couldn't figure out how to add react-jsonschema-form-conditionals as a dependency, and import applyRules in JSFiddle

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.