date picker component not changing date using onChange - reactjs

I am trying to use react-datepicker package to create my own date picker component, i have modified some of the basic stuff to have a control on it and it all seems to work but my onChange doesn't change the date on the view... i can see the log the onChange does fire but nothing happens when i choose a new date on screen. Am fairly new and trying to understand what i did wrong.
Here is my DateTimePicker.tsx
import type { FC } from 'react';
import React, { useState } from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import './DateTimePicker.css';
export interface Props {
/**
* Determines format of date to be displayed
* tag i.e. format=dd/MM/yyyy => 24/12/2020
*/
format?: 'dd/MM/yyyy' | 'mm/dd/yyyy' | 'yyyy/MM/dd' | 'yyyy/dd/MM';
/** on change method updates the date input field with selected date */
onChange(date: Date | null, event: React.SyntheticEvent<any, Event> | undefined): void;
/** Determine the type of dropdown of year and month selecter */
mode?: 'select' | 'scroll';
/** Placeholder for no date is selected */
placeholder?: string;
}
/**
* Component that serves as an datepicker input field to let the user select Date
*
* #return DateTimePicker component
*/
export const DateTimePicker: FC<Props> = ({
format = 'dd/MM/yyyy',
mode = 'select',
placeholder = 'Click to add a date',
onChange,
}) => {
return (
<DatePicker
className="apollo-component-library-date-picker-component"
placeholderText={placeholder}
dateFormat={format}
onChange={onChange}
dropdownMode={mode}
showMonthDropdown
showYearDropdown
adjustDateOnChange
/>
);
};

react-datepicker takes a selected prop that keeps track of the currently selected date. You'll need to manage it in state and provide it to the component:
const Example = () => {
const [startDate, setStartDate] = useState(new Date());
return (
<DatePicker selected={startDate} onChange={(date) => setStartDate(date)} />
);
};

Related

React-datepicker with custom input with manual typing

I am trying to create react-datepicker component with custom styled input:
<DatePicker
selected={startDate}
customInput={<CustomInput inputRef={inputRef} />}
onChangeRaw={(e) => handleChangeRaw(e)}
onChange={(date: Date | null) => {
setStartDate(date);
}}
/>
const CustomInput = forwardRef((props: any, ref) => {
return <Input {...props} ref={ref} />;
});
I wanted to introduce validation (just simple validation that whatever user inputs will be formatted to the date so user cannot just type whatever he wants).
I created a function that would be formatting the input using onChangeRaw attribute in react-datepicker:
const handleChangeRaw = (date) => {
date.currentTarget.value = format(startDate, "dd/MM/yyyy");
};
However after using this I cannot type anymore. Anyone has idea how to use manual typing with custom input in react-datepicker with simple validation function?
Here is recreated issue in the sandbox:
https://codesandbox.io/s/react-datepicker-custominput-sample-forked-elx310?file=/src/App.tsx
selected={startDate}
Here react-datepicker expects a Date object.
However, when you trigger handleChangeRaw, you get an change event, with some value (date.currentTarget.value), but thats not in the Date notation that datepicker expects.
So my take on this would be;
onChangeRaw, convert the input value to a new Date.
If the date isn't valid, just ignore the change
Otherwise, create a new Date, and use setStartDate to change to that new date
const handleChangeRaw = (date) => {
const newRaw = new Date(date.currentTarget.value);
if (newRaw instanceof Date && !isNaN(newRaw)) {
setStartDate(newRaw);
}
};
Try it online
If I understand your issue correctly, you can add the following type to fix the typing.
const handleChangeRaw = (date: React.FocusEvent<HTMLInputElement>) => {
if (!startDate){
return
}
date.currentTarget.value = format(startDate, "dd/MM/yyyy");
};
date should have the type React.FocusEvent<HTMLInputElement>. Since startDate can be null, we have to check before using it. In this case I just return from the function. But you could also assign a default value here or something else.

Validating a MUI Datepicker with YUP in a react-hook-form

I have a date picker that needs to validate the following scenarios:
It is a required field and cannot be left blank
The date must be later minDate (21 days from today )
The date must be earlier than six months from minDate
The validation schema I came up with looks as follows, which, works unexpectedly.
import * as yup from 'yup'
import { addDays, addMonths, format} from 'date-fns'
const today = Date.now()
const minDate = addDays(today, 21)
const maxDate = addMonths(minDate, 6)
const schema = yup.object().shape({
Date: yup
.date()
.min(minDate, `Date must be after ${format(minDate,'MM/dd/yyyy')}`)
.max(maxDate, `Date must be before ${format(maxDate, 'MM/dd/yyyy') }`)
.typeError("Date is required")
})
What do I mean by it works unexpectedly
When you click the submit button for the first time, the
typeError() message is display.(Expected behavior)
Then, if you selected a date from by clicking on the calendar icon,
the date gets displayed and the error disappear. (Expected behavior)
Finally, if I manually clear the selected day the min() error
message gets displayed. (make sense but is not what I expected)
It makes sense because, as the date gets transformed with every keystroke, the min() the error message is triggered, but eventually, the date is not valid and its value becomes null.
Question: So, at this point should the typeError() message be displayed?
Date picker code
import React, { useState } from 'react'
import { Controller } from 'react-hook-form'
import TextField from '#mui/material/TextField'
import { AdapterDateFns } from '#mui/x-date-pickers/AdapterDateFns'
import { LocalizationProvider } from '#mui/x-date-pickers/LocalizationProvider'
import { DesktopDatePicker } from '#mui/x-date-pickers/DesktopDatePicker'
import { addDays, addMonths, format, isValid } from 'date-fns'
const Calendar = ({ control, name, label, error, helperText, ...rest }) => {
const today = Date.now()
const [value, setValue] = useState(null);
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Controller
name={name}
control={control}
render={({ field }) => (
<DesktopDatePicker
label={label}
value={value}
minDate={addDays(today, 21)}
maxDate={addMonths(addDays(today, 21), 6)}
onChange={(newValue) => {
if (isValid(newValue))
{
field.onChange(format(newValue, 'MM/dd/yyyy'))
setValue(newValue)
} else
{
setValue(newValue)
}
}}
renderInput={(params) =>
<TextField
margin='normal'
helperText={helperText}
{...params}
{...rest}
error={error}
/>
}
/>
)}
/>
</LocalizationProvider>
)
}
export default Calendar
CHECK WORKING EXAMPLE >

Material UI - Autocomplete with React Hook Form

I am building a form using Material UI's Autocomplete feature and using recoil for state management. I am also using react-hook-form. I am needing the following criteria met:
Need typeahead that allows first letter typed to return a list of options from an API to show in Autocomplete. Each letter will return a different list of options to select from.
Need to also allow freeSolo so that user can enter a value manually
Needs to be required and follow a pattern to validate the form.
I am using react-hook-form's <Controller> to control the input and allow for features like validation, displaying error messages in the helper text, etc.
The Problems: I am having issues with the typeahead filtering out options based on what I type, along with allowing freeSolo. As I type a new value, the list of options do not filter. The popup just stays open. I also need to validate on change of input for the pattern validation. I have tried with the following example with onInputChange to make use of react-hook-form's useForm and setValue to manually set the value of the field and validate the form. ({shouldValidate: true}). The below example is a custom, reusable component I created for Autocomplete, as well as using that custom component in other parent components. I hope I included as much details as possilbe, but if not, please let me know if you need anything more. Any assistance would be very appreciative!
Parent Component:
const setTrainType = useSetRecoilState(TrainType)
// Trains would return a list of trains based on letter of train type that was passed from input
const trainsList = useRecoilValue(Trains)
const trainOptions = useMemo(() => trainsList.map(trainIDFormatted), [
trainsList,
])
const handleInputChange = useCallback(
(_e: unknown, option: string, reason: string) => {
const capitalized =
option === capitalize(option) ? option : capitalize(option)
setValue('trainID', capitalized, {shouldValidate: true})
if (['input', 'reset'].includes(reason) && capitalized !== '') {
setTrainType(capitalized.charAt(0))
} else {
setTrainType(undefined)
}
},
[setTrainType, setValue],
)
<Autocomplete
autoSelect
freeSolo
disabled={disabled}
helperText=" "
label="Select a train"
name="trainID"
options={trainOptions}
rules={{
pattern: {
message: 'Must match train ID pattern',
value: /^(?:[A-Z]-?[A-Z ]{6}-?[0-9 ]-?[0-9 ]{2}[A-Z ])?$/,
},
required: 'Train is required',
}}
onInputChange={handleInputChange}
/>
Custom autocomplete component:
import {
AutocompleteProps,
Autocomplete as MuiAutocomplete,
} from '#material-ui/lab'
import {get} from 'lodash'
import React, {ReactNode, useCallback} from 'react'
import {
Controller,
ControllerProps,
FieldError,
useFormContext,
} from 'react-hook-form'
import {useRenderInput} from './hooks'
interface Props
extends Pick<ControllerProps<'select'>, 'rules'>,
Omit<
AutocompleteProps<string, false, false, true>,
'error' | 'onChange' | 'required' | 'renderInput'
> {
helperText?: ReactNode
label?: string
name: string
}
/**
* Render controlled autocomplete. Use react-form-hook's FormProvider.
* #param props Component properties
* #param props.helperText Default helper text for error
* #param props.label Input label
* #param props.name Name identifier for react-hook-form
* #param props.required If true then item is required
* #param props.rules Select rules
* #return React component
*/
export const Autocomplete = ({
helperText,
label,
name,
rules,
...props
}: Props) => {
// eslint-disable-next-line #typescript-eslint/unbound-method
const {control, errors, watch} = useFormContext()
const error: FieldError | undefined = get(errors, name)
const required = get(rules, 'required') !== undefined
const value = watch(name)
const renderAutocompleteInput = useRenderInput({
error: error !== undefined,
helperText: get(error, 'message', helperText),
label,
required,
})
const handleOnChange = useCallback(
(_e: unknown, option: string | null) => option,
[],
)
const renderAutocomplete = useCallback(
params => (
<MuiAutocomplete
{...props}
{...params}
renderInput={renderAutocompleteInput}
onChange={handleOnChange}
/>
),
[handleOnChange, props, renderAutocompleteInput],
)
return (
<Controller
control={control}
defaultValue={value ?? ''}
name={name}
render={renderAutocomplete}
rules={rules}
/>
)
}
What it looks like:

Set timezone React-Datepicker

I'm using the react-datepicker to let user select a date. However, right now it uses local time (PDT), but I want to hardcode it to use a specific timezone (PST).
I tried using utcOffset prop but it doesn't seem to be doing anything. Does anyone know how to accomplish this?
For my part I set the defaultTimezone before rendering the React-dates plugin.
React-dates will just use the default timezone.
moment.tz.setDefault('America/Los_Angeles');
This works for me:
import React, { ComponentProps } from "react"
import DatePicker from "react-datepicker"
import moment from "moment"
interface Props {
timezone: string
}
const DatePickerWithTimezone = ({
selected,
onChange,
timezone,
...props
}: Props & ComponentProps<typeof DatePicker>) => (
<DatePicker
selected={selected ? setLocalZone(selected, timezone) : null}
onChange={(v, e) => {
onChange(v ? setOtherZone(v, timezone) : null, e)
}}
{...props}
/>
)
const setLocalZone = (date: Date, timezone: string) => {
const dateWithoutZone = moment
.tz(date, timezone)
.format("YYYY-MM-DDTHH:mm:ss.SSS")
const localZone = moment(dateWithoutZone).format("Z")
const dateWithLocalZone = [dateWithoutZone, localZone].join("")
return new Date(dateWithLocalZone)
}
const setOtherZone = (date: Date, timezone: string) => {
const dateWithoutZone = moment(date).format("YYYY-MM-DDTHH:mm:ss.SSS")
const otherZone = moment.tz(date, timezone).format("Z")
const dateWithOtherZone = [dateWithoutZone, otherZone].join("")
return new Date(dateWithOtherZone)
}
export default DatePickerWithTimezone
since datepicker doesn't use moment.js anymore i tried to implement a hacky solution for this issue, assuming the initial value is a string for instance:
export const formatUTC = (dateInt, addOffset = false) => {
let date = (!dateInt || dateInt.length < 1) ? new Date : new Date(dateInt);
if (typeof dateInt === "string") {
return date;
} else {
const offset = addOffset ? date.getTimezoneOffset() : -(date.getTimezoneOffset());
const offsetDate = new Date();
offsetDate.setTime(date.getTime() + offset * 60000)
return offsetDate;
}
}
inside date i call the formatter like this:
selected={formatUTC(this.props.input.value,true)}
onChange={date => formatUTC(date)}
I've been thru this, If you decided that you want to just ignore your local offset then you can hardcode the zone.
Observation just to give a complete answer: PST will always be -08:00, but if you want for example pacific time, right now is -07:00, in this case, you may want to install 'moment.timezone' then import moment from 'moment-timezone' and just get the current offset with moment.tz('US/Pacific').format('Z')
The code in typescript (I can change it to Javascript if you want):
interface ICalendarInputProps {
handleChange: (newDate: moment.Moment) => void;
}
const CalendarInput = ({ handleChange }: ICalendarInputProps) => {
const onChange = (date: Date) => {
handleChange(moment(`${moment(date).format('YYYY-MM-DDThh:mm:ss')}-08:00`));
// This is to get the offset from a timezone: handleChange(moment(`${moment(date).format('YYYY-MM-DDThh:mm:ss')}${moment.tz('US/Pacific').format('Z')}`));
};
return (<DatePicker onChange={onChange} />);
};
export default CalendarInput;
This component outputs Date objects set to midnight local-time at the start of the chosen day. This is a problem. If there is a way of configuring this behaviour, I haven't found it.
The only way to stay sane when dealing with dates is to make sure that your dates are always midnight UTC at the start of the date in question. To get this behaviour from react-datepicker, the only thing I've found is to subtract the timezone offset from the output...
interface IProps {
value: any
setValue: (value: Date) => void
}
const DayPicker: FC<IProps> = ({ value, setValue, placeholderText = "", minDate = new Date() }) => {
function correctToUtcThenSet(val: Date) {
setValue(new Date(val.getTime() - val.getTimezoneOffset() * 60000))
}
return <DatePicker
onChange={correctToUtcThenSet}
selected={value}
/>
}
Other answers didn't work as I'd hoped, and sometimes the dates were off by 1 day because of time zone differences.
This is what I needed:
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { getEndOfDayUtc, treatLocalDateAsUtcMidnight, treatUtcMidnightAsLocalDate } from '../../../shared/helpers/datetime';
type DatePickerUtcProps = {
selected: Date | string;
onChange: any;
isEndOfDay: boolean;
};
function getSelectedAsLocal(selected: Date | string): Date {
const selectedDate = typeof selected === 'string' ? new Date(selected) : selected;
return treatUtcMidnightAsLocalDate(selectedDate);
}
export function DatePickerUtc({ selected, onChange, isEndOfDay, ...datePickerProps }: DatePickerUtcProps): JSX.Element {
function onChangeAsUtc(local: Date) {
const utc = treatLocalDateAsUtcMidnight(local);
const adjusted = isEndOfDay ? getEndOfDayUtc(utc) : utc;
console.log('onChangeAsUtc', { local, utc, adjusted, isEndOfDay });
onChange(adjusted);
}
return <DatePicker onChange={onChangeAsUtc} selected={getSelectedAsLocal(selected)} {...datePickerProps} />;
}
export function treatLocalDateAsUtcMidnight(localDate: Date): Date {
const moment = dayjs(localDate).tz('UTC', true); // https://day.js.org/docs/en/plugin/timezone
const utcMidnight = getStartOfDayUtc(moment.toDate());
console.log({ localDate, utcMidnight });
return utcMidnight;
}
export function treatUtcMidnightAsLocalDate(utcMidnight: Date): Date {
const sliceOfJustTheDatePart = utcMidnight.toISOString().slice(0, 10);
const localDate = dayjs(sliceOfJustTheDatePart).toDate();
console.log({ localDate, sliceOfJustTheDatePart, utcMidnight });
return localDate;
}
From: <DatePickerUtc selected={startDate} onChange={(utcDate: Date) => setStartDate(utcDate)} {...datePickerProps} />
To: <DatePickerUtc selected={endDate} onChange={(utcDate: Date) => setEndDate(utcDate)} {...datePickerPropsEndOfDay} />
I also didn't have luck with utcOffset. You could use moment-timezone in your project instead of moment and convert it yourself:
import moment from "moment-timezone";
onDateChange = date => {
console.log("as is:", date.format("YYYY-MM-DD HH:mm:ss"));
console.log("PST:", moment(date).tz("America/Los_Angeles").format("YYYY-MM-DD HH:mm:ss"));
};
Sandbox: https://codesandbox.io/s/y2vwj9mwpz
Since you're using moment.js, you can try using moment.utc() and subtract hours to pst timezone.
moment.utc().startOf('day').subtract(8, 'hours')

DateInput default parse function

I wanted to wrap DateInput into MyDateInput with pre-filled parse function argument in order not to repeat parse={dateParser} everywhere.
So I created such element:
import React from 'react';
import { DateInput } from 'admin-on-rest';
import moment from 'moment';
const dateParser = date => {
// v is a `Date` object
if (!(date instanceof Date) || isNaN(date)) return;
return moment(date).format('YYYY-MM-DD');
};
const MyDateInput = (props) => {
return (
<DateInput {...props} parse={dateParser} />
);
};
Which did not work as I expected, so from docs I found that I have to do something like this, but source argument not parsed as it has to be:
import { Field } from 'redux-form';
const MyDateInput = (props) => {
return (
<Field name='date' component={DateInput} {...props} parse={dateParser} />
);
};
When I put 2 such inputs into one form it result in merge of input for both of them.
Is there a way to it properly?
Redux Form name prop sets the name of the field in the state that the Field Component will affect.
So you need to give a different name prop every time you want to use the Field component.
You can provide name as a prop from the form that is calling the Wrapped DateInput comp make sure it is unique ever time.

Resources