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

[docs] Add full custom field creation example #15194

Merged
merged 35 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
59c8cd5
[docs] Migrate remaining demos to the new custom field DX
flaviendelangle Oct 31, 2024
5b0029f
Add migration guide
flaviendelangle Oct 31, 2024
33bdaff
Merge
flaviendelangle Oct 31, 2024
22ec31d
Merge
flaviendelangle Nov 6, 2024
e04759c
Work
flaviendelangle Nov 6, 2024
9f13aae
Fix
flaviendelangle Nov 6, 2024
3436184
Merge
flaviendelangle Nov 6, 2024
c8f4de7
Fix
flaviendelangle Nov 6, 2024
9592ee9
Fix
flaviendelangle Nov 8, 2024
a67429c
Merge
flaviendelangle Nov 12, 2024
380c3bc
Review: Lukas
flaviendelangle Nov 13, 2024
94e2bef
Merge branch 'master' into custom-field-finish-migration
flaviendelangle Nov 13, 2024
c6aa132
[docs] Add proper documentation on how to create custom fields
flaviendelangle Nov 13, 2024
e040f2e
Bring back inputRef on accessible DOM structure
flaviendelangle Nov 14, 2024
263c4a9
Merge branch 'custom-field-finish-migration' into custom-field-refacto
flaviendelangle Nov 14, 2024
ebe3619
Merge
flaviendelangle Nov 14, 2024
f7d8d71
Improve doc example
flaviendelangle Nov 14, 2024
4481d3e
Merge branch 'master' into custom-field-refacto
flaviendelangle Nov 19, 2024
c9a75bc
Regen preview
flaviendelangle Nov 19, 2024
e8282dd
Merge branch 'master' into custom-field-refacto
flaviendelangle Dec 11, 2024
32a70c3
Update docs/data/date-pickers/custom-field/custom-field.md
flaviendelangle Jan 9, 2025
99d94e7
Update docs/data/date-pickers/custom-field/custom-field.md
flaviendelangle Jan 9, 2025
088e4ee
Update docs/data/date-pickers/custom-field/custom-field.md
flaviendelangle Jan 9, 2025
473e3c5
Update docs/data/date-pickers/custom-field/custom-field.md
flaviendelangle Jan 9, 2025
57eb520
Update docs/data/date-pickers/custom-field/custom-field.md
flaviendelangle Jan 9, 2025
8d9e713
Update docs/data/date-pickers/custom-field/custom-field.md
flaviendelangle Jan 9, 2025
63824e3
Merge branch 'master' into custom-field-refacto
flaviendelangle Jan 9, 2025
727cd24
Remove i.e
flaviendelangle Jan 9, 2025
f416a05
Merge branch 'master' into custom-field-refacto
flaviendelangle Jan 13, 2025
b974f44
Simplify doc
flaviendelangle Jan 13, 2025
7cd41e3
Move to standalone section
flaviendelangle Jan 13, 2025
d82b85b
Apply suggestions from code review
LukasTy Jan 13, 2025
7f3c63e
Add demo without MUI components
flaviendelangle Jan 13, 2025
f5dd877
Merge remote-tracking branch 'origin/custom-field-refacto' into custo…
flaviendelangle Jan 13, 2025
51297b2
Review: Lukas
flaviendelangle Jan 13, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function MaskedDateField(props) {
return (
<TextField
placeholder={parsedFormat}
error={!!hasValidationError}
error={hasValidationError}
{...rifmProps}
{...forwardedProps}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function MaskedDateField(props: DatePickerFieldProps) {
return (
<TextField
placeholder={parsedFormat}
error={!!hasValidationError}
error={hasValidationError}
{...rifmProps}
{...forwardedProps}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as React from 'react';
import dayjs from 'dayjs';
import TextField from '@mui/material/TextField';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import {
useSplitFieldProps,
useParsedFormat,
usePickerContext,
} from '@mui/x-date-pickers/hooks';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';

function CustomDateField(props) {
// TextField does not support slots and slotProps before `@mui/material` v6.0
const { slots, slotProps, ...other } = props;
const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date');

const pickerContext = usePickerContext();
const placeholder = useParsedFormat();
const [inputValue, setInputValue] = useInputValue();

// Check if the current value is valid or not.
const { hasValidationError } = useValidation({
value: pickerContext.value,
timezone: pickerContext.timezone,
props: internalProps,
validator: validateDate,
});

const handleChange = (event) => {
const newInputValue = event.target.value;
const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
setInputValue(newInputValue);
pickerContext.setValue(newValue);
};

return (
<TextField
{...forwardedProps}
placeholder={placeholder}
value={inputValue}
onChange={handleChange}
error={hasValidationError}
/>
);
}

function useInputValue() {
const pickerContext = usePickerContext();
const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value);
const [inputValue, setInputValue] = React.useState(() =>
createInputValue(pickerContext.value, pickerContext.fieldFormat),
);

if (lastValueProp !== pickerContext.value) {
setLastValueProp(pickerContext.value);
if (pickerContext.value && pickerContext.value.isValid()) {
setInputValue(
createInputValue(pickerContext.value, pickerContext.fieldFormat),
);
}
}

return [inputValue, setInputValue];
}

function createInputValue(value, format) {
if (value == null) {
return '';
}

return value.isValid() ? value.format(format) : '';
}

function CustomFieldDatePicker(props) {
return (
<DatePicker slots={{ ...props.slots, field: CustomDateField }} {...props} />
);
}

export default function MaterialDatePicker() {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<CustomFieldDatePicker />
</LocalizationProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from 'react';
import dayjs, { Dayjs } from 'dayjs';
import TextField from '@mui/material/TextField';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import {
DatePicker,
DatePickerProps,
DatePickerFieldProps,
} from '@mui/x-date-pickers/DatePicker';
import {
useSplitFieldProps,
useParsedFormat,
usePickerContext,
} from '@mui/x-date-pickers/hooks';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';

function CustomDateField(props: DatePickerFieldProps) {
// TextField does not support slots and slotProps before `@mui/material` v6.0
const { slots, slotProps, ...other } = props;
const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date');

const pickerContext = usePickerContext();
const placeholder = useParsedFormat();
const [inputValue, setInputValue] = useInputValue();

// Check if the current value is valid or not.
const { hasValidationError } = useValidation({
value: pickerContext.value,
timezone: pickerContext.timezone,
props: internalProps,
validator: validateDate,
});

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newInputValue = event.target.value;
const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
setInputValue(newInputValue);
pickerContext.setValue(newValue);
};

return (
<TextField
{...forwardedProps}
placeholder={placeholder}
value={inputValue}
onChange={handleChange}
error={hasValidationError}
/>
);
}

function useInputValue() {
const pickerContext = usePickerContext();
const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value);
const [inputValue, setInputValue] = React.useState(() =>
createInputValue(pickerContext.value, pickerContext.fieldFormat),
);

if (lastValueProp !== pickerContext.value) {
setLastValueProp(pickerContext.value);
if (pickerContext.value && pickerContext.value.isValid()) {
setInputValue(
createInputValue(pickerContext.value, pickerContext.fieldFormat),
);
}
}

return [inputValue, setInputValue] as const;
}

function createInputValue(value: Dayjs | null, format: string) {
if (value == null) {
return '';
}

return value.isValid() ? value.format(format) : '';
}

function CustomFieldDatePicker(props: DatePickerProps) {
return (
<DatePicker slots={{ ...props.slots, field: CustomDateField }} {...props} />
);
}

export default function MaterialDatePicker() {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<CustomFieldDatePicker />
</LocalizationProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<CustomFieldDatePicker />
154 changes: 154 additions & 0 deletions docs/data/date-pickers/custom-field/custom-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,157 @@ and you don't want the UI to look like a Text Field, you can replace the field w
The same logic can be applied to any Range Picker:

{{"demo": "behavior-button/MaterialDateRangePicker.js", "defaultCodeOpen": false}}

## Build your own custom field

:::success
The sections below show how to build a field for your Picker.
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.
:::

### Typing

Each Picker component exposes an interface describing the props it passes to its field.
You can import it from the same endpoint as the Picker component and use it to type the props of your field:

```tsx
import { DatePickerFieldProps } from '@mui/x-date-pickers/DatePicker';
import { DateRangePickerFieldProps } from '@mui/x-date-pickers-pro/DateRangePicker';

function CustomDateField(props: DatePickerFieldProps) {
// Your custom field
}

function CustomDateRangeField(props: DateRangePickerFieldProps) {
// Your custom field
}
```

#### Import

| Picker component | Field props interface |
| ---------------------: | :------------------------------ |
| Date Picker | `DatePickerFieldProps` |
| Time Picker | `TimePickerFieldProps` |
| Date Time Picker | `DateTimePickerFieldProps` |
| Date Range Picker | `DateRangePickerFieldProps` |
| Date Time Range Picker | `DateTimeRangePickerFieldProps` |

### Validation

You can use the `useValidation` hook to check if the current value passed to your field is valid or not:

```ts
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';

const {
// The error associated with the current value.
// For example: "minDate" if `props.value < props.minDate`.
validationError,
// `true` if the value is invalid.
// On range Pickers it is true if the start date or the end date is invalid.
hasValidationError,
// Imperatively get the error of a value.
getValidationErrorForNewValue,
} = useValidation({
// If you have a value in an internal state, you should pass it here.
// Otherwise, you can pass the value returned by `usePickerContext()`.
value,
timezone,
props,
validator: validateDate,
});
```

#### Import

Each Picker component has a validator adapted to its value type:

| Picker component | Import validator |
| ---------------------: | :--------------------------------------------------------------------------- |
| Date Picker | `import { validateDate } from '@mui/x-date-pickers/validation'` |
| Time Picker | `import { validateTime } from '@mui/x-date-pickers/validation'` |
| Date Time Picker | `import { validateDateTime } from '@mui/x-date-pickers/validation'` |
| Date Range Picker | `import { validateDateRange } from '@mui/x-date-pickers-pro/validation'` |
| Date Time Range Picker | `import { validateDateTimeRange } from '@mui/x-date-pickers-pro/validation'` |

### Localized placeholder

You can use the `useParsedFormat` to get a clean placeholder.
This hook applies two main transformations on the format:

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`).
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).

:::warning
The format returned by `useParsedFormat` cannot be parsed by your date library.
:::

```js
import { useParsedFormat } from '@mui/x-date-pickers/hooks';

// Uses the format defined by your Picker
const parsedFormat = useParsedFormat();

// Uses the custom format provided
const parsedFormat = useParsedFormat({ format: 'MM/DD/YYYY' });
```

### Spread props to the DOM

The field receives a lot of props that cannot be forwarded to the DOM element without warnings.
You can use the `useSplitFieldProps` hook to get the props that can be forwarded safely to the DOM:

```tsx
const { internalProps, forwardedProps } = useSplitFieldProps(
// The props received by the field component
props,
// The value type ("date", "time" or "date-time")
'date',
);

return (
<TextField {...forwardedProps} value={inputValue} onChange={handleChange}>
)
```

:::success
The `forwardedProps` contain props like `slots`, `slotProps` and `sx` that are specific to MUI.
You can omit them if the component your are forwarding the props to does not support those concepts:

```jsx
const { slots, slotProps, sx, ...other } = props;
const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date');

return (
<input {...forwardedProps} value={inputValue} onChange={handleChange}>
)
```

:::

### Pass the field to the Picker

You can pass your custom field to your Picker using the `field` slot:

```tsx
function DatePickerWithCustomField() {
return (
<DatePicker slots={{ field: CustomDateField }}>
)
}

// Also works with the other variants of the component
function DesktopDatePickerWithCustomField() {
return (
<DesktopDatePicker slots={{ field: CustomDateField }}>
)
}

```

### Full custom example

Here is a live demo of the example created in all the previous sections:

{{"demo": "behavior-tutorial/MaterialDatePicker.js", "defaultCodeOpen": false}}
Loading