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

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 >

Related

How to test dates with jest?

I'm writing a test to check if the selected date is greater than the current date.
This is the component I'm trying to test:
const [selectedDate, setSelectedDate] = useState<Moment>();
<TimeComponent>
<SetTimeButton
data-testid="scheduleTime"
onClick={() =>
scheduleTimeHandler(
selectedDate,
timeSelect,
setSelectedDate,
setTimeDialog,
setDisableConfirm
)
}
/>
<DisplayDate data-testid="selectedDate">{selectedDate}</DisplayDate>
</TimeComponent>
and this is the test that I wrote:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import TimeComponent from './TimeComponent';
test('set schedule time', () => {
render(
//#ts-ignore
<TimeComponent />
);
const mockDateObject = new Date();
const scheduleTimeBtn = screen.queryByTestId('scheduleTime');
const selectedDate = screen.queryByTestId('selectedDate');
fireEvent.click(scheduleTimeBtn);
expect(selectedDate).toBeGreaterThan(+mockDateObject);
});
and I'm getting this kind of error regarding the selectedDate in expect
expect(received).toBeGreaterThan(expected)
Matcher error: received value must be a number or bigint
Received has value: null
I understand this is because selectedDate is an HTML element instead of being date, but I'm not sure how can I make it a date. What am I doing wrong?

React-hook-form and MUI TextField testing with react-testing-library

I'm using react-hook-form with zod for schema validation and MUI for UI components.
I have this MUI TextField wrapped in Controll HOC from RHF:
<Controller
name="docRegistrationNumber"
control={control}
defaultValue=""
render={({
field: { ref, onChange, value, ...field },
}) => (
<TextField
label="docRegistrationNumber"
variant="outlined"
error={Boolean(errors.docRegistrationNumber)}
helperText={
errors.docRegistrationNumber?.message
}
inputRef={ref}
onChange={(event) => {
if (errors.docRegistrationNumber) {
trigger('docRegistrationNumber')
}
onChange(event.target.value)
}}
{...field}
value={value}
/>
)}
/>
I'm trying to test the input for error messages.
A use case would be: if the input text is longer than 10 characters, display a error message below the input field "Invalid input".
What I'm trying in my test file is the following:
test('Should display error message for text longer than 10 characters', async () => {
const input = screen.getByRole('textbox', {
name: /docRegistrationNumber/i,
})
await user.type(input, 'this text is longer than 10 characters')
await user.tab()
const errorMessage = await screen.findByText(/invalid input/i)
expect(errorMessage).toBeInTheDocument()
})
This does not seems to work. It cannot find element with that error text.
I tried using fireEvent to blur the input, but got same results.
I tried wrapping the expect in a waitFor block with no results.
This is my test setup:
const Component = () => {
const {
control,
formState: { errors },
} = useForm()
return (
<ContactAttributes
errors={errors}
control={control as any}
trigger={jest.fn()}
contactDetails={EMPTY_DETAILS_OBJECT}
/>
)
}
beforeEach(() => {
render(<Component />, { wrapper: BrowserRouter })
})
My guess is that the errors are not generated at test runtime. If I pass the error explicitly in the errors props I can query that error message.
My question is, how should I test this input?
In react-hook-form documentation I saw they test the whole form and check for errors only when submitting the form button.
Thank you!
Using userEvent instead of fireEvent
Wrapping the expectation in waitFor
My expectation was to be able to query the error message.

date picker component not changing date using onChange

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)} />
);
};

ReactJs Show state value in TextInput

I am working on a form where the user must fill many fields.
Some of those fields have data that comes from entities, so I created a search component where the user clicks on a button, a grid of the entity opens and when selecting I return the selected row as a prop and save in states the values. (this works ok)
Those values that are stored in States, to be shown in diferents TextInput (react-admin)
import React, {useState, useCallback} from "react";
import {Edit, TextInput, SimpleForm} from 'react-admin';
import ProcesoSearchButton from "../proceso/ProcesoBusquedaBtn"
const ProcesoEdit = props => {
const [state, setState] = useState({});
const classes = useStyles();
const {
idProceso = '', proceso = '', d_proceso = '',
} = state;
const updateProceso = useCallback(async (who) => {
setState(state => ({
...state,
idProceso: who.id,
proceso: who.proceso,
d_proceso: who.d_proceso,
}));
})
return (
<Edit {...props} title={<ProcesoStdTitle/>}>
<SimpleForm>
<TextInput source="proceso" label={"N°Proceso"}
defaulValue={proceso}
fullWidth={true}
formClassName={classes.proceso} inputProps={{readOnly: true,}} variant="filled"/>
<TextInput source="d_proceso" label={"Descripción"} fullWidth={true}
defaulValue ={d_proceso}
formClassName={classes.d_proceso} inputProps={{readOnly: true,}}
variant="filled"/>
<ProcesoSearchButton callbackProceso={updateProceso} formClassName={classes.btnBusqueda}/>
....
</SimpleForm>
</Edit>
)
};
export default ProcesoEdit;
With the previously detailed code it works in a first search, States are updated and shown in the TextFields. But if i do a second search, the States are updated but not the displayed value in TextFields.
If I use the value prop it doesn't work.
How can I solve that? I am trying to keep using react-admin because it's what I use throughout the app. This code using MUI TextField works, but it would not be ideal. Thanks

defaultValue of showTime attribute in antd datePicker couldn't set the time

I'm using ant design for my react hook based development. I am retrieving date from database over a network call and the date retrieved from database is look like "2020-09-01T05:02:00.000+0000". It is a string. I have to parse this date and show the date value and time value through antd datePicker in my page. Though I've success with setting up date part but I'm stuck with setting up the time. Here is my code,
###########
Here is my datepicker
###########
<DatePicker
name="Campaign-date"
defaultValue={moment(formData['Campaign-date'].split('T')[0])}
showTime={{
defaultValue: moment(formData['Campaign-date'].split('T')[1],'HH:mm'),
format: 'HH:mm'
}}
format={'YYYY-MM-DD HH:mm'}
style={{ width: '100%' }}
onChange={handleDateChange}
/>
##########################
Here is my handleDateChange function
##########################
const handleDateChange = date => {
setFormdata({
...formData,
'Campaign-date': date
.format('YYYY-MM-DD HH:mm')
.split(' ')
.join('T')
});
};
formData['Campaign-date'] is the date retrieved from the database. I feel the showTime attribute is not working as expected. I want to get a result of "05:02" but instead the value shown is, "00:00".
I have tried different variation in showTime such as,
defaultValue: moment('05:02','HH:mm'),
//or
defaultValue: moment(formData['Campaign-date'],'HH:mm')
//or
defaultValue: moment(formData['Campaign-date'].split('T')[1].split('.),'HH:mm')
but nothing is working. It is always setting the value as "00:00".
And I have no problem with my onChange functionality. It is working perfect. The date/time change code wise working as expected. It is just the view on my page is not showing correct time.
I'm really stuck with. Any help therefore, is much appreciated.
Ant Design DatePicker:
(sandbox)
import React from "react";
import ReactDOM from "react-dom";
import { DatePicker, Space } from "antd";
import moment from "moment";
import "./index.css";
import "antd/dist/antd.css";
const initialState = {
data1: "form field",
data2: "form field 2",
"Campaign-date": "2020-09-01T05:02:00.000+0000"
};
function FormComponent() {
const [formData, setFormData] = React.useState(initialState);
console.log(formData);
function onChange(value, dateString) {
if (value) {
setFormData({
...formData,
"Campaign-date": value.toISOString(true)
});
}
//console.log("Selected Time: ", value); //Moment object
//console.log("Formatted Selected Time: ", dateString); //String
}
return (
<Space direction="vertical" size={12}>
<DatePicker
name="Campaign-date"
defaultValue={moment(formData["Campaign-date"], "YYYY/MM/DD HH:mm")}
format={"YYYY/MM/DD HH:mm"}
showTime={{ format: "HH:mm" }}
onChange={onChange}
/>
<div style={{ marginTop: 16 }}>
Selected Date:{" "}
{formData["Campaign-date"]
? moment(formData["Campaign-date"]).format("YYYY-MM-YY HH:mm")
: "None"}
</div>
</Space>
);
}
ReactDOM.render(<FormComponent />, document.getElementById("container"));
If you are using react-datepicker 2.0 or up, you need to use a javascript Date object. Here is a working setup.
function parseISOString(s) {
var b = s.split(/\D+/);
return new Date(Date.UTC(b[0], --b[1], b[2], b[3], b[4], b[5], b[6]));
}
const isoString = '2020-09-01T05:02:00.000+0000';
const dateFromISOString = parseISOString(isoString);
<DatePicker
selected={dateFromISOString}
dateFormat="dd/MM/yyyy HH:mm"
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
placeholderText="Select date"
onChange={handleChange}
/>
Overview
The string you are receiving from the database is an ISO date string.
You can use moment.js to easily parse it or parse it manually and create a new Date object from it.
Here is a snippet illustrating both. The Date example uses this answer: Change ISO Date String to Date Object - JavaScript
const isoString = "2020-09-01T05:02:00.000+0000"
// with Moment
console.log("With Moment");
const timeFromMoment = moment(isoString).format("HH:mm");
console.log(timeFromMoment);
// with Date
console.log("With Date");
// https://stackoverflow.com/questions/27012854/change-iso-date-string-to-date-object-javascript
function parseISOString(s) {
var b = s.split(/\D+/);
return new Date(Date.UTC(b[0], --b[1], b[2], b[3], b[4], b[5], b[6]));
}
const dateFromISOString = parseISOString(isoString);
console.log(dateFromISOString);
// return Local adusted time
const localHours = dateFromISOString.getHours();
console.log(localHours);
// return UTC time
const utcHours = dateFromISOString.getUTCHours();
console.log(utcHours);
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js"></script>
<DatePicker
name="Campaign-date"
defaultValue={moment(formData['Campaign-date'].split('T')[0])}
showTime={{
defaultValue: moment(formData['Campaign-date'].split('T')[1].split('.')[0],'HH:mm:ss'),
format: 'HH:mm:ss'
}}
format={'YYYY-MM-DD HH:mm'}
style={{ width: '100%' }}
onChange={handleDateChange}
/>

Resources