Skip to content

Commit 01b08f0

Browse files
LukasBollsdirix
authored andcommitted
fix(material): Improve usability of date renderers
Previously, data wasn't cleared when the input field was emptied. This commit will set the value to 'undefined' when the input is empty or invalid upon blurring. During editing, the data is only updated when the current input is valid. Closes #2183
1 parent dca7aaa commit 01b08f0

File tree

12 files changed

+237
-35
lines changed

12 files changed

+237
-35
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
The MIT License
3+
4+
Copyright (c) 2023-2023 EclipseSource Munich
5+
https://github.com/eclipsesource/jsonforms
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
24+
*/
25+
26+
export const defaultDateFormat = 'YYYY-MM-DD';
27+
export const defaultTimeFormat = 'HH:mm:ss';
28+
export const defaultDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss.sssZ';

packages/core/src/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ export * from './type';
3838
export * from './uischema';
3939
export * from './util';
4040
export * from './validator';
41+
export * from './defaultDateFormat';

packages/examples/src/examples/dates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,12 @@ export const uischema = {
129129
export const data = {
130130
schemaBased: {
131131
date: new Date().toISOString().substr(0, 10),
132-
time: '13:37',
132+
time: '13:37:00',
133133
datetime: new Date().toISOString(),
134134
},
135135
uiSchemaBased: {
136136
date: new Date().toISOString().substr(0, 10),
137-
time: '13:37',
137+
time: '13:37:00',
138138
datetime: '1999/12/11 10:05 am',
139139
},
140140
};

packages/material-renderers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
},
6060
"testEnvironment": "jsdom",
6161
"testMatch": [
62-
"**/test/**/*.test.tsx"
62+
"**/test/**/*.test.tsx",
63+
"**/test/**.test.ts"
6364
],
6465
"testPathIgnorePatterns": [
6566
"/node_modules/",

packages/material-renderers/src/controls/MaterialDateControl.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
THE SOFTWARE.
2424
*/
2525
import merge from 'lodash/merge';
26-
import React, { useMemo } from 'react';
26+
import React, { useCallback, useMemo, useState } from 'react';
2727
import {
2828
ControlProps,
29+
defaultDateFormat,
2930
isDateControl,
3031
isDescriptionHidden,
3132
RankedTester,
@@ -35,7 +36,12 @@ import { withJsonFormsControlProps } from '@jsonforms/react';
3536
import { FormHelperText, Hidden } from '@mui/material';
3637
import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
3738
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
38-
import { createOnChangeHandler, getData, useFocus } from '../util';
39+
import {
40+
createOnBlurHandler,
41+
createOnChangeHandler,
42+
getData,
43+
useFocus,
44+
} from '../util';
3945

4046
export const MaterialDateControl = (props: ControlProps) => {
4147
const [focused, onFocus, onBlur] = useFocus();
@@ -62,8 +68,10 @@ export const MaterialDateControl = (props: ControlProps) => {
6268
appliedUiSchemaOptions.showUnfocusedDescription
6369
);
6470

71+
const [key, setKey] = useState<number>(0);
72+
6573
const format = appliedUiSchemaOptions.dateFormat ?? 'YYYY-MM-DD';
66-
const saveFormat = appliedUiSchemaOptions.dateSaveFormat ?? 'YYYY-MM-DD';
74+
const saveFormat = appliedUiSchemaOptions.dateSaveFormat ?? defaultDateFormat;
6775

6876
const views = appliedUiSchemaOptions.views ?? ['year', 'day'];
6977

@@ -73,20 +81,36 @@ export const MaterialDateControl = (props: ControlProps) => {
7381
? errors
7482
: null;
7583
const secondFormHelperText = showDescription && !isValid ? errors : null;
84+
85+
const updateChild = useCallback(() => setKey((key) => key + 1), []);
86+
7687
const onChange = useMemo(
7788
() => createOnChangeHandler(path, handleChange, saveFormat),
7889
[path, handleChange, saveFormat]
7990
);
8091

92+
const onBlurHandler = useMemo(
93+
() =>
94+
createOnBlurHandler(
95+
path,
96+
handleChange,
97+
format,
98+
saveFormat,
99+
updateChild,
100+
onBlur
101+
),
102+
[path, handleChange, format, saveFormat, updateChild]
103+
);
81104
const value = getData(data, saveFormat);
82105

83106
return (
84107
<Hidden xsUp={!visible}>
85108
<LocalizationProvider dateAdapter={AdapterDayjs}>
86109
<DatePicker
110+
key={key}
87111
label={label}
88112
value={value}
89-
onChange={onChange}
113+
onAccept={onChange}
90114
format={format}
91115
views={views}
92116
disabled={!enabled}
@@ -109,7 +133,7 @@ export const MaterialDateControl = (props: ControlProps) => {
109133
},
110134
InputLabelProps: data ? { shrink: true } : undefined,
111135
onFocus: onFocus,
112-
onBlur: onBlur,
136+
onBlur: onBlurHandler,
113137
},
114138
}}
115139
/>

packages/material-renderers/src/controls/MaterialDateTimeControl.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import React, { useMemo } from 'react';
25+
import React, { useCallback, useMemo, useState } from 'react';
2626
import merge from 'lodash/merge';
2727
import {
2828
ControlProps,
29+
defaultDateTimeFormat,
2930
isDateTimeControl,
3031
isDescriptionHidden,
3132
RankedTester,
@@ -35,7 +36,12 @@ import { withJsonFormsControlProps } from '@jsonforms/react';
3536
import { FormHelperText, Hidden } from '@mui/material';
3637
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
3738
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
38-
import { createOnChangeHandler, getData, useFocus } from '../util';
39+
import {
40+
createOnBlurHandler,
41+
createOnChangeHandler,
42+
getData,
43+
useFocus,
44+
} from '../util';
3945

4046
export const MaterialDateTimeControl = (props: ControlProps) => {
4147
const [focused, onFocus, onBlur] = useFocus();
@@ -64,7 +70,10 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
6470
);
6571

6672
const format = appliedUiSchemaOptions.dateTimeFormat ?? 'YYYY-MM-DD HH:mm';
67-
const saveFormat = appliedUiSchemaOptions.dateTimeSaveFormat ?? undefined;
73+
const saveFormat =
74+
appliedUiSchemaOptions.dateTimeSaveFormat ?? defaultDateTimeFormat;
75+
76+
const [key, setKey] = useState<number>(0);
6877

6978
const views = appliedUiSchemaOptions.views ?? [
7079
'year',
@@ -80,20 +89,35 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
8089
: null;
8190
const secondFormHelperText = showDescription && !isValid ? errors : null;
8291

92+
const updateChild = useCallback(() => setKey((key) => key + 1), []);
93+
8394
const onChange = useMemo(
8495
() => createOnChangeHandler(path, handleChange, saveFormat),
8596
[path, handleChange, saveFormat]
8697
);
8798

99+
const onBlurHandler = useMemo(
100+
() =>
101+
createOnBlurHandler(
102+
path,
103+
handleChange,
104+
format,
105+
saveFormat,
106+
updateChild,
107+
onBlur
108+
),
109+
[path, handleChange, format, saveFormat, updateChild]
110+
);
88111
const value = getData(data, saveFormat);
89112

90113
return (
91114
<Hidden xsUp={!visible}>
92115
<LocalizationProvider dateAdapter={AdapterDayjs}>
93116
<DateTimePicker
117+
key={key}
94118
label={label}
95119
value={value}
96-
onChange={onChange}
120+
onAccept={onChange}
97121
format={format}
98122
ampm={!!appliedUiSchemaOptions.ampm}
99123
views={views}
@@ -117,7 +141,7 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
117141
},
118142
InputLabelProps: data ? { shrink: true } : undefined,
119143
onFocus: onFocus,
120-
onBlur: onBlur,
144+
onBlur: onBlurHandler,
121145
},
122146
}}
123147
/>

packages/material-renderers/src/controls/MaterialTimeControl.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,26 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import React, { useMemo } from 'react';
25+
import React, { useCallback, useMemo, useState } from 'react';
2626
import merge from 'lodash/merge';
2727
import {
2828
ControlProps,
2929
isTimeControl,
3030
isDescriptionHidden,
3131
RankedTester,
3232
rankWith,
33+
defaultTimeFormat,
3334
} from '@jsonforms/core';
3435
import { withJsonFormsControlProps } from '@jsonforms/react';
3536
import { FormHelperText, Hidden } from '@mui/material';
3637
import { TimePicker, LocalizationProvider } from '@mui/x-date-pickers';
3738
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
38-
import { createOnChangeHandler, getData, useFocus } from '../util';
39+
import {
40+
createOnBlurHandler,
41+
createOnChangeHandler,
42+
getData,
43+
useFocus,
44+
} from '../util';
3945

4046
export const MaterialTimeControl = (props: ControlProps) => {
4147
const [focused, onFocus, onBlur] = useFocus();
@@ -56,6 +62,8 @@ export const MaterialTimeControl = (props: ControlProps) => {
5662
const appliedUiSchemaOptions = merge({}, config, uischema.options);
5763
const isValid = errors.length === 0;
5864

65+
const [key, setKey] = useState<number>(0);
66+
5967
const showDescription = !isDescriptionHidden(
6068
visible,
6169
description,
@@ -64,7 +72,7 @@ export const MaterialTimeControl = (props: ControlProps) => {
6472
);
6573

6674
const format = appliedUiSchemaOptions.timeFormat ?? 'HH:mm';
67-
const saveFormat = appliedUiSchemaOptions.timeSaveFormat ?? 'HH:mm:ss';
75+
const saveFormat = appliedUiSchemaOptions.timeSaveFormat ?? defaultTimeFormat;
6876

6977
const views = appliedUiSchemaOptions.views ?? ['hours', 'minutes'];
7078

@@ -75,19 +83,35 @@ export const MaterialTimeControl = (props: ControlProps) => {
7583
: null;
7684
const secondFormHelperText = showDescription && !isValid ? errors : null;
7785

86+
const updateChild = useCallback(() => setKey((key) => key + 1), []);
87+
7888
const onChange = useMemo(
7989
() => createOnChangeHandler(path, handleChange, saveFormat),
8090
[path, handleChange, saveFormat]
8191
);
8292

93+
const onBlurHandler = useMemo(
94+
() =>
95+
createOnBlurHandler(
96+
path,
97+
handleChange,
98+
format,
99+
saveFormat,
100+
updateChild,
101+
onBlur
102+
),
103+
[path, handleChange, format, saveFormat, updateChild]
104+
);
83105
const value = getData(data, saveFormat);
106+
84107
return (
85108
<Hidden xsUp={!visible}>
86109
<LocalizationProvider dateAdapter={AdapterDayjs}>
87110
<TimePicker
111+
key={key}
88112
label={label}
89113
value={value}
90-
onChange={onChange}
114+
onAccept={onChange}
91115
format={format}
92116
ampm={!!appliedUiSchemaOptions.ampm}
93117
views={views}
@@ -111,7 +135,7 @@ export const MaterialTimeControl = (props: ControlProps) => {
111135
},
112136
InputLabelProps: data ? { shrink: true } : undefined,
113137
onFocus: onFocus,
114-
onBlur: onBlur,
138+
onBlur: onBlurHandler,
115139
},
116140
}}
117141
/>

packages/material-renderers/src/util/datejs.tsx

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,56 @@ export const createOnChangeHandler =
88
(
99
path: string,
1010
handleChange: (path: string, value: any) => void,
11-
saveFormat: string | undefined
11+
saveFormat: string
1212
) =>
13-
(time: dayjs.Dayjs) => {
14-
if (!time) {
13+
(value: dayjs.Dayjs) => {
14+
if (!value) {
1515
handleChange(path, undefined);
16-
return;
16+
} else if (value.toString() !== 'Invalid Date') {
17+
const formatedDate = formatDate(value, saveFormat);
18+
handleChange(path, formatedDate);
1719
}
18-
const result = dayjs(time).format(saveFormat);
19-
handleChange(path, result);
2020
};
2121

22+
export const createOnBlurHandler =
23+
(
24+
path: string,
25+
handleChange: (path: string, value: any) => void,
26+
format: string,
27+
saveFormat: string,
28+
rerenderChild: () => void,
29+
onBlur: () => void
30+
) =>
31+
(e: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement, Element>) => {
32+
const date = dayjs(e.target.value, format);
33+
const formatedDate = formatDate(date, saveFormat);
34+
if (formatedDate.toString() === 'Invalid Date') {
35+
handleChange(path, undefined);
36+
rerenderChild();
37+
} else {
38+
handleChange(path, formatedDate);
39+
}
40+
onBlur();
41+
};
42+
43+
export const formatDate = (date: dayjs.Dayjs, saveFormat: string) => {
44+
let formatedDate = date.format(saveFormat);
45+
// Workaround to address a bug in Dayjs, neglecting leading 0 (https://github.com/iamkun/dayjs/issues/1849)
46+
const indexOfYear = saveFormat.indexOf('YYYY');
47+
if (date.year() < 1000 && indexOfYear !== -1) {
48+
const stringUpToYear = formatedDate.slice(0, indexOfYear);
49+
const stringFromYear = formatedDate.slice(indexOfYear);
50+
if (date.year() >= 100) {
51+
formatedDate = [stringUpToYear, 0, stringFromYear].join('');
52+
} else if (date.year() >= 10) {
53+
formatedDate = [stringUpToYear, 0, 0, stringFromYear].join('');
54+
} else if (date.year() >= 1) {
55+
formatedDate = [stringUpToYear, 0, 0, 0, stringFromYear].join('');
56+
}
57+
}
58+
return formatedDate;
59+
};
60+
2261
export const getData = (
2362
data: any,
2463
saveFormat: string | undefined

0 commit comments

Comments
 (0)