Mui autocomplete multiple Enum

Hello,
I’ve (re)created a multi-select custom renderer based on :

I’ve also an issue with the validation…
“must be string must be equal to one of the allowed values” is displayed under the control

my code :

import React, { ReactNode } from 'react';
import merge from 'lodash/merge';
import isEqual from 'lodash/isEqual';
import {
  ControlProps,
  OwnPropsOfEnum,
  WithClassname,
  EnumOption,
  isDescriptionHidden,
  RankedTester,
  rankWith,
  and,
  isControl,
  isEnumControl,
  optionIs,
} from '@jsonforms/core';
import {
  withJsonFormsEnumProps,
  withJsonFormsControlProps,
  TranslateProps,
  withTranslateProps,
} from '@jsonforms/react';
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'
            }
            //type="text"
            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>
  );
};

export const MultiSelectEnumArrayTester: RankedTester = rankWith(
  10,
  //isEnumControl,
  and(isEnumControl, optionIs('xxmultiple', true)),
);

export default withJsonFormsEnumProps(
  withTranslateProps(React.memo(MultiSelectEnumArray)),
  false,
);

extract schema.json
“v09”: {
“title”: “member of ?”,
“type”: “string”,
“enum”: [“ABC”, “DEF”, “GHI”, “Forum”],
},

(extract) uischema.json
{
“type”: “Control”,
“scope”: “#/properties/VENDOR/properties/v09”,
“options”: {
“xxmultiple”: true
}
},
I imagine it’s probably something bad on schemas… but what ?!
Thx!!

Hi @clysss,

How does the data look like at VENDOR.v09? If it’s for example a number then I would expect these two errors two occur.

it’s supposed to be string…:slight_smile:
is there anything for you saying that it could be a number ?
when I look into data, I’ve :
“VENDOR”: {
“v09”: [
“ABC”,
“Forum”
],

Hi @clysss,

Well that is also invalid. In the JSON Schema it is specified that v09 is a string with a value of either 'ABC', 'DEF', 'GHI' or 'Forum'. However in your data is is actually an array with the value ['ABC', 'Forum']. This value is not a string and it also does not match one of the enum values. This is why you got these two error messages.

You either need to make sure that your data fits to your schema, or adjust the schema to fit to your data.

if I use “array” type in schema, I have the “array” display with [+] button and several select box.
“type”: “array”,
“items”: {
“type”:“string”,
“enum”: [“ABC”, “DEF”, “Forum”]
},
The current data and the current display is what I need. How to define correctly the schema ? or should I change something in the component ?
Thx

and if i use
“type”: “array”,
“enum”: [“ABC”, “DEF”, “Forum”],

i’ve the good display and only the message :
must be equal to one of the allowed values

We have an example in the documentation on how to model a multi-select enum in JSON Schema, see here.

I think that my schema is already like the example…
with : schema.json =
“type”: “array”,
“uniqueItems”: true,
“options”: { “xxmultiple”: true },
“items”: {
“type”: “string”,
“enum”: [“ABC”, “DEF”,“InCyber Forum”]
},
i see the checkboxes and no issue with validation
with : schema.json =
“type”: “array”,
“options”: { “xxmultiple”: true },
“items”: {
“type”: “string”,
“enum”: [“ABC”, “DEF”,“Forum”]
},
I see an array [+] of select (so probably not my component)

to register the component, should I use isEnumControl ???

This is the tester we are using in out off-the-shelf renderer. You can use the same or a simpler one, depending on your requirements.

Hello Stefan,
Thanks a lot for your return
been able with it to make it work with this tester

there was a last error with withJsonFormsMultiEnumProps instead of withJsonFormsEnumProps
Thanks to your pointer, as it was the line following the tester, I was able to see it !

I’ve now a multiselect working with Autocomplete instead of Checkboxes

import React, { ReactNode } from 'react';
import merge from 'lodash/merge';
import isEqual from 'lodash/isEqual';
import {
  ControlProps,
  OwnPropsOfEnum,
  WithClassname,
  EnumOption,
  isDescriptionHidden,
  RankedTester,
  rankWith,
  and,
  Paths,
  hasType,
  resolveSchema,
  schemaMatches,
  schemaSubPathMatches,
  showAsRequired,
  uiTypeIs,
  isControl,
  isEnumControl,
  optionIs,
  JsonSchema,
} from '@jsonforms/core';
import {
  withJsonFormsMultiEnumProps,
  withJsonFormsControlProps,
  TranslateProps,
  withTranslateProps,
} from '@jsonforms/react';
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 MultiSelectEnum = (
  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 errors = props.errors;
  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'
            }
            //type="text"
            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}>
        array--{secondFormHelperText}
      </FormHelperText>
    </Hidden>
  );
};

const hasOneOfItems = (schema: JsonSchema): boolean =>
  schema.oneOf !== undefined &&
  schema.oneOf.length > 0 &&
  (schema.oneOf as JsonSchema[]).every((entry: JsonSchema) => {
    return entry.const !== undefined;
  });

const hasEnumItems = (schema: JsonSchema): boolean =>
  schema.type === 'string' && schema.enum !== undefined;

export const MultiSelectEnumTester: RankedTester = rankWith(
  10,
  and(
    uiTypeIs('Control'),
    and(
      schemaMatches(
        schema =>
          hasType(schema, 'array') &&
          !Array.isArray(schema.items) &&
          schema.uniqueItems === true,
      ),
      schemaSubPathMatches('items', (schema, rootSchema) => {
        const resolvedSchema = schema.$ref
          ? resolveSchema(rootSchema, schema.$ref, rootSchema)
          : schema;
        return hasOneOfItems(resolvedSchema) || hasEnumItems(resolvedSchema);
      }),
    ),
  ),
);

export default withJsonFormsMultiEnumProps(
  withTranslateProps(React.memo(MultiSelectEnum)),
  false,
);

(still need some light cleaning)
could we integrate it to jsonforms library ?

Hi @clysss,

We generally avoid adding too many alternative renderers for the same concept to the base renderer sets as this increases maintenance effort considerably over time. So there is no need to contribute it to the JSON Forms project itself.

However if you make it available open source, then we’ll happily link to it in the community section of the website.

However a good contribution would be to move and export the tester to @jsonforms/core instead of burying it in @jsonforms/material-renderers. The same for the other util functions. This would make the existing and your custom renderers simpler which is always a win.

I’ll be happy to follow your guides , but I’m not sure to understand

However a good contribution would be to move and export the tester to @jsonforms/core instead of burying it in @jsonforms/material-renderers. The same for the other util functions. This would make the existing and your custom renderers simpler which is always a win.
Could you send me an example of what you mean ?

Or if it’s a simple operation, I can also publish first the code on github and let you commit the changes!