Multiple components to result in one value with Formik - reactjs

I have 3 <select> components. In the first component I have the days, in the second the months and in the third the years. These components act as one date picker. I tried to wrap with Formik's connect and use setFieldValue and setFieldError, but the error disappears after I edit an other component in that form.
How can I solve it to the error remain persistent. As I saw, this behaviour is known.
class DateField extends Component {
constructor(props) {
super(props);
const value = props.value;
this.state = {
day: moment.utc(value).date(),
month: moment.utc(value).month(),
year: moment.utc(value).year(),
};
}
handleChange = e => {
const stateProp = e.target.name.split('.').pop();
this.setState(
{
[stateProp]: e.target.value,
},
() => {
const { formik } = this.props;
const date = this.dateFromState();
const error = this.props.validate(date);
formik.setFieldValue(this.props.name, date, false);
formik.setFieldError(this.props.name, error);
}
);
};
handleBlur = () => {
const { formik } = this.props;
const date = this.dateFromState();
const error = this.props.validate(date);
formik.setFieldTouched(this.props.name, true, false);
formik.setFieldError(this.props.name, error);
};
dateFromState() {
// returns date constructed from the state
}
render() {
const { formik, name } = this.props;
const error = getValueByPath(formik.errors, name);
return (
<div className="form-group">
<label>{this.props.label}</label>
<select
name={`${this.props.name}.day`}
value={this.state.day}
onChange={this.handleChange}
onBlur={this.handleBlur}
>
{
// options for days
}
</select>
<select
name={`${this.props.name}.month`}
value={this.state.month}
onChange={this.handleChange}
onBlur={this.handleBlur}
>
{
// options for months
}
</select>
<select
name={`${this.props.name}.year`}
value={this.state.year}
onChange={this.handleChange}
onBlur={this.handleBlur}
>
{
// options for years
}
</select>
{error && (
<div className="validation-message">
<span>{error}</span>
</div>
)}
</div>
);
}
}
const getValueByPath = (obj, path) => path.split('.').reduce((o, i) => o && o[i], obj);
export default connect(DateField);

My advice would be in de-coupling the component from formik, let it provide a date and handle change accordingly. the form in which you use this can then determine if the provided date is valid or not and pass the error to your controlled component to show until the date is valid. This is also great for re-usability.
const DatePicker = ({ name, value, error, onChange, onBlur }) => {
const today = new Date(value);
const [day, setDay] = useState(today.getUTCDay());
const [month, setMonth] = useState(today.getUTCMonth());
const [year, setYear] = useState(today.getUTCYear());
useEffect() => {
const newDate = new Date([year, month, day]);
const target = { name, value: newDate };
onChange({ target });
}, [day, month, year]);
return (
//do your JSX here;
);
}

Related

useState in parent component not keep state, when parent component's callback function is called from child component

Update
This maybe a Formik bug, and I have switched to https://react-hook-form.com, as Formik has not been updated for a while.
https://github.com/jaredpalmer/formik/issues/3716
Context
I'm using React, Formik, and google-map-react to allow store owner edit their store address with google map place autocomplete.
I have three components:
EditStoreInfoPage is the page component, which contains EditStoreInfoForm.
EditStoreInfoForm is the form component, which contains FormikAddressField. I uses Formik here.
FormikAddressField is the one form field that supports google place autocomplete.
Store information will be fetched from backend in EditStoreInfoPage, and passed down to EditStoreInfoForm and FormikAddressField. Whenever a new address is typed in FormikAddressField, it calls a callback function handleStoreLocationUpdate passed down from EditStoreInfoPage.
Issue
Render the page without any issue. I see that formValues are populated corrected with the data fetched from backend.
However, once I finished typing the address, the form get cleared except the store address is still there.
From the console output of above screenshot, I can see that function handleStoreLocationUpdate get called, however, console.log(formValues); in function handleStoreLocationUpdate of EditStoreInfoPage contains empty value for store fields. I was expecting that the formValues here still kept the value fetched from backend, not sure why these values get wiped out as I use React useState.
Any idea what went wrong?
Code
EditStoreInfoPage
This is the React component that first call backend API to get the store information based on storeIdentifier. formValues will be populated with these information, as you can see that setFormValues is being called. formValues is passed down to child component EditStoreInfoForm as props.
type EditStoreInfoPageProps = {
storeIdentifier: string;
};
const EditStoreInfoPage = (props: EditStoreInfoPageProps) => {
let navigate = useNavigate();
const [formValues, setFormValues] = React.useState<StoreAttributes>({
storeName: "",
storeLocation: "",
storeLocationLongitude: 0,
storeLocationLatitude: 0,
});
// Get store info.
React.useEffect(() => {
const user: CognitoUser | null = getCurrentBusinessAccountUser();
if (!user) {
Toast("Store Not Found!", "Failed to get store information!", "danger");
} else {
const storeIdentifier: string = user?.getUsername();
getStoreInfo(storeIdentifier)
.then((response) => {
setFormValues({
storeName: response?.storeName || "",
storeLocation: response?.storeLocation || "",
storeLocationLatitude: response?.storeLocationLatitude!,
storeLocationLongitude: response?.storeLocationLongitude!,
});
})
.catch((error) =>
Toast(
"Store Not Found!",
"Failed to get store information!",
"danger"
)
);
}
}, []);
const handleStoreLocationUpdate = (newStoreLocation: string) => {
const geocoder = new window.google.maps.Geocoder();
console.log("handleStoreLocationUpdate");
console.log(newStoreLocation);
console.log(formValues);
const geocodeRequest = { address: newStoreLocation };
const geocodeCallback = (
results: google.maps.GeocoderResult[] | null,
status: google.maps.GeocoderStatus
) => {
if (status === "OK") {
if (results && results[0]) {
const formValuesClone: StoreAttributes = structuredClone(formValues);
formValuesClone.storeLocation = newStoreLocation;
formValuesClone.storeLocationLatitude =
results[0].geometry.location.lat();
formValuesClone.storeLocationLongitude =
results[0].geometry.location.lng();
setFormValues(formValuesClone);
} else {
Toast("Not valid address!", "Please input a valid address", "danger");
}
} else {
Toast("Not valid address!", "Please input a valid address", "danger");
}
};
geocoder.geocode(geocodeRequest, geocodeCallback);
};
const handleSubmit = (data: StoreAttributes) => {
updateStore(props.storeIdentifier, JSON.stringify(data, null, 2))
.then((response) => {
if (response.status == 200) {
Toast(
"Updated!",
"The store information has been updated. Redirect to store page...",
"success"
);
navigate("/stores/" + props.storeIdentifier);
} else {
Toast(
"Updated failed!",
"Failed to update store information.",
"danger"
);
}
})
.catch((error) => {
Toast("Updated failed!!", error.message, "danger");
});
};
const handleUpdate = (data: StoreAttributes) => {
// make a deep clone here, as formValues here is an object.
console.log("handleUpdate");
const copy = structuredClone(data);
setFormValues(copy);
};
return (
<EditStoreInfoForm
formValues={formValues}
handleStoreLocationUpdate={handleStoreLocationUpdate}
handleUpdate={handleUpdate}
handleSubmit={handleSubmit}
/>
);
};
export default EditStoreInfoPage;
EditStoreInfoForm
EditStoreInfoForm is the form component. I use Formik here. It renders the form with props.formValues. It contains a child component FormikAddressField which will be used to support google place auto complete.
export type EditStoreInfoFormProps = {
formValues: StoreAttributes;
handleStoreLocationUpdate: any;
handleUpdate: any;
handleSubmit: any;
};
const EditStoreInfoForm = (props: EditStoreInfoFormProps) => {
console.log("EditStoreInfoForm");
const onBlur = () => {
console.log(props.formValues);
}
return (
<div className="flex justify-center items-center">
<Formik.Formik
initialValues={props.formValues}
enableReinitialize={true}
validationSchema={validationSchema}
validateOnChange={false}
validateOnBlur={false}
onSubmit={(values) => {
props.handleSubmit(values);
}}
>
{({ }) => (
<Formik.Form className="w-1/3">
<div className="form-group">
<div>
<FormikTextField
label="Store Name"
name="storeName"
placeholder={props.formValues?.storeName}
/>
</div>
<div className="form-group">
<FormikAddressField
label="Store Location"
name="storeLocation"
onAddressUpdate={props.handleStoreLocationUpdate}
placeholder={props.formValues?.storeLocation}
/>
</div>
<div className="w-full h-60">
{/* <GoogleMapLocationPin latitude={10} longitude={10} text="store"/> */}
<StoresGoogleMapLocation
googleMapCenter={{
lat: props.formValues.storeLocationLatitude,
lng: props.formValues.storeLocationLongitude,
}}
storeAddress={props.formValues?.storeLocation}
storeLocationLongitude={
props.formValues?.storeLocationLongitude
}
storeLocationLatitude={props.formValues?.storeLocationLatitude}
/>
</div>
<div className="form-group">
<button type="submit" className="form-button m-2 w-20 h-10">
Update
</button>
</div>
</Formik.Form>
)}
</Formik.Formik>
</div>
);
};
export default EditStoreInfoForm;
FormikAddressField
FormikAddressField is the field for autocomplete. See https://developers.google.com/maps/documentation/javascript/place-autocomplete to know what it is.
const FormikAddressField = ({ label, onAddressUpdate, ...props }: any) => {
const [field, meta] = useField(props);
const loader = new Loader({
apiKey: process.env.REACT_APP_GOOGLE_MAP_API_KEY!,
libraries: ["places", "geometry"],
});
const locationInputId = "locationInputId";
let searchInput: HTMLInputElement;
const autoCompleteInstanceRef = React.useRef<any>(null);
React.useEffect(() => {
loader.load().then(() => {
let searchInput = document.getElementById(
locationInputId
) as HTMLInputElement;
//console.log(searchInput);
autoCompleteInstanceRef.current = new google.maps.places.Autocomplete(
searchInput!,
{
// restrict your search to a specific type of resultmap
//types: ["address"],
// restrict your search to a specific country, or an array of countries
// componentRestrictions: { country: ['gb', 'us'] },
}
);
autoCompleteInstanceRef.current.addListener(
"place_changed",
onPlaceChanged
);
});
// returned function will be called on component unmount
return () => {
google.maps.event.clearInstanceListeners(searchInput!);
};
}, []);
const onPlaceChanged = () => {
const place: google.maps.places.PlaceResult =
autoCompleteInstanceRef.current.getPlace();
if (!place) return;
onAddressUpdate(place.formatted_address);
};
return (
<>
<label htmlFor={props.id || props.name} className="form-label">
{label}
</label>
<Field
id={locationInputId}
className="text-md w-full h-full m-0 p-0"
type="text"
{...field}
{...props}
/>
{meta.touched && meta.error ? (
<div className="error">{meta.error}</div>
) : null}
</>
);
};
export default FormikAddressField;
CodeSandbox
Here is a simplified version: https://nv1m89.csb.app/
The EditStoreInfoPage is above the EditStoreInfoForm. The formikValues in EditStoreInfoPage appears to be a copy, which is not updated every time the actual real-time formik values in EditStoreInfoForm are changed. Your real problem here is that you shouldn't have the clone in the first place.
Just pass the real store values up to the handler:
<FormikAddressField
label="Store Location"
name="storeLocation"
onAddressUpdate={(newAddress) => props.handleStoreLocationUpdate(newAddress, formValues)}
placeholder={props.formValues?.storeLocation}
/>
Now change:
const handleStoreLocationUpdate = (newStoreLocation: string) => {
To:
const handleStoreLocationUpdate = (newStoreLocation: string, formValues: StoreAttributes) => {
And use that argument.
As mentioned there are other issues here. Really you should refactor to get rid of this completely:
const [formValues, setFormValues] = React.useState<StoreAttributes>({
storeName: "",
storeLocation: "",
storeLocationLongitude: 0,
storeLocationLatitude: 0,
});
You'd do it by making the actual form state accessible to that component. Probably by changing to the useFormik pattern and loading that hook in the parent.

onClick and/or onChange not functioning

I have a formik form with a select field; two options. When i use onClick I always get "yes" submitted and if i use onChange it does not work in that it does not allow me to choose anything, just always leaves the field the same as before.
I have read a ton of different things. I have tried the setFieldValue, and I have tried onBlur, I have tried numerous different ways in writing the onChange handler without any success. Maybe i am just not changing and writing it properly.
I have looked over the suggested questions already on here and for whatever reason i can not get them to work in my code.
import React, { useState, useRef } from 'react';
import { Form, Field, } from 'formik';
import emailjs from '#emailjs/browser';
const RsvpForm = ({ errors, touched, isValid, dirty }) => {
const form = useRef();
const sendEmail = (e) => {
e.preventDefault();
const userName = e.target[0].value;
const email = e.target[1].value;
const attending = state;
const plusOne = plusone;
const guests = e.target[4].value;
const guestNumber = e.target[5].value;
const guest_name = e.target[6].value;
const song = e.target[7].value;
const message = e.target[8].value;
let templateParams = {
userName: userName,
email: email,
attending: attending,
plusOne: plusOne,
guests: guests,
guestNumber: guestNumber,
guest_name: guest_name,
song: song,
message: message,
};
emailjs.send(process.env.REACT_APP_SERVICE_ID, process.env.REACT_APP_TEMPLATE_ID, templateParams, process.env.REACT_APP_PUBLIC_KEY)
.then((result) => {
console.log(result.text);
e.target.reset();
}, (error) => {
console.log(error.text);
});
};
const[state, attendingState] = useState("");
const onClick = (e) => {
let{value} = e.target;
if(value=== 'yes') {
attendingState('yes')
}else {
attendingState('no')
}
}
const[plusone, plusOnestate] = useState("");
const onPick = (e) => {
let{value} = e.target;
if(value=== 'no') {
plusOnestate('no')
}else {
plusOnestate('yes')
}
}
return (
<div className='form-group'>
<label className='col-form-label'>Plus One:</label>
<Field
component='select'
className={
touched.plusOne
? `form-control ${errors.plusOne ? 'invalid' : 'valid'}`
: `form-control`
}
name='plusOne'
type='select'
// onChange={(e) => setFieldValue('plusOne', e.target.value)}
onClick={onPick}
>
<option value="">Please select an answer</option>
<option value="yes">Yes, please add a plus one or few</option>
<option value="no">Just me!</option>
</Field>
{touched.plusOne && errors.plusOne && (
<small className='text-warning'><strong>{errors.plusOne}</strong></small>
)}
</div>

React: problem with adding item, instead of adding 1 item it add 2 items at once

could you please help me,
I add item by clicking "On" button but instead of adding 1 item it add 2 items at once, video: https://www.loom.com/share/2f9d0b48817b4480934de63a781ed6a9
Could you please help why and how to add just 1 item?
StepForm.js:
import React from 'react';
import { useState } from "react";
import OneDay from "./OneDay";
function StepsForm () {
const [form, setForm] = useState ({
date: '',
distance: ''
});
const [dataList, setList] = useState([
{id: '1613847600000', date: '21.01.2021', distance: '3.8'},
{id: '1624579200000', date: '25.05.2021', distance: '5.7'},
{id: '1642723200000', date: '21.12.2021', distance: '1.8'}
]);
const handleChange = ({target}) => {
const name = target.name;
const value = target.value;
setForm(prevForm => ({...prevForm, [name]: value}));
}
function dateValue (data) {
const year = data.substring(6,10);
const month = data.substring(3,5);
const day = data.substring(0,2);
const id = new Date(year, month, day).valueOf();
return `${id}`;
}
const handleSubmit = evt => {
evt.preventDefault();
const newData = {
id: dateValue (form.date),
date: form.date,
distance: form.distance
}
const index = dataList.findIndex((item) => item.id === newData.id);
setList(prevList => {
if (index === -1) {
prevList.push(newData);
prevList
.sort((a, b) => {return a.id - b.id})
.reverse();
} else {
prevList[index].distance = String(prevList[index].distance * 1 + newData.distance * 1)
}
return [...prevList];
})
setForm({date: '', distance: ''});
}
const clickToDelete = (evt) => {
const index = dataList.findIndex((item) => item.id === evt.target.dataset.id);
if (index === -1) {
console.log('Что-то пошло не так')
return;
}
setList(prevList => {
prevList.splice(index, 1);
return [...prevList];
})
}
return (
<div className="box">
<form onSubmit={handleSubmit}>
<div className="formBox">
<div className="inputBox">
<label htmlFor="date">Дата (ДД.ММ.ГГ)</label>
<input id="date" name="date" onChange={handleChange} value={form.date} />
</div>
<div className="inputBox">
<label htmlFor="distance">Пройдено км</label>
<input id="distance" name="distance" onChange={handleChange} value={form.distance} />
</div>
<button className="btn" type="submit">Ok</button>
</div>
</form>
<div className="headings">
<div className="heading">Дата (ДД.ММ.ГГ)</div>
<div className="heading">Пройдено км</div>
<div className="heading">Действия</div>
</div>
<div className="table">
{dataList.map(
o => <OneDay key={o.id} item={o} delDay={clickToDelete} />
)}
</div>
</div>
)
}
export default StepsForm;
App.js:
import React from 'react';
import StepsForm from './StepsForm';
import './style.css';
export default function App() {
return (
<div className="App">
<StepsForm />
</div>
);
}
export default App;
I add item by clicking "On" button but instead of adding 1 item it add 2 items at once, video: https://www.loom.com/share/2f9d0b48817b4480934de63a781ed6a9
Could you please help why and how to add just 1 item?
adding elements into the array seems working.
but, there might be an issue with dateValue(data) as it can returns NaN as an id for the element which might cause a problem with the rendering element on the screen.
function dateValue(data) {
const year = data.substring(6, 10);
const month = data.substring(3, 5);
const day = data.substring(0, 2);
const id = new Date(year, month, day).valueOf();
return `${id}`; // this return id as NaN
}
// You can create Uid like code example below
function dateValue() {
const id = new Date().valueOf();
return `${id}`;
}
Hope this solves the problem.

How can I have the placeholder text in my input instead of the value (today's date) when the page first loads?

I have an input, which gets the value from full calendar, so whenever I pick a date, the input value updates. The problem is, when the page first loads, I'm getting today's date instead of the placeholder of the input, which is "YYYY/MM/DD".
How can I show the placeholder as the initial value and only update the value when the user picks a date from the calendar?
Here's my code:
const [showCalendar, setShowCalendar] = React.useState(false);
React.useEffect(() => {
showCalendar
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "visible");
document.addEventListener("click", onclick, false);
return () => {
document.removeEventListener("click", onclick, false);
};
}, [showCalendar]);
const { t, i18n } = useTranslation();
const wrapperRef = useRef(null);
const { value} = props;
const { language } = i18n;
const formattedDate = format(value, "y.MM.dd")
debugger
return (
<DatePickerWrapper ref={wrapperRef}>
<InputContainer>
<Label>Date picker</Label>
<DatePickerInput
value = { value ? formattedDate.toString() : 0}
placeholder= {"YYYY/MM/DD"}
iconId={"calendar"}
onClick={() => setShowCalendar(true)}
/>
</InputContainer>
When using the component:
const [date, setDate] = React.useState<Date>(null);
return (
<DatePicker
value={date}
onChange={setDate}/>

Set value to textfield with hooks and redux material ui

I'm building an app using react, redux, and redux-saga.
The situation is that I'm getting information from an API. In this case, I'm getting the information about a movie, and I will update this information using a basic form.
What I would like to have in my text fields is the value from the object of the movie that I'm calling form the DB.
This is a brief part of my code:
Im using 'name' as an example.
Parent component:
const MovieForm = (props) => {
const {
movie,
} = props;
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({
name,
});
};
const handleSetValues = () => {
console.log('hi');
console.log(movie, name);
setName(movie.name);
setValues(true);
};
useEffect(() => {
if (movie && values === false) {
handleSetValues();
}
});
return (
<Container>
<TextField
required
**defaultValue={() => {
console.log(movie, name);
return movie ? movie.name : name;
}}**
label='Movie Title'
onChange={(e) => setName(e.target.value)}
/>
</Container>
);
};
export default MovieForm;
....
child component
const MovieUpdate = (props) => {
const { history } = props;
const { id } = props.match.params;
const dispatch = useDispatch();
const loading = useSelector((state) => _.get(state, 'MovieUpdate.loading'));
const created = useSelector((state) => _.get(state, 'MovieUpdate.created'));
const loadingFetch = useSelector((state) =>
_.get(state, 'MovieById.loading')
);
const movie = useSelector((state) => _.get(state, 'MovieById.results'));
useEffect(() => {
if (loading === false && created === true) {
dispatch({
type: MOVIE_UPDATE_RESET,
});
}
if (loadingFetch === false && movie === null) {
dispatch({
type: MOVIE_GET_BY_ID_STARTED,
payload: id,
});
}
});
const updateMovie = (_movie) => {
const _id = id;
const obj = {
id: _id,
name: _movie.name,
}
console.log(obj);
dispatch({
type: MOVIE_UPDATE_STARTED,
payload: obj,
});
};
return (
<div>
<MovieForm
title='Update a movie'
buttonTitle='update'
movie={movie}
onCancel={() => history.push('/app/movies/list')}
onSubmit={updateMovie}
/>
</div>
);
};
export default MovieUpdate;
Then, the actual problem is that when I use the default prop on the text field the information appears without any problem, but if i use defaultValue it is empty.
Ok, I kind of got the answer, I read somewhere that the defaultValue can't be used int the rendering.
So I cheat in a way, I set the properties multiline and row={1} (according material-ui documentation) and I was able to edit this field an receive a value to display it in the textfield

Resources