Async dynamic enum for autocomplete text field

While looking for a way to populate an enum or oneOf list for a text by fetching options.

I got a working solution with a custom renderer but using @mui/material

I would be pleased to have some hints about how I could use @jsonform/* Components instead;

There is a working demo on github: GitHub - perki/jsonforms-autocomplete-async-test: JsonForms AutoComplete Async Test

App.tsx see full content here

The Renderer takes an async callback options autoCompleteAsyncCallBack to fetch the data:

const uischema = {
   type: 'Control',
   scope: '#/properties/api_field',
   options: {
     autoCompleteAsyncCallBack,
     autoCompleteHelperText // optional, render a text when a choice is made
   }
};

AutoCompleteAsync.tsx

// AutoCompleteAsyncControl.js
import { useState, useEffect } from 'react';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { Autocomplete , FormControl, TextField } from '@mui/material';

// no direct API import here; options are provided via uischema callback

interface AutoCompleteAsyncProps {
  data: any;
  handleChange(path: string, value: any): void;
  path: string;
  schema: JsonSchema;
  uischema: UISchemaElement;
}


export type AutoCompleteOption = {
  data: any,
  const: string,
  title: string
}

/**
 * @return a textual description of the selected item if any
 */
export type AutoCompleteAsyncCallBack = (query: string) => Promise<AutoCompleteOption[]>;

/**
 * @return a textual description of the selected item if any
 */
export type AutoCompleteHelperText = (option: AutoCompleteOption) => string;

const AutoCompleteAsyncControl = ({ data, handleChange, path, schema, uischema }: AutoCompleteAsyncProps) => {
  // -- load from usischema config
  const autoCompleteAsyncCallBack: AutoCompleteAsyncCallBack = uischema?.options?.autoCompleteAsyncCallBack;
  if (autoCompleteAsyncCallBack == null) throw new Error('Missing uischema.options.autoCompleteAsyncCallBack');
  const autoCompleteHelperText: AutoCompleteHelperText = uischema?.options?.autoCompleteHelperText || (() => schema.description);
  // --
  const [options, setOptions] = useState<AutoCompleteOption[]>([]);
  const [loading, setLoading] = useState(true);
  const [inputValue, setInputValue] = useState( data?.title || '');

  // API call on input change
  useEffect(() => {
    const fetchOptions = async () => {
      setLoading(true);
      try {
        const newOptions = await autoCompleteAsyncCallBack(inputValue);
        if (Array.isArray(newOptions)) {
          setOptions(newOptions);
        } else {
          setOptions([]);
        }
      } catch (error) {
        setOptions([]);
        console.error('Failed to fetch options:', error);
      } finally {
        setLoading(false);
      }
    };
    fetchOptions();
  }, [inputValue]);

  const myOnChange = function (event: any, newValue: AutoCompleteOption | null) {
    handleChange(path, newValue);
  };

  const newInputValue = function (event: any, newInputValue: string) {
    if (event == null) return; // avoid change on init
    setInputValue(newInputValue);
  };

  return (
    <FormControl fullWidth>
      <Autocomplete
        options={options}
        loading={loading}
        forcePopupIcon={false}
        onChange={myOnChange}
        inputValue={inputValue}
        onInputChange={newInputValue}
        getOptionLabel={(option) => option?.title ?? ''}
        isOptionEqualToValue={(option, value) => option?.const === value?.const}
        renderOption={(liProps, option) => (
          <li {...liProps} key={option.const}>{option.title}</li>
        )}
        renderInput={(params) => (
          <TextField
            {...params}
            label={schema.title || path}
            helperText={autoCompleteHelperText(data)}
          />
        )}
      />
    </FormControl>
  );
};

// --- Tester
import { rankWith, hasOption, isStringControl, and, UISchemaElement, JsonSchema } from '@jsonforms/core';

// This tester will trigger the custom renderer when a UI schema control has
// a specific custom property, like "render: 'autocomplete-api'".
export const AutocompleteAsyncTester = rankWith(
  3, // A higher rank gives this renderer precedence
  and(
    isStringControl,
    hasOption('autoCompleteAsyncCallBack')
  )
);


const AutoCompleteAsyncRenderer = withJsonFormsControlProps(AutoCompleteAsyncControl);

export default AutoCompleteAsyncRenderer;


Hi @perki ,

You have a look at the docs regarding re-using existing controls: Custom Renderers - JSON Forms

In any case you have to do the async fetching within the custom part of your code. You can look into reusing the MaterialEnumControl . To use it within your custom renderer’s code, use the unwrapped variant:

import { Unwrapped } from ‘@jsonforms/material-renderers’;
const { MaterialEnumControl } = Unwrapped;

Thanks @lucas-koehler ,

Yes MaterialEnumControl is the component I want to customize.

I have already tried following the custom rendering guide - without success.

I’ll have a deeper look into it.