Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass through original values for invalid dates #1949

Merged
merged 4 commits into from
Jun 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions packages/material/src/controls/MaterialDateControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ 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
} from '@mui/lab';
import AdapterDayjs from '@mui/lab/AdapterDayjs';
import { createOnChangeHandler, getData, useFocus } from '../util';
import {
createOnChangeHandler,
getData,
ResettableTextField,
useFocus,
} from '../util';

export const MaterialDateControl = (props: ControlProps)=> {
const [focused, onFocus, onBlur] = useFocus();
Expand Down Expand Up @@ -80,12 +85,15 @@ export const MaterialDateControl = (props: ControlProps)=> {
saveFormat
),[path, handleChange, saveFormat]);

const value = getData(data, saveFormat);
const valueInInputFormat = value ? value.format(format) : '';

return (
<Hidden xsUp={!visible}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DatePicker
label={label}
value={getData(data, saveFormat)}
value={value}
clearable
onChange={onChange}
inputFormat={format}
Expand All @@ -96,14 +104,21 @@ export const MaterialDateControl = (props: ControlProps)=> {
clearText={appliedUiSchemaOptions.clearLabel}
okText={appliedUiSchemaOptions.okLabel}
renderInput={params => (
<TextField
<ResettableTextField
{...params}
rawValue={data}
dayjsValueIsValid={value !== null}
valueInInputFormat={valueInInputFormat}
focused={focused}
id={id + '-input'}
required={required && !appliedUiSchemaOptions.hideRequiredAsterisk}
autoFocus={appliedUiSchemaOptions.focus}
error={!isValid}
fullWidth={!appliedUiSchemaOptions.trim}
inputProps={{ ...params.inputProps, type: 'text' }}
inputProps={{
...params.inputProps,
type: 'text',
}}
InputLabelProps={data ? { shrink: true } : undefined}
onFocus={onFocus}
onBlur={onBlur}
Expand Down
25 changes: 20 additions & 5 deletions packages/material/src/controls/MaterialDateTimeControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ 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
} from '@mui/lab';
import AdapterDayjs from '@mui/lab/AdapterDayjs';
import { createOnChangeHandler, getData, useFocus } from '../util';
import {
createOnChangeHandler,
getData,
ResettableTextField,
useFocus
} from '../util';

export const MaterialDateTimeControl = (props: ControlProps) => {
const [focused, onFocus, onBlur] = useFocus();
Expand Down Expand Up @@ -82,12 +87,15 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
saveFormat
),[path, handleChange, saveFormat]);

const value = getData(data, saveFormat);
const valueInInputFormat = value ? value.format(format) : '';

return (
<Hidden xsUp={!visible}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
label={label}
value={getData(data, saveFormat)}
value={value}
clearable
onChange={onChange}
inputFormat={format}
Expand All @@ -99,14 +107,21 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
clearText={appliedUiSchemaOptions.clearLabel}
okText={appliedUiSchemaOptions.okLabel}
renderInput={params => (
<TextField
<ResettableTextField
{...params}
rawValue={data}
dayjsValueIsValid={value !== null}
valueInInputFormat={valueInInputFormat}
focused={focused}
id={id + '-input'}
required={required && !appliedUiSchemaOptions.hideRequiredAsterisk}
autoFocus={appliedUiSchemaOptions.focus}
error={!isValid}
fullWidth={!appliedUiSchemaOptions.trim}
inputProps={{ ...params.inputProps, type: 'text' }}
inputProps={{
...params.inputProps,
type: 'text',
}}
InputLabelProps={data ? { shrink: true } : undefined}
onFocus={onFocus}
onBlur={onBlur}
Expand Down
25 changes: 20 additions & 5 deletions packages/material/src/controls/MaterialTimeControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ 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
} from '@mui/lab';
import AdapterDayjs from '@mui/lab/AdapterDayjs';
import { createOnChangeHandler, getData, useFocus } from '../util';
import {
createOnChangeHandler,
getData,
ResettableTextField,
useFocus
} from '../util';

export const MaterialTimeControl = (props: ControlProps) => {
const [focused, onFocus, onBlur] = useFocus();
Expand Down Expand Up @@ -82,12 +87,15 @@ export const MaterialTimeControl = (props: ControlProps) => {
saveFormat
),[path, handleChange, saveFormat]);

const value = getData(data, saveFormat);
const valueInInputFormat = value ? value.format(format) : '';

return (
<Hidden xsUp={!visible}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<TimePicker
label={label}
value={getData(data, saveFormat)}
value={value}
clearable
onChange={onChange}
inputFormat={format}
Expand All @@ -99,14 +107,21 @@ export const MaterialTimeControl = (props: ControlProps) => {
clearText={appliedUiSchemaOptions.clearLabel}
okText={appliedUiSchemaOptions.okLabel}
renderInput={params => (
<TextField
<ResettableTextField
{...params}
rawValue={data}
dayjsValueIsValid={value !== null}
valueInInputFormat={valueInInputFormat}
focused={focused}
id={id + '-input'}
required={required && !appliedUiSchemaOptions.hideRequiredAsterisk}
autoFocus={appliedUiSchemaOptions.focus}
error={!isValid}
fullWidth={!appliedUiSchemaOptions.trim}
inputProps={{ ...params.inputProps, type: 'text' }}
inputProps={{
...params.inputProps,
type: 'text'
}}
InputLabelProps={data ? { shrink: true } : undefined}
onFocus={onFocus}
onBlur={onBlur}
Expand Down
32 changes: 0 additions & 32 deletions packages/material/src/util/datejs.ts

This file was deleted.

73 changes: 73 additions & 0 deletions packages/material/src/util/datejs.tsx
Original file line number Diff line number Diff line change
@@ -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<ResettableTextFieldProps> = ({ rawValue, dayjsValueIsValid, valueInInputFormat, focused, inputProps, ...props }) => {
const value = useRef<InputRef>({ 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 <TextField {...props} inputProps={{ ...inputProps, value: value.current.toShow || '' }} />
}
27 changes: 27 additions & 0 deletions packages/material/test/renderers/MaterialDateControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,31 @@ describe('Material date control', () => {
input.simulate('change', input);
expect(onChangeData.data.foo).toBe('04---1961');
});

it('should call onChange with original input value for invalid date strings', () => {
const core = initCore(schema, uischema);
const onChangeData: any = {
data: undefined
};
wrapper = mount(
<JsonFormsStateProvider initState={{ renderers: materialRenderers, core }}>
<TestEmitter
onChange={({ data }) => {
onChangeData.data = data;
}}
/>
<MaterialDateControl
schema={schema}
uischema={{...uischema}}
/>
</JsonFormsStateProvider>
);

const input = wrapper.find('input').first();
expect(input.props().value).toBe('');

(input.getDOMNode() as HTMLInputElement).value = 'invalid date string';
input.simulate('change', input);
expect(onChangeData.data.foo).toBe('invalid date string');
});
});
27 changes: 27 additions & 0 deletions packages/material/test/renderers/MaterialDateTimeControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,31 @@ describe('Material date time control', () => {
input.simulate('change', input);
expect(onChangeData.data.foo).toBe('2005/12/10 11:22 am');
});

it('should call onChange with original input value for invalid date strings', () => {
const core = initCore(schema, uischema);
const onChangeData: any = {
data: undefined
};
wrapper = mount(
<JsonFormsStateProvider initState={{ renderers: materialRenderers, core }}>
<TestEmitter
onChange={({ data }) => {
onChangeData.data = data;
}}
/>
<MaterialDateTimeControl
schema={schema}
uischema={{...uischema}}
/>
</JsonFormsStateProvider>
);

const input = wrapper.find('input').first();
expect(input.props().value).toBe('');

(input.getDOMNode() as HTMLInputElement).value = 'invalid date string';
input.simulate('change', input);
expect(onChangeData.data.foo).toBe('invalid date string');
});
});
27 changes: 27 additions & 0 deletions packages/material/test/renderers/MaterialTimeControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,31 @@ describe('Material time control', () => {
input.simulate('change', input);
expect(onChangeData.data.foo).toBe('1//12 am');
});

it('should call onChange with original input value for invalid date strings', () => {
const core = initCore(schema, uischema);
const onChangeData: any = {
data: undefined
};
wrapper = mount(
<JsonFormsStateProvider initState={{ renderers: materialRenderers, core }}>
<TestEmitter
onChange={({ data }) => {
onChangeData.data = data;
}}
/>
<MaterialTimeControl
schema={schema}
uischema={{...uischema}}
/>
</JsonFormsStateProvider>
);

const input = wrapper.find('input').first();
expect(input.props().value).toBe('');

(input.getDOMNode() as HTMLInputElement).value = 'invalid date string';
input.simulate('change', input);
expect(onChangeData.data.foo).toBe('invalid date string');
});
});