import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { Autocomplete, AutocompleteChangeDetails, AutocompleteChangeReason, CircularProgress, FilterOptionsState, Grid, TextField, TextFieldPropsSizeOverrides } from '@mui/material'
import { OverridableStringUnion } from '@mui/types';
import { Controller, ControllerRenderProps, FieldValues, RegisterOptions, useFormContext } from 'react-hook-form'
import { FormRowContext, FormRowFieldSize } from './FormRow'
export function AComboField<TItem extends ICodeName>(props: {
name: string,
codeRepo: CodeNameRepo<TItem>,
label?: string,
groupFor?: string, // 之前連動(分群)欄位
chainFor?: string, // 之後連動欄位
required?: boolean,
readOnly?: boolean,
placeholder?: string,
helperText?: string,
size?: OverridableStringUnion<'small' | 'medium', TextFieldPropsSizeOverrides>,
grid?: FormRowFieldSize
//rules?: RegisterOptions<FieldValues, string>,
validate?: (value: string, formValues: FieldValues) => string | boolean,
// notify change
notifyChange?: (newValue: string) => void // 通告輸入值有變更
}) {
const [codeList, f_loading, error] = props.codeRepo
const {
control,
setValue,
getValues,
watch,
trigger,
} = useFormContext() // retrieve all hook methods
const formRow = useContext(FormRowContext)
const rules = useMemo(() => {
const rules: RegisterOptions<FieldValues, string> = {}
if (props.required)
rules.required = `${props.label} 為必填欄位`
if (props.validate)
rules.validate = props.validate
return rules
}, [props.label, props.required, props.validate]);
// 內層元件真正的值 & 初值填入
const [innerValue, setInnerValue] = useState<TItem | null>(null)
// codeRepo 載入完成後刷新
useEffect(() => {
//console.log('codeList →', codeList?.length)
const curValue = getValues(props.name)
const valueItem = codeList.find(v => v.code === curValue) ?? null // undefined → null
setInnerValue(valueItem)
}, [codeList])
//※ 當值已改變 => 反應到 UI
const thisFieldValue = watch(props.name)
useLayoutEffect(() => {
//console.log('thisFieldValue →', thisFieldValue)
const valueItem = codeList.find(v => v.code === thisFieldValue) ?? null // undefined → null
setInnerValue(valueItem)
}, [thisFieldValue])
// used to render
const _readOnly = useMemo(() => props.readOnly ?? formRow?.readOnly, [props.readOnly, formRow])
const _size = useMemo(() => props.size ?? formRow?.size, [props.size, formRow])
const fieldElement = (
<Controller
control={control}
name={props.name}
rules={rules}
render={({ field, fieldState }) => {
const onChangeHandler = useCallback((_event: React.SyntheticEvent, value: TItem, _reason: AutocompleteChangeReason, _details: AutocompleteChangeDetails<TItem>) => {
field.onChange(value?.code) // 將觸發:當值已改變 => 反應到 UI
Boolean(props.notifyChange) && props.notifyChange(value?.code)
// 當值變了則清除之後連動欄位,讓使用者重選。
if (Boolean(props.chainFor)) {
setValue(props.chainFor, null, { shouldDirty: true, shouldValidate: true }) // 將觸發之後連動欄位:當值已改變 => 反應到 UI
}
}, [field.onChange, props.notifyChange])
return (
<Autocomplete {...field}
options={codeList}
getOptionLabel={(option) => `${option?.code}.${option?.name}`}
loading={f_loading}
size={_size}
value={innerValue}
readOnly={_readOnly}
filterOptions={filterOptionsHandler}
onChange={onChangeHandler}
onBlur={() => handleBlur(field)}
renderInput={(params) => (
<TextField {...params}
label={props.label}
required={props.required}
placeholder={props.placeholder}
variant={_readOnly ? 'filled' : 'standard'}
error={fieldState.invalid}
helperText={fieldState.error?.message || props.helperText}
InputProps={{
...params.InputProps,
readOnly: _readOnly,
endAdornment: (
<React.Fragment>
{f_loading ? <CircularProgress color="info" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
)
}}
/>
)
if (Boolean(formRow)) {
const [xs, sm, md, lg, xl] = useMemo(() => props.grid ?? formRow.grid, [props.grid, formRow.grid])
return (
<Grid item xs={xs} sm={sm} md={md} lg={lg} xl={xl}>
{fieldElement}
</Grid>
)
}
return (<>{fieldElement}</>)
//----------------
function handleBlur(field: ControllerRenderProps<FieldValues, string>) {
trigger(props.name) // 將觸發 validation 並更新編輯狀態。
field.onBlur()
}
function filterOptionsHandler(options: TItem[], state: FilterOptionsState<TItem>) {
// 依之前連動(分群)欄位過濾選項
if (Boolean(props.groupFor)) {
const groupValue = getValues(props.groupFor)
return options.filter(item => item.group === groupValue)
} else {
return options
}
}
}