UNPKG

@rjsf/core

Version:

A simple React component capable of building HTML forms out of a JSON schema.

709 lines (707 loc) 37.1 kB
import { createElement as _createElement } from "react"; import { jsx as _jsx } from "react/jsx-runtime"; import { ANY_OF_KEY, getDiscriminatorFieldFromSchema, getTemplate, getTestIds, getUiOptions, hashObject, ID_KEY, lookupFromFormContext, ONE_OF_KEY, PROPERTIES_KEY, READONLY_KEY, toFieldPathId, UI_OPTIONS_KEY, UI_GLOBAL_OPTIONS_KEY, ITEMS_KEY, useDeepCompareMemo, } from '@rjsf/utils'; import each from 'lodash-es/each.js'; import flatten from 'lodash-es/flatten.js'; import get from 'lodash-es/get.js'; import has from 'lodash-es/has.js'; import includes from 'lodash-es/includes.js'; import intersection from 'lodash-es/intersection.js'; import isEmpty from 'lodash-es/isEmpty.js'; import isFunction from 'lodash-es/isFunction.js'; import isEqual from 'lodash-es/isEqual.js'; import isObject from 'lodash-es/isObject.js'; import isPlainObject from 'lodash-es/isPlainObject.js'; import isString from 'lodash-es/isString.js'; import isUndefined from 'lodash-es/isUndefined.js'; import last from 'lodash-es/last.js'; import set from 'lodash-es/set.js'; /** The enumeration of the three different Layout GridTemplate type values */ export var GridType; (function (GridType) { GridType["ROW"] = "ui:row"; GridType["COLUMN"] = "ui:col"; GridType["COLUMNS"] = "ui:columns"; GridType["CONDITION"] = "ui:condition"; })(GridType || (GridType = {})); /** The enumeration of the different operators within a condition */ export var Operators; (function (Operators) { Operators["ALL"] = "all"; Operators["SOME"] = "some"; Operators["NONE"] = "none"; })(Operators || (Operators = {})); /** The regular expression that is used to detect whether a string contains a lookup key */ export const LOOKUP_REGEX = /^\$lookup=(.+)/; /** The constant representing the main layout grid schema option name in the `uiSchema` */ export const LAYOUT_GRID_UI_OPTION = 'layoutGrid'; /** The constant representing the main layout grid schema option name in the `uiSchema` */ export const LAYOUT_GRID_OPTION = `ui:${LAYOUT_GRID_UI_OPTION}`; /** Returns either the `value` if it is non-nullish or the fallback * * @param [value] - The potential value to return if it is non-nullish * @param [fallback] - The fallback value to return if `value` is nullish * @returns - `value` if it is non-nullish otherwise `fallback` */ function getNonNullishValue(value, fallback) { return value ?? fallback; } /** Detects if a `str` is made up entirely of numeric characters * * @param str - The string to check to see if it is a numeric index * @return - True if the string consists entirely of numeric characters */ function isNumericIndex(str) { return /^\d+?$/.test(str); // Matches positive integers } const LAYOUT_GRID_FIELD_TEST_IDS = getTestIds(); /** Computes the uiSchema for the field with `name` from the `uiProps` and `uiSchema` provided. The field UI Schema * will always contain a copy of the global options from the `uiSchema` (so they can be passed down) as well as * copying them into the local ui options. When the `forceReadonly` flag is true, then the field UI Schema is * updated to make "readonly" be true. When the `schemaReadonly` flag is true AND the field UI Schema does NOT have * the flag already provided, then we also make "readonly" true. We always make sure to return the final value of the * field UI Schema's "readonly" flag as `uiReadonly` along with the `fieldUiSchema` in the return value. * * @param field - The name of the field to pull the existing UI Schema for * @param uiProps - Any props that should be put into the field's uiSchema * @param [uiSchema] - The optional UI Schema from which to get the UI schema for the field * @param [schemaReadonly] - Optional flag indicating whether the schema indicates the field is readonly * @param [forceReadonly] - Optional flag indicating whether the Form itself is in readonly mode */ export function computeFieldUiSchema(field, uiProps, uiSchema, schemaReadonly, forceReadonly) { const globalUiOptions = get(uiSchema, [UI_GLOBAL_OPTIONS_KEY], {}); const localUiSchema = get(uiSchema, field); const localUiOptions = { ...get(localUiSchema, [UI_OPTIONS_KEY], {}), ...uiProps, ...globalUiOptions }; const fieldUiSchema = { ...localUiSchema }; if (!isEmpty(localUiOptions)) { set(fieldUiSchema, [UI_OPTIONS_KEY], localUiOptions); } if (!isEmpty(globalUiOptions)) { // pass the global uiOptions down to the field uiSchema so that they can be applied to all nested fields set(fieldUiSchema, [UI_GLOBAL_OPTIONS_KEY], globalUiOptions); } let { readonly: uiReadonly } = getUiOptions(fieldUiSchema); if (forceReadonly === true || (isUndefined(uiReadonly) && schemaReadonly === true)) { // If we are forcing all widgets to be readonly, OR the schema indicates it is readonly AND the uiSchema does not // have an overriding value, then update the uiSchema to set readonly to true. Doing this will uiReadonly = true; if (has(localUiOptions, READONLY_KEY)) { // If the local options has the key value provided in it, then set that one to true set(fieldUiSchema, [UI_OPTIONS_KEY, READONLY_KEY], true); } else { // otherwise set the `ui:` version set(fieldUiSchema, `ui:${READONLY_KEY}`, true); } } return { fieldUiSchema, uiReadonly }; } /** Given an `operator`, `datum` and `value` determines whether this condition is considered matching. Matching * depends on the `operator`. The `datum` and `value` are converted into arrays if they aren't already and then the * contents of the two arrays are compared using the `operator`. When `operator` is All, then the two arrays must be * equal to match. When `operator` is SOME then the intersection of the two arrays must have at least one value in * common to match. When `operator` is NONE then the intersection of the two arrays must not have any values in common * to match. * * @param [operator] - The optional operator for the condition * @param [datum] - The optional datum for the condition, this can be an item or a list of items of type unknown * @param [value='$0m3tH1nG Un3xP3cT3d'] The optional value for the condition, defaulting to a highly unlikely value * to avoid comparing two undefined elements when `value` was forgotten in the condition definition. * This can be an item or a list of items of type unknown * @returns - True if the condition matches, false otherwise */ export function conditionMatches(operator, datum, value = '$0m3tH1nG Un3xP3cT3d') { const data = flatten([datum]).sort(); const values = flatten([value]).sort(); switch (operator) { case Operators.ALL: return isEqual(data, values); case Operators.SOME: return intersection(data, values).length > 0; case Operators.NONE: return intersection(data, values).length === 0; default: return false; } } /** From within the `layoutGridSchema` finds the `children` and any extra `gridProps` from the object keyed by * `schemaKey`. If the `children` contains extra `gridProps` and those props contain a `className` string, try to * lookup whether that `className` has a replacement value in the `registry` using the `FORM_CONTEXT_LOOKUP_BASE`. * When the `className` value contains multiple classNames separated by a space, the lookup will look for a * replacement value for each `className` and combine them into one. * * @param layoutGridSchema - The GridSchemaType instance from which to obtain the `schemaKey` children and extra props * @param schemaKey - A `GridType` value, used to get the children and extra props from within the `layoutGridSchema` * @param registry - The `@rjsf` Registry from which to look up `classNames` if they are present in the extra props * @returns - An object containing the list of `LayoutGridSchemaType` `children` and any extra `gridProps` * @throws - A `TypeError` when the `children` is not an array */ export function findChildrenAndProps(layoutGridSchema, schemaKey, registry) { let gridProps = {}; let children = layoutGridSchema[schemaKey]; if (isPlainObject(children)) { const { children: elements, className: toMapClassNames, ...otherProps } = children; children = elements; if (toMapClassNames) { const classes = toMapClassNames.split(' '); const className = classes.map((ele) => lookupFromFormContext(registry, ele, ele)).join(' '); gridProps = { ...otherProps, className }; } else { gridProps = otherProps; } } if (!Array.isArray(children)) { throw new TypeError(`Expected array for "${schemaKey}" in ${JSON.stringify(layoutGridSchema)}`); } return { children: children, gridProps }; } /** Computes the `rawSchema` and `fieldPathId` for a `schema` and a `potentialIndex`. If the `schema` is of type array, * has an `ITEMS_KEY` element and `potentialIndex` represents a numeric value, the element at `ITEMS_KEY` is checked * to see if it is an array. If it is AND the `potentialIndex`th element is available, it is used as the `rawSchema`, * otherwise the last value of the element is used. If it is not, then the element is used as the `rawSchema`. In * either case, an `fieldPathId` is computed for the array index. If the `schema` does not represent an array or the * `potentialIndex` is not a numeric value, then `rawSchema` is returned as undefined and given `fieldPathId` is returned * as is. * * @param schema - The schema to generate the fieldPathId for * @param fieldPathId - The FieldPathId for the schema * @param potentialIndex - A string containing a potential index * @returns - An object containing the `rawSchema` and `fieldPathId` of an array item, otherwise an undefined `rawSchema` */ export function computeArraySchemasIfPresent(schema, fieldPathId, potentialIndex) { let rawSchema; if (isNumericIndex(potentialIndex) && schema && schema?.type === 'array' && has(schema, ITEMS_KEY)) { const index = Number(potentialIndex); const items = schema[ITEMS_KEY]; if (Array.isArray(items)) { if (index > items.length) { rawSchema = last(items); } else { rawSchema = items[index]; } } else { rawSchema = items; } fieldPathId = { [ID_KEY]: fieldPathId[ID_KEY], path: [...fieldPathId.path.slice(0, fieldPathId.path.length - 1), index], }; } return { rawSchema, fieldPathId }; } /** Given a `dottedPath` to a field in the `initialSchema`, iterate through each individual path in the schema until * the leaf path is found and returned (along with whether that leaf path `isRequired`) OR no schema exists for an * element in the path. If the leaf schema element happens to be a oneOf/anyOf then also return the oneOf/anyOf as * `options`. * * @param registry - The registry * @param dottedPath - The dotted-path to the field for which to get the schema * @param initialSchema - The initial schema to start the search from * @param formData - The formData, useful for resolving a oneOf/anyOf selection in the path hierarchy * @param initialFieldIdPath - The initial fieldPathId to start the search from * @returns - An object containing the destination schema, isRequired and isReadonly flags for the field and options * info if a oneOf/anyOf */ export function getSchemaDetailsForField(registry, dottedPath, initialSchema, formData, initialFieldIdPath) { const { schemaUtils, globalFormOptions } = registry; let rawSchema = initialSchema; let fieldPathId = initialFieldIdPath; const parts = dottedPath.split('.'); const leafPath = parts.pop(); // pop off the last element in the list as the leaf let schema = schemaUtils.retrieveSchema(rawSchema, formData); // always returns an object let innerData = formData; let isReadonly = schema.readOnly; // For all the remaining path parts parts.forEach((part) => { // dive into the properties of the current schema (when it exists) and get the schema for the next part fieldPathId = toFieldPathId(part, globalFormOptions, fieldPathId); if (has(schema, PROPERTIES_KEY)) { rawSchema = get(schema, [PROPERTIES_KEY, part], {}); } else if (schema && (has(schema, ONE_OF_KEY) || has(schema, ANY_OF_KEY))) { const xxx = has(schema, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY; // When the schema represents a oneOf/anyOf, find the selected schema for it and grab the inner part const selectedSchema = schemaUtils.findSelectedOptionInXxxOf(schema, part, xxx, innerData); rawSchema = get(selectedSchema, [PROPERTIES_KEY, part], {}); } else { const result = computeArraySchemasIfPresent(schema, fieldPathId, part); rawSchema = result.rawSchema ?? {}; fieldPathId = result.fieldPathId; } // Now drill into the innerData for the part, returning an empty object by default if it doesn't exist innerData = get(innerData, part, {}); // Resolve any `$ref`s for the current rawSchema schema = schemaUtils.retrieveSchema(rawSchema, innerData); isReadonly = getNonNullishValue(schema.readOnly, isReadonly); }); let optionsInfo; let isRequired = false; // retrieveSchema will return an empty schema in the worst case scenario, convert it to undefined if (isEmpty(schema)) { schema = undefined; } if (schema && leafPath) { // When we have both a schema and a leafPath... if (schema && (has(schema, ONE_OF_KEY) || has(schema, ANY_OF_KEY))) { const xxx = has(schema, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY; // Grab the selected schema for the oneOf/anyOf value for the leafPath using the innerData schema = schemaUtils.findSelectedOptionInXxxOf(schema, leafPath, xxx, innerData); } fieldPathId = toFieldPathId(leafPath, globalFormOptions, fieldPathId); isRequired = schema !== undefined && Array.isArray(schema.required) && includes(schema.required, leafPath); const result = computeArraySchemasIfPresent(schema, fieldPathId, leafPath); if (result.rawSchema) { schema = result.rawSchema; fieldPathId = result.fieldPathId; } else { // Now grab the schema from the leafPath of the current schema properties schema = get(schema, [PROPERTIES_KEY, leafPath]); // Resolve any `$ref`s for the current schema schema = schema ? schemaUtils.retrieveSchema(schema) : schema; } isReadonly = getNonNullishValue(schema?.readOnly, isReadonly); if (schema && (has(schema, ONE_OF_KEY) || has(schema, ANY_OF_KEY))) { const xxx = has(schema, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY; // Set the options if we have a schema with a oneOf/anyOf const discriminator = getDiscriminatorFieldFromSchema(schema); optionsInfo = { options: schema[xxx], hasDiscriminator: !!discriminator }; } } return { schema, isRequired, isReadonly, optionsInfo, fieldPathId }; } /** Gets the custom render component from the `render`, by either determining that it is either already a function or * it is a non-function value that can be used to look up the function in the registry. If no function can be found, * null is returned. * * @param render - The potential render function or lookup name to one * @param registry - The `@rjsf` Registry from which to look up `classNames` if they are present in the extra props * @returns - Either a render function if available, or null if not */ export function getCustomRenderComponent(render, registry) { let customRenderer = render; if (isString(customRenderer)) { customRenderer = lookupFromFormContext(registry, customRenderer); } if (isFunction(customRenderer)) { return customRenderer; } return null; } /** Extract the `name`, and optional `render` and all other props from the `gridSchema`. We look up the `render` to * see if can be resolved to a UIComponent. If `name` does not exist and there is an optional `render` UIComponent, we * set the `rendered` component with only specified props for that component in the object. * * @param registry - The `@rjsf` Registry from which to look up `classNames` if they are present in the extra props * @param gridSchema - The string or object that represents the configuration for the grid field * @returns - The UIComponentPropsType computed from the gridSchema */ export function computeUIComponentPropsFromGridSchema(registry, gridSchema) { let name; let UIComponent = null; let uiProps = {}; let rendered; if (isString(gridSchema) || isUndefined(gridSchema)) { name = gridSchema ?? ''; } else { const { name: innerName = '', render, ...innerProps } = gridSchema; name = innerName; uiProps = innerProps; if (!isEmpty(uiProps)) { // Transform any `$lookup=` in the uiProps props with the appropriate value each(uiProps, (prop, key) => { if (isString(prop)) { const match = LOOKUP_REGEX.exec(prop); if (Array.isArray(match) && match.length > 1) { const name = match[1]; uiProps[key] = lookupFromFormContext(registry, name, name); } } }); } UIComponent = getCustomRenderComponent(render, registry); if (!innerName && UIComponent) { rendered = _jsx(UIComponent, { ...innerProps, "data-testid": LAYOUT_GRID_FIELD_TEST_IDS.uiComponent }); } } return { name, UIComponent, uiProps, rendered }; } /** Iterates through all the `childrenLayoutGridSchemaId`, rendering a nested `LayoutGridField` for each item in the * list, passing all the props for the current `LayoutGridField` along, updating the `schema` by calling * `retrieveSchema()` on it to resolve any `$ref`s. In addition to the updated `schema`, each item in * `childrenLayoutGridSchemaId` is passed as `layoutGridSchema`. * * @returns - The nested `LayoutGridField`s */ function LayoutGridFieldChildren(props) { const { childrenLayoutGridSchemaId, ...layoutGridFieldProps } = props; const { registry, schema: rawSchema, formData } = layoutGridFieldProps; const { schemaUtils } = registry; const schema = schemaUtils.retrieveSchema(rawSchema, formData); return childrenLayoutGridSchemaId.map((layoutGridSchema) => (_createElement(LayoutGridField, { ...layoutGridFieldProps, key: `layoutGrid-${hashObject(layoutGridSchema)}`, schema: schema, layoutGridSchema: layoutGridSchema }))); } /** Renders the `children` of the `GridType.CONDITION` if it passes. The `layoutGridSchema` for the * `GridType.CONDITION` is separated into the `children` and other `gridProps`. The `gridProps` are used to extract * the `operator`, `field` and `value` of the condition. If the condition matches, then all of the `children` are * rendered, otherwise null is returned. * * @returns - The rendered the children for the `GridType.CONDITION` or null */ function LayoutGridCondition(props) { const { layoutGridSchema, ...layoutGridFieldProps } = props; const { formData, registry } = layoutGridFieldProps; const { children, gridProps } = findChildrenAndProps(layoutGridSchema, GridType.CONDITION, registry); const { operator, field = '', value } = gridProps; const fieldData = get(formData, field, null); if (conditionMatches(operator, fieldData, value)) { return _jsx(LayoutGridFieldChildren, { ...layoutGridFieldProps, childrenLayoutGridSchemaId: children }); } return null; } /** Renders a `GridTemplate` as an item. The `layoutGridSchema` for the `GridType.COLUMN` is separated into the * `children` and other `gridProps`. The `gridProps` will be spread onto the outer `GridTemplate`. Inside the * `GridTemplate` all the `children` are rendered. * * @returns - The rendered `GridTemplate` containing the children for the `GridType.COLUMN` */ function LayoutGridCol(props) { const { layoutGridSchema, ...layoutGridFieldProps } = props; const { registry, uiSchema } = layoutGridFieldProps; const { children, gridProps } = findChildrenAndProps(layoutGridSchema, GridType.COLUMN, registry); const uiOptions = getUiOptions(uiSchema); const GridTemplate = getTemplate('GridTemplate', registry, uiOptions); return (_jsx(GridTemplate, { column: true, "data-testid": LAYOUT_GRID_FIELD_TEST_IDS.col, ...gridProps, children: _jsx(LayoutGridFieldChildren, { ...layoutGridFieldProps, childrenLayoutGridSchemaId: children }) })); } /** Renders a `GridTemplate` as an item. The `layoutGridSchema` for the `GridType.COLUMNS` is separated into the * `children` and other `gridProps`. The `children` is iterated on and `gridProps` will be spread onto the outer * `GridTemplate`. Each child will have their own rendered `GridTemplate`. * * @returns - The rendered `GridTemplate` containing the children for the `GridType.COLUMNS` */ function LayoutGridColumns(props) { const { layoutGridSchema, ...layoutGridFieldProps } = props; const { registry, uiSchema } = layoutGridFieldProps; const { children, gridProps } = findChildrenAndProps(layoutGridSchema, GridType.COLUMNS, registry); const uiOptions = getUiOptions(uiSchema); const GridTemplate = getTemplate('GridTemplate', registry, uiOptions); return children.map((child) => (_jsx(GridTemplate, { column: true, "data-testid": LAYOUT_GRID_FIELD_TEST_IDS.col, ...gridProps, children: _jsx(LayoutGridFieldChildren, { ...layoutGridFieldProps, childrenLayoutGridSchemaId: [child] }) }, `column-${hashObject(child)}`))); } /** Renders a `GridTemplate` as a container. The `layoutGridSchema` for the `GridType.ROW` is separated into the * `children` and other `gridProps`. The `gridProps` will be spread onto the outer `GridTemplate`. Inside of the * `GridTemplate` all of the `children` are rendered. * * @returns - The rendered `GridTemplate` containing the children for the `GridType.ROW` */ function LayoutGridRow(props) { const { layoutGridSchema, ...layoutGridFieldProps } = props; const { registry, uiSchema } = layoutGridFieldProps; const { children, gridProps } = findChildrenAndProps(layoutGridSchema, GridType.ROW, registry); const uiOptions = getUiOptions(uiSchema); const GridTemplate = getTemplate('GridTemplate', registry, uiOptions); return (_jsx(GridTemplate, { ...gridProps, "data-testid": LAYOUT_GRID_FIELD_TEST_IDS.row, children: _jsx(LayoutGridFieldChildren, { ...layoutGridFieldProps, childrenLayoutGridSchemaId: children }) })); } /** Renders the field described by `gridSchema`. If `gridSchema` is not an object, then is will be assumed * to be the dotted-path to the field in the schema. Otherwise, we extract the `name`, and optional `render` and all * other props. If `name` does not exist and there is an optional `render`, we return the `render` component with only * specified props for that component. If `name` exists, we take the name, the initial & root schemas and the formData * and get the destination schema, is required state and optional oneOf/anyOf options for it. If the destination * schema was located along with oneOf/anyOf options then a `LayoutMultiSchemaField` will be rendered with the * `uiSchema`, `errorSchema`, `fieldPathId` and `formData` drilled down to the dotted-path field, spreading any other * props from `gridSchema` into the `ui:options`. If the destination schema located without any oneOf/anyOf options, * then a `SchemaField` will be rendered with the same props as mentioned in the previous sentence. If no destination * schema was located, but a custom render component was found, then it will be rendered with many of the non-event * handling props. If none of the previous render paths are valid, then a null is returned. * * @returns - One of `LayoutMultiSchemaField`, `SchemaField`, a custom render component or null, depending */ function LayoutGridFieldComponent(props) { const { gridSchema, schema: initialSchema, uiSchema, errorSchema, fieldPathId, onBlur, onFocus, formData, readonly, registry, layoutGridSchema, // Used to pull this out of otherProps since we don't want to pass it through ...otherProps } = props; const { onChange } = otherProps; const { fields } = registry; const { SchemaField, LayoutMultiSchemaField } = fields; const uiComponentProps = computeUIComponentPropsFromGridSchema(registry, gridSchema); const { name, UIComponent, uiProps } = uiComponentProps; const { schema, isRequired, isReadonly, optionsInfo, fieldPathId: fieldIdSchema, } = getSchemaDetailsForField(registry, name, initialSchema, formData, fieldPathId); const memoFieldPathId = useDeepCompareMemo(fieldIdSchema); if (uiComponentProps.rendered) { return uiComponentProps.rendered; } if (schema) { const Field = optionsInfo?.hasDiscriminator ? LayoutMultiSchemaField : SchemaField; // Call this function to get the appropriate UISchema, which will always have its `readonly` state matching the // `uiReadonly` flag that it returns. This is done since the `SchemaField` will always defer to the `readonly` // state in the uiSchema over anything in the props or schema. Because we are implementing the "readonly" state of // the `Form` via the prop passed to `LayoutGridField` we need to make sure the uiSchema always has a true value // when it is needed const { fieldUiSchema, uiReadonly } = computeFieldUiSchema(name, uiProps, uiSchema, isReadonly, readonly); return (_jsx(Field, { "data-testid": optionsInfo?.hasDiscriminator ? LAYOUT_GRID_FIELD_TEST_IDS.layoutMultiSchemaField : LAYOUT_GRID_FIELD_TEST_IDS.field, ...otherProps, name: name, required: isRequired, readonly: uiReadonly, schema: schema, uiSchema: fieldUiSchema, errorSchema: get(errorSchema, name), fieldPathId: memoFieldPathId, formData: get(formData, name), onChange: onChange, onBlur: onBlur, onFocus: onFocus, options: optionsInfo?.options, registry: registry })); } if (UIComponent) { return (_jsx(UIComponent, { "data-testid": LAYOUT_GRID_FIELD_TEST_IDS.uiComponent, ...otherProps, name: name, required: isRequired, formData: formData, readOnly: !!isReadonly || readonly, errorSchema: errorSchema, uiSchema: uiSchema, schema: initialSchema, fieldPathId: fieldPathId, onBlur: onBlur, onFocus: onFocus, registry: registry, ...uiProps })); } return null; } /** The `LayoutGridField` will render a schema, uiSchema and formData combination out into a GridTemplate in the shape * described in the uiSchema. To define the grid to use to render the elements within a field in the schema, provide in * the uiSchema for that field the object contained under a `ui:layoutGrid` element. E.g. (as a JSON object): * * ``` * { * "field1" : { * "ui:field": "LayoutGridField", * "ui:layoutGrid": { * "ui:row": { ... } * } * } * } * ``` * * The outermost level of a `LayoutGridField` is the `ui:row` that defines the nested rows, columns, and/or condition * elements (i.e. "grid elements") in the grid. This definition is either a simple "grid elements" OR an object with * native `GridTemplate` implementation specific props and a `children` array of "grid elements". E.g. (as JSON objects): * * Simple `ui:row` definition, without additional `GridTemplate` props: * ``` * "ui:row": [ * { "ui:row"|"ui:col"|"ui:columns"|"ui:condition": ... }, * ... * ] * ``` * * Complex `ui:row` definition, with additional `GridTemplate` (this example uses @mui/material/Grid2 native props): * ``` * "ui:row": { * "spacing": 2, * "size": { md": 4 }, * "alignContent": "flex-start", * "className": "GridRow", * "children": [ * { "ui:row"|"ui:col"|"ui:columns"|"ui:condition": ... }, * ... * ] * } * ``` * * NOTE: Special note about the native `className` prop values. All className values will automatically be looked up in * the `formContext.lookupMap` in case they have been defined using a CSS-in-JS approach. In other words, from the * example above, if the `Form` was constructed with a `lookupMap` set to `{ GridRow: cssInJs.GridRowClass }` * then when rendered, the native `GridTemplate` will get the `className` with the value from * `cssInJs.GridRowClass`. This automatic lookup will happen for any of the "grid elements" when rendering with * `GridTemplate` props. If multiple className values are present, for example: * `{ className: 'GridRow GridColumn' }`, the classNames are split apart, looked up individually, and joined * together to form one className with the values from `cssInJs.GridRowClass` and `cssInJs.GridColumnClass`. * * The `ui:col` grid element is used to specify the list of columns within a grid row. A `ui:col` element can take on * several forms: 1) a simple list of dotted-path field names within the root field; 2) a list of objects containing the * dotted-path field `name` any other props that are gathered into `ui:options` for the field; 3) a list with a one-off * `render` functional component with or without a non-field `name` identifier and any other to-be-spread props; and * 4) an object with native `GridTemplate` implementation specific props and a `children` array with 1) or 2) or even a * nested `ui:row` or a `ui:condition` containing a `ui:row` (although this should be used carefully). E.g. * (as JSON objects): * * Simple `ui:col` definition, without additional `GridTemplate` props and form 1 only children: * ``` * "ui:col": ["innerField", "inner.grandChild", ...] * ``` * * Complicated `ui:col` definition, without additional `GridTemplate` props and form 2 only children: * ``` * "ui:col": [ * { "name": "innerField", "fullWidth": true }, * { "name": "inner.grandChild", "convertOther": true }, * ... * ] * ``` * * More complicated `ui:col` definition, without additional `GridTemplate` props and form 2 children, one being a * one-off `render` functional component without a non-field `name` identifier * ``` * "ui:col": [ * "innerField", * { * "render": "WizardNavButton", * "isNext": true, * "size": "large" * } * ] * ``` * * Most complicated `ui:col` definition, additional `GridTemplate` props and form 1, 2 and 3 children (this example * uses @mui/material/Grid2 native props): * ``` * "ui:col": { * "size": { "md": 4 }, * "className": "GridColumn", * "children": [ * "innerField", * { "name": "inner.grandChild", "convertOther": true }, * { "name": "customRender", "render": "CustomRender", toSpread: "prop-value" } * { "ui:row|ui:condition": ... } * ... * ] * } * ``` * * NOTE: If a `name` prop does not exist or its value does not match any field in a schema, then it is assumed to be a * custom `render` component. If the `render` prop does not exist, a null render will occur. If `render` is a * string, its value will be looked up in the `formContext.lookupMap` first before defaulting to a null render. * * The `ui:columns` grid element is syntactic sugar to specify a set of `ui:col` columns that all share the same set of * native `GridTemplate` props. In other words rather than writing the following configuration that renders a * `<GridTemplate>` element with 3 `<GridTemplate column className="GridColumn col-md-4">` nodes and 2 * `<GridTemplate column className="col-md-6">` nodes within it (one for each of the fields contained in the `children` * list): * * ``` * "ui:row": { * "children": [ * { * "ui:col": { * "className": "GridColumn col-md-4", * "children": ["innerField"], * } * }, * { * "ui:col": { * "className": "GridColumn col-md-4", * "children": ["inner.grandChild"], * } * }, * { * "ui:col": { * "className": "GridColumn col-md-4", * "children": [{ "name": "inner.grandChild2" }], * } * }, * { * "ui:col": { * "className": "col-md-6", * "children": ["innerField2"], * } * }, * { * "ui:col": { * "className": "col-md-6", * "children": ["inner.grandChild3"], * } * }, * ] * } * ``` * * One can write this instead: * ``` * "ui:row": { * "children": [ * { * "ui:columns": { * "className": "GridColumn col-md-4", * "children": ["innerField", "inner.grandChild", { "name": "inner.grandChild2", "convertOther": true }], * } * }, * { * "ui:columns": { * "className": "col-md-6", * "children": ["innerField2", "inner.grandChild3"], * } * } * ] * } * ``` * * NOTE: This syntax differs from * `"ui:col": { "className": "col-md-6", "children": ["innerField2", "inner.grandChild3"] }` in that * the `ui:col` will render the two children fields inside a single `<GridTemplate "className": "col-md-6",>` * element. * * The final grid element, `ui:condition`, allows for conditionally displaying "grid elements" within a row based on the * current value of a field as it relates to a (list of) hard-coded value(s). There are four elements that make up a * `ui:condition`: 1) the dotted-path `field` name within the root field that makes up the left-side of the condition; * 2) the hard-coded `value` (single or list) that makes up the right-side of the condition; 3) the `operator` that * controls how the left and right sides of the condition are compared; and 4) the `children` array that defines the * "grid elements" to display if the condition passes. * * A `ui:condition` uses one of three `operators` when deciding if a condition passes: 1) The `all` operator will pass * when the right-side and left-side contains all the same value(s); 2) the `some` operator will pass when the * right-side and left-side contain as least one value in common; 3) the `none` operator will pass when the right-side * and left-side do not contain any values in common. E.g. (as JSON objects): * * Here is how to render an if-then-else for `field2` which is an enum that has 3 known values and supports allowing * any other value: * ``` * "ui:row": [ * { * "ui:condition": { * "field": "field2", * "operator": "all", * "value": "value1", * "children": [ * { "ui:row": [...] }, * ], * } * }, * { * "ui:condition": { * "field": "field2", * "operator": "some", * "value": ["value2", "value3"], * "children": [ * { "ui:row": [...] }, * ], * } * }, * { * "ui:condition": { * "field": "field2", * "operator": "none", * "value": ["value1", "value2", "value3"], * "children": [ * { "ui:row": [...] }, * ], * } * } * ] * ``` */ export default function LayoutGridField(props) { /** Render the `LayoutGridField`. If there isn't a `layoutGridSchema` prop defined, then try pulling it out of the * `uiSchema` via `ui:LayoutGridField`. If `layoutGridSchema` is an object, then check to see if any of the properties * match one of the `GridType`s. If so, call the appropriate render function for the type. Otherwise, just call the * generic `renderField()` function with the `layoutGridSchema`. */ const { uiSchema } = props; let { layoutGridSchema } = props; const uiOptions = getUiOptions(uiSchema); if (!layoutGridSchema && LAYOUT_GRID_UI_OPTION in uiOptions && isObject(uiOptions[LAYOUT_GRID_UI_OPTION])) { layoutGridSchema = uiOptions[LAYOUT_GRID_UI_OPTION]; } if (isObject(layoutGridSchema)) { if (GridType.ROW in layoutGridSchema) { return _jsx(LayoutGridRow, { ...props, layoutGridSchema: layoutGridSchema }); } if (GridType.COLUMN in layoutGridSchema) { return _jsx(LayoutGridCol, { ...props, layoutGridSchema: layoutGridSchema }); } if (GridType.COLUMNS in layoutGridSchema) { return _jsx(LayoutGridColumns, { ...props, layoutGridSchema: layoutGridSchema }); } if (GridType.CONDITION in layoutGridSchema) { return _jsx(LayoutGridCondition, { ...props, layoutGridSchema: layoutGridSchema }); } } return _jsx(LayoutGridFieldComponent, { ...props, gridSchema: layoutGridSchema }); } LayoutGridField.TEST_IDS = LAYOUT_GRID_FIELD_TEST_IDS;