@rjsf/core
Version:
A simple React component capable of building HTML forms out of a JSON schema.
709 lines (707 loc) • 37.1 kB
JavaScript
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;