diff --git a/package.json b/package.json index 0f4a0cd..d3155f5 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ }, "dependencies": { "clsx": "^1.2.1", - "formik": "^2.2.9" + "formik": "^2.2.9", + "lodash": "^4.17.21" }, "resolutions": { "react-formio/formiojs": "~4.13.0" diff --git a/src/components/formio/textfield/textfield.component.tsx b/src/components/formio/textfield/textfield.component.tsx index 0bdd46e..9cb8b24 100644 --- a/src/components/formio/textfield/textfield.component.tsx +++ b/src/components/formio/textfield/textfield.component.tsx @@ -62,7 +62,7 @@ export const TextField: React.FC = ({callbacks, component, valu {..._callbacks} /> - {!pristineState && component.showCharCount && } + {!pristineState && component.showCharCount && value && } {component.description && } {!pristineState && errors?.length > 0 && } diff --git a/src/components/utils/errors/errors.component.tsx b/src/components/utils/errors/errors.component.tsx index 2d940ae..145db5a 100644 --- a/src/components/utils/errors/errors.component.tsx +++ b/src/components/utils/errors/errors.component.tsx @@ -1,9 +1,10 @@ +import {ValidationError} from '@lib/validation'; import clsx from 'clsx'; import React from 'react'; export interface IErrorsProps { componentId: string; - errors: string[]; + errors: ValidationError[]; } /** @@ -16,11 +17,11 @@ export const Errors: React.FC = ({componentId, errors}) => { return (
    - {errors?.map((error: string, index: number) => { + {errors?.map((error, index: number) => { return (
  • ); diff --git a/src/components/utils/errors/errors.stories.tsx b/src/components/utils/errors/errors.stories.tsx index fa951d8..6e0717c 100644 --- a/src/components/utils/errors/errors.stories.tsx +++ b/src/components/utils/errors/errors.stories.tsx @@ -13,7 +13,11 @@ export default meta; export const errors: ComponentStory = args => ; errors.args = { - errors: ['Postcode is required', 'Postcode does not match the pattern ^\\d{4}\\s?[a-zA-Z]{2}$'], + // errors: [{message: 'Postcode is required'}, {message: 'Postcode does not match the pattern ^\\d{4}\\s?[a-zA-Z]{2}$'}], + errors: [ + {name: 'required', message: 'Postcode is required'}, + {name: 'pattern', message: 'Postcode does not match the pattern ^\\d{4}\\s?[a-zA-Z]{2}$'}, + ], }; errors.play = async ({canvasElement}) => { const canvas = within(canvasElement); diff --git a/src/containers/index.ts b/src/containers/index.ts new file mode 100644 index 0000000..e6a6634 --- /dev/null +++ b/src/containers/index.ts @@ -0,0 +1 @@ +export * from './multiple'; diff --git a/src/containers/multiple/index.ts b/src/containers/multiple/index.ts new file mode 100644 index 0000000..16bc0aa --- /dev/null +++ b/src/containers/multiple/index.ts @@ -0,0 +1 @@ +export * from './multiple.container'; diff --git a/src/containers/multiple/multiple.container.tsx b/src/containers/multiple/multiple.container.tsx new file mode 100644 index 0000000..ac81c75 --- /dev/null +++ b/src/containers/multiple/multiple.container.tsx @@ -0,0 +1,84 @@ +import {Component, Description, Label} from '@components'; +import {IRenderable, RenderComponent} from '@lib/renderer'; +import {IComponentProps, Value, Values} from '@types'; +import {ArrayHelpers, FieldArray} from 'formik'; +import {ComponentSchema} from 'formiojs'; +import React from 'react'; + +export interface IMultipleComponent extends ComponentSchema { + key: string; + type: string; +} + +export interface IMultipleProps extends IComponentProps { + component: IMultipleComponent; + value: Values; +} + +/** + * Implements `multiple: true` behaviour. + * + * Provide a thin wrapper around a component with controls for adding multiple instances. Utilizes + * to render individual instances. + */ +export const Multiple: React.FC = props => { + const {component, form, path, value = []} = props; // FIXME: Awaits future pr. + + /** Renders individual components utilizing . */ + const renderComponent = (value: Value, index: number, remove: ArrayHelpers['remove']) => { + // Clone and adjust component to fit nested needs. + const renderable: IRenderable = { + ...structuredClone(component), + key: `${path}.${index}`, // Trigger Formik array values. + multiple: false, // Handled by + description: '', // One description rendered for all components. + label: '', // One label rendered for all components. + }; + + return ( + + + + + + + + + ); + }; + + return ( + + {props.component.label && ( + + ); +}; diff --git a/src/containers/multiple/multiple.stories.tsx b/src/containers/multiple/multiple.stories.tsx new file mode 100644 index 0000000..637aee5 --- /dev/null +++ b/src/containers/multiple/multiple.stories.tsx @@ -0,0 +1,114 @@ +import {DEFAULT_RENDER_CONFIGURATION, RenderForm} from '@lib/renderer'; +import {expect} from '@storybook/jest'; +import type {ComponentStory, Meta} from '@storybook/react'; +import {userEvent, waitFor, within} from '@storybook/testing-library'; + +import {Multiple} from './multiple.container'; + +const meta: Meta = { + title: 'Containers / Multiple', + component: Multiple, + decorators: [], + parameters: {}, +}; +export default meta; + +export const multipleTextfields: ComponentStory = args => ( + +); +multipleTextfields.args = { + configuration: DEFAULT_RENDER_CONFIGURATION, + form: { + display: 'form', + components: [ + { + type: 'textfield', + key: 'multiple-inputs', + description: 'Array of strings instead of a single string value', + label: 'Multiple inputs', + multiple: true, + showCharCount: true, + validate: { + required: true, + maxLength: 3, + minLength: 1, + }, + }, + ], + }, + initialValues: { + 'multiple-inputs': ['first value'], + }, +}; +multipleTextfields.play = async ({canvasElement}) => { + const canvas = within(canvasElement); + + // check that new items can be added + await userEvent.click(canvas.getByRole('button', {name: 'Add another'})); + const input1 = await canvas.getAllByRole('textbox')[0]; + expect(input1).toHaveDisplayValue('first value'); + await userEvent.clear(input1); + await userEvent.type(input1, 'Foo'); + expect(input1).toHaveDisplayValue('Foo'); + + const input2 = await canvas.getAllByRole('textbox')[1]; + expect(input2).toHaveDisplayValue(''); + + // the label & description should be rendered only once, even with > 1 inputs + expect(canvas.queryAllByText('Multiple inputs')).toHaveLength(1); + expect(canvas.queryAllByText('Array of strings instead of a single string value')).toHaveLength( + 1 + ); + + // finally, it should be possible delete rows again + const removeButtons = await canvas.findAllByRole('button', {name: 'Remove item'}); + expect(removeButtons).toHaveLength(2); + await userEvent.click(removeButtons[0]); + expect(await canvas.getAllByRole('textbox')[0]).toHaveDisplayValue(''); + expect(await canvas.getAllByRole('textbox')).toHaveLength(1); +}; + +export const multipleTextfieldsWithValidation: ComponentStory = args => ( + +); +multipleTextfieldsWithValidation.args = { + configuration: DEFAULT_RENDER_CONFIGURATION, + form: { + display: 'form', + components: [ + { + type: 'textfield', + key: 'multiple-inputs', + description: 'Array of strings instead of a single string value', + label: 'Multiple inputs', + multiple: true, + showCharCount: true, + validate: { + required: true, + }, + }, + ], + }, + initialValues: { + 'multiple-inputs': ['first value'], + }, +}; +multipleTextfieldsWithValidation.play = async ({canvasElement}) => { + const canvas = within(canvasElement); + + // check that new items can be added + await userEvent.click(canvas.getByRole('button', {name: 'Add another'})); + const input1 = await canvas.getAllByRole('textbox')[0]; + const input2 = await canvas.getAllByRole('textbox')[1]; + + await userEvent.type(input1, 'foo', {delay: 300}); + await userEvent.type(input2, 'bar', {delay: 300}); + + await userEvent.clear(input1); + expect(await canvas.findAllByText('Multiple inputs is required')).toHaveLength(1); + + await userEvent.clear(input2); + waitFor(async () => { + expect(await canvas.findAllByText('Multiple inputs is required')).toHaveLength(2); + }); +}; diff --git a/src/lib/renderer/renderer.tsx b/src/lib/renderer/renderer.tsx index 797aa0a..dee4e29 100644 --- a/src/lib/renderer/renderer.tsx +++ b/src/lib/renderer/renderer.tsx @@ -1,6 +1,7 @@ import {Column, Columns, Content, IColumnProps, TextField} from '@components'; -import {DEFAULT_VALIDATORS, getFormErrors} from '@lib/validation'; -import {IComponentProps, IFormioForm, IRenderConfiguration, IValues} from '@types'; +import {Multiple} from '@containers'; +import {DEFAULT_VALIDATORS, ValidationError, getFormErrors} from '@lib/validation'; +import {IComponentProps, IFormioForm, IRenderConfiguration, IValues, Value, Values} from '@types'; import {Formik, useField, useFormikContext} from 'formik'; import {FormikHelpers} from 'formik/dist/types'; import {Utils} from 'formiojs'; @@ -14,6 +15,9 @@ export const DEFAULT_RENDER_CONFIGURATION: IRenderConfiguration = { content: Content, textfield: TextField, }, + containers: { + multiple: Multiple, + }, validators: DEFAULT_VALIDATORS, }; @@ -87,17 +91,25 @@ export const RenderForm: React.FC = ({ initialValues = {}, onSubmit, }) => { - const childComponents = - form.components?.map((c: IRenderable) => ( - - )) || null; + const childComponents: React.ReactElement[] = []; + // TODO: Refactor types + Utils.eachComponent( + form.components, + (c: IRenderable, path: string) => { + const component = ( + + ); + childComponents.push(component); + }, + true + ); return ( getFormErrors(form, values, configuration.validators)} + validate={async values => await getFormErrors(form, values, configuration.validators)} > {props => { return ( @@ -115,6 +127,8 @@ export const RenderForm: React.FC = ({ export interface IRenderComponentProps { component: IRenderable; form: IFormioForm; + path: string; + value?: Value | Values; } /** @@ -143,9 +157,14 @@ export interface IRenderComponentProps { * @external {FormikContext} Expects `Formik`/`FormikContext` to be available. * @external {RenderContext} Expects `RenderContext` to be available. */ -export const RenderComponent: React.FC = ({component, form}) => { - const {setFieldValue, values} = useFormikContext(); +export const RenderComponent: React.FC = ({ + component, + form, + path, + value = undefined, +}) => { const key = component.key || OF_MISSING_KEY; + const {setFieldValue, values} = useFormikContext(); const Component = useComponentType(component); const field = useField(key); @@ -162,9 +181,13 @@ export const RenderComponent: React.FC = ({component, for return null; } - const [{value, onBlur, onChange}, {error}] = field; + const [{onBlur, onChange}, {error}] = field; + const errors = error || []; + + // Allow the value to be overriden. + const _value = value !== undefined ? value : field[0].value; + const callbacks = {onBlur, onChange}; - const errors = error?.split('\n') || []; // Reconstruct array. // In certain cases a component (is not defined as) a component but something else (e.g. a column) // We deal with these edge cases by extending the schema with a custom (component) type allowing @@ -177,7 +200,7 @@ export const RenderComponent: React.FC = ({component, for // Regular children, either from component or column. const childComponents = cComponents?.map(c => ( - + )); // Columns from component. @@ -190,12 +213,21 @@ export const RenderComponent: React.FC = ({component, for type: 'column', }} form={form} + path={path} /> )); // Return the component, pass children. return ( - + {childComponents || childColumns} ); @@ -206,16 +238,36 @@ export const RenderComponent: React.FC = ({component, for * The Fallback component makes sure (child) components keep being rendered with as little side * effects as possible. */ -const Fallback = (props: IComponentProps) => {props.children}; +const Fallback = () => ; /** - * Custom hook resolving the `React.ComponentType` from `RenderContext`. + * Custom hook resolving the `React.ComponentType` based on the configuration in `RenderContext`. + * Resolving is performed in the following order: + * + * 1. - A `React.ComponentType` configured for a certain "containers" entry indicating an internal + * edge case. + * 2. - A `React.ComponentType` configured for a certain "components" entry indicating a regular + * component. + * 3. - A fallback component solely rendering `props.children`. * @external {RenderContext} Expects `RenderContext` to be available. */ export const useComponentType = ( component: IRenderable ): React.ComponentType => { const renderConfiguration = useContext(RenderContext); + const ContainerType = useContainerConfiguration(component); const ComponentType = renderConfiguration.components[component.type]; - return ComponentType || Fallback; + return ContainerType || ComponentType || Fallback; +}; + +/** + * Returns the applicable `containerConfiguration` (if any) for component. + * @see {IContainerConfiguration} + */ +export const useContainerConfiguration = (component: IRenderable) => { + const renderConfiguration = useContext(RenderContext); + if (component.multiple) { + return renderConfiguration.containers.multiple || null; + } + return null; }; diff --git a/src/lib/validation/validate.ts b/src/lib/validation/validate.ts index fb23cdd..3100baf 100644 --- a/src/lib/validation/validate.ts +++ b/src/lib/validation/validate.ts @@ -9,8 +9,9 @@ import { import {IFormioForm, IValues, Value, Values} from '@types'; import {FormikErrors} from 'formik'; import {ExtendedComponentSchema, Utils} from 'formiojs'; +import _ from 'lodash'; -export type validator = [ +export type Validator = [ ( ExtendedComponentSchema: ExtendedComponentSchema, value: Value, @@ -20,19 +21,13 @@ export type validator = [ string ]; -export const DEFAULT_VALIDATORS: validator[] = [ +export const DEFAULT_VALIDATORS: Validator[] = [ [validateMaxLength, '{{ label }} must have no more than {{ limit }} characters.'], [validateMinLength, '{{ label }} must have at least {{ limit }} characters.'], [validatePattern, '{{ label }} does not match the pattern {{ pattern }}'], [validateRequired, '{{ label }} is required'], ]; -type ErrorMap = - | Record - | { - [key: string]: ErrorMap; - }; - /** * Validates `form` and combines errors for each component. * TODO: Implement "scoring/thresholds" for validators (determine what errors to show in specific cases). @@ -41,44 +36,22 @@ type ErrorMap = * @param form Formio form. * @param values The values to validate. * @param validators See `validate` for more information. - * @return A promise which resolves (`void`) if all `values` are considered valid and rejects - * (`FormikErrors`) it is considered invalid. + * @return A promise which resolves (`void`) if all `values` are considered valid or + * (`FormikErrors`) if it is considered invalid. */ export const getFormErrors = async ( form: IFormioForm, values: IValues, - validators: validator[] = DEFAULT_VALIDATORS + validators: Validator[] = DEFAULT_VALIDATORS ): Promise | void> => { try { await validateForm(form, values, validators); return; } catch (result) { - const errors: FormikErrors = {}; - // Convert the validation errors to messages. - Object.entries(result).forEach(([key, validationErrors]: [string, ValidationError[]]) => { - const [tail, ...bits] = key.split('.').reverse(); - let localErrorObject = errors as ErrorMap; - // deep-assign errors - bits.forEach(bit => { - if (!localErrorObject[bit]) { - localErrorObject[bit] = {}; - } - localErrorObject = localErrorObject[bit] as ErrorMap; - }); - const messages = validationErrors - .map(validationError => validationError.message.trim()) - .join('\n'); - localErrorObject[tail] = messages; - }); - return errors; + return result; } }; -const isSingleValue = (obj: IValues | Value | Values): obj is Value => { - if (obj === null) return true; // typeof null === 'object' - return typeof obj !== 'object'; -}; - /** * Validates `form`. * @@ -92,13 +65,13 @@ const isSingleValue = (obj: IValues | Value | Values): obj is Value => { * @param validators See `validate` for more information. * @throws {ValidationError[]} As promise rejection if invalid. * @return A promise which resolves (`void`) if all `values` are considered valid and rejects - * (`{[index: string]: ValidationError[]}`) it is considered invalid. + * (`{[index: string]: ValidationError[] | ValidationError[][]}`) it is considered invalid. */ export const validateForm = async ( form: IFormioForm, values: IValues, validators = DEFAULT_VALIDATORS -): Promise<{[index: string]: ValidationError[]} | void> => { +): Promise<{[index: string]: ValidationError[] | ValidationError[][]} | void> => { const errors: {[key: string]: Error} = {}; // Collect all the components in the form definition so that each components set of @@ -111,83 +84,109 @@ export const validateForm = async ( // Run validation for all components in the form definition const promises = componentsToValidate.map(async component => { const key = component.key || OF_MISSING_KEY; - // lodash.get like to support nested data structures/keys with dots - // TODO: works only for objects right now - // FIXME: types can be more complex, value of a file upload is not a scalar, but an - // object! - // FIXME: the accumulator casting is also less than ideal, it should be possible - // to infer this correctly. - const value = key.split('.').reduce((acc: Value | IValues, bit: string) => { - if (Array.isArray(acc)) { - throw new Error('Arrays not supported yet'); - } - if (acc === null) return null; - const nestedProp = (acc as IValues)[bit]; - if (isSingleValue(nestedProp)) { - return nestedProp; - } - return nestedProp; - }, values) as Value; + const valueOrValues = _.get(values, key) as Value | Values; try { - await validate(component, value, values, validators); - } catch (e) { - errors[key] = e; + await validate(component, valueOrValues, values, validators); + } catch (validationErrors) { + _.set(errors, key, validationErrors); } }); await Promise.all(promises); if (Object.keys(errors).length) { - return Promise.reject(errors); + throw errors; } - return; }; /** * Validates a component. * - * Runs each function in `validators` and passing it `ExtendedComponentSchema` and `message`. + * Runs each function in `validators` and passing it `component`, `valueOrValues`, `message`. and + * `formValues`. * If all validators resolve a components is considered valid and the returned `Promise` resolves. - * If not: the returned `Promise` rejects with an array of `ValidationError` (subclasses). + * If not: the returned `Promise` rejects with a (possibly nested) array of `ValidationError` (subclasses). * - * @param ExtendedComponentSchema Formio component schema passed to each validator. - * @param value The value to validate. + * @param component Formio component schema passed to each validator. + * @param valueOrValues The value(s) to validate. * @param formValues All the form values. * * @param validators An array of `async function`/`string` tuples. The function being the validator * function, the string a message used to indicate an invalid value. Each validator is run in order. * - * The validator function is called with `ExtendedComponentSchema`, the `value` and the error `message`. + * The validator function is called with `component`, the `valueOrValues`, the 'message' and all the + * `formValues`. * The (async/sync) function should return a `Promise` which either resolves (valid) or rejects * (invalid) with a (subclass of) `ValidationError` instantiated with `message`. * - * @throws {ValidationError[]} As promise rejection if invalid. + * @throws {ValidationError[] | ValidationError[][]} As promise rejection if invalid. * * @return A promise which resolves (`void`) if `value` is considered valid and rejects - * (`ValidationError[]`) it is considered invalid. + * (`ValidationError[] | ValidationError[][]`) it is considered invalid. */ export const validate = async ( - ExtendedComponentSchema: ExtendedComponentSchema, - value: Value, + component: ExtendedComponentSchema, + valueOrValues: Value | Values, formValues: IValues, validators = DEFAULT_VALIDATORS +): Promise => { + // Array of values (multiple), simple implementation for now. + if (Array.isArray(valueOrValues)) { + return validateValues(component, valueOrValues, formValues, validators); + } + // Single value. + return validateValue(component, valueOrValues, formValues, validators); +}; + +/** + * Implements validate for a multiple values. + * @see {validate} + */ +export const validateValues = async ( + component: ExtendedComponentSchema, + values: Values, + formValues: IValues, + validators: Validator[] +): Promise => { + // Array of values (multiple) simple implementation for now. + const results = Array(values.length); + const promises = values.map((value, index) => { + return validateValue(component, value, formValues, validators).catch(errorsForInstance => { + return (results[index] = errorsForInstance); + }); + }); + + await Promise.all(promises); + if (results.some(v => v)) { + throw results; + } +}; + +/** + * Implements validate for a singular value. + * @see {validate} + */ +export const validateValue = async ( + component: ExtendedComponentSchema, + value: Value, + formValues: IValues, + validators: Validator[] ): Promise => { // Map all validators into an array of `Promise`s, implementing a catch handler building the // `errors` array. - const errors: ValidationError[] = []; + const validationErrors: ValidationError[] = []; const promises = validators.map(async ([validatorFunction, message]) => { try { - await validatorFunction(ExtendedComponentSchema, value, message, formValues); + await validatorFunction(component, value, message, formValues); } catch (e) { - errors.push(e); + validationErrors.push({name: e.name, message: e.message}); } }); // Wait until all promises are completed. When so: check if the `errors` array contains items, // indicating that at least one validation error exists in the array. await Promise.all(promises); - if (errors.length) { - return Promise.reject(errors); + if (validationErrors.length) { + throw validationErrors; } - return Promise.resolve(); }; diff --git a/src/lib/validation/validators/maxlength.ts b/src/lib/validation/validators/maxlength.ts index 38f0b98..7841fcd 100644 --- a/src/lib/validation/validators/maxlength.ts +++ b/src/lib/validation/validators/maxlength.ts @@ -22,5 +22,5 @@ export const validateMaxLength = async ( }; export class MaxLengthValidationError extends ValidationError { - validator = 'maxlength'; + name = 'maxlength'; } diff --git a/src/lib/validation/validators/minlength.ts b/src/lib/validation/validators/minlength.ts index da3c9e9..eddf656 100644 --- a/src/lib/validation/validators/minlength.ts +++ b/src/lib/validation/validators/minlength.ts @@ -22,5 +22,5 @@ export const validateMinLength = async ( }; export class MinLengthValidationError extends ValidationError { - validator = 'minlength'; + name = 'minlength'; } diff --git a/src/lib/validation/validators/pattern.ts b/src/lib/validation/validators/pattern.ts index d54c573..fcae26f 100644 --- a/src/lib/validation/validators/pattern.ts +++ b/src/lib/validation/validators/pattern.ts @@ -21,5 +21,5 @@ export const validatePattern = async ( }; export class PatternValidationError extends ValidationError { - validator = 'pattern'; + name = 'pattern'; } diff --git a/src/lib/validation/validators/required.ts b/src/lib/validation/validators/required.ts index 83d48a0..637678f 100644 --- a/src/lib/validation/validators/required.ts +++ b/src/lib/validation/validators/required.ts @@ -21,5 +21,5 @@ export const validateRequired = async ( }; export class RequiredValidationError extends ValidationError { - validator = 'required'; + name = 'required'; } diff --git a/src/tests/custom-component/custom-component.stories.tsx b/src/tests/custom-component/custom-component.stories.tsx index 1bb69fb..1500c43 100644 --- a/src/tests/custom-component/custom-component.stories.tsx +++ b/src/tests/custom-component/custom-component.stories.tsx @@ -1,5 +1,5 @@ import {DEFAULT_RENDER_CONFIGURATION, RenderForm} from '@lib/renderer'; -import {DEFAULT_VALIDATORS, validator} from '@lib/validation'; +import {DEFAULT_VALIDATORS, Validator} from '@lib/validation'; import {expect} from '@storybook/jest'; import type {ComponentStory, Meta} from '@storybook/react'; import {userEvent, within} from '@storybook/testing-library'; @@ -14,7 +14,7 @@ const meta: Meta = { }; export default meta; -const VALIDATORS = DEFAULT_VALIDATORS.map(([fn, mssg], index): validator => { +const VALIDATORS = DEFAULT_VALIDATORS.map(([fn, mssg], index): Validator => { if (index === 3) { return [fn, 'Vul het verplichte veld in']; } @@ -42,9 +42,9 @@ const BootstrapInput: React.FC = ({callbacks, component, errors {...callbacks} /> - {errors.map(error => ( -
    - {error} + {errors.map((error, i) => ( +
    + {error.message}
    ))}
    diff --git a/src/types/componentprops.d.ts b/src/types/componentprops.d.ts index cd42247..e046eaa 100644 --- a/src/types/componentprops.d.ts +++ b/src/types/componentprops.d.ts @@ -1,17 +1,18 @@ +import {ValidationError} from '@lib/validation'; import {ComponentSchema} from 'formiojs'; import React from 'react'; import {ICallbackConfiguration} from './config'; +import {IFormioForm} from './form'; import {Value, Values} from './value'; -interface IComponentProps { +export interface IComponentProps { callbacks: ICallbackConfiguration; - - children?: React.ReactNode; - component: ComponentSchema; - - errors: string[]; - + errors: ValidationError[]; + form: IFormioForm; value: Value | Values | undefined; + path: string; + setValue: (field: string, value: any, shouldValidate?: boolean) => void; + children?: React.ReactNode; } diff --git a/src/types/config.d.ts b/src/types/config.d.ts index 0718d3b..22eb139 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -1,12 +1,14 @@ import {IColumnProps} from '@components'; -import {validator} from '@lib/validation'; +import {IRenderable} from '@lib/renderer'; +import {Validator} from '@lib/validation'; import React from 'react'; import {IComponentProps} from './componentprops'; export interface IRenderConfiguration { components: IComponentConfiguration; - validators: validator[]; + containers: IContainerConfiguration; + validators: Validator[]; } /** @@ -22,6 +24,21 @@ export interface ICallbackConfiguration { export type callback = (e: Event | React.BaseSyntheticEvent) => void; +/** + * Describes a mapping between a component type (`ComponentSchema.type`) and a (React) component to + * render. + * @example `{type: "textfield"}` -> + */ export interface IComponentConfiguration { [index: string]: React.ComponentType; } + +/** + * Describes a mapping between an (internal) container type and a (React) component to render. The + * internal container type is not directly linked to a specific key in `ComponentSchema` but may be + * referenced directly by the renderer based specific conditions. + * @example `{multiple: "true"}` -> + */ +export interface IContainerConfiguration { + [index: string]: React.ComponentType; +} diff --git a/tsconfig.json b/tsconfig.json index bf43f33..e9a8caa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ "paths": { "@components": ["src/components/index"], "@components/*": ["src/components/*"], + "@containers": ["src/containers/index"], "@fixtures": ["src/fixtures/index"], "@lib/*": ["src/lib/*"], "@types": ["src/types/index"]