data-driven-forms / react-forms

React library for rendering forms.
https://data-driven-forms.org/
Apache License 2.0
295 stars 85 forks source link

Question: Is it possible to use a custom function for conditions within the schema in JSON format? #1418

Closed bytecast-de closed 10 months ago

bytecast-de commented 10 months ago

Scope: react-form-renderer / conditions

Description

I am looking for a way to define a condition via custom function (https://www.data-driven-forms.org/schema/is#customfunction) BUT i cannot use a javascript object, the schema has to be in JSON format since it will be retrieved from the backend.

I've looked into the ActionMapper (https://www.data-driven-forms.org/mappers/action-mapper), but as far as I understand, the mapped functions can only be applied to a field property. In my case I would need to assign a mapped function inside a (possibly nested) condition - some sort of "ConditionMapper" if that makes any sense. See example below.

My question is: Can this somehow be done with the current implementation, or would this be a feature request?

Schema

This is the javascript version, works as documented:

const schema = {
  title: 'Is condition',
  fields: [
    {
      component: componentTypes.TEXT_FIELD,
      name: 'field-1',
      label: 'Field 1'
    },
    {
      component: componentTypes.TEXT_FIELD,
      name: 'field-2',
      label: 'Field 2',
      condition: { when: 'field-1', is: (value, config) => someCustomLogic(value, config) },
    },
  ],
};

This would be the corresponding JSON-Version, I have no idea if/how this can be done at the moment:

const schemaJSON = {
  "title": "Is condition",
  "fields": [
    {
      "component": "text-field",
      "name": "field-1",
      "label": "Field 1"
    },
    {
      "component": "text-field",
      "name": "field-2",
      "label": "Field 2"
      "condition": { "when": "field-1", is: ["??? somehow mapped function ???"] },
    },
  ],
};
Hyperkid123 commented 10 months ago

Hi @bytecast-de this is not possible at the moment.

I think we can come up with something similar to the action mapper though. I'll have to think about some reasonable format. Do you have some syntax in mind?

bytecast-de commented 10 months ago

Hi @Hyperkid123,

thank you for your quick response.

Based on the examples from the docs, there are some variations that come to mind from "user" perspective:

// value and config are always passed as arguments, "age" is the first "variable arg" from config 
const olderThan = (value, config, age): boolean => {
  return calculateAge(value) > age;
}

// this will be passed to the FormRenderer
const conditionMapper = {
  "olderThan": olderThan,
}

// variant 1: using "is" with an array (function name + variable arguments), similar to ActionMapper
const schemaJSON = {
  "title": "Is condition",
  "fields": [
      {
        "component": "text-field",
        "name": "field-2",
        "label": "Field 2",
        "condition": { "when": "field-1", "is": ["olderThan", 18] },
      },
  ],
};

// variant 2: using a new condition type with same ActionMapper-like array as in variant 1
const schemaJSON = {
  ...
      "condition": { "when": "field-1", "isFunction": ["olderThan", 18] },
  ...
};

// variant 3: using "is" (or a new condition type), but with a config object instead of an array
const schemaJSON = {
  ...
    "condition": { "when": "field-1", "is": {"fn": "olderThan", "params": [18]} },
  ...
};

Variant 1 possibly won't work, as far as I've seen in the source, you can currently pass an array to "is", that behaves like "oneOf" - which is an undocumented feature, btw, or i missed it in the docs ;-) It maybe would be hard to make a distinction between the new feature and the old behaviour with arrays.

Variant 2 is probably more work - i don't know how many other parts must be changed, to add another condition type.

Variant 3 is in my opinion the most self-explaining and could possibly be used within the "is"-condition without the need to add another condition.

Please let me know what you think about it, and if I can be of any help with this feature.

Hyperkid123 commented 10 months ago

@bytecast-de thanks for the suggestions.

I was thinking about something similar, bit there is an issue. All of the examples of is implementation are valid values that can appear in a form state. This is not an issue within the action mapper as we map the entire prop to an action.

I think we will have to add some flag to the condition object that states that a particular condition is supposed to be mapped to a function

"condition": { "when": "field-1", "is": {"fn": "olderThan", "params": [18]}, mappedAttributes: { is: true } },

We can have an attribute like mappedAttributes that would specify which condition attribute is supposed to be mapped to a function at runtime. There can be many attributes in that can be mapped, not just the is. This mappedAttributes syntax is in a way similar to the subscription configuration.

@rvsia opinion?

rvsia commented 10 months ago

@Hyperkid123

1) regarding the format of mapping, we should stay consistent with usage of action mappers so I prefer this variant: ["olderThan", 18]

2) regarding mappedAttributes

Following attributes could be mapped to some function, nothing else makes sense in the condition context:

  1. when
  2. is
  3. set

And we could use mappedAttributes (or similar name) to map them as we do with the actions:

mappedAttributes: { is: ['olderThan', 18] }
Hyperkid123 commented 10 months ago

@rvsia agreed.

I hope I'll have some time later this week to implement the condition mapper. Feel free to pick the issue if you want.

bytecast-de commented 10 months ago

@Hyperkid123 @rvsia thanks for your feedback, sounds great to me - really looking forward to this feature.

I'd like to add that this is an awesome project, much appreciated!

Hyperkid123 commented 10 months ago

OK I got some time on my hands. I'll work on the implementation

Hyperkid123 commented 10 months ago

@rvsia if we were to follow the current option for condition function config, it does not make sens to send anything other than the mapped attribute name. We do not pass any custom argument function conditions. Its always just a value and the condition config,

rvsia commented 10 months ago

@rvsia

Would it make sense to change it to higher-order-function to allow customization from the schema? 🤔

conditionMapper: {
   ageValidation: (age = 18) => (value, config) => calculateAge(value, config) > (age)
}

// alcohol related page
schema = {
   name: 'age',
   component: 'date-picker',
   condition: {
              mappedAttributes: {
                   is: ['ageValidation', 18]
              },
   }
}

// senior sale related page :D 
schema = {
   name: 'age',
   component: 'date-picker',
   condition: {
              mappedAttributes: {
                   is: ['ageValidation', 65]
              },
   }
}
Hyperkid123 commented 10 months ago

We can do that or append the arguments after the regular arguments. HOF will follow the pattern set within the validation mapper.

rvsia commented 10 months ago

@Hyperkid123 I would not append the arguments after regular - that could limit extensibility in the future.

Hyperkid123 commented 10 months ago

@rvsia I've added the HOC. Right now the mapped conditions will work only for the is and when attributes. If we ever required the set, it can be added later on.

rvsia commented 10 months ago

:tada: This issue has been resolved in version 3.22.0 :tada:

The release is available on

Data-Driven-Forms.org!

Hyperkid123 commented 10 months ago

@bytecast-de You can check the docs here: https://www.data-driven-forms.org/mappers/condition-mapper

bytecast-de commented 10 months ago

@Hyperkid123 great job, thank you!