diff --git a/rollup.config.mjs b/rollup.config.mjs index 563e5af..9bb3c6f 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -26,7 +26,7 @@ const plugins = [ terser({ output: { comments: false }, compress: { - drop_console: true, + drop_console: false, }, }), ]; diff --git a/src/lib/Form.tsx b/src/lib/Form.tsx index 598f281..aaa55c3 100644 --- a/src/lib/Form.tsx +++ b/src/lib/Form.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from "react"; +import { ReactNode, useMemo } from "react"; import { DeepPartial, FieldValues, Resolver, SubmitHandler, useForm, UseFormReturn } from "react-hook-form"; import { jsonIsoDateReviver } from "./helpers/dateUtils"; import { FormContext, FormContextProps } from "./context/FormContext"; @@ -80,8 +80,41 @@ const Form = ({ const formMethods = useForm({ resolver, defaultValues: revivedDefaultValues }); const autoSubmitHandler = useAutoSubmit({ onSubmit, formMethods, autoSubmitConfig }); + // Memoize the object passed to function children to avoid creating new reference each render + const formPropsForChildren = useMemo( + () => ({ + ...formMethods, + disabled, + requiredFields, + hideValidationMessages, + disableAriaAutocomplete, + }), + [formMethods, disabled, requiredFields, hideValidationMessages, disableAriaAutocomplete], + ); + + // Memoize context value to prevent unnecessary re-renders of context consumers + const contextValue = useMemo( + () => ({ + requiredFields, + disabled, + hideValidationMessages, + disableAriaAutocomplete, + ...formMethods, + }), + [formMethods, requiredFields, disabled, hideValidationMessages, disableAriaAutocomplete], + ); + + // Only recompute children if the props object changes + const resolvedChildren = useMemo( + () => (typeof children === "function" ? children(formPropsForChildren) : children), + [children, formPropsForChildren], + ); + + console.log("Rendering Form", { contextValue }); + console.log("Rendering Form", { formPropsForChildren }); + return ( - +
{ if (formRef) { @@ -92,9 +125,7 @@ const Form = ({ method="POST" autoComplete={autoComplete} > - {children instanceof Function - ? children({ ...formMethods, disabled, requiredFields, hideValidationMessages, disableAriaAutocomplete }) - : children} + {resolvedChildren}
); diff --git a/src/lib/StaticTypeaheadInput.tsx b/src/lib/StaticTypeaheadInput.tsx index 1e23df8..78c5f6b 100644 --- a/src/lib/StaticTypeaheadInput.tsx +++ b/src/lib/StaticTypeaheadInput.tsx @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import { useEffect, useMemo, useState } from "react"; -import { FieldValues, useController } from "react-hook-form"; +import { FieldValues, Controller } from "react-hook-form"; import { useSafeNameId } from "src/lib/hooks/useSafeNameId"; import { CommonTypeaheadProps, StaticTypeaheadAutocompleteProps, TypeaheadOption, TypeaheadOptions } from "./types/Typeahead"; import { useFormContext } from "./context/FormContext"; @@ -29,8 +29,7 @@ interface StaticTypeaheadInputProps extends CommonTypeahe autocompleteProps?: StaticTypeaheadAutocompleteProps; } -// eslint-disable-next-line complexity -const StaticTypeaheadInput = (props: StaticTypeaheadInputProps) => { +const AutoComplete = (props: StaticTypeaheadInputProps) => { const { options, multiple, @@ -66,24 +65,14 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput fixedOptions, withFixedOptionsInValue = true, innerRef, + name, + id, } = props; const [page, setPage] = useState(1); const [loadMoreOptions, setLoadMoreOptions] = useState(limitResults !== undefined && limitResults < options.length); - const { name, id } = useSafeNameId(props.name ?? "", props.id); - const { control, disabled: formDisabled, getFieldState, clearErrors, watch } = useFormContext(); - const { - field: { ref, ...field }, - } = useController({ - name, - control, - rules: { - validate: { - required: () => getFieldState(name)?.error?.message, - }, - }, - }); + const { control, disabled: formDisabled, clearErrors } = useFormContext(); const isDisabled = useMemo(() => formDisabled || disabled, [formDisabled, disabled]); const paginatedOptions = useMemo( @@ -91,16 +80,10 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput [limitResults, page, options], ); - const fieldValue = watch(name) as string | number | string[] | number[] | undefined; - const value = useMemo( - () => - multiple - ? getMultipleAutoCompleteValue(combineOptions(options, fixedOptions), fieldValue as string[] | number[] | undefined) - : getSingleAutoCompleteValue(options, fieldValue as string | number | undefined), - [fieldValue, multiple, options, fixedOptions], - ); - - validateFixedOptions(fixedOptions, multiple, autocompleteProps, withFixedOptionsInValue, value); + // Validate fixed options upfront + useEffect(() => { + validateFixedOptions(fixedOptions, multiple, autocompleteProps, withFixedOptionsInValue, []); + }, [fixedOptions, multiple, autocompleteProps, withFixedOptionsInValue]); useEffect(() => { if (limitResults !== undefined) { @@ -108,6 +91,114 @@ const StaticTypeaheadInput = (props: StaticTypeaheadInput } }, [options, page, limitResults]); + return ( + { + const fieldState = control.getFieldState(name); + return fieldState?.error?.message; + }, + }, + }} + render={({ field }) => { + const fieldValue = field.value; + const value = multiple + ? getMultipleAutoCompleteValue(combineOptions(options, fixedOptions), fieldValue as string[] | number[] | undefined) + : getSingleAutoCompleteValue(options, fieldValue as string | number | undefined); + + return ( + + {...autocompleteProps} + {...field} + id={id} + multiple={multiple} + groupBy={useGroupBy ? groupOptions : undefined} + options={useGroupBy ? sortOptionsByGroup(paginatedOptions) : paginatedOptions} + isOptionEqualToValue={ + autocompleteProps?.isOptionEqualToValue ?? + ((option, value) => (typeof option === "string" ? option === value : option.value === (value as LabelValueOption).value)) + } + getOptionKey={ + autocompleteProps?.getOptionKey ?? + ((option: TypeaheadOption) => (typeof option === "string" ? option : `${option.label}-${option.value ?? ""}`)) + } + disableCloseOnSelect={multiple} + value={resolveInputValue(multiple, fixedOptions, withFixedOptionsInValue, value)} + getOptionLabel={(option: TypeaheadOption) => (typeof option === "string" ? option : option.label)} + getOptionDisabled={(option) => + getOptionDisabled?.(option) || + (useGroupBy && isDisabledGroup(option)) || + (typeof option === "string" ? false : (option.disabled ?? false)) + } + disabled={isDisabled} + readOnly={readOnly} + selectOnFocus={markAllOnFocus} + style={inputGroupStyle} + className={className} + autoSelect={autoSelect} + autoHighlight={autoHighlight} + onClose={readOnly ? undefined : onClose} + onOpen={readOnly ? undefined : onOpen} + onBlur={() => { + if (onBlur) { + onBlur(); + } + field.onBlur(); + }} + onChange={(_, selectedValue) => { + const optionsArray = getOptionsFromValue(selectedValue, fixedOptions, withFixedOptionsInValue); + const values = convertAutoCompleteOptionsToStringArray(optionsArray); + const finalValue = multiple ? values : values[0]; + clearErrors(field.name); + if (onChange) { + onChange(finalValue); + } + }} + onInputChange={(_e, value, reason) => { + onInputChange?.(value, reason); + }} + renderOption={highlightOptions ? renderHighlightedOptionFunction : undefined} + renderInput={(params) => ( + 0 ? undefined : placeholder} + paginationIcon={paginationIcon} + paginationText={paginationText} + variant={variant} + limitResults={limitResults} + loadMoreOptions={loadMoreOptions} + setPage={setPage} + inputRef={(elem) => { + if (innerRef) innerRef.current = elem as HTMLInputElement; + field.ref(elem); + }} + /> + )} + renderTags={createTagRenderer(fixedOptions, autocompleteProps)} + /> + ); + }} + /> + ); +}; + +const StaticTypeaheadInput = (props: StaticTypeaheadInputProps) => { + const { label, useBootstrapStyle = false } = props; + const { name, id } = useSafeNameId(props.name ?? "", props.id); + return ( (props: StaticTypeaheadInput labelStyle={useBootstrapStyle ? { color: "#8493A5", fontSize: 14 } : undefined} layout="muiInput" > - - {...autocompleteProps} - {...field} - id={id} - multiple={multiple} - groupBy={useGroupBy ? groupOptions : undefined} - options={useGroupBy ? sortOptionsByGroup(paginatedOptions) : paginatedOptions} - isOptionEqualToValue={ - autocompleteProps?.isOptionEqualToValue ?? - ((option, value) => (typeof option === "string" ? option === value : option.value === (value as LabelValueOption).value)) - } - getOptionKey={ - autocompleteProps?.getOptionKey ?? - ((option: TypeaheadOption) => (typeof option === "string" ? option : `${option.label}-${option.value ?? ""}`)) - } - disableCloseOnSelect={multiple} - value={resolveInputValue(multiple, fixedOptions, withFixedOptionsInValue, value)} - getOptionLabel={(option: TypeaheadOption) => (typeof option === "string" ? option : option.label)} - getOptionDisabled={(option) => - getOptionDisabled?.(option) || - (useGroupBy && isDisabledGroup(option)) || - (typeof option === "string" ? false : (option.disabled ?? false)) - } - disabled={isDisabled} - readOnly={readOnly} - selectOnFocus={markAllOnFocus} - style={inputGroupStyle} - className={className} - autoSelect={autoSelect} - autoHighlight={autoHighlight} - onClose={readOnly ? undefined : onClose} - onOpen={readOnly ? undefined : onOpen} - onBlur={() => { - if (onBlur) { - onBlur(); - } - field.onBlur(); - }} - onChange={(_, value) => { - // value is typed as Autocomplete (aka TypeaheadOption) or an array of Autocomplete (aka TypeaheadOption[]) - // however, the component is not intended to be used with mixed types - const optionsArray = getOptionsFromValue(value, fixedOptions, withFixedOptionsInValue); - const values = convertAutoCompleteOptionsToStringArray(optionsArray); - const finalValue = multiple ? values : values[0]; - clearErrors(field.name); - if (onChange) { - onChange(finalValue); - } - field.onChange(finalValue); - }} - onInputChange={(_e, value, reason) => { - if (onInputChange) { - onInputChange(value, reason); - } - }} - renderOption={highlightOptions ? renderHighlightedOptionFunction : undefined} - renderInput={(params) => ( - 0 ? undefined : placeholder} - paginationIcon={paginationIcon} - paginationText={paginationText} - variant={variant} - limitResults={limitResults} - loadMoreOptions={loadMoreOptions} - setPage={setPage} - {...params} - inputRef={(elem) => { - if (innerRef) { - innerRef.current = elem as HTMLInputElement; - } - ref(elem); - }} - /> - )} - renderTags={createTagRenderer(fixedOptions, autocompleteProps)} - /> + {...props} id={id} name={props.name} /> ); };