Skip to content

Instantly share code, notes, and snippets.

@truh
Created May 16, 2022 19:31
Show Gist options
  • Save truh/ce6a1b93bdd0b7533d2d65f5c5290300 to your computer and use it in GitHub Desktop.
Save truh/ce6a1b93bdd0b7533d2d65f5c5290300 to your computer and use it in GitHub Desktop.
import React from "react";
import PropTypes from "prop-types";
import IconButton from "@rjsf/core/lib/components/IconButton";
import {
ADDITIONAL_PROPERTY_FLAG,
deepEquals,
getDisplayLabel,
getSchemaType,
isSelect,
mergeObjects,
retrieveSchema,
toIdSchema,
} from "@rjsf/core/lib/utils";
import * as types from "@rjsf/core/lib/types";
const NullComponent = () => null;
const REQUIRED_FIELD_SYMBOL = "*";
const COMPONENT_TYPES = {
array: "ArrayField",
boolean: "BooleanField",
integer: "NumberField",
number: "NumberField",
object: "ObjectField",
string: "StringField",
null: "NullField",
};
function getFieldComponent(schema, uiSchema, idSchema, fields) {
const field = uiSchema["ui:field"];
if (typeof field === "function") {
return field;
}
if (typeof field === "string" && field in fields) {
return fields[field];
}
const componentName = COMPONENT_TYPES[getSchemaType(schema)];
// If the type is not defined and the schema uses 'anyOf' or 'oneOf', don't
// render a field and let the MultiSchemaField component handle the form display
if (!componentName && (schema.anyOf || schema.oneOf)) {
return NullComponent;
}
return componentName in fields
? fields[componentName]
: () => {
const { UnsupportedField } = fields;
return (
<UnsupportedField
schema={schema}
idSchema={idSchema}
reason={`Unknown field type ${schema.type}`}
/>
);
};
}
function Label(props) {
const { label, required, id } = props;
if (!label) {
return null;
}
return (
<label className="control-label" htmlFor={id}>
{label}
{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>}
</label>
);
}
function LabelInput(props) {
const { id, label, onChange } = props;
return (
<input
className="form-control"
type="text"
id={id}
onBlur={(event) => onChange(event.target.value)}
defaultValue={label}
/>
);
}
function Help(props) {
const { id, help } = props;
if (!help) {
return null;
}
if (typeof help === "string") {
return (
<p id={id} className="help-block">
{help}
</p>
);
}
return (
<div id={id} className="help-block">
{help}
</div>
);
}
function ErrorList(props) {
const { errors = [] } = props;
if (errors.length === 0) {
return null;
}
return (
<div>
<ul className="error-detail bs-callout bs-callout-info">
{errors
.filter((elem) => !!elem)
.map((error, index) => {
return (
<li className="text-danger" key={index}>
{error}
</li>
);
})}
</ul>
</div>
);
}
function DefaultTemplate(props) {
const {
id,
label,
children,
errors,
help,
description,
hidden,
required,
displayLabel,
} = props;
if (hidden) {
return <div className="hidden">{children}</div>;
}
return (
<WrapIfAdditional {...props}>
{displayLabel && <Label label={label} required={required} id={id} />}
{displayLabel && description ? description : null}
{children}
{errors}
{help}
</WrapIfAdditional>
);
}
// eslint-disable-next-line no-undef
if (process.env.NODE_ENV !== "production") {
DefaultTemplate.propTypes = {
id: PropTypes.string,
classNames: PropTypes.string,
label: PropTypes.string,
children: PropTypes.node.isRequired,
errors: PropTypes.element,
rawErrors: PropTypes.arrayOf(PropTypes.string),
help: PropTypes.element,
rawHelp: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
description: PropTypes.element,
rawDescription: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
hidden: PropTypes.bool,
required: PropTypes.bool,
readonly: PropTypes.bool,
displayLabel: PropTypes.bool,
fields: PropTypes.object,
formContext: PropTypes.object,
};
}
DefaultTemplate.defaultProps = {
hidden: false,
readonly: false,
required: false,
displayLabel: true,
};
function WrapIfAdditional(props) {
const {
id,
classNames,
disabled,
label,
onKeyChange,
onDropPropertyClick,
readonly,
required,
schema,
} = props;
const keyLabel = `${label} Key`; // i18n ?
const additional = schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
if (!additional) {
return <div className={classNames}>{props.children}</div>;
}
return (
<div className={classNames}>
<div className="row">
<div className="col-xs-5 form-additional">
<div className="form-group">
<Label label={keyLabel} required={required} id={`${id}-key`} />
<LabelInput
label={label}
required={required}
id={`${id}-key`}
onChange={onKeyChange}
/>
</div>
</div>
<div className="form-additional form-group col-xs-5">
{props.children}
</div>
<div className="col-xs-2">
<IconButton
type="danger"
icon="remove"
className="array-item-remove btn-block"
tabIndex="-1"
style={{ border: "0" }}
disabled={disabled || readonly}
onClick={onDropPropertyClick(label)}
/>
</div>
</div>
</div>
);
}
function SchemaFieldRender(props) {
const {
uiSchema,
formData,
errorSchema,
idPrefix,
idSeparator,
name,
onChange,
onKeyChange,
onDropPropertyClick,
required,
registry,
wasPropertyKeyModified = false,
} = props;
const { rootSchema, fields, formContext } = registry;
const FieldTemplate =
uiSchema["ui:FieldTemplate"] || registry.FieldTemplate || DefaultTemplate;
let idSchema = props.idSchema;
const schema = retrieveSchema(props.schema, rootSchema, formData);
idSchema = mergeObjects(
toIdSchema(schema, null, rootSchema, formData, idPrefix, idSeparator),
idSchema,
);
const FieldComponent = getFieldComponent(schema, uiSchema, idSchema, fields);
const { DescriptionField } = fields;
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]);
const readonly = Boolean(
props.readonly ||
uiSchema["ui:readonly"] ||
props.schema.readOnly ||
schema.readOnly,
);
const uiSchemaHideError = uiSchema["ui:hideError"];
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children
const hideError =
uiSchemaHideError === undefined
? props.hideError
: Boolean(uiSchemaHideError);
const autofocus = Boolean(props.autofocus || uiSchema["ui:autofocus"]);
if (Object.keys(schema).length === 0) {
return null;
}
const displayLabel = getDisplayLabel(schema, uiSchema, rootSchema);
const { __errors, ...fieldErrorSchema } = errorSchema;
// See #439: uiSchema: Don't pass consumed class names to child components
const field = (
<FieldComponent
{...props}
idSchema={idSchema}
schema={schema}
uiSchema={{ ...uiSchema, classNames: undefined }}
disabled={disabled}
readonly={readonly}
hideError={hideError}
autofocus={autofocus}
errorSchema={fieldErrorSchema}
formContext={formContext}
rawErrors={__errors}
/>
);
const id = idSchema.$id;
// If this schema has a title defined, but the user has set a new key/label, retain their input.
let label;
if (wasPropertyKeyModified) {
label = name;
} else {
label = uiSchema["ui:title"] || props.schema.title || schema.title || name;
}
const description =
uiSchema["ui:description"] ||
props.schema.description ||
schema.description;
const errors = __errors;
const help = uiSchema["ui:help"];
const hidden = uiSchema["ui:widget"] === "hidden";
let classNames = ["form-group", "field", `field-${schema.type}`];
if (!hideError && errors && errors.length > 0) {
classNames.push("field-error has-error has-danger");
}
classNames.push(uiSchema.classNames);
classNames = classNames.join(" ").trim();
const fieldProps = {
description: (
<DescriptionField
id={id + "__description"}
description={description}
formContext={formContext}
/>
),
rawDescription: description,
help: <Help id={id + "__help"} help={help} />,
rawHelp: typeof help === "string" ? help : undefined,
errors: hideError ? undefined : <ErrorList errors={errors} />,
rawErrors: hideError ? undefined : errors,
id,
label,
hidden,
onChange,
onKeyChange,
onDropPropertyClick,
required,
disabled,
readonly,
hideError,
displayLabel,
classNames,
formContext,
formData,
fields,
schema,
uiSchema,
registry,
};
const _AnyOfField = registry.fields.AnyOfField;
const _OneOfField = registry.fields.OneOfField;
return (
<FieldTemplate {...fieldProps}>
<React.Fragment>
{field}
{/*
If the schema `anyOf` or 'oneOf' can be rendered as a select control, don't
render the selection and let `StringField` component handle
rendering
*/}
{FieldComponent === NullComponent && schema.anyOf && !isSelect(schema) && (
// eslint-disable-next-line react/jsx-pascal-case
<_AnyOfField
disabled={disabled}
readonly={readonly}
hideError={hideError}
errorSchema={errorSchema}
formData={formData}
idPrefix={idPrefix}
idSchema={idSchema}
idSeparator={idSeparator}
onBlur={props.onBlur}
onChange={props.onChange}
onFocus={props.onFocus}
options={schema.anyOf.map((_schema) =>
retrieveSchema(_schema, rootSchema, formData),
)}
baseType={schema.type}
registry={registry}
schema={schema}
uiSchema={uiSchema}
/>
)}
{FieldComponent === NullComponent && schema.oneOf && !isSelect(schema) && (
// eslint-disable-next-line react/jsx-pascal-case
<_OneOfField
disabled={disabled}
readonly={readonly}
hideError={hideError}
errorSchema={errorSchema}
formData={formData}
idPrefix={idPrefix}
idSchema={idSchema}
idSeparator={idSeparator}
onBlur={props.onBlur}
onChange={props.onChange}
onFocus={props.onFocus}
options={schema.oneOf.map((_schema) =>
retrieveSchema(_schema, rootSchema, formData),
)}
baseType={schema.type}
registry={registry}
schema={schema}
uiSchema={uiSchema}
/>
)}
</React.Fragment>
</FieldTemplate>
);
}
class SchemaField extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return !deepEquals(this.props, nextProps);
}
render() {
return SchemaFieldRender(this.props);
}
}
SchemaField.defaultProps = {
uiSchema: {},
errorSchema: {},
idSchema: {},
disabled: false,
readonly: false,
autofocus: false,
hideError: false,
};
// eslint-disable-next-line no-undef
if (process.env.NODE_ENV !== "production") {
SchemaField.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object,
idSchema: PropTypes.object,
formData: PropTypes.any,
errorSchema: PropTypes.object,
registry: types.registry.isRequired,
};
}
export default SchemaField;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment