Thanks Stefan, makes sense, though this does not happen at all when you don’t pass in a custom instance of Ajv, it only occurs when that is included. So, I guess we are getting a re-render due to the custom Ajv?
I’ve snipped out a few functions to reduce the size here (they fetch data into the MuiAutocomplete control and call setOptions, setFormSchemaData and setFormUiSchemaData when initially clicking and then when selecting an item in the autocomplete control).
import React, { ReactNode, useContext, useEffect, useState } from 'react';
import {
and,
ControlProps,
createAjv,
EnumCellProps,
isDescriptionHidden,
JsonSchema,
optionIs,
OwnPropsOfEnum,
RankedTester,
rankWith,
UISchemaElement,
uiTypeIs,
WithClassname,
} from '@jsonforms/core';
import {
JsonForms,
TranslateProps,
withJsonFormsOneOfEnumProps,
withTranslateProps,
} from '@jsonforms/react';
import { ErrorObject } from 'ajv';
import { materialCells, materialRenderers } from '@jsonforms/material-renderers';
import merge from 'lodash/merge';
import {
Autocomplete,
AutocompleteRenderOptionState,
Box,
CircularProgress,
createFilterOptions,
FilterOptionsState,
FormHelperText,
Hidden,
TextField,
Tooltip,
Typography
} from '@mui/material';
import MoreIcon from '@mui/icons-material/More';
import { useFocus } from '@jsonforms/material-renderers';
import parse from 'autosuggest-highlight/parse';
import match from 'autosuggest-highlight/match';
import { authRequest, protectedResources } from '../../../authConfig';
import { customRenderers } from '../CustomRenderers';
import { useSessionStorage } from '../../../hooks/useSessionStorage';
import { FeatureDataErrorsContext } from '../../../pages/InformationForm';
export interface WithOptionLabel {
getOptionLabel?(option: Feature): string;
renderOption?(
props: React.HTMLAttributes<HTMLLIElement>,
option: Feature,
state: AutocompleteRenderOptionState
): ReactNode;
filterOptions?(
options: Feature[],
state: FilterOptionsState<Feature>
): Feature[];
}
interface Feature {
name: string;
id: number;
tags: string;
formSchema?: string;
formUiSchema?: string;
}
const MuiAutocomplete = (
props: ControlProps & EnumCellProps & WithClassname & WithOptionLabel
) => {
const {
description,
errors,
visible,
required,
label,
data,
className,
id,
enabled,
uischema,
path,
handleChange,
config,
getOptionLabel,
isValid,
} = props;
const appliedUiSchemaOptions = merge({}, config, uischema.options);
const renderers = [
...materialRenderers,
...customRenderers
];
const customAjv = createAjv({ useDefaults: true, allErrors: true });
require("ajv-errors")(customAjv);
const [formSchemaData, setFormSchemaData] = useState<JsonSchema | undefined>(undefined);
const [formUiSchemaData, setFormUiSchemaData] = useState<UISchemaElement | undefined>(undefined);
const [featureData, setFeatureData] = useState(data?.data ?? {});
const { setFeatureDataErrors } = useContext(FeatureDataErrorsContext);
const [focused, onFocus, onBlur] = useFocus();
const [open, setOpen] = useState(false);
const [options, setOptions] = useSessionStorage('features', []);
const [selectedFeature, setSelectedFeature] = useSessionStorage('selectedFeature', data ?? null);
const [selectedFeatureName, setSelectedFeatureName] = useState<string>(data?.name ?? '');
const [selectedFeatureId, setSelectedFeatureId] = useState<number | null>(data?.id ?? null);
const loading = (open && options?.length === 0);
const filterOptions = createFilterOptions({
matchFrom: 'any',
limit: 1000,
stringify: (option: Feature) => option.name + option.tags,
});
const findOption = options?.find((o: { id: any; }) => data?.id === o.id) ?? null;
**<SNIPPED OUT UseEffect calls that fetch the data into the MuiAutoComplete>**
const showDescription = !isDescriptionHidden(
visible,
description,
focused,
appliedUiSchemaOptions.showUnfocusedDescription
);
const firstFormHelperText = showDescription
? description
: !isValid
? errors
: null;
const secondFormHelperText = showDescription && !isValid ? errors : null;
const setDataAndValidate = (data: {}, errors: ErrorObject[] | undefined) => {
if (data != null && featureData !== data) {
setFeatureData(data)
handleChange(path + ".data", data);
}
if (errors && setFeatureDataErrors !== undefined) {
setFeatureDataErrors(() => (errors));
}
}
return (
<Hidden xsUp={!visible}>
<Autocomplete
className={className}
id={id}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
disabled={!enabled}
value={findOption}
onChange={(_event, newValue) => {
if (newValue === null) {
handleChange(path, null);
setSelectedFeatureName('');
} else {
handleChange(path, { id: newValue?.id, name: newValue?.name, data: {} });
setSelectedFeatureId(newValue?.id);
}
}}
inputValue={selectedFeatureName}
onInputChange={(_event, newInputValue) => {
if (newInputValue !== '') {
setSelectedFeatureName(newInputValue);
}
else {
setSelectedFeatureName('');
}
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
options={options}
loading={loading}
autoHighlight
autoComplete
fullWidth
getOptionLabel={getOptionLabel || ((option) => option?.name)}
freeSolo={false}
renderInput={(params) => {
return (
<TextField
label={label}
variant={'standard'}
type='text'
inputRef={params.InputProps.ref}
autoFocus={appliedUiSchemaOptions.focus}
{...params}
inputProps={params.inputProps}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
disabled={!enabled}
id={id + '-input'}
required={
required && !appliedUiSchemaOptions.hideRequiredAsterisk
}
error={!isValid}
fullWidth={!appliedUiSchemaOptions.trim}
InputLabelProps={data ? { shrink: true } : undefined}
onFocus={onFocus}
onBlur={onBlur}
focused={focused}
/>
);
}}
renderOption={(props, option, { inputValue }) => {
const matches = match(option.name, inputValue, { insideWords: true });
const parts = parse(option.name, matches);
const matchesTags = match(option.tags, inputValue, { insideWords: true });
const partsTags = parse(option.tags, matchesTags);
const tooltipTitle = partsTags.map((part, index) => (
<span
key={index}
style={{
fontWeight: part.highlight ? 700 : 400,
color: part.highlight ? "yellow" : "white",
}}
>
{part.text}
</span>
));
return (
<li {...props}>
<div>
<Box>
{parts.map((part, index) => (
<span
key={index}
style={{
fontWeight: part.highlight ? 700 : 400,
}}
>
{part.text}
</span>
))}
{matchesTags.length > 0 && (
<Tooltip arrow sx={{ ml: 1, verticalAlign: "middle" }} title={<Typography variant="caption">Feature Alt Tags: <br /><br />{tooltipTitle}</Typography>}>
<MoreIcon color="secondary" />
</Tooltip>
)}
</Box>
</div>
</li>
);
}}
filterOptions={filterOptions}
/>
<FormHelperText error={!isValid && !showDescription}>
{firstFormHelperText}
</FormHelperText>
<FormHelperText error={!isValid}>{secondFormHelperText}</FormHelperText>
{selectedFeature != null &&
<JsonForms
ajv={customAjv}
schema={formSchemaData}
uischema={formUiSchemaData}
data={featureData}
renderers={renderers}
cells={materialCells}
onChange={({ data, errors }) => setDataAndValidate(data, errors)}
/>
}
</Hidden>
);
};
export const FeaturesAutocomplete = (
props: ControlProps & OwnPropsOfEnum & WithOptionLabel & TranslateProps
) => {
const { errors } = props;
const isValid = errors.length === 0;
return (
<MuiAutocomplete {...props} isValid={isValid} />
);
};
const isFeatureAutocompleteControl = and(
uiTypeIs('Control'),
optionIs('asyncFeatureControl', true)
);
export const featuresAutocompleteTester: RankedTester = rankWith(
1000,
isFeatureAutocompleteControl
);
export default withJsonFormsOneOfEnumProps(
withTranslateProps(React.memo(FeaturesAutocomplete)),
false
);