Wrapping any JSONForms formfield

Hi,

when trying to add a generic wrapper around JsonForm form fields, I encounter problems for propagating the right props to the underlying components.

The idea is to add a custom renderer that, depending on available properties in the schema, will add a wrapping div around the form field with some custom classes.

The problem is that when using the withJsonFormsControlProps HOC, the schema prop of the underlying component, rendered by using the JsonFormsDispatch component, needs to replaced by the root schema in order to keep all the custom props for other custom renderers.

But when doing this, the default prop is lost in the process, because no defaults are being rendered any more.

Could you please tell me how to use withJsonFormControlProps and JsonFormsDispatch properly to achieve what I’m looking for? Thanks! Here is my current implementation of the Wrapper renderer:

const WrapperRenderer = (props: ControlProps) => {
  const hasProp = (p: string) => props.schema.hasOwnProperty(p);
  const isEssential = hasProp('isEssential');
  const inCalculation = hasProp('inCalculation');

  const classes = useStyles();
  return (
    <div
      className={clsx(classes.wrapped, {
        [classes.essential]: isEssential,
        [classes.calculation]: inCalculation,
        [classes.both]: isEssential && inCalculation,
      })}
    >
      <JsonFormsDispatch {...props} schema={props.rootSchema} renderers={renderers(['wrapper'])} />
    </div>
  );
};

export default withJsonFormsControlProps(WrapperRenderer);

export const isWrapperControl = and(
  uiTypeIs('Control'),
  schemaMatches(
    (schema) =>
      !isEmpty(schema) &&
      (schema.hasOwnProperty('isEssential') || schema.hasOwnProperty('inCalculation')),
  ),
);

export const tester = rankWith(10000, isWrapperControl);

Some example schema:

export const example: JsonFormSchemas = {
  schema: {
    type: 'object',
    properties: {
      firstName: {
        type: 'string',
        default: 'John', // displayed correctly
        readOnly: true, // correctly being disabled
      },
      lastName: {
        type: 'string',
        isEssential: true,
        default: 'Doe',  // NOT being displayed correctly (because wrapped by Wrapper component)
        readOnly: true, // NOT being disabled
      },
      density: {
        type: 'integer',
        unit: '<sup>kg</sup>&frasl;<sub>m<sup>3</sup></sub>',
        isEssential: true,
        inCalculation: true,
        default: 15, // NOT being used
        minimum: 10,
        maximum: 20,
      },
      density2: {
        type: 'integer',
        unit: '<sup>kg</sup>&frasl;<sub>m<sup>3</sup></sub>',
        inCalculation: true,
        default: 15, // NOT being used
        minimum: 10,
        maximum: 20,
      },
    },
    required: ['lastName', 'density'],
  },
  data: {},
};

Now, when I remove the withJsonFormsControlProps HOC, the readOnly and disabled prop are being used correctly, but the wrapper classes are not set, because the schema of the Wrapper renderer is replaced by the root schema.

I have now resolved the prop schema of the current control myself in the renderer with following code. I also used the withJsonFormsRendererProps HOC instead of the withJsonFormsControlProps.

This way it works as expected.
Is this the way to go?

const WrapperRenderer = (props: JsonFormsProps) => {
  const resolvedSchema = Resolve.schema(
    props.schema ?? props.rootSchema,
    // @ts-expect-error
    props.uischema?.scope ?? '',
    props.rootSchema,
  );

  const hasProp = (p: string) => resolvedSchema?.hasOwnProperty(p) ?? false;
  const isEssential = hasProp('isEssential');
  const inCalculation = hasProp('inCalculation');

  const classes = useStyles();
  return (
    <div
      className={clsx(classes.wrapped, {
        [classes.essential]: isEssential,
        [classes.calculation]: inCalculation,
        [classes.both]: isEssential && inCalculation,
      })}
    >
      <JsonFormsDispatch {...props} renderers={renderers(['wrapper'])} />
    </div>
  );
};

export default withJsonFormsRendererProps(WrapperRenderer);

Hi @Sewdn,

Bindings like withJsonFormsControlProps work like this: They receive the props handed over to JsonFormsDispatch and consume the form-wide state. Based on these two data points they then determine the specific props handed over to the renderers.

Example

The JsonFormsDispatch receives schema

{
  "title": "Person",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "default": "John Doe"
    },
  }
}

and uischema

{
  "type": "Control"
  "scope": "#/properties/name"
}

The withJsonFormsControlProps will be given these as props, and then at the end determine the following schema and uischema props for the control renderer

{
  "type": "string",
  "default": "John Doe"
}
{
  "type": "Control"
  "scope": "#/properties/name"
}

As you can see, the control renderer receives the already resolved sub schema of schema so it can directly access it and doesn’t need to do the resolving itself.

This is why using withJsonFormsControlProps in the “wrapper” does not work when it then calls JsonFormsDispatch again. The uischema and sub schema don’t fit together.

What would work is calling a specific unwrapped renderer here, so instead of calling JsonFormsDispatch you would call Unwrapped.MaterialTextControl instead, see here for the wrapping documentation.


Yes, that works because withJsonFormsRendererProps, in contrast to withJsonFormsControlProps, does almost nothing. As the props given to JsonFormsDispatch are not modified, you can call JsonFormsDispatch again without breaking anything. Of course you then need to manually execute some of the functionality of withJsonFormsControlProps if you need it, e.g. resolving the schema.

Hi @sdirix

thanks for your response.

What would work is calling a specific unwrapped renderer here, so instead of calling JsonFormsDispatch you would call Unwrapped.MaterialTextControl instead

This is not desirable because I would need to map to all of the available Unwrapped controls. Also custom controls. So I would need to keep updating this Wrapper renderer when new Custom Renderers are added.

I stick to resolving the schema in the Wrapper renderer manually (based on the rootSchema and the scope prop of the uischema).

Hi @Sewdn,

Feel free to use the withJsonFormsRendererProps approach. Just wanted to make clear what needs to be done if withJsonFormsControlProps is used.