ReactJS/Firebase: How to set a timestamp field to null/undefined? - reactjs

I want the ability for users not to select a date.
<LocalizationProvider dateAdapter={AdapterDateFns}>
<MobileDatePicker
label={"Date From"}
inputFormat="dd/MM/yyyy"
disablePast={true}
value={datefrom}
minDate={new Date()}
onChange={(newValue) => {
setDateFrom(format(newValue, 'MM/dd/yyyy'));
if (dateto == '') {
setDateTo(format(newValue, 'MM/dd/yyyy'));
}
}}
renderInput={(params) => <TextField variant="outlined" {...params} helperText={null} fullWidth />}
/>
I thought this would work:
<Button onClick={(newValue) => {setDateFrom(null);}}>CLEAR DATES</Button>
And while it clears the date, it does not allow me to save a blank field

If you only want to ignore undefined value for this case, you can conditionally spread object.
Something like this should work.
await setDoc(docRef, {
mandatoryField: "value",
...(dateTo === "" ? {} : {timeStamp: dateTo} )
})
If you want to ignore all undefined value for future usage you can initialize firestore setting as below.
import { getFirestore, initializeFirestore } from 'firebase/firestore'; // Firebase v9+
// Must be called before getFirestore()
initializeFirestore(app, {
ignoreUndefinedProperties: true
});
const firestore = getFirestore(app);

Related

Disable manual input for MUI TimePicker

I have a bit of a custom TimePicker provided from Material UI. I add an ability for the user to select only whole hours, such as 15:00, 16:00 etc. by clicking on a clock icon
What I want to achieve is to add same for manual input of the text field. For now user can manually add any valid time, for example 14:34, which is incorrect for my case
Can anyone help me?
Here is my TimePicker:
<LocalizationProvider dateAdapter={AdapterDateFns} locale={locale}>
<TimePicker
ampm={false}
openTo="hours"
views={['hours']}
inputFormat="HH:mm"
mask="__:__"
value={dayStartValue}
InputAdornmentProps={{
position: 'start',
}}
components={{
OpenPickerIcon: ClockFilledIcon,
}}
onChange={(newValue) => {
setDayStartValue(newValue)
}}
renderInput={(params) =>
<TextField
{...params}
helperText="Input start of Day time"
/>
}
/>
</LocalizationProvider>
You can control and validate the user's input when he clicks away from the time picker by using onBlur inside InputProps.
import * as React from "react";
import TextField from "#mui/material/TextField";
import { AdapterDateFns } from "#mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "#mui/x-date-pickers/LocalizationProvider";
import { TimePicker } from "#mui/x-date-pickers/TimePicker";
export default function BasicTimePicker() {
const [dayStartValue, setDayStartValue] = React.useState<Date | null>(null);
return (
<LocalizationProvider
dateAdapter={AdapterDateFns}
locale={locale}
>
<TimePicker
ampm={false}
openTo="hours"
views={["hours"]}
inputFormat="HH:mm"
mask="__:__"
value={dayStartValue}
InputAdornmentProps={{
position: "start"
}}
components={{
OpenPickerIcon: ClockFilledIcon,
}}
onChange={(newValue: Date, keyboardInputValue?: string) => {
setDayStartValue(newValue);
}}
renderInput={(params) => (
<TextField {...params} helperText="Input start of Day time" />
)}
InputProps={{
onBlur: (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement, Element>
) => {
const newDate = new Date(dayStartValue);
newDate.setMinutes(0);
if (e.target.value && e.target.value.length === 5) {
setDayStartValue(newDate);
}
}
}}
/>
</LocalizationProvider>
);
}
For the validation we check the user input and if it's a valid Date (5 characters length), we create a new Date with that and set the minutes to 0.
Code Sandbox example
You can change your input format and the mask to only accept whole hours like so:
<LocalizationProvider dateAdapter={AdapterDayjs}>
<TimePicker
ampm={false}
openTo="hours"
views={['hours']}
inputFormat="HH:00"
mask="__:00"
value={value}
InputAdornmentProps={{
position: 'start',
}}
onChange={(newValue) => {
setValue(newValue)
}}
renderInput={(params) =>
<TextField
{...params}
helperText="Input start of Day time"
/>
}
/>
</LocalizationProvider>
sandbox

DesktopDatePicker onChange getting the old value

I am trying to get the date of the date in a custom function in the onChange function.
The problem is that the function handleDatesChanged(), gets the previous date not the updated one.
<DesktopDatePicker
inputVariant="outlined"
id="date-picker-dialog"
label="Return date"
disabled={queryParameterUsed ? true : false}
fullWidth
name="endDate"
onChange={(val) => {
formik.setFieldValue("endDate", val);
handleDatesChanged(); //the function that I fetch data based on the date updated
}}
renderInput={(params) => <TextField {...params} />}
onBlur={formik.handleBlur}
value={formik.values.endDate}
inputFormat="dd/MM/yyyy"
/>
const handleDatesChanged = () => {
console.log(formik.values.endDate); //Prints 24/08/2022 (previous date) When in fact it should supposed to print to 28/08/2022
}
The setFieldValue uses useReducer under the hood. It is async and new value is not available immediately. The same applies to the useState hook. You can pass new value as parameter of handler:
onChange={(val) => {
formik.setFieldValue("endDate", val);
handleDatesChanged(val);
}
const handleDatesChanged = (val) => {
console.log(val);
}

ReactJS: Firebase timestamp converting with MobileDatePicker

Am using MobileDatePicker to set a date (chosen by user) and saving that as a timestamp within Firebase.
For the adding of the record it is working fine:
<MobileDatePicker
label={"Date From"}
inputFormat="dd/MM/yyyy"
closeOnSelect={true}
value={datefrom}
minDate={new Date()}
views={['day']}
onChange={(newValue) => {
setDateFrom(format(newValue, 'MM/dd/yyyy')); }}
renderInput={(params) => <TextField variant="outlined" {...params} fullWidth />}
/>
firebase
.firestore()
.collection("collection")
.doc(tripid)
.set({
DateFrom: firebase.firestore.Timestamp.fromDate(new Date(datefrom))
...
When they update the collection, this is where I am having problems - I convert the timestamp into a date that MobileDatePicker understands and displays properly but am having issues then allowing the user to update the date and re-save it back as a timestamp field.
<MobileDatePicker
style={{ width: '100%' }}
label={"Date From"}
inputFormat="dd/MM/yyyy"
disablePast
views={['day']}
value={item.DateFrom == '' ? item.DateFrom : new Date(item.DateFrom.toDate()).toISOString()}
onChange={handleChangeFrom}
renderInput={(params) => <TextFields {...params} fullWidth />}
/>
const handleChangeFrom = (event) => {
setItem({ ...item, DateFrom: firebase.firestore.Timestamp.fromDate(new Date(item.DateFrom)) });
settogles(true)
};
I get the error: RangeError: Invalid time value
I think it has something to do with the handleChangeFrom function (so it is not assigning a time) ... but have no idea how to then convert it back to a timestamp that firebase understands.

React Hook Form V7 - Material UI 5 Autocomplete: Lazy loaded values not validated

I use react-hook-form V7 with an Material UI 5 Autocomplete input. My lazy loaded values in the Autocomplete are not validated on first submit click (but on second). What's wrong?
useEffect(() => {
setItemList([
{ id: 1, name: "item1" },
{ id: 2, name: "item2" }
]);
setItemIdSelected(2);
}, []);
[...]
<Autocomplete
options={itemList}
getOptionLabel={(item) => (item.name ? item.name : "")}
getOptionSelected={(option, value) =>
value === undefined || value === "" || option.id === value.id
}
value={
itemIdSelected
? itemList.find((item) => item.id === itemIdSelected)
: ""
}
onChange={(event, items) => {
if (items !== null) {
setItemIdSelected(items.id);
}
}}
renderInput={(params) => (
<TextField
{...params}
{...itemsIdFormHookRest}
label="items"
margin="normal"
variant="outlined"
inputRef={itemsIdFormHookRef}
error={errors.itemsId ? true : false}
helperText={errors.itemsId && "item required"}
required
/>
)}
/>
[...]
Please have a look to the code on code sandbox:
You should wrap the Material UI Autocomplete with the Controller Component provided by React Hook Form. See this section in the documentation for further information.
I would also suggest to remove the useState saving the id, as you could also access the value via React Hook Form either through getValues or after the handleSubmit callback is called.
Here is the updated CodeSandbox.
How can i implement datepickers?
This works only with modal, but when typing yourself is not working.
<LocalizationProvider dateAdapter={AdapterDateFns} locale={ruLocale}>
<Controller
render={({ field }) => (
<DatePicker
value={field.value}
onChange={(value) => field.onChange(value)}
renderInput={(params) => (
<TextField {...params} />
)}
/>
)}
name={name}
control={control}
/>
</LocalizationProvider>
I handle react-hook-form with mui Autocomplete in the following way
"devDependencies": {
"#mui/material": "^5.8.3",
"react-hook-form": "^7.33.1",
}
const Country = [{ label: 'United Kingdem', value: 'uk' }];
<Controller
name={"country"}
control={control}
rules={{
required: true,
}}
render={({ field: { onChange, ..._field } }) => (
<Autocomplete
options={Country}
onChange={(_, data) => onChange(data.value)}
renderInput={(params) => {
return (
<TextField
{...params}
label="disableCloseOnSelect"
variant="standard"
/>
);
}}
{..._field}
/>
)}
/>

Proper way to use react-hook-form Controller with Material-UI Autocomplete

I am trying to use a custom Material-UI Autocomplete component and connect it to react-hook-form.
TLDR: Need to use MUI Autocomplete with react-hook-form Controller without defaultValue
My custom Autocomplete component takes an object with the structure {_id:'', name: ''} it displays the name and returns the _id when an option is selected. The Autocomplete works just fine.
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) => {
handler(name, reason === 'clear' ? null : newValue._id);
}}
renderInput={params => <TextField {...params} {...inputProps} />}
/>
In order to make it work with react-hook-form I've set the setValues to be the handler for onChange in the Autocomplete and manually register the component in an useEffect as follows
useEffect(() => {
register({ name: "country1" });
},[]);
This works fine but I would like to not have the useEffect hook and just make use of the register somehow directly.
Next I tried to use the Controller component from react-hook-form to proper register the field in the form and not to use the useEffect hook
<Controller
name="country2"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) =>
reason === "clear" ? null : newValue._id
}
renderInput={params => (
<TextField {...params} label="Country" />
)}
/>
}
control={control}
/>
I've changed the onChange in the Autocomplete component to return the value directly but it doesn't seem to work.
Using inputRef={register} on the <TextField/> would not cut it for me because I want to save the _id and not the name
HERE is a working sandbox with the two cases. The first with useEffect and setValue in the Autocomplete that works. The second my attempt in using Controller component
Any help is appreciated.
LE
After the comment from Bill with the working sandbox of MUI Autocomplete, I Managed to get a functional result
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, { _id }]) => _id}
control={control}
/>
The only problem is that I get an MUI Error in the console
Material-UI: A component is changing the uncontrolled value state of Autocomplete to be controlled.
I've tried to set an defaultValue for it but it still behaves like that. Also I would not want to set a default value from the options array due to the fact that these fields in the form are not required.
The updated sandbox HERE
Any help is still very much appreciated
The accepted answer (probably) works for the bugged version of Autocomplete. I think the bug was fixed some time after that, so that the solution can be slightly simplified.
This is very useful reference/codesandbox when working with react-hook-form and material-ui: https://codesandbox.io/s/react-hook-form-controller-601-j2df5?
From the above link, I modified the Autocomplete example:
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
const ControlledAutocomplete = ({ options = [], renderInput, getOptionLabel, onChange: ignored, control, defaultValue, name, renderOption }) => {
return (
<Controller
render={({ onChange, ...props }) => (
<Autocomplete
options={options}
getOptionLabel={getOptionLabel}
renderOption={renderOption}
renderInput={renderInput}
onChange={(e, data) => onChange(data)}
{...props}
/>
)}
onChange={([, data]) => data}
defaultValue={defaultValue}
name={name}
control={control}
/>
);
}
With the usage:
<ControlledAutocomplete
control={control}
name="inputName"
options={[{ name: 'test' }]}
getOptionLabel={(option) => `Option: ${option.name}`}
renderInput={(params) => <TextField {...params} label="My label" margin="normal" />}
defaultValue={null}
/>
control is from the return value of useForm(}
Note that I'm passing null as defaultValue as in my case this input is not required. If you'll leave defaultValue you might get some errors from material-ui library.
UPDATE:
Per Steve question in the comments, this is how I'm rendering the input, so that it checks for errors:
renderInput={(params) => (
<TextField
{...params}
label="Field Label"
margin="normal"
error={errors[fieldName]}
/>
)}
Where errors is an object from react-hook-form's formMethods:
const { control, watch, errors, handleSubmit } = formMethods
So, I fixed this. But it revealed what I believe to be an error in Autocomplete.
First... specifically to your issue, you can eliminate the MUI Error by adding a defaultValue to the <Controller>. But that was only the beginning of another round or problems.
The problem is that functions for getOptionLabel, getOptionSelected, and onChange are sometimes passed the value (i.e. the _id in this case) and sometimes passed the option structure - as you would expect.
Here's the code I finally came up with:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "#material-ui/core";
import { Autocomplete } from "#material-ui/lab";
import { Button } from "#material-ui/core";
export default function FormTwo({ options }) {
const { register, handleSubmit, control } = useForm();
const getOpObj = option => {
if (!option._id) option = options.find(op => op._id === option);
return option;
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => getOpObj(option).name}
getOptionSelected={(option, value) => {
return option._id === getOpObj(value)._id;
}}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, obj]) => getOpObj(obj)._id}
control={control}
defaultValue={options[0]}
/>
<Button type="submit">Submit</Button>
</form>
);
}
import { Button } from "#material-ui/core";
import Autocomplete from "#material-ui/core/Autocomplete";
import { red } from "#material-ui/core/colors";
import Container from "#material-ui/core/Container";
import CssBaseline from "#material-ui/core/CssBaseline";
import { makeStyles } from "#material-ui/core/styles";
import TextField from "#material-ui/core/TextField";
import AdapterDateFns from "#material-ui/lab/AdapterDateFns";
import LocalizationProvider from "#material-ui/lab/LocalizationProvider";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
export default function App() {
const [itemList, setItemList] = useState([]);
// const classes = useStyles();
const {
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
mode: "onChange",
defaultValues: { item: null }
});
const onSubmit = (formInputs) => {
console.log("formInputs", formInputs);
};
useEffect(() => {
setItemList([
{ id: 1, name: "item1" },
{ id: 2, name: "item2" }
]);
setValue("item", { id: 3, name: "item3" });
}, [setValue]);
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<Controller
control={control}
name="item"
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Autocomplete
onChange={(event, item) => {
onChange(item);
}}
value={value}
options={itemList}
getOptionLabel={(item) => (item.name ? item.name : "")}
getOptionSelected={(option, value) =>
value === undefined || value === "" || option.id === value.id
}
renderInput={(params) => (
<TextField
{...params}
label="items"
margin="normal"
variant="outlined"
error={!!errors.item}
helperText={errors.item && "item required"}
required
/>
)}
/>
)}
/>
<button
onClick={() => {
setValue("item", { id: 1, name: "item1" });
}}
>
setValue
</button>
<Button
type="submit"
fullWidth
size="large"
variant="contained"
color="primary"
// className={classes.submit}
>
submit
</Button>
</form>
</Container>
</LocalizationProvider>
);
}
I do not know why the above answers did not work for me, here is the simplest code that worked for me, I used render function of Controller with onChange to change the value according to the selected one.
<Controller
control={control}
name="type"
rules={{
required: 'Veuillez choisir une réponse',
}}
render={({ field: { onChange, value } }) => (
<Autocomplete
freeSolo
options={['field', 'select', 'multiple', 'date']}
onChange={(event, values) => onChange(values)}
value={value}
renderInput={(params) => (
<TextField
{...params}
label="type"
variant="outlined"
onChange={onChange}
/>
)}
/>
)}
Thanks to all the other answers, as of April 15 2022, I was able to figure out how to get this working and render the label in the TextField component:
const ControlledAutocomplete = ({
options,
name,
control,
defaultValue,
error,
rules,
helperText,
}) => (
<Controller
name={name}
control={control}
defaultValue={defaultValue}
rules={rules}
render={({ field }) => (
<Autocomplete
disablePortal
options={options}
getOptionLabel={(option) =>
option?.label ??
options.find(({ code }) => code === option)?.label ??
''
}
{...field}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(error)}
helperText={helperText}
/>
)}
onChange={(_event, data) => field.onChange(data?.code ?? '')}
/>
)}
/>
);
ControlledAutocomplete.propTypes = {
options: PropTypes.arrayOf({
label: PropTypes.string,
code: PropTypes.string,
}),
name: PropTypes.string,
control: PropTypes.func,
defaultValue: PropTypes.string,
error: PropTypes.object,
rules: PropTypes.object,
helperText: PropTypes.string,
};
In my case, options is an array of {code: 'US', label: 'United States'} objects. The biggest difference is the getOptionLabel, which I guess needs to account for if both when you have the list open (and option is an object) and when the option is rendered in the TextField (when option is a string) as well as when nothing is selected.
I have made it work pretty well including multiple tags selector as follow bellow. It will work fine with mui5 and react-hook-form 7
import { useForm, Controller } from 'react-hook-form';
import Autocomplete from '#mui/material/Autocomplete';
//setup your form and control
<Controller
control={control}
name="yourFiledSubmitName"
rules={{
required: 'required field',
}}
render={({ field: { onChange } }) => (
<Autocomplete
multiple
options={yourDataArray}
getOptionLabel={(option) => option.label}
onChange={(event, item) => {
onChange(item);
}}
renderInput={(params) => (
<TextField {...params} label="Your label" placeholder="Your placeholder"
/>
)}
)}
/>
Instead of using controller, with the help of register, setValue of useForm and value, onChange of Autocomplete we can achieve the same result.
const [selectedCaste, setSelectedCaste] = useState([]);
const {register, errors, setValue} = useForm();
useEffect(() => {
register("caste");
}, [register]);
return (
<Autocomplete
multiple
options={casteList}
disableCloseOnSelect
value={selectedCaste}
onChange={(_, values) => {
setSelectedCaste([...values]);
setValue("caste", [...values]);
}}
getOptionLabel={(option) => option}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option}
</React.Fragment>
)}
style={{ width: "100%" }}
renderInput={(params) => (
<TextField
{...params}
id="caste"
error={!!errors.caste}
helperText={errors.caste?.message}
variant="outlined"
label="Select caste"
placeholder="Caste"
/>
)}
/>
);

Resources