From 6ae36bfa30c449f9ff4a82d965d3c6a6e885ecc2 Mon Sep 17 00:00:00 2001 From: joepuzzo Date: Mon, 5 Apr 2021 06:10:41 -0700 Subject: [PATCH 1/2] added ability to render relevance where you want to --- src/Context.js | 4 ++- src/components/FormField.js | 13 ++++++-- src/components/Relevant.js | 60 ++++++++++++++++++++++++++++++++++++- src/index.js | 2 ++ 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/Context.js b/src/Context.js index 72b43f74..d9cab8c3 100644 --- a/src/Context.js +++ b/src/Context.js @@ -34,6 +34,7 @@ const ArrayFieldItemStateContext = React.createContext(); const MultistepStateContext = React.createContext(); const MultistepApiContext = React.createContext(); const MultistepStepContext = React.createContext(); +const RelevantContext = React.createContext(); export { FormRegisterContext, @@ -47,5 +48,6 @@ export { ArrayFieldItemStateContext, MultistepStateContext, MultistepApiContext, - MultistepStepContext + MultistepStepContext, + RelevantContext }; diff --git a/src/components/FormField.js b/src/components/FormField.js index 2650e3a1..3be65e81 100644 --- a/src/components/FormField.js +++ b/src/components/FormField.js @@ -2,11 +2,12 @@ import React, { useContext } from 'react'; import useFormApi from '../hooks/useFormApi'; import { computeFieldFromProperty, getSchemaPathFromJsonPath } from '../utils'; import ObjectMap from '../ObjectMap'; -import { FormRegisterContext } from '../Context'; +import { FormRegisterContext, RelevantContext } from '../Context'; const FormField = ({ field }) => { // Get the field map off the forms context const { fieldMap } = useContext(FormRegisterContext); + const relevant = useContext(RelevantContext); // Grab the form api ( we need it to get the actual field name because might be in scope ) const { getFullField, getOptions } = useFormApi(); @@ -24,7 +25,15 @@ const FormField = ({ field }) => { // field = "brother.name" ---> properties.brother.properties.name // field = "brother.siblings[1].friend.name" ---> properties.brother.properties.siblings.items[1].properties.friend.properties.name const path = getSchemaPathFromJsonPath(fullField); - const property = ObjectMap.get(schema, path); + let property = ObjectMap.get(schema, path); + + // We might have definition inside of conditional so check there + if (relevant) { + // Find conditional from schema + const conditional = schema.allOf.find(c => c.relevant === relevant); + const subSchema = conditional.then; + property = ObjectMap.get(subSchema, path); + } // If property was not found return null if (!property) { diff --git a/src/components/Relevant.js b/src/components/Relevant.js index afa8e3b3..43dc03ac 100644 --- a/src/components/Relevant.js +++ b/src/components/Relevant.js @@ -1,11 +1,22 @@ import React from 'react'; +import useFormApi from '../hooks/useFormApi'; import useFormState from '../hooks/useFormState'; +import FormFields from '../components/FormFields'; +import { RelevantContext } from '../Context'; -const Relevant = ({ when, children }) => { +const Relevant = ({ when, children, relevant }) => { const formState = useFormState(); const isRelevant = when(formState); + if (isRelevant && relevant) { + return ( + + {children} + + ); + } + if (isRelevant) { return children; } @@ -13,4 +24,51 @@ const Relevant = ({ when, children }) => { return null; }; +export const RelevantFields = ({ relevant, children }) => { + // Grab the form api ( we need it to get the actual field name because might be in scope ) + const { getOptions } = useFormApi(); + + // Grap the schema + const { schema } = getOptions(); + + // Find conditional from schema + const conditional = schema.allOf.find(c => c.relevant === relevant); + + // Example then ( its a subschema ) + // then: { + // properties: { + // spouse: { + // type: 'string', + // title: 'Spouse name', + // 'ui:control': 'input' + // } + // } + // } + const subSchema = conditional.then; + + // Turn the if into a when function for informed + // Example if condition + // if: { + // properties: { + // married: { const: 'yes' } + // }, + // required: ['married'] + // }, + const { properties: conditions } = conditional.if; + const when = ({ values }) => { + // Example key "married, Example condition: "{ const: 'yes' }" + return Object.keys(conditions).every(key => { + const condition = conditions[key]; + // values.married === 'yes' + return values[key] === condition.const; + }); + }; + + return ( + + {children ? children : } + + ); +}; + export default Relevant; diff --git a/src/index.js b/src/index.js index d6409604..b93390ec 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ import { BasicRadioGroup } from './components/form-fields/RadioGroup'; import { BasicTextArea } from './components/form-fields/TextArea'; import { BasicSelect } from './components/form-fields/Select'; import { BasicCheckbox } from './components/form-fields/Checkbox'; +import { RelevantFields } from './components/Relevant'; import * as utils from './utils'; @@ -93,5 +94,6 @@ export { SchemaFields, FormFields, FormComponents, + RelevantFields, utils }; From 17831bb46f406b36e435c54d3e579f6a22ed9b25 Mon Sep 17 00:00:00 2001 From: joepuzzo Date: Mon, 5 Apr 2021 07:31:20 -0700 Subject: [PATCH 2/2] added ability to render relevance where you want based soley on schema --- src/components/FormFields.js | 10 +- src/components/Label.js | 5 + src/fieldMap.js | 4 +- src/utils.js | 36 +++++- .../FormattedConditionalSchema/README.md | 100 +++++++++++++++++ .../FormattedConditionalSchema/index.js | 99 +++++++++++++++++ stories/Schema/LocationControl/README.md | 103 ++++++++++++++++++ stories/Schema/LocationControl/index.js | 98 +++++++++++++++++ stories/index.js | 4 + 9 files changed, 452 insertions(+), 7 deletions(-) create mode 100644 src/components/Label.js create mode 100644 stories/Schema/FormattedConditionalSchema/README.md create mode 100644 stories/Schema/FormattedConditionalSchema/index.js create mode 100644 stories/Schema/LocationControl/README.md create mode 100644 stories/Schema/LocationControl/index.js diff --git a/src/components/FormFields.js b/src/components/FormFields.js index 0bdf09a0..f21d2a0f 100644 --- a/src/components/FormFields.js +++ b/src/components/FormFields.js @@ -1,7 +1,7 @@ import React, { useMemo, useContext } from 'react'; import { computeFieldsFromSchema } from '../utils'; import ArrayField from './form-fields/ArrayField'; -import Relevant from './Relevant'; +import Relevant, { RelevantFields } from './Relevant'; import Debug from '../debug'; import { FormRegisterContext } from '../Context'; @@ -29,6 +29,7 @@ const FormFields = ({ schema, prefix, onlyValidateSchema }) => { properties, items, componentType, + relevant, uiBefore, uiAfter, allOf @@ -39,6 +40,13 @@ const FormFields = ({ schema, prefix, onlyValidateSchema }) => { // console.log('WTF', schemaField); logger('Rendering Field', field, schemaField); + // For Relevant Fields + if (componentType === 'relevantFields') { + return ( + + ); + } + // Scope for nested if (!Component && type === 'object' && properties) { return ( diff --git a/src/components/Label.js b/src/components/Label.js new file mode 100644 index 00000000..8b0d0745 --- /dev/null +++ b/src/components/Label.js @@ -0,0 +1,5 @@ +import React from 'react'; + +const Label = ({ title }) => ; + +export default Label; diff --git a/src/fieldMap.js b/src/fieldMap.js index dd2cbbd7..006e3eaa 100644 --- a/src/fieldMap.js +++ b/src/fieldMap.js @@ -6,6 +6,7 @@ import RadioGroup from './components/form-fields/RadioGroup'; import AddButton from './components/form-fields/AddButton'; import RemoveButton from './components/form-fields/RemoveButton'; import ArrayField from './components/form-fields/ArrayField'; +import Label from './components/Label'; export default { select: Select, @@ -15,5 +16,6 @@ export default { radio: RadioGroup, add: AddButton, remove: RemoveButton, - array: ArrayField + array: ArrayField, + label: Label }; diff --git a/src/utils.js b/src/utils.js index 7d5ebe1d..bf12d955 100644 --- a/src/utils.js +++ b/src/utils.js @@ -100,6 +100,29 @@ export const debounce = (func, wait) => { }; export const computeFieldFromProperty = (propertyName, property, prefix) => { + // Special case for relevant fields + // Example 'conditional:over21' + if (/conditional:.*/.test(propertyName)) { + return { + componentType: 'relevantFields', + // Example 'conditional:over21' ---> over21 + relevant: propertyName.replace(/conditional:(.*)/, '$1') + }; + } + + // Special case for component fields + // Example 'component:marriedLabel' + if (/component:.*/.test(propertyName)) { + const { 'ui:control': uiControl, ...props } = property; + + return { + componentType: uiControl, + // Not needed as this is not an informed field + field: undefined, + props + }; + } + const { 'ui:control': uiControl, 'informed:props': informedProps, @@ -213,11 +236,14 @@ export const computeFieldsFromSchema = (schema, onlyValidateSchema, prefix) => { // Check for all of ( we have conditionals ) if (allOf) { - fields.push({ - componentType: 'conditionals', - // Each element of the "allOf" array is a conditional - allOf: allOf - }); + // Only do this if the user did not chose to control location + if (!allOf[0].relevant) { + fields.push({ + componentType: 'conditionals', + // Each element of the "allOf" array is a conditional + allOf: allOf + }); + } } return fields; diff --git a/stories/Schema/FormattedConditionalSchema/README.md b/stories/Schema/FormattedConditionalSchema/README.md new file mode 100644 index 00000000..ad7938f6 --- /dev/null +++ b/stories/Schema/FormattedConditionalSchema/README.md @@ -0,0 +1,100 @@ +# Formatted Conditional Schema + +** Note: This is in beta and is subject to change! ** + + + +```jsx +import { Form, FormField, RelevantFields } from 'informed'; + +const schema = { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + title: 'First name', + 'ui:control': 'input' + }, + married: { + type: 'string', + title: 'Are you married?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + }, + ofAge: { + type: 'string', + title: 'Are you over 21?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + } + }, + allOf: [ + { + relevant: 'married', + if: { + properties: { + married: { const: 'yes' } + }, + required: ['married'] + }, + then: { + properties: { + spouse: { + type: 'string', + title: 'Spouse name', + 'ui:control': 'input' + } + } + } + }, + { + relevant: 'over21', + if: { + properties: { + ofAge: { const: 'yes' } + }, + required: ['ofAge'] + }, + then: { + properties: { + age: { + type: 'number', + title: 'Age', + 'ui:control': 'input', + 'input:props': { + type: 'number' + } + }, + drink: { + type: 'string', + title: 'What do you want to drink?', + 'ui:control': 'input' + } + } + } + } + ] +}; + +const Component = () => { + return ( +
+ + + + + + + + + + + + + + + + ); +}; +``` diff --git a/stories/Schema/FormattedConditionalSchema/index.js b/stories/Schema/FormattedConditionalSchema/index.js new file mode 100644 index 00000000..d6b1c8f6 --- /dev/null +++ b/stories/Schema/FormattedConditionalSchema/index.js @@ -0,0 +1,99 @@ +import React from 'react'; +import withDocs from '../../utils/withDocs'; +import readme from './README.md'; +import FormState from '../../utils/FormState'; + +import { Form, FormField, RelevantFields } from '../../../src'; + +const schema = { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + title: 'First name', + 'ui:control': 'input' + }, + married: { + type: 'string', + title: 'Are you married?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + }, + ofAge: { + type: 'string', + title: 'Are you over 21?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + } + }, + allOf: [ + { + relevant: 'married', + if: { + properties: { + married: { const: 'yes' } + }, + required: ['married'] + }, + then: { + properties: { + spouse: { + type: 'string', + title: 'Spouse name', + 'ui:control': 'input' + } + } + } + }, + { + relevant: 'over21', + if: { + properties: { + ofAge: { const: 'yes' } + }, + required: ['ofAge'] + }, + then: { + properties: { + age: { + type: 'number', + title: 'Age', + 'ui:control': 'input', + 'input:props': { + type: 'number' + } + }, + drink: { + type: 'string', + title: 'What do you want to drink?', + 'ui:control': 'input' + } + } + } + } + ] +}; + +const Component = () => { + return ( +
+ + + + + + + + + + + + + + + + ); +}; + +export default withDocs(readme, Component); diff --git a/stories/Schema/LocationControl/README.md b/stories/Schema/LocationControl/README.md new file mode 100644 index 00000000..6cc02ee7 --- /dev/null +++ b/stories/Schema/LocationControl/README.md @@ -0,0 +1,103 @@ +# Location control + +Below is an example of how you can control the location of the fields in the dom via pure schema. + +By default, conditionals will render at the end. You can use `conditional:[label]` to tell informed where to render the conditional schema. + +** Note: This is in beta and is subject to change! ** + + + +```jsx +import { Form, SchemaFields } from 'informed'; + +const schema = { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + title: 'First name', + 'ui:control': 'input' + }, + 'component:marriedLabel': { + 'ui:control': 'label', + title: 'Are you married?' + }, + married: { + type: 'string', + title: 'Are you married?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + }, + 'conditional:married': {}, + 'component:ofAgeLabel': { + 'ui:control': 'label', + title: 'Are you over 21?' + }, + ofAge: { + type: 'string', + title: 'Are you over 21?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + }, + 'conditional:over21': {} + }, + allOf: [ + { + relevant: 'married', + if: { + properties: { + married: { const: 'yes' } + }, + required: ['married'] + }, + then: { + properties: { + spouse: { + type: 'string', + title: 'Spouse name', + 'ui:control': 'input' + } + } + } + }, + { + relevant: 'over21', + if: { + properties: { + ofAge: { const: 'yes' } + }, + required: ['ofAge'] + }, + then: { + properties: { + age: { + type: 'number', + title: 'Age', + 'ui:control': 'input', + 'input:props': { + type: 'number' + } + }, + drink: { + type: 'string', + title: 'What do you want to drink?', + 'ui:control': 'input' + } + } + } + } + ] +}; + +const Component = () => { + return ( +
+ + + + + ); +}; +``` diff --git a/stories/Schema/LocationControl/index.js b/stories/Schema/LocationControl/index.js new file mode 100644 index 00000000..75fd2037 --- /dev/null +++ b/stories/Schema/LocationControl/index.js @@ -0,0 +1,98 @@ +import React from 'react'; +import withDocs from '../../utils/withDocs'; +import readme from './README.md'; +import FormState from '../../utils/FormState'; + +import { Form, SchemaFields } from '../../../src'; + +const schema = { + type: 'object', + required: ['name'], + properties: { + name: { + type: 'string', + title: 'First name', + 'ui:control': 'input' + }, + 'component:marriedLabel': { + 'ui:control': 'label', + title: 'Are you married?' + }, + married: { + type: 'string', + title: 'Are you married?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + }, + 'conditional:married': {}, + 'component:ofAgeLabel': { + 'ui:control': 'label', + title: 'Are you over 21?' + }, + ofAge: { + type: 'string', + title: 'Are you over 21?', + enum: ['yes', 'no'], + 'ui:control': 'radio' + }, + 'conditional:over21': {} + }, + allOf: [ + { + relevant: 'married', + if: { + properties: { + married: { const: 'yes' } + }, + required: ['married'] + }, + then: { + properties: { + spouse: { + type: 'string', + title: 'Spouse name', + 'ui:control': 'input' + } + } + } + }, + { + relevant: 'over21', + if: { + properties: { + ofAge: { const: 'yes' } + }, + required: ['ofAge'] + }, + then: { + properties: { + age: { + type: 'number', + title: 'Age', + 'ui:control': 'input', + 'input:props': { + type: 'number' + } + }, + drink: { + type: 'string', + title: 'What do you want to drink?', + 'ui:control': 'input' + } + } + } + } + ] +}; + +const Component = () => { + return ( +
+ + + + + ); +}; + +export default withDocs(readme, Component); diff --git a/stories/index.js b/stories/index.js index fba4b45b..04784be1 100644 --- a/stories/index.js +++ b/stories/index.js @@ -12,6 +12,8 @@ import NestedSchema from './Schema/NestedSchema'; import ArrayFieldSchema from './Schema/ArrayFieldSchema'; import ArrayFieldSchemaRelevant from './Schema/ArrayFieldSchemaRelevant'; import ArrayFieldSchemaNested from './Schema/ArrayFieldSchemaNested'; +import FormattedConditionalSchema from './Schema/FormattedConditionalSchema'; +import LocationControl from './Schema/LocationControl'; import CustomSchema from './Schema/CustomSchema'; import Complex from './Form/Complex'; import Big from './Form/Big'; @@ -203,6 +205,8 @@ storiesOf('Schema', module) .add('Formatted Schema', FormattedSchema) .add('Array Field Schema', ArrayFieldSchema) .add('Conditional Schema', ConditionalSchema) + .add('Formatted Conditional Schema', FormattedConditionalSchema) + .add('Location Control', LocationControl) .add('Custom Schema', CustomSchema) .add('Nested Array Fields', ArrayFieldSchemaNested) .add('Relevant ArrayField Schema', ArrayFieldSchemaRelevant);