oneOfMultiEnum and button custom renderers

I have reviewed similar topics and have not found the answer I am looking for.

I am struggling to get multi-select inputs to render with an MUI Autocomplete. I had this working earlier and now I can’t reproduce what was working. It seems that the problem is my custom renderer is not getting the right props. Can anyone see my issue or suggest another way to do this?

Also, I am rendering a button. The button is used to start an OAuth flow. I am having similar issues.

schema:

{
    "type": "object",
    "properties": {
      "enum": {
        "type": "string",
        "enum": ["foo", "bar"]
      },
      "enumMulti": {
        "type": "array",
        "uniqueItems": true,
        "items": {
          "type": "string",
          "enum": ["foo", "bar"]
        }
      },
      "oneOf": {
        "type": "string",
        "description": "One of the following values",
        "oneOf": [
          {
            "const": "1",
            "title": "One"
          },
          {
            "const": "2",
            "title": "Two"
          }
        ]
      },
      "oneOfMulti": {
        "type": "array",
        "uniqueItems": true,
        "items": {
          "oneOf": [
            {
              "const": "1",
              "title": "One"
            },
            {
              "const": "2",
              "title": "Two"
            }
          ]
        }
      },
      "button": {
        "type": "null",
        "description": "Click to navigate"
      }
    },
    "required": []
  }

uischema:

{
    "type": "VerticalLayout",
    "elements": [
      {
        "type": "Control",
        "scope": "#/properties/enum"
      },
      {
        "type": "Control",
        "scope": "#/properties/enumMulti",
        "options": {
          "format": "multi"
        }
      },
      {
        "type": "Control",
        "scope": "#/properties/oneOf"
      },
      {
        "type": "Control",
        "label": "Select all that apply",
        "scope": "#/properties/oneOfMulti",
        "options": {
          "format": "multi"
        }
      },
      {
        "type": "Control",
        "label": "Go",
        "scope": "#/properties/button"
      }
    ]
  }

data:

{
    "enum": "foo",
    "enumMulti": ["foo"],
    "oneOf": "1",
    "oneOfMulti": ["1", "2"],
    "button": "https://jsonforms.io"
  }

renderers:

const buttonRenderer: JsonFormsRendererRegistryEntry = {
  tester: rankWith(6, scopeEndsWith('button')),
  renderer: (props: ControlProps) => {
    const { data, label, description } = props

    const url = isValidURL(data) ? new URL(data) : null

    // Always attach a uuid to the url as a search param
    if (url) url.searchParams.append('state', uuidv4())

    return (
      <Tooltip
        title={description}
        enterDelay={500}
        hidden={description ? false : true}
      >
        <Button
          variant='contained'
          color='success'
          size='large'
          href={url ? url.toString() : undefined}
          target='_blank'
          rel='noopener noreferrer'
        >
          {label}
        </Button>
      </Tooltip>
    )
  }
}

type MultiSelectOption = string | { const: string | number; title: string }
/**
 * @description A renderer for multi-select fields. Handles enum and oneOf items.
 * @requires uischema.options.format = 'multi'
 */
const multiSelectRenderer: JsonFormsRendererRegistryEntry = {
  tester: rankWith(
    20,
    or(scopeEndsWith('enumMulti'), scopeEndsWith('oneOfMulti'))
  ),
  renderer: (props: ControlProps) => {
    const { data, label, description, handleChange, path, schema } = props
    const options: MultiSelectOption[] = Object.values(schema?.items || {})[0]

    const getOptionLabel = (option: MultiSelectOption) => {
      if (typeof option === 'string') {
        return option
      }
      return option.title
    }

    const isOptionEqualToValue = (option: MultiSelectOption, value) => {
      if (typeof option === 'string') {
        return option === value
      }
      return option.const === value
    }

    const onChange = (_, newValue: MultiSelectOption[]) => {
      handleChange(
        path,
        newValue.map((o: MultiSelectOption) =>
          typeof o === 'string' ? o : o.const
        )
      )
    }

    return (
      <Autocomplete
        data-testid='multi-select-renderer'
        multiple
        disableCloseOnSelect
        value={data ?? []}
        options={options ?? []}
        getOptionLabel={getOptionLabel}
        isOptionEqualToValue={isOptionEqualToValue}
        onChange={onChange}
        renderInput={params => (
          <TextField
            {...params}
            label={label}
            variant='outlined'
            helperText={description}
          />
        )}
      />
    )
  }
}

const renderers: JsonFormsRendererRegistryEntry[] = [
  ...materialRenderers,
  buttonRenderer,
  multiSelectRenderer
]

export default renderers

component:

import { useState } from 'react'
import { JsonForms } from '@jsonforms/react'

import renderers from './renderers'

export default function Form({ schema, uischema, defaultData }) {
  const [data, setData] = useState(defaultData)

  return (
    <JsonForms
      schema={schema}
      uischema={uischema}
      renderers={renderers}
      data={data}
      onChange={({ data }) => setData(data)}
      validationMode='ValidateAndShow'
    />
  )
}

Hello,
Not sure to understand what is your issue
I’ve just published a multi select control here
Perhaps you can have a look to it

I am able to render multi-select checkboxes with this schema and uischema from materialRenderers, but I want to use a custom renderer for multi-select inputs. The issue is that when I use a custom renderer, the renderer does not see the correct props. path is undefined and data is missing.

Screenshot 2024-08-15 at 7.22.53 AM

Found the issue. My renderers were missing the withJsonFormsControlProps wrapper. :person_facepalming:

1 Like

Great ! Could you post you final custom control w/ renderer that I can compare with mine ?

const multiSelectRenderer: JsonFormsRendererRegistryEntry = {
  tester: rankWith(20, optionIs('format', 'multi')),
  renderer: withJsonFormsControlProps((props: ControlProps) => {
    const {
      data,
      label,
      description,
      handleChange,
      path,
      schema,
      enabled,
      errors,
      required
    } = props

    const options: MultiSelectOption[] = useMemo(
      () =>
        Object.entries(schema?.items || {}).reduce((acc, [key, value]) => {
          if (key === 'enum' || key === 'oneOf') {
            return value
          }
          return acc
        }, []),
      [schema?.items]
    )

    const getOptionLabel = (option: MultiSelectOption) => {
      if (typeof option === 'string') {
        const foundOption = options.find(o =>
          typeof o === 'object' ? o?.const === option : o === option
        )
        if (typeof foundOption === 'object' && foundOption?.title) {
          return foundOption.title
        }
        return option
      }
      return option.title
    }

    const isOptionEqualToValue = (option: MultiSelectOption, value) => {
      if (typeof option === 'string') {
        return option === value
      }
      return option.const === value || option.const === value?.const
    }

    const onChange = (_, newValue: MultiSelectOption[]) => {
      handleChange(
        path,
        newValue.map(o => (typeof o === 'string' ? o : o.const))
      )
    }

    return (
      <Autocomplete
        data-testid='multi-select-renderer'
        multiple
        disableCloseOnSelect
        value={data}
        options={options}
        getOptionLabel={getOptionLabel}
        isOptionEqualToValue={isOptionEqualToValue}
        onChange={onChange}
        disabled={!enabled}
        renderInput={params => (
          <TextField
            {...params}
            label={label}
            variant='outlined'
            required={required}
            helperText={errors ? errors : description}
            error={!!errors}
            FormHelperTextProps={{ style: { marginLeft: 0 } }}
          />
        )}
      />
    )
  })
}```
1 Like