MUI Autocomplete Multi-Select

Hello all!

I am working to build a MUI Autocomplete control which supports multiple selections. I have the control rendering successfully in within JSONForms, but I am having an issue with the initial data binding. When I attempt to pass in the pre-selected enum values, I am passed the raw value, but when it is binding it is evaluating against the options in the select list. I’ve tried mapping the items but cannot seem to get them to bind correctly. Any help would be appreciated. Below is my current control. I am hoping I am missing something simple! Thanks in advance.

import merge from 'lodash/merge';
import {
  ControlProps,
  OwnPropsOfEnum,
  WithClassname,
  EnumOption,
  isDescriptionHidden,
} from '@jsonforms/core';
import { Autocomplete, FormHelperText, Hidden, TextField } from '@mui/material';
import { WithOptionLabel } from '@jsonforms/material-renderers/lib/mui-controls/MuiAutocomplete';
import { useFocus } from '@jsonforms/material-renderers';

export const MultiSelectEnumArray = (
  props: ControlProps & OwnPropsOfEnum & WithClassname & WithOptionLabel
) => {
  const [focused, onFocus, onBlur] = useFocus();
  const {
    description,
    className,
    config,
    id,
    label,
    required,
    errors,
    data,
    visible,
    options,
    handleChange,
    path,
    enabled,
    getOptionLabel,
  } = props;

  const isValid = errors.length === 0;
  const appliedUiSchemaOptions = merge({}, config, props.uischema.options);

  const showDescription = !isDescriptionHidden(
    visible,
    description,
    focused,
    appliedUiSchemaOptions.showUnfocusedDescription
  );

  const firstFormHelperText = showDescription
    ? description
    : !isValid
    ? errors
    : null;
  const secondFormHelperText = showDescription && !isValid ? errors : null;

  const onChange = (_ev: any, value: EnumOption[]) => {
    var values: EnumOption[] = [];

    value.forEach((el) => {
      values.push(el.value);
    });

    return handleChange(path, values);
  };

  return (
    <Hidden xsUp={!visible}>
      <Autocomplete
        id={id}
        multiple
        className={className}
        disabled={!enabled}
        autoHighlight
        autoSelect
        autoComplete
        fullWidth
        getOptionLabel={getOptionLabel || ((option) => option?.label)}
        freeSolo={false}
        options={options ? options : []}
        onChange={onChange}
        renderInput={(params) => {
          return (
            <TextField
              label={label}
              variant={
                appliedUiSchemaOptions.variant
                  ? appliedUiSchemaOptions.variant
                  : 'outlined'
              }
              inputRef={params.InputProps.ref}
              autoFocus={appliedUiSchemaOptions.focus}
              {...params}
              id={id + '-input'}
              required={
                required && !appliedUiSchemaOptions.hideRequiredAsterisk
              }
              error={!isValid}
              fullWidth={!appliedUiSchemaOptions.trim}
              InputLabelProps={data ? { shrink: true } : undefined}
              onFocus={onFocus}
              onBlur={onBlur}
              focused={focused}
            />
          );
        }}
      />
      <FormHelperText error={!isValid && !showDescription}>
        {firstFormHelperText}
      </FormHelperText>
      <FormHelperText error={!isValid}>{secondFormHelperText}</FormHelperText>
    </Hidden>
  );
};

Hey All! I was able to successfully get the enum values to bind bi-directionally, but have now run into an issue with ajv failing the validation. When I inspect the values being passed for validation, an array of selected enum values is being validated (e.g. [2020,2021]) against the array provided in the schema and failing. Does anyone have an idea on what changes are needed for the control(s) to pass validation?

schema.json:

{
  "type": "object",
  "properties": {
    "multiSelectEnumArray_2": {
      "type": "array",
      "items": {
        "type": "integer"
      },
      "enum": [2022, 2021, 2020, 2019, 2018, 2017, 2016, 2015]
    }
  }
}

uischema.json:

{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "Control",
      "scope": "#/properties/multiSelectEnumArray",
      "options": {
        "format": "multi"
      }
    }
  ]
}

MultiSelectEnumArrayControl.tsx:

import {
  and,
  ControlProps,
  isEnumControl,
  optionIs,
  OwnPropsOfEnum,
  RankedTester,
  rankWith,
} from '@jsonforms/core';
import { withJsonFormsEnumProps } from '@jsonforms/react';
import { MultiSelectEnumArray } from './MultiSelectEnumArray';

export const MultiSelectEnumArrayControl = (
  props: ControlProps & OwnPropsOfEnum
) => {
  return <MultiSelectEnumArray {...props} />;
};

export const multiSelectEnumArrayControlTester: RankedTester = rankWith(
  20,
  and(isEnumControl, optionIs('format', 'multi'))
);
export default withJsonFormsEnumProps(MultiSelectEnumArrayControl);

MultiSelectEnumArray.tsx:

import merge from 'lodash/merge';
import isEqual from 'lodash/isEqual';
import {
  ControlProps,
  OwnPropsOfEnum,
  WithClassname,
  EnumOption,
  isDescriptionHidden,
} from '@jsonforms/core';
import { Autocomplete, FormHelperText, Hidden, TextField } from '@mui/material';
import { WithOptionLabel } from '@jsonforms/material-renderers/lib/mui-controls/MuiAutocomplete';
import { useFocus } from '@jsonforms/material-renderers';

export const MultiSelectEnumArray = (
  props: ControlProps & OwnPropsOfEnum & WithClassname & WithOptionLabel
) => {
  const [focused, onFocus, onBlur] = useFocus();
  const {
    description,
    className,
    config,
    id,
    label,
    required,
    errors,
    data,
    visible,
    options,
    handleChange,
    path,
    enabled,
    getOptionLabel,
  } = props;

  const isValid = errors.length === 0;
  const appliedUiSchemaOptions = merge({}, config, props.uischema.options);

  const showDescription = !isDescriptionHidden(
    visible,
    description,
    focused,
    appliedUiSchemaOptions.showUnfocusedDescription
  );

  const firstFormHelperText = showDescription
    ? description
    : !isValid
    ? errors
    : null;
  const secondFormHelperText = showDescription && !isValid ? errors : null;

  const onChange = (_ev: any, newValues: EnumOption[]) => {
    // convert from an EnumOption to its value
    var values = newValues.map((o) => (o.value ? o.value : o));
    handleChange(path, values);
  };

  return (
    <Hidden xsUp={!visible}>
      <Autocomplete
        multiple
        id={id}
        className={className}
        disabled={!enabled}
        autoHighlight
        autoSelect
        autoComplete
        fullWidth
        freeSolo={false}
        options={options ? options : []}
        value={data}
        getOptionLabel={(option) => (option.label ? option.label : option)}
        onChange={onChange}
        isOptionEqualToValue={(o, v) => {
          return isEqual(o.value, v);
        }}
        renderInput={(params) => (
          <TextField
            label={label}
            variant={
              appliedUiSchemaOptions.variant
                ? appliedUiSchemaOptions.variant
                : 'outlined'
            }
            inputRef={params.InputProps.ref}
            autoFocus={appliedUiSchemaOptions.focus}
            placeholder='Favorites'
            {...params}
            id={id + '-input'}
            required={required && !appliedUiSchemaOptions.hideRequiredAsterisk}
            error={!isValid}
            fullWidth={!appliedUiSchemaOptions.trim}
            InputLabelProps={data ? { shrink: true } : undefined}
            onFocus={onFocus}
            onBlur={onBlur}
            focused={focused}
          />
        )}
      />
      <FormHelperText error={!isValid && !showDescription}>
        {firstFormHelperText}
      </FormHelperText>
      <FormHelperText error={!isValid}>{secondFormHelperText}</FormHelperText>
    </Hidden>
  );
};

Hi @richkroll,

The JSON Schema seems to be inconsistent. It looks like this:

      "type": "array",
      "items": {
        "type": "integer"
      },
      "enum": [2022, 2021, 2020, 2019, 2018, 2017, 2016, 2015]
  • type: 'array' declares the value to be an array
  • However enum: [2022, 2021 ....] defines the value to be a number

Both constraints can never be fulfilled at the same time, so you will always have a validation error.

Probably you want to move the enum into the items to declare that only these values are allowed within the array. So you might want to try the following:

      "type": "array",
      "items": {
        "type": "integer",
        "enum": [2022, 2021, 2020, 2019, 2018, 2017, 2016, 2015]
      },

Does this solve your problem?

Thanks @sdirix! I initially misunderstood the JSON Schema. That was indeed the missing part, thanks!

1 Like