Skip to content

Commit c7f8cd6

Browse files
[docs] Add full custom field creation example (mui#15194)
Signed-off-by: Flavien DELANGLE <[email protected]> Signed-off-by: Lukas Tyla <[email protected]> Co-authored-by: Lukas Tyla <[email protected]>
1 parent aa515ec commit c7f8cd6

File tree

6 files changed

+337
-2
lines changed

6 files changed

+337
-2
lines changed

docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function MaskedDateField(props) {
131131
return (
132132
<TextField
133133
placeholder={parsedFormat}
134-
error={!!hasValidationError}
134+
error={hasValidationError}
135135
{...rifmProps}
136136
{...forwardedProps}
137137
/>

docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ function MaskedDateField(props: DatePickerFieldProps) {
135135
return (
136136
<TextField
137137
placeholder={parsedFormat}
138-
error={!!hasValidationError}
138+
error={hasValidationError}
139139
{...rifmProps}
140140
{...forwardedProps}
141141
/>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import * as React from 'react';
2+
import dayjs from 'dayjs';
3+
import TextField from '@mui/material/TextField';
4+
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
5+
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
6+
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
7+
import {
8+
useSplitFieldProps,
9+
useParsedFormat,
10+
usePickerContext,
11+
} from '@mui/x-date-pickers/hooks';
12+
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
13+
14+
function CustomDateField(props) {
15+
// TextField does not support slots and slotProps before `@mui/material` v6.0
16+
const { slots, slotProps, ...other } = props;
17+
const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date');
18+
19+
const pickerContext = usePickerContext();
20+
const placeholder = useParsedFormat();
21+
const [inputValue, setInputValue] = useInputValue();
22+
23+
// Check if the current value is valid or not.
24+
const { hasValidationError } = useValidation({
25+
value: pickerContext.value,
26+
timezone: pickerContext.timezone,
27+
props: internalProps,
28+
validator: validateDate,
29+
});
30+
31+
const handleChange = (event) => {
32+
const newInputValue = event.target.value;
33+
const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
34+
setInputValue(newInputValue);
35+
pickerContext.setValue(newValue);
36+
};
37+
38+
return (
39+
<TextField
40+
{...forwardedProps}
41+
placeholder={placeholder}
42+
value={inputValue}
43+
onChange={handleChange}
44+
error={hasValidationError}
45+
/>
46+
);
47+
}
48+
49+
function useInputValue() {
50+
const pickerContext = usePickerContext();
51+
const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value);
52+
const [inputValue, setInputValue] = React.useState(() =>
53+
createInputValue(pickerContext.value, pickerContext.fieldFormat),
54+
);
55+
56+
if (lastValueProp !== pickerContext.value) {
57+
setLastValueProp(pickerContext.value);
58+
if (pickerContext.value && pickerContext.value.isValid()) {
59+
setInputValue(
60+
createInputValue(pickerContext.value, pickerContext.fieldFormat),
61+
);
62+
}
63+
}
64+
65+
return [inputValue, setInputValue];
66+
}
67+
68+
function createInputValue(value, format) {
69+
if (value == null) {
70+
return '';
71+
}
72+
73+
return value.isValid() ? value.format(format) : '';
74+
}
75+
76+
function CustomFieldDatePicker(props) {
77+
return (
78+
<DatePicker slots={{ ...props.slots, field: CustomDateField }} {...props} />
79+
);
80+
}
81+
82+
export default function MaterialDatePicker() {
83+
return (
84+
<LocalizationProvider dateAdapter={AdapterDayjs}>
85+
<CustomFieldDatePicker />
86+
</LocalizationProvider>
87+
);
88+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as React from 'react';
2+
import dayjs, { Dayjs } from 'dayjs';
3+
import TextField from '@mui/material/TextField';
4+
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
5+
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
6+
import {
7+
DatePicker,
8+
DatePickerProps,
9+
DatePickerFieldProps,
10+
} from '@mui/x-date-pickers/DatePicker';
11+
import {
12+
useSplitFieldProps,
13+
useParsedFormat,
14+
usePickerContext,
15+
} from '@mui/x-date-pickers/hooks';
16+
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
17+
18+
function CustomDateField(props: DatePickerFieldProps) {
19+
// TextField does not support slots and slotProps before `@mui/material` v6.0
20+
const { slots, slotProps, ...other } = props;
21+
const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date');
22+
23+
const pickerContext = usePickerContext();
24+
const placeholder = useParsedFormat();
25+
const [inputValue, setInputValue] = useInputValue();
26+
27+
// Check if the current value is valid or not.
28+
const { hasValidationError } = useValidation({
29+
value: pickerContext.value,
30+
timezone: pickerContext.timezone,
31+
props: internalProps,
32+
validator: validateDate,
33+
});
34+
35+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
36+
const newInputValue = event.target.value;
37+
const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
38+
setInputValue(newInputValue);
39+
pickerContext.setValue(newValue);
40+
};
41+
42+
return (
43+
<TextField
44+
{...forwardedProps}
45+
placeholder={placeholder}
46+
value={inputValue}
47+
onChange={handleChange}
48+
error={hasValidationError}
49+
/>
50+
);
51+
}
52+
53+
function useInputValue() {
54+
const pickerContext = usePickerContext();
55+
const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value);
56+
const [inputValue, setInputValue] = React.useState(() =>
57+
createInputValue(pickerContext.value, pickerContext.fieldFormat),
58+
);
59+
60+
if (lastValueProp !== pickerContext.value) {
61+
setLastValueProp(pickerContext.value);
62+
if (pickerContext.value && pickerContext.value.isValid()) {
63+
setInputValue(
64+
createInputValue(pickerContext.value, pickerContext.fieldFormat),
65+
);
66+
}
67+
}
68+
69+
return [inputValue, setInputValue] as const;
70+
}
71+
72+
function createInputValue(value: Dayjs | null, format: string) {
73+
if (value == null) {
74+
return '';
75+
}
76+
77+
return value.isValid() ? value.format(format) : '';
78+
}
79+
80+
function CustomFieldDatePicker(props: DatePickerProps) {
81+
return (
82+
<DatePicker slots={{ ...props.slots, field: CustomDateField }} {...props} />
83+
);
84+
}
85+
86+
export default function MaterialDatePicker() {
87+
return (
88+
<LocalizationProvider dateAdapter={AdapterDayjs}>
89+
<CustomFieldDatePicker />
90+
</LocalizationProvider>
91+
);
92+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<CustomFieldDatePicker />

docs/data/date-pickers/custom-field/custom-field.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,157 @@ and you don't want the UI to look like a Text Field, you can replace the field w
154154
The same logic can be applied to any Range Picker:
155155

156156
{{"demo": "behavior-button/MaterialDateRangePicker.js", "defaultCodeOpen": false}}
157+
158+
## Build your own custom field
159+
160+
:::success
161+
The sections below show how to build a field for your Picker.
162+
Unlike the field components exposed by `@mui/x-date-pickers` and `@mui/x-date-pickers-pro`, those fields are not suitable for a standalone usage.
163+
:::
164+
165+
### Typing
166+
167+
Each Picker component exposes an interface describing the props it passes to its field.
168+
You can import it from the same endpoint as the Picker component and use it to type the props of your field:
169+
170+
```tsx
171+
import { DatePickerFieldProps } from '@mui/x-date-pickers/DatePicker';
172+
import { DateRangePickerFieldProps } from '@mui/x-date-pickers-pro/DateRangePicker';
173+
174+
function CustomDateField(props: DatePickerFieldProps) {
175+
// Your custom field
176+
}
177+
178+
function CustomDateRangeField(props: DateRangePickerFieldProps) {
179+
// Your custom field
180+
}
181+
```
182+
183+
#### Import
184+
185+
| Picker component | Field props interface |
186+
| ---------------------: | :------------------------------ |
187+
| Date Picker | `DatePickerFieldProps` |
188+
| Time Picker | `TimePickerFieldProps` |
189+
| Date Time Picker | `DateTimePickerFieldProps` |
190+
| Date Range Picker | `DateRangePickerFieldProps` |
191+
| Date Time Range Picker | `DateTimeRangePickerFieldProps` |
192+
193+
### Validation
194+
195+
You can use the `useValidation` hook to check if the current value passed to your field is valid or not:
196+
197+
```ts
198+
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
199+
200+
const {
201+
// The error associated with the current value.
202+
// For example: "minDate" if `props.value < props.minDate`.
203+
validationError,
204+
// `true` if the value is invalid.
205+
// On range Pickers it is true if the start date or the end date is invalid.
206+
hasValidationError,
207+
// Imperatively get the error of a value.
208+
getValidationErrorForNewValue,
209+
} = useValidation({
210+
// If you have a value in an internal state, you should pass it here.
211+
// Otherwise, you can pass the value returned by `usePickerContext()`.
212+
value,
213+
timezone,
214+
props,
215+
validator: validateDate,
216+
});
217+
```
218+
219+
#### Import
220+
221+
Each Picker component has a validator adapted to its value type:
222+
223+
| Picker component | Import validator |
224+
| ---------------------: | :--------------------------------------------------------------------------- |
225+
| Date Picker | `import { validateDate } from '@mui/x-date-pickers/validation'` |
226+
| Time Picker | `import { validateTime } from '@mui/x-date-pickers/validation'` |
227+
| Date Time Picker | `import { validateDateTime } from '@mui/x-date-pickers/validation'` |
228+
| Date Range Picker | `import { validateDateRange } from '@mui/x-date-pickers-pro/validation'` |
229+
| Date Time Range Picker | `import { validateDateTimeRange } from '@mui/x-date-pickers-pro/validation'` |
230+
231+
### Localized placeholder
232+
233+
You can use the `useParsedFormat` to get a clean placeholder.
234+
This hook applies two main transformations on the format:
235+
236+
1. It replaces all the localized tokens (for example `L` for a date with `dayjs`) with their expanded value (`DD/MM/YYYY` for the same date with `dayjs`).
237+
2. It replaces each token with its token from the localization object (for example `YYYY` remains `YYYY` for the English locale but becomes `AAAA` for the French locale).
238+
239+
:::warning
240+
The format returned by `useParsedFormat` cannot be parsed by your date library.
241+
:::
242+
243+
```js
244+
import { useParsedFormat } from '@mui/x-date-pickers/hooks';
245+
246+
// Uses the format defined by your Picker
247+
const parsedFormat = useParsedFormat();
248+
249+
// Uses the custom format provided
250+
const parsedFormat = useParsedFormat({ format: 'MM/DD/YYYY' });
251+
```
252+
253+
### Spread props to the DOM
254+
255+
The field receives a lot of props that cannot be forwarded to the DOM element without warnings.
256+
You can use the `useSplitFieldProps` hook to get the props that can be forwarded safely to the DOM:
257+
258+
```tsx
259+
const { internalProps, forwardedProps } = useSplitFieldProps(
260+
// The props received by the field component
261+
props,
262+
// The value type ("date", "time" or "date-time")
263+
'date',
264+
);
265+
266+
return (
267+
<TextField {...forwardedProps} value={inputValue} onChange={handleChange}>
268+
)
269+
```
270+
271+
:::success
272+
The `forwardedProps` contain props like `slots`, `slotProps` and `sx` that are specific to MUI.
273+
You can omit them if the component your are forwarding the props to does not support those concepts:
274+
275+
```jsx
276+
const { slots, slotProps, sx, ...other } = props;
277+
const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date');
278+
279+
return (
280+
<input {...forwardedProps} value={inputValue} onChange={handleChange}>
281+
)
282+
```
283+
284+
:::
285+
286+
### Pass the field to the Picker
287+
288+
You can pass your custom field to your Picker using the `field` slot:
289+
290+
```tsx
291+
function DatePickerWithCustomField() {
292+
return (
293+
<DatePicker slots={{ field: CustomDateField }}>
294+
)
295+
}
296+
297+
// Also works with the other variants of the component
298+
function DesktopDatePickerWithCustomField() {
299+
return (
300+
<DesktopDatePicker slots={{ field: CustomDateField }}>
301+
)
302+
}
303+
304+
```
305+
306+
### Full custom example
307+
308+
Here is a live demo of the example created in all the previous sections:
309+
310+
{{"demo": "behavior-tutorial/MaterialDatePicker.js", "defaultCodeOpen": false}}

0 commit comments

Comments
 (0)