Wrapper for every form element

Stack: [React + MUI5 + JsonForms]

Hello!

I’m trying to wrap every form element in a custom component while maintaining the use of Material-UI renderers. In this process, I developed a custom renderer, but I’m facing issues where Material-UI styles and certain properties, such as labels from the UI schema, are not being applied as expected. Is there a specialized component, like MaterialDispatch, that I should be using instead?

Additionally, I attempted to replace Dispatch component with JsonFormsDispatch, but it led to the app freezing without throwing any errors.

Here is the snippet which utilizes schema and UI schema from the JsonForms documentation.

In order to compare the behavior with&without the custom render, remove WrapperRenderer from renderers array prop :slight_smile:

Could you help me out with a solution or point me to the right docs? :innocent:

import { ControlProps, isControl, rankWith } from '@jsonforms/core';
import {
  materialCells,
  materialRenderers,
} from '@jsonforms/material-renderers';
import {
  Dispatch,
  JsonForms,
  withJsonFormsControlProps,
} from '@jsonforms/react';
import { FunctionComponent, useState } from 'react';

const CustomWrapper: FunctionComponent<ControlProps> = (props) => (
  <div>
    some wrapping view
    <Dispatch {...props} />
  </div>
);

const wrapperTester = rankWith(3, isControl);

export const WrapperRenderer = {
  tester: wrapperTester,
  renderer: withJsonFormsControlProps(CustomWrapper),
};

export const Poc: FunctionComponent = () => {
  const [data, setData] = useState({});

  return (
    <JsonForms
      schema={schema}
      uischema={uiSchema}
      data={data}
      renderers={[...materialRenderers, WrapperRenderer]}
      cells={materialCells}
      onChange={({ data }) => setData(data)}
    />
  );
};

const schema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
      minLength: 3,
      description: 'Please enter your name',
    },
    vegetarian: {
      type: 'boolean',
    },
    birthDate: {
      type: 'string',
      format: 'date',
    },
    nationality: {
      type: 'string',
      enum: ['DE', 'IT', 'JP', 'US', 'RU', 'Other'],
    },
    personalData: {
      type: 'object',
      properties: {
        age: {
          type: 'integer',
          description: 'Please enter your age.',
        },
        height: {
          type: 'number',
        },
        drivingSkill: {
          type: 'number',
          maximum: 10,
          minimum: 1,
          default: 7,
        },
      },
      required: ['age', 'height'],
    },
    occupation: {
      type: 'string',
    },
    postalCode: {
      type: 'string',
      maxLength: 5,
    },
  },
  required: ['occupation', 'nationality'],
};
const uiSchema = {
  type: 'VerticalLayout',
  elements: [
    {
      type: 'HorizontalLayout',
      elements: [
        {
          type: 'Control',
          scope: '#/properties/name',
        },
        {
          type: 'Control',
          scope: '#/properties/personalData/properties/age',
        },
        {
          type: 'Control',
          scope: '#/properties/birthDate',
        },
      ],
    },
    {
      type: 'Label',
      text: 'Additional Information',
    },
    {
      type: 'HorizontalLayout',
      elements: [
        {
          type: 'Control',
          scope: '#/properties/personalData/properties/height',
        },
        {
          type: 'Control',
          scope: '#/properties/nationality',
        },
        {
          type: 'Control',
          scope: '#/properties/occupation',
          suggestion: [
            'Accountant',
            'Engineer',
            'Freelancer',
            'Journalism',
            'Physician',
            'Student',
            'Teacher',
            'Other',
          ],
        },
      ],
    },
  ],
};

export default WrapperRenderer;

Hi @nightingbear,

The Dispatch is a specialized dispatcher for cells, so it’s likely not what you want to use. The JsonFormsDispatch is the one you generally want to use.

There are two issues with the code:

  1. The wrapper is wrapped itself with withJsonFormsControlProps. The consequence is that its props are modified as is needed for a proper control renderer. Just handing the props then over to dispatch again will lead to issues as the props given to the dispatch should fit for the dispatcher, instead it receives props intended for a control.
  2. When writing such a wrapper you must make sure that the wrapper is not again dispatched to. Otherwise you will end up in an endless loop where the wrapper is endlessly dispatching to itself.

Issue 1 can be resolved simply by not wrapping the wrapper with withJsonFormsControlProps. If you avoid that, then you can simply pass all the props through to JsonFormsDispatch. However you will then lose all the specialized control bindings. Depending on your use cases you might want to call the bindings then here manually. Can you describe in more detail why you want such a wrapper and what you need access to?

Issue 2 can be resolved by filtering the wrapper renderer from the renderers prop given to JsonFormsDispatch. Then it can’t be dispatched to again. In case you want to use the wrapper renderer not only for leaf-controls but for every nested element, then you need to have custom wrappers in between which re-add the wrapper renderer to the renderers again.
Alternatively, instead of filtering, you could modify the props given to JsonFormsDispatch to recognize whether you were called from the wrapper renderer. Then the tester of the wrapper renderer must recognize that and return a -1 priority in these cases.

Thanks for the detailed answer, @sdirix! :slight_smile:
It seems a bit complex, though.
Essentially, I’m trying to enclose each form element within a customized MUI Card.
Perhaps there’s more efficient approach?

In case there is not:

Simply for UX purposes - all elements should be wrapped in a customizable MUI card.
Regarding access - not sure if I understood your question, but… everything :smiley: I don’t want to overwrite the default JsonForms material renderers behaviors. I only wish to wrap them in a card.

Could you provide a basic poc of what such a tester might look like?
I’m also trying to understand how to access the props passed to JsonFormsDispatch in the tester function, but I’m finding it difficult to locate this information in the documentation.