rjsf-team / react-jsonschema-form

A React component for building Web forms from JSON Schema.
https://rjsf-team.github.io/react-jsonschema-form/
Apache License 2.0
14.14k stars 2.18k forks source link

Multi-column support in React JSONSchema Form Antd #4013

Open SachinKotnala opened 8 months ago

SachinKotnala commented 8 months ago

Prerequisites

What theme are you using?

antd

Is your feature request related to a problem? Please describe.

It appears that it is not currently possible to create a multi-column form using Antd. For instance, I would like to have a login form with a firstname and lastname field positioned next to each other instead of being generated below one another by default.

What I want to achieve is this: [firstname ] [lastname ] [company] [personalemail] [workemail]

but it is [firstname ] [lastname ] [company] [personalemail] [workemail] [submit button]

Describe the solution you'd like

I think something similar to what we have for MUI will work for antd

<Grid
      item={true}
      xs={uiSchema![element.name]['ui:fieldFlexWidth'] || 12}
      key={index}
      style={{ marginBottom: '10px' }}
    >
      {element.content}
</Grid>

Describe alternatives you've considered

No response

heath-freenome commented 8 months ago

@SachinKotnala This is a popular request that requires quite a bit of work to build in a generic manner across all themes. Are you feeling ambitious enough to try build it?

priyank-R commented 6 months ago

Hi @heath-freenome , I'm not a very ambitious person, but I've created this modification as a part of rjsf-team/react-jsonschema-form/packages/antd/src/templates/ObjectFieldTemplate/index.tsx for one of my own projects.

It's a simple change, wherein I've added a ui:grid check in ui:schema. If present, it will set the column span in accordance to the element in the grid:

                            {uiSchema?.['ui:grid'] && Array.isArray(uiSchema['ui:grid']) ?
                                uiSchema['ui:grid'].map((ui_row) => {
                                    return Object.keys(ui_row).map((row_item) => {
                                            let element = properties.find((p => p.name == row_item))
                                            if (element) {
                                                return <Col key={element.name} span={ui_row[row_item]}>
                                                    {element.content}
                                                </Col>
                                            } else {
                                                return <></>
                                            }
                                        })

                                })
                                : properties
                                    .filter((e) => !e.hidden)
                                    .map((element: ObjectFieldTemplatePropertyType) => (
                                        <Col key={element.name} span={calculateColSpan(element)}>
                                            {element.content}
                                        </Col>
                                    ))}

Here's the complete ObjectFieldTemplate/index.tsx file - Please chip in your inputs for improvements, so if possible, will try raising a PR:

import classNames from 'classnames';
import isObject from 'lodash/isObject';
import isNumber from 'lodash/isNumber';
import isString from 'lodash/isString';
import {
    FormContextType,
    GenericObjectType,
    ObjectFieldTemplateProps,
    ObjectFieldTemplatePropertyType,
    RJSFSchema,
    StrictRJSFSchema,
    UiSchema,
    canExpand,
    descriptionId,
    getTemplate,
    getUiOptions,
    titleId,
} from '@rjsf/utils';
import Col from 'antd/lib/col';
import Row from 'antd/lib/row';
import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider/context';

const DESCRIPTION_COL_STYLE = {
    paddingBottom: '8px',
};

/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
 * title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
 * the properties.
 *
 * @param props - The `ObjectFieldTemplateProps` for this component
 */
export default function ObjectFieldTemplate<
    T = any,
    S extends StrictRJSFSchema = RJSFSchema,
    F extends FormContextType = any
>(props: ObjectFieldTemplateProps<T, S, F>) {
    const {
        description,
        disabled,
        formContext,
        formData,
        idSchema,
        onAddClick,
        properties,
        readonly,
        required,
        registry,
        schema,
        title,
        uiSchema,
    } = props;
    console.log('ObjectFieldTemplate props: ')
    console.log(props)
    const uiOptions = getUiOptions<T, S, F>(uiSchema);
    const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>('TitleFieldTemplate', registry, uiOptions);
    const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>(
        'DescriptionFieldTemplate',
        registry,
        uiOptions
    );
    // Button templates are not overridden in the uiSchema
    const {
        ButtonTemplates: { AddButton },
    } = registry.templates;
    const { colSpan = 24, labelAlign = 'right', rowGutter = 24 } = formContext as GenericObjectType;

    const findSchema = (element: ObjectFieldTemplatePropertyType): S => element.content.props.schema;

    const findSchemaType = (element: ObjectFieldTemplatePropertyType) => findSchema(element).type;

    const findUiSchema = (element: ObjectFieldTemplatePropertyType): UiSchema<T, S, F> | undefined =>
        element.content.props.uiSchema;

    const findUiSchemaField = (element: ObjectFieldTemplatePropertyType) => getUiOptions(findUiSchema(element)).field;

    const findUiSchemaWidget = (element: ObjectFieldTemplatePropertyType) => getUiOptions(findUiSchema(element)).widget;

    const calculateColSpan = (element: ObjectFieldTemplatePropertyType) => {
        const type = findSchemaType(element);
        const field = findUiSchemaField(element);
        const widget = findUiSchemaWidget(element);

        const defaultColSpan =
            properties.length < 2 || // Single or no field in object.
                type === 'object' ||
                type === 'array' ||
                widget === 'textarea'
                ? 24
                : 12;

        if (isObject(colSpan)) {
            const colSpanObj: GenericObjectType = colSpan;
            if (isString(widget)) {
                return colSpanObj[widget];
            }
            if (isString(field)) {
                return colSpanObj[field];
            }
            if (isString(type)) {
                return colSpanObj[type];
            }
        }
        if (isNumber(colSpan)) {
            return colSpan;
        }
        return defaultColSpan;
    };

    return (
        <ConfigConsumer>
            {(configProps: ConfigConsumerProps) => {
                console.log('config props are')
                console.log(configProps)
                const { getPrefixCls } = configProps;
                const prefixCls = getPrefixCls('form');
                const labelClsBasic = `${prefixCls}-item-label`;
                const labelColClassName = classNames(
                    labelClsBasic,
                    labelAlign === 'left' && `${labelClsBasic}-left`
                    // labelCol.className,
                );

                return (
                    <fieldset id={idSchema.$id}>
                        <Row gutter={rowGutter}>
                            {title && (
                                <Col className={labelColClassName} span={24}>
                                    <TitleFieldTemplate
                                        id={titleId<T>(idSchema)}
                                        title={title}
                                        required={required}
                                        schema={schema}
                                        uiSchema={uiSchema}
                                        registry={registry}
                                    />
                                </Col>
                            )}
                            {description && (
                                <Col span={24} style={DESCRIPTION_COL_STYLE}>
                                    <DescriptionFieldTemplate
                                        id={descriptionId<T>(idSchema)}
                                        description={description}
                                        schema={schema}
                                        uiSchema={uiSchema}
                                        registry={registry}
                                    />
                                </Col>
                            )}
                            {uiSchema?.['ui:grid'] && Array.isArray(uiSchema['ui:grid']) ?
                                uiSchema['ui:grid'].map((ui_row) => {
                                    return Object.keys(ui_row).map((row_item) => {
                                            let element = properties.find((p => p.name == row_item))
                                            if (element) {
                                                return <Col key={element.name} span={ui_row[row_item]}>
                                                    {element.content}
                                                </Col>
                                            } else {
                                                return <></>
                                            }
                                        })

                                })
                                : properties
                                    .filter((e) => !e.hidden)
                                    .map((element: ObjectFieldTemplatePropertyType) => (
                                        <Col key={element.name} span={calculateColSpan(element)}>
                                            {element.content}
                                        </Col>
                                    ))}
                        </Row>

                        {canExpand(schema, uiSchema, formData) && (
                            <Col span={24}>
                                <Row gutter={rowGutter} justify='end'>
                                    <Col flex='192px'>
                                        <AddButton
                                            className='object-property-expand'
                                            disabled={disabled || readonly}
                                            onClick={onAddClick(schema)}
                                            uiSchema={uiSchema}
                                            registry={registry}
                                        />
                                    </Col>
                                </Row>
                            </Col>
                        )}
                    </fieldset>
                );
            }}
        </ConfigConsumer>
    );
}
heath-freenome commented 6 months ago

@priyank-R The main challenge to make this a standard feature in RJSF is the fact that every Theme has it own, differing, implementation of doing a grid. So to build an out-of-the-box capability is going to be quite the undertaking so that it supports all themes. You are welcome to create your own custom field implementation for your local use. I do have a ticket (#3752) in the v6 world that is aimed at creating a library of well-known, RJSF supported theming elements that would allow people build way more customizable forms. Now if only I had time from work to be able to build it.