From 3e01b86278b9c4cf7bbd8c4813ba6843beb2ec37 Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Tue, 21 Jun 2022 12:29:42 +0200 Subject: [PATCH] Fix issues via ResettableTextField --- .../src/controls/MaterialDateControl.tsx | 20 +++-- .../src/controls/MaterialDateTimeControl.tsx | 21 +++--- .../src/controls/MaterialTimeControl.tsx | 23 +++--- packages/material/src/util/datejs.ts | 68 ----------------- packages/material/src/util/datejs.tsx | 73 +++++++++++++++++++ 5 files changed, 110 insertions(+), 95 deletions(-) delete mode 100644 packages/material/src/util/datejs.ts create mode 100644 packages/material/src/util/datejs.tsx diff --git a/packages/material/src/controls/MaterialDateControl.tsx b/packages/material/src/controls/MaterialDateControl.tsx index 2bdafbd45..baea6cfe7 100644 --- a/packages/material/src/controls/MaterialDateControl.tsx +++ b/packages/material/src/controls/MaterialDateControl.tsx @@ -32,7 +32,7 @@ import { rankWith, } from '@jsonforms/core'; import { withJsonFormsControlProps } from '@jsonforms/react'; -import { FormHelperText, Hidden, TextField } from '@mui/material'; +import { FormHelperText, Hidden } from '@mui/material'; import { DatePicker, LocalizationProvider @@ -41,8 +41,8 @@ import AdapterDayjs from '@mui/lab/AdapterDayjs'; import { createOnChangeHandler, getData, + ResettableTextField, useFocus, - useParsedDateSynchronizer, } from '../util'; export const MaterialDateControl = (props: ControlProps)=> { @@ -84,14 +84,16 @@ export const MaterialDateControl = (props: ControlProps)=> { handleChange, saveFormat ),[path, handleChange, saveFormat]); - const parsedDateSynchronizer = useParsedDateSynchronizer({ data, onBlur }); + + const value = getData(data, saveFormat); + const valueInInputFormat = value ? value.format(format) : ''; return ( { clearText={appliedUiSchemaOptions.clearLabel} okText={appliedUiSchemaOptions.okLabel} renderInput={params => ( - { inputProps={{ ...params.inputProps, type: 'text', - value: parsedDateSynchronizer.value, - onChange: parsedDateSynchronizer.createOnChangeHandler(params.inputProps.onChange), }} InputLabelProps={data ? { shrink: true } : undefined} onFocus={onFocus} - onBlur={parsedDateSynchronizer.onBlur} + onBlur={onBlur} variant={'standard'} /> )} diff --git a/packages/material/src/controls/MaterialDateTimeControl.tsx b/packages/material/src/controls/MaterialDateTimeControl.tsx index 956759b16..6adabdc33 100644 --- a/packages/material/src/controls/MaterialDateTimeControl.tsx +++ b/packages/material/src/controls/MaterialDateTimeControl.tsx @@ -32,7 +32,7 @@ import { rankWith } from '@jsonforms/core'; import { withJsonFormsControlProps } from '@jsonforms/react'; -import { FormHelperText, Hidden, TextField } from '@mui/material'; +import { FormHelperText, Hidden } from '@mui/material'; import { DateTimePicker, LocalizationProvider @@ -41,8 +41,8 @@ import AdapterDayjs from '@mui/lab/AdapterDayjs'; import { createOnChangeHandler, getData, - useFocus, - useParsedDateSynchronizer, + ResettableTextField, + useFocus } from '../util'; export const MaterialDateTimeControl = (props: ControlProps) => { @@ -87,14 +87,15 @@ export const MaterialDateTimeControl = (props: ControlProps) => { saveFormat ),[path, handleChange, saveFormat]); - const parsedDateSynchronizer = useParsedDateSynchronizer({ data, onBlur }); + const value = getData(data, saveFormat); + const valueInInputFormat = value ? value.format(format) : ''; return ( { clearText={appliedUiSchemaOptions.clearLabel} okText={appliedUiSchemaOptions.okLabel} renderInput={params => ( - { inputProps={{ ...params.inputProps, type: 'text', - value: parsedDateSynchronizer.value, - onChange: parsedDateSynchronizer.createOnChangeHandler(params.inputProps.onChange), }} InputLabelProps={data ? { shrink: true } : undefined} onFocus={onFocus} - onBlur={parsedDateSynchronizer.onBlur} + onBlur={onBlur} variant={'standard'} /> )} diff --git a/packages/material/src/controls/MaterialTimeControl.tsx b/packages/material/src/controls/MaterialTimeControl.tsx index f4f73fc62..5777acb33 100644 --- a/packages/material/src/controls/MaterialTimeControl.tsx +++ b/packages/material/src/controls/MaterialTimeControl.tsx @@ -32,7 +32,7 @@ import { rankWith } from '@jsonforms/core'; import { withJsonFormsControlProps } from '@jsonforms/react'; -import { FormHelperText, Hidden, TextField } from '@mui/material'; +import { FormHelperText, Hidden } from '@mui/material'; import { TimePicker, LocalizationProvider @@ -41,8 +41,8 @@ import AdapterDayjs from '@mui/lab/AdapterDayjs'; import { createOnChangeHandler, getData, - useFocus, - useParsedDateSynchronizer, + ResettableTextField, + useFocus } from '../util'; export const MaterialTimeControl = (props: ControlProps) => { @@ -87,14 +87,15 @@ export const MaterialTimeControl = (props: ControlProps) => { saveFormat ),[path, handleChange, saveFormat]); - const parsedDateSynchronizer = useParsedDateSynchronizer({ data, onBlur }); + const value = getData(data, saveFormat); + const valueInInputFormat = value ? value.format(format) : ''; return ( { clearText={appliedUiSchemaOptions.clearLabel} okText={appliedUiSchemaOptions.okLabel} renderInput={params => ( - { fullWidth={!appliedUiSchemaOptions.trim} inputProps={{ ...params.inputProps, - type: 'text', - value: parsedDateSynchronizer.value, - onChange: parsedDateSynchronizer.createOnChangeHandler(params.inputProps.onChange), + type: 'text' }} InputLabelProps={data ? { shrink: true } : undefined} onFocus={onFocus} - onBlur={parsedDateSynchronizer.onBlur} + onBlur={onBlur} variant={'standard'} /> )} diff --git a/packages/material/src/util/datejs.ts b/packages/material/src/util/datejs.ts deleted file mode 100644 index 76be9b799..000000000 --- a/packages/material/src/util/datejs.ts +++ /dev/null @@ -1,68 +0,0 @@ -import dayjs from 'dayjs'; -import customParsing from 'dayjs/plugin/customParseFormat'; -import { useState, useMemo, FormEvent, FormEventHandler } from 'react'; - -// required for the custom save formats in the date, time and date-time pickers -dayjs.extend(customParsing); - -export const createOnChangeHandler = ( - path: string, - handleChange: (path: string, value: any) => void, - saveFormat: string | undefined -) => (time: dayjs.Dayjs, textInputValue: string) => { - if (!time) { - handleChange(path, undefined); - return; - } - const result = dayjs(time).format(saveFormat); - handleChange(path, result === 'Invalid Date' ? textInputValue : result); -}; - -export const getData = ( - data: any, - saveFormat: string | undefined -): dayjs.Dayjs | null => { - if (!data) { - return null; - } - const dayjsData = dayjs(data, saveFormat); - if (dayjsData.toString() === 'Invalid Date') { - return null; - } - return dayjsData; -}; - -type DateInputFormEvent = FormEvent; - -/** - * Improves the UX of date fields by controlling the rendered input value. - * When a user enters a date value that ends up being different than the - * the value of `data`, then on blur we sync the rendered input value. - * - * @param data The parsed date value. - * @param onBlur Additional handler to run after input value is sync'd. - * @returns Props to pass to the rendered input element. - */ -export const useParsedDateSynchronizer = (props: { - data: any; - onBlur: FormEventHandler | undefined; -}) => { - const [value, setValue] = useState(props.data); - - const onBlur = useMemo( - () => (event: DateInputFormEvent) => { - setValue(props.data); - if (props.onBlur) props.onBlur(event); - }, - [props.data, props.onBlur] - ); - - const createOnChangeHandler = ( - onChange: (event: DateInputFormEvent) => void - ) => (event: DateInputFormEvent) => { - setValue((event.target as HTMLInputElement | HTMLTextAreaElement).value); - if (onChange) onChange(event); - }; - - return { value, onBlur, createOnChangeHandler }; -}; diff --git a/packages/material/src/util/datejs.tsx b/packages/material/src/util/datejs.tsx new file mode 100644 index 000000000..fd83272ee --- /dev/null +++ b/packages/material/src/util/datejs.tsx @@ -0,0 +1,73 @@ +import { TextField, TextFieldProps } from '@mui/material'; +import dayjs from 'dayjs'; +import customParsing from 'dayjs/plugin/customParseFormat'; +import React, { useRef} from 'react'; + +// required for the custom save formats in the date, time and date-time pickers +dayjs.extend(customParsing); + +export const createOnChangeHandler = ( + path: string, + handleChange: (path: string, value: any) => void, + saveFormat: string | undefined +) => (time: dayjs.Dayjs, textInputValue: string) => { + if (!time) { + handleChange(path, undefined); + return; + } + const result = dayjs(time).format(saveFormat); + handleChange(path, result === 'Invalid Date' ? textInputValue : result); +}; + +export const getData = ( + data: any, + saveFormat: string | undefined +): dayjs.Dayjs | null => { + if (!data) { + return null; + } + const dayjsData = dayjs(data, saveFormat); + if (dayjsData.toString() === 'Invalid Date') { + return null; + } + return dayjsData; +}; + + +interface InputRef { + lastInput: string; + toShow: string; +} + +type ResettableTextFieldProps = TextFieldProps & { + rawValue: any; + dayjsValueIsValid: boolean; + valueInInputFormat: string; + focused: boolean; +} + +/** + * The dayjs formatter/parser is very lenient and for example ignores additional digits and/or characters. + * In these cases the input text can look vastly different than the actual value stored in the data. + * The 'ResettableTextField' component adjusts the text field to reflect the actual value stored in the data + * once it's no longer 'focused', i.e. when the user stops editing. + */ +export const ResettableTextField: React.FC = ({ rawValue, dayjsValueIsValid, valueInInputFormat, focused, inputProps, ...props }) => { + const value = useRef({ lastInput: inputProps?.value, toShow: inputProps?.value }); + if (!focused) { + // The input text is not focused, therefore let's show the value actually stored in the data + if (!dayjsValueIsValid) { + // pass through the "raw" value in case it can't be formatted by dayjs + value.current.toShow = typeof rawValue === 'string' || rawValue === null || rawValue === undefined ? rawValue : JSON.stringify(rawValue) + } else { + // otherwise use the specified format + value.current.toShow = valueInInputFormat; + } + } + if (focused && inputProps?.value !== value.current.lastInput) { + // Show the current text the user is typing into the text input + value.current.lastInput = inputProps?.value; + value.current.toShow = inputProps?.value; + } + return +}