can't use recursive map in react component - javascript

i stumbled into an issue i cant solve, i have an object 'customerDraft' which has nested object in it. i want to render every field plus the fields which are inside of 'customerDraft.metadata'.
my component looks like this:
const CustomerDetailEditModal = (props) => {
const {
open,
setOpen,
customerDraft,
customerProfileDraft,
setDraftCustomer,
setDraftProfile,
onUpdate
} = props;
const classes = useStyles();
const dispatch = useDispatch();
const [isPasswordHidden, setIsPasswordHidden] = useState(true);
// const [attributes, setAttributes] = useState({});
const projectId = useSelector(({ project }) => project.currentProject._id);
const generatedPassword = useSelector(({ customer }) => customer.password);
const isCurrentProjectCapstone = projectId === '4387564328756435';
const onModalCancel = () => {
setOpen(false);
if (isCurrentProjectCapstone) {
dispatch(removeItemFromCustomerDraftAction('password'));
}
};
const generatePassword = () => {
dispatch(getGeneratedPassword());
};
useEffect(() => {
if (!generatedPassword) return;
setDraftCustomer({
...customerDraft,
password: generatedPassword
});
// eslint-disable-next-line
}, [generatedPassword]);
console.log(customerDraft);
return (
<div>
<Modal
bodyStyle={{
fontSize: '12px',
height: 500,
margin: '0 auto'
}}
centered
footer={
<div
style={{
display: 'flex',
justifyContent: 'flex-end'
}}>
<CButton
htmlType="submit"
onClick={(e) => {
setOpen(false);
e.preventDefault();
}}
size="large"
type="secondary">
Cancel
</CButton>
<CButton
htmlType="submit"
onClick={onUpdate}
size="large"
type="primary"
// disabled={!isSaveEnabled}
>
Save
</CButton>
</div>
}
onCancel={onModalCancel}
title={
<span
style={{
fontSize: '24px',
fontWeight: 700,
lineHeight: '24px'
}}>
Edit User
</span>
}
visible={open}
width={customerProfileDraft ? 770 : 385}>
<form className={classes.form} id="customer-edit-form">
<div className={classes.wrapperDiv}>
{Object.entries(customerDraft).map((item, i) => {
if (customerDraft.fullName) {
if (restrictedData.includes(item[0] || item[0].toLowerCase().includes('id'))) {
return false;
}
}
if (restrictedData.includes(item[0]) || item[0].toLowerCase().includes('id')) {
return false;
}
return (
<CStandardInput
key={i}
allowClear
defaultValue={item[1]}
disableUnderline
formclasses={{ root: classes.root }}
htmlFor={`standard-customer-edit-${item[0]}`}
id={`standard-customer-edit-${item[0]}`}
label={item[0]}
onChange={(event) => {
setDraftCustomer({
...customerDraft,
fullName: event.target.value
});
setDraftProfile({
...customerProfileDraft,
fullName: event.target.value
});
}}
size="large"
/>
);
})}
{isCurrentProjectCapstone && (
<div className={classes.passwordWrapper}>
<CStandardInput
adornment={
<>
<button
className={classes.buttonSvg}
onClick={() => {
navigator.clipboard.writeText(customerDraft.password || '');
}}
style={{
marginRight: '5px'
}}
type="button">
<img alt="copy password" src={copyIcon} />
</button>
<button
className={classes.buttonSvg}
onClick={() => setIsPasswordHidden(!isPasswordHidden)}
type="button">
<img
alt="toggle password visibility"
src={isPasswordHidden ? crossedEyeIcon : eyeIcon}
/>
</button>
</>
}
disableUnderline
formclasses={{ root: classes.root }}
htmlFor="standard-input-user-password"
id="standard-input-user-password"
label="Password"
onChange={(e) => setDraftCustomer({ ...customerDraft, password: e.target.value })}
size="large"
type={isPasswordHidden ? 'password' : 'text'}
value={customerDraft.password || ''}
width="true"
/>
<CButton
onClick={generatePassword}
type="primary"
xstyle={{
borderRadius: '12px',
margin: '16px 0px 0px 16px'
}}>
Generate
</CButton>
</div>
)}
</div>
</form>
</Modal>
</div>
);
};
export default CustomerDetailEditModal;
notice how metdata field is rendered? i want to use recursion to output every field which metadata contains,
i know recursion but what i cant seem to figure out is where should this component call itself to do it.
any help with explanation so that i can understand the answer would be much appreciated!
this is the object im iterating on:
const customerData = {
createdAt: "2022-10-28T08:42:08.015Z",
email: "company#gmail.com",
firstName: "$$$$$$$",
fullName: "$$$$$$",
idNumber: "2813921321",
isEmailVerified: true,
isPhoneVerified: true,
lastName: "$$$$$",
metadata: {
birthDate: "2000-08-19 00:00:00.000",
gender: "Male",,
region: "",
status: "Adult",
statusExtra: "Student",
},
phone: "######",
project: "hlkjhkljhkjhk",
updatedAt: "2022-11-01T10:26:32.677Z",
username: null,
_id: "hlkjhlkjhlkjhlkjhlkjh",
};
see metadata? currently im outputting only the fields of the main(parent) object, but i also want to output the data which is contained in the 'metadata' key using recursion.

A solution to this could be to check if the key item[0] is "metadata". Then you could do the same as you did with the customerDraft object. Get the entries an map over them.
Note that I destructured the array you get from the .entries to make it more explicit what the variables are.
if (item[0] === "metadata") {
const inputs = Object.entries(item[1]).map(([metaKey, metaValue]) => (
<CStandardInput
key={metaKey}
allowClear
defaultValue={metaValue}
disableUnderline
formclasses={{ root: classes.root }}
htmlFor={`standard-customer-edit-${metaKey}`}
id={`standard-customer-edit-${metaKey}`}
label={metaKey}
onChange={(event) => {
setDraftCustomer({
...customerDraft,
fullName: event.target.value,
});
setDraftProfile({
...customerProfileDraft,
fullName: event.target.value,
});
}}
size="large"
/>
));
return <>{inputs}</>;
}
return (
<CStandardInput
...
EDIT:
To support the nested data with recursion, I've created a function with returns an input for every key, value pair in the data.
You can add your extra if statements as desired
const renderInputs = (data) => {
const inputs = Object.entries(data).map(([key, value]) => {
if (
typeof value === "object" &&
!Array.isArray(value) &&
value !== null
) {
return renderInputs(value);
}
return (
<CStandardInput
key={key}
allowClear
defaultValue={value}
disableUnderline
formclasses={{ root: classes.root }}
htmlFor={`standard-customer-edit-${key}`}
id={`standard-customer-edit-${key}`}
label={key}
onChange={(event) => {
setDraftCustomer({
...customerDraft,
fullName: event.target.value,
});
setDraftProfile({
...customerProfileDraft,
fullName: event.target.value,
});
}}
size="large"
/>
);
});
return inputs;
};
return <>{renderInputs(customerData)}</>;
Hope this helps you with your project!

Related

React is forgetting select value on multi step form

and so I faced with a problem. I'm using react-form-hook with material-ui to create multi step form. the problem is when I stem forward react-form-hook forgets select value. the select component:
const Second_step_component = ({ register, errors }) => {
const [age, setAge] = React.useState('');
const handleChange = (event) => {
setAge(event.target.value);
};
return (
<FormControl sx={{ m: 1, minWidth: 120 }} error={Boolean(errors.Age)}>
<InputLabel id="demo-simple-select-helper-label">Age</InputLabel>
<Select
labelId="demo-simple-select-helper-label"
id="demo-simple-select-helper"
value={age}
label="Age"
{...register("Age", {required: "Age Is Required"})}
onChange={handleChange}
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>{errors.Age?.message}</FormHelperText>
</FormControl>
);
}
export default Second_step_component
I understand that it's reverts back to its default state of '', but when its seems my knowledge is to limited to change useState to register("Age"), if someone could give me some hints, I would be very grateful
also including main stepper component:
const steps = ['House', 'Materials'];
export default function HorizontalLinearStepper() {
const [activeStep, setActiveStep] = React.useState(0);
const [skipped, setSkipped] = React.useState(new Set());
const {register, handleSubmit, formState: {errors}, trigger} = useForm()
const [state, setstate] = useState([{}]);
const isStepOptional = (step) => {
return step === 1;
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const handlesub = (data) =>{
setstate(data)
console.log(data)
}
const handleNext = async (e) => {
e.preventDefault()
let isNext = false
switch(activeStep){
case 0: isNext = await trigger(["Miestas", "GetvÄ—", "Numeris"]);
break;
case 1: isNext = await trigger(["Age"]);
break;
}
if(isNext){
setActiveStep((prevActiveStep) => prevActiveStep + 1);
}
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleSkip = () => {
if (!isStepOptional(activeStep)) {
// You probably want to guard against something like this,
// it should never occur unless someone's actively trying to break something.
throw new Error("You can't skip a step that isn't optional.");
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
});
};
let Swithing = null
switch (activeStep) {
case 0:
Swithing = <First_step_component register={register} errors={errors}/>
break
case 1:
Swithing = <Second_step_component register={register} errors={errors}/>
break
}
const handleReset = () => {
setActiveStep(0);
};
return (
<Box sx={{padding: "2rem", display: "grid", placeContent: "center"}}>
<Box sx={{ width: '500px' }}>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (isStepOptional(index)) {
labelProps.optional = (
<Typography variant="caption">Optional</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === steps.length ? (
<React.Fragment>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you&apos;re finished
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Box sx={{ flex: '1 1 auto' }} />
<Button onClick={handleReset}>Reset</Button>
</Box>
</React.Fragment>
) : (
<React.Fragment>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
<form id='step-form-sub' onSubmit={handleSubmit(handlesub)}>
<Box>{Swithing}</Box>
</form>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: '1 1 auto' }} />
{/* {isStepOptional(activeStep) && (
<Button color="inherit" onClick={handleSkip} sx={{ mr: 1 }}>
Skip
</Button>
)} */}
{activeStep === steps.length - 1?
(<Button form="step-form-sub" type='submit'>submit</Button>)
:
(<Button type='button' onClick={handleNext}>next</Button>)
}
</Box>
</React.Fragment>
)}
</Box>
</Box>
);
}
It's because of the way you architected your stepper.
The state in the second step will be reverted to the original value once it's unmounted.
The solution is to move the state Into the wrapper component (e.g. if your wrapper component is App.jsx => move all stepper states into it)
Please have a look at the following stepper on code pen to understand:
React stepper
const App = () => {
const [acceptFirstTerms, setAcceptFirstTerms] = useState({
checked: false,
touched: false,
}),
[acceptSecondTerms, setAcceptSecondTerms] = useState({
checked: false,
touched: false,
}),
[acceptThirdTerms, setAcceptThirdTerms] = useState({
checked: false,
touched: false,
}),
[isSecondStepLoading, setIsSecondStepLoading] = useState(false);
const firstTermsHandler = () => {
setAcceptFirstTerms((prev) => ({ checked: !prev.checked, touched: true }));
};
const secondTermsHandler = () => {
setAcceptSecondTerms((prev) => ({ checked: !prev.checked, touched: true }));
};
const thirdTermsHandler = () => {
setAcceptThirdTerms((prev) => ({ checked: !prev.checked, touched: true }));
};
//for demo purposes only
const timeout = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const secondStepAsyncFunc = async () => {
//it can be an API call
setIsSecondStepLoading(true);
await timeout(3000);
setIsSecondStepLoading(false);
console.log('second step clicked');
};
const stepperContent = [
{
label: 'Step 1',
content: (
<div>
<label>
<input
type="checkbox"
checked={acceptFirstTerms.checked}
onChange={firstTermsHandler}
/>{' '}
Accept first terms and conditions
</label>
</div>
),
isError: !acceptFirstTerms.checked && acceptFirstTerms.touched,
isComplete: acceptFirstTerms.checked,
},
{
label: 'Step 2',
content: (
<div>
<label>
<input
type="checkbox"
checked={acceptSecondTerms.checked}
onChange={secondTermsHandler}
/>{' '}
Accept second terms and conditions
</label>
</div>
),
clicked: () => secondStepAsyncFunc(),
isLoading: isSecondStepLoading,
isError: !acceptSecondTerms.checked && acceptSecondTerms.touched,
isComplete: acceptSecondTerms.checked,
},
{
label: 'Step 3',
content: (
<div>
<label>
<input
type="checkbox"
checked={acceptThirdTerms.checked}
onChange={thirdTermsHandler}
/>{' '}
Accept third terms and conditions
</label>
</div>
),
isError: !acceptThirdTerms.checked && acceptThirdTerms.touched,
isComplete: acceptThirdTerms.checked,
},
];
const submitStepper = () => {
console.log('submitted');
};
return (
<div className="container">
<h2>Default stepper</h2>
<Stepper stepperContent={stepperContent} submitStepper={submitStepper} />
<hr />
<h2>Inline stepper</h2>
<Stepper stepperContent={stepperContent} submitStepper={submitStepper} isInline />
<hr />
<h2>Vertical stepper</h2>
<Stepper stepperContent={stepperContent} submitStepper={submitStepper} isVertical />
</div>
);
};

Unable to Type in <Formik> Field

interface FormValues {
friendEmail: string;
}
const initialValues: FormValues = {
friendEmail: '',
};
export const Page: React.FunctionComponent<PageProps> = ({
toggleShowPage,
showPage,
}) => {
const [errorMessage, setErrorMessage] = useState('');
const validationSchema = emailValidationSchema;
useEffect(() => {
if (showPage) return;
initialValues.friendEmail = '';
}, [showPage]);
const [
createUserRelationMutation,
{
data: addingFriendData,
loading: addingFriendLoading,
error: addingFriendError,
called: isMutationCalled,
},
] = useCreateUserRelationMutation({
onCompleted: (data: any) => {
showAlert();
},
});
const addFriend = React.useCallback(
(id: Number) => {
console.log('Whats the Id', id);
createUserRelationMutation({
variables: {
input: { relatedUserId: id, type: RelationType.Friend, userId: 7 },
},
});
},
[createUserRelationMutation],
);
const getFriendId = React.useCallback(
(data: any) => {
//console.log('Email', initialValues.friendEmail);
if (data) {
if (data.users.nodes.length == 0) {
console.log('No user');
setErrorMessage('User Not Found');
Alert.alert('User Not Found');
} else {
console.log('ID', data.users.nodes[0].id);
addFriend(Number(data.users.nodes[0].id));
}
}
},
[addFriend],
//[friendEmail, addFriend],
);
const [loadUsers] = useUsersLazyQuery({
onCompleted: getFriendId,
onError: _onLoadUserError,
});
const handleSubmitForm = React.useCallback(
(values: FormValues, helpers: FormikHelpers<FormValues>) => {
console.log('Submitted');
loadUsers({
variables: {
where: { email: values.friendEmail },
},
});
//setFriendEmail('');
values.friendEmail = '';
},
[loadUsers],
//[loadUsers, friendEmail]
);
if (!addingFriendLoading && isMutationCalled) {
if (addingFriendData) {
console.log('Checking');
}
if (addingFriendError) {
console.log('errorFriend', addingFriendError.message);
}
}
return (
<Modal
visible={showAddFriendEmailPage}
animationType="slide"
transparent={true}>
<SafeAreaView>
<View style={scaledAddFriendEmailStyles.container}>
<View style={scaledAddFriendEmailStyles.searchTopContainer}>
<View style={scaledAddFriendEmailStyles.searchTopTextContainer}>
<Text
style={scaledAddFriendEmailStyles.searchCancelDoneText}
onPress={toggleShowPage}>
Cancel
</Text>
<Text style={scaledAddFriendEmailStyles.searchTopMiddleText}>
Add Friend by Email
</Text>
<Text style={scaledAddFriendEmailStyles.searchCancelDoneText}>
Done
</Text>
</View>
<View style={scaledAddFriendEmailStyles.searchFieldContainer}>
<Formik
initialValues={initialValues}
onSubmit={handleSubmitForm}
validationSchema={validationSchema}>
{({
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
values,
}) => (
<View>
<View>
<Item style={scaledAddFriendEmailStyles.searchField}>
<TextInput
style={scaledAddFriendEmailStyles.searchText}
placeholder="Email"
onChangeText={handleChange('friendEmail')}
//onChangeText={e => console.log('Workinggg')}
onBlur={handleBlur('friendEmail')}
value={values.friendEmail}
autoCapitalize="none"
/>
{/* <Field
component={Input}
name='friendEmail'
placeholder="Email"
//handleChange={handleChange}
handleBlur={handleBlur}
//onChange={handleChange}
//onChangeText={handleChange('friendEmail')}
//onBlur={handleBlur('friendEmail')}
value={values.friendEmail}
autoCapitalize="none"
/> */}
</Item>
</View>
<View>
<Button
onPress={handleSubmit}>
<Text>
Add Friend{' '}
</Text>
</Button>
</View>
</View>
)}
</Formik>
</View>
</View>
</View>
</SafeAreaView>
</Modal>
);
};
Why am I unable to write anything inside my Input field? I have tried using onChangeand handleChangeboth but it doesn't make a difference. Other SO answers suggested that I should remove value but examples of Formik usage that I see online suggest otherwise.
I am trying to follow this for my Formik validation:
https://heartbeat.fritz.ai/build-and-validate-forms-in-react-native-using-formik-and-yup-6489e2dff6a2
EDIT:
I also tried with setFieldValuebut I still cannot type anything.
const initialValues: FormValues = {
friendEmail: '',
};
export const AddFriendEmailPage: React.FunctionComponent<AddFriendEmailPageProps> = ({
toggleShowPage,
showAddFriendEmailPage,
}) => {
const [errorMessage, setErrorMessage] = useState('');
const validationSchema = emailValidationSchema;
useEffect(() => {
if (showAddFriendEmailPage) return;
initialValues.friendEmail = '';
}, [showAddFriendEmailPage]);
const _onLoadUserError = React.useCallback((error: ApolloError) => {
setErrorMessage(error.message);
Alert.alert('Unable to Add Friend');
}, []);
const [
createUserRelationMutation,
{
data: addingFriendData,
loading: addingFriendLoading,
error: addingFriendError,
called: isMutationCalled,
},
] = useCreateUserRelationMutation({
onCompleted: (data: any) => {
showAlert();
},
});
const addFriend = React.useCallback(
(id: Number) => {
console.log('Whats the Id', id);
createUserRelationMutation({
variables: {
input: { relatedUserId: id, type: RelationType.Friend, userId: 7 },
},
});
},
[createUserRelationMutation],
);
const getFriendId = React.useCallback(
(data: any) => {
//console.log('Email', initialValues.friendEmail);
if (data) {
if (data.users.nodes.length == 0) {
console.log('No user');
} else {
console.log('ID', data.users.nodes[0].id);
addFriend(Number(data.users.nodes[0].id));
}
}
},
[addFriend],
//[friendEmail, addFriend],
);
const [loadUsers] = useUsersLazyQuery({
onCompleted: getFriendId,
onError: _onLoadUserError,
});
const handleSubmitForm = React.useCallback(
(values: FormValues, helpers: FormikHelpers<FormValues>) => {
console.log('Submitted');
loadUsers({
variables: {
where: { email: values.friendEmail },
},
});
//setFriendEmail('');
values.friendEmail = '';
},
[loadUsers],
//[loadUsers, friendEmail]
);
return (
<Modal
visible={showPage}
animationType="slide"
transparent={true}>
<SafeAreaView>
<View style={scaledAddFriendEmailStyles.container}>
<View style={scaledAddFriendEmailStyles.searchFieldContainer}>
<Formik
initialValues={initialValues}
onSubmit={handleSubmitForm}
validationSchema={validationSchema}>
{({
handleChange,
setFieldValue,
handleBlur,
handleSubmit,
isSubmitting,
values,
}) => {
const setEmail = (friendEmail: string) => {
setFieldValue('friendEmail', friendEmail)
}
return(
<View>
<View>
<Item>
<TextInput
placeholder="Email"
onChangeText={setEmail}
onBlur={handleBlur('friendEmail')}
value={values.friendEmail}
autoCapitalize="none"
/>
</Item>
</View>
<View >
<Button
onPress={handleSubmit}>
<Text >
Add Friend{' '}
</Text>
</Button>
</View>
</View>
)}}
</Formik>
</View>
</View>
</View>
</SafeAreaView>
</Modal>
);
};
Formik's Field component doesn't support React native yet. Check this github issue for more details
However you can make use of TextInput in place of field and use it with onChangeText handler
<Formik
initialValues={initialValues}
onSubmit={handleSubmitForm}
validationSchema={validationSchema}>
{({
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
values,
}) => (
<View>
<View>
<Item style={scaledAddFriendEmailStyles.searchField}>
<TextInput
placeholder="Email"
onChangeText={handleChange('friendEmail')}
onBlur={handleBlur('friendEmail')}
value={values.friendEmail}
/>
</Item>
</View>
<View >
<Button
onPress={handleSubmit}
>
<Text >
Add Friend{' '}
</Text>
</Button>
</View>
</View>
)}
</Formik>
you can read more about Formik's usage with react-native in its documentation here
try this:
<Input
placeholder="Email"
onChange={e => setFieldValue('friendEmail', e.currentTarget.value)}
onBlur={handleBlur}
value={values.friendEmail}
autoCapitalize="none"
/>
I think there are a couple of issues in your codebase.
onChangeText={handleChange('friendEmail')}. It will trigger the handleChange while rendering the component not when you are actualy typing in the input box.
handleChange function of Formik takes React.ChangeEvent instead of value. Check here . While onChangeText of react-native provides changed text of the input not event. Check here
You can use setFieldValue function for this case:
<Formik
initialValues={initialValues}
onSubmit={handleSubmitForm}
validationSchema={validationSchema}>
{({
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
values,
setFieldValue
}) => {
const setEmail = (email) => {
setFieldValue('friendEmail', email)
}
return (
<View>
<View>
<Item style={scaledAddFriendEmailStyles.searchField}>
<TextInput
placeholder="Email"
onChangeText={setEmail}
value={values.friendEmail}
/>
</Item>
</View>
</View>
)
}}
</Formik>
Please Note: I've never used formik with react-native. Just trying to connect the dots.
Formik now works fine with React Native, but another thing to be aware of is that the name in your form control must match the name of the property in the schema used by Formik.
For example, if using the following Yup schema:
const schema = yup.object({
personName: yup.string().required('Name required')
});
Then the following won't work because the Yup schema (personName) and the form control (nameofPerson) don't match; the user won't be able to type into the field:
<Form.Control
name="nameOfPerson"
value={values.personName}
onChange={handleChange}
onBlur={handleBlur}
type="text"
isValid={!errors.personName && !!touched.personName}
isInvalid={!!errors.personName && !!touched.personName}
/>
To make it work, the name should be the same as the Yup property; in this case, personName:
<Form.Control
name="personName"
value={values.personName}
onChange={handleChange}
onBlur={handleBlur}
type="text"
isValid={!errors.personName && !!touched.personName}
isInvalid={!!errors.personName && !!touched.personName}
/>
This example is using React-Bootstrap Form.Control but should apply to any manner of creating form controls.

Formik custom color picker

i'm trying to create a custom color picker with formik form
the probleme her is that parent component color are not changed :
import {SketchPicker} from "react-color";
export const MyColorPicker = ({label, ...props}) => {
// with useField is should not use onChange but i get an error without defining it myself
const [field] = useField(props);
const [color, setColor] = useState("#333");
const handleChange = color => {
setColor(color.hex);
field.onChange(color.hex);
};
const onComplete = color => {
setColor(color.hex);
field.onChange(color.hex);
};
return (
<div style={{padding: 10}}>
<label>{label}</label>
<SketchPicker {...props} {...field} color={color} onChange={handleChange} onChangeComplete={onComplete} />
</div>
);
};
as exemple this work :
export const MyTextAreaField = ({label, ...props}) => {
const [field, meta] = useField(props);
if (field && field.value === null) {
field.value = "";
}
return (
<div style={{display: "flex", flexDirection: "column"}}>
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated MuiInputLabel-shrink MuiFormLabel-filled">
{label}
</label>
<TextareaAutosize
rows={10}
{...field}
{...props}
style={{marginTop: 10, fontFamily: "Helvetica Neue", fontSize: 15}}
/>
{meta.touched && meta.error ? <div className="error">{meta.error}</div> : null}
</div>
);
};
and parent code :
<Formik
initialValues={{
data: { title :'', shortDescription:'', description:'', color:'')
}}
onSubmit={values => {
console.log(values.data) ; // data.color stay null
}}>
<Form>
<MyTextAreaField id="data.description" name="data.description" label={t("PROJECT.DESCRIPTION")} />
<MyColorPicker id="data.color" label={t("PROJET.COLOR")} name="data.color" />
</Form>
</Formik>
finally i ended doing something like this :
In parent Component :
<MyColorPicker
label={t("PROJECT.COLOR")}
onChange={color => {
data.project.color = color;
}}
/>
Component definition
export const MyColorPicker = ({label, onChange}) => {
const [color, setColor] = useState("#333");
const handleChange = color => {
setColor(color.hex);
};
return (
<div
style={{display: "flex", flexDirection: "row", justifyContent: "flex-start", alignItems: "center", padding: 10}}>
<label>{label}</label>
<ChromePicker color={color} onChange={handleChange} onChangeComplete={color=> onChange(color.hex) } />
</div>
)
})

Redirection Not Working Because of Order of Executions of Functions

I have a login page. If login is successful and token is present in local storage, I want to redirect to a private page /panel. I am calling all functions on the onSubmit() of my form.
Here's what my code looks like:
function LoginPage (){
const [state, setState] = useState({
email: '',
password: '',
});
const [submitted, setSubmitted] = useState(false);
function ShowError(){
if (!localStorage.getItem('token'))
{
console.log('Login Not Successful');
}
}
function FormSubmitted(){
setSubmitted(true);
console.log('Form submitted');
}
function RedirectionToPanel(){
console.log('ha');
if(submitted && localStorage.getItem('token')){
console.log('FInall');
return <Redirect to='/panel'/>
}
}
function submitForm(LoginMutation: any) {
const { email, password } = state;
if(email && password){
LoginMutation({
variables: {
email: email,
password: password,
},
}).then(({ data }: any) => {
localStorage.setItem('token', data.loginEmail.accessToken);
})
.catch(console.log)
}
}
return (
<Mutation mutation={LoginMutation}>
{submitted && <Redirect to='/panel'/>}
{(LoginMutation: any) => (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
<Avatar>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Formik
initialValues={{ email: '', password: '' }}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
validationSchema={schema}
>
{props => {
const {
values: { email, password },
errors,
touched,
handleChange,
isValid,
setFieldTouched
} = props;
const change = (name: string, e: any) => {
e.persist();
handleChange(e);
setFieldTouched(name, true, false);
setState( prevState => ({ ...prevState, [name]: e.target.value }));
};
return (
<form style={{ width: '100%' }}
onSubmit={e => {e.preventDefault();
submitForm(LoginMutation);FormSubmitted();RedirectionToPanel()}}>
<TextField
variant="outlined"
margin="normal"
id="email"
fullWidth
name="email"
helperText={touched.email ? errors.email : ""}
error={touched.email && Boolean(errors.email)}
label="Email"
value={email}
onChange={change.bind(null, "email")}
/>
<TextField
variant="outlined"
margin="normal"
fullWidth
id="password"
name="password"
helperText={touched.password ? errors.password : ""}
error={touched.password && Boolean(errors.password)}
label="Password"
type="password"
value={password}
onChange={change.bind(null, "password")}
/>
{submitted && ShowError()}
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<br />
<Button className='button-center'
type="submit"
disabled={!isValid || !email || !password}
// onClick={handleOpen}
style={{
background: '#6c74cc',
borderRadius: 3,
border: 0,
color: 'white',
height: 48,
padding: '0 30px'
}}
>
Submit</Button>
</form>
)
}}
</Formik>
</div>
</Container>
)
}
</Mutation>
);
}
export default LoginPage;
When I hit the submit button, I check the console for what happens inside the RedirectionToPanel() function. The first time, 'Ha' is printed but when I click on it for the second time, both 'Ha' & 'Finally' are printed. However, the redirection still doesn't happen.
If I use {submitted && <Redirect to='/panel'/>}after Mutation, I get this error on mutation:
This JSX tag's 'children' prop expects a single child of type '(mutateFunction: MutationFunction<any, Record<string, any>>, result: MutationResult<any>) => Element | null', but multiple children were provided.ts(2746)
If I use it after the return and before mutation, I get syntax errors on && and }.
As #wxker stated, you need to return <Redirect> elements in the render method in order for the redirect to actually occur. E.g. https://codesandbox.io/s/proud-rgb-d0g8e.
This is happening because submitForm and FormSubmitted both contain asynchronous operations. This means that calling them one after another followed by RedirectionToPanel does not guarantee that they execute in that order. Consider using the useEffect hook to do the redirect once the token is set.
Also, the reason why the page is not redirecting is because you are not returning the Redirect component into the DOM tree. You can fix this by inserting it with a conditional statement that checks the submitted state
//when localStorage.getItem('token') changes, execute the callback
useEffect(() => {
setSubmitted(true);
}, [localStorage.getItem('token')])
...
return (
...
<form style={{ width: '100%' }}
onSubmit={e => {e.preventDefault();
submitForm(LoginMutation);}}>
...
//This line can be anywhere in the return. Even at the end is fine
{submitted && <Redirect to='/panel'/>}
<Container />
);
If you want to do this without useEffect, you can use setSubmitted within submitForm. But even then, you must have {submitted && <Redirect to='/panel'/>} somewhere in your DOM
function submitForm(LoginMutation: any) {
const { email, password } = state;
if(email && password){
LoginMutation({
variables: {
email: email,
password: password,
},
}).then(({ data }: any) => {
localStorage.setItem('token', data.loginEmail.accessToken);
setSubmitted(true);
})
.catch(console.log)
}
}
return (
...
<form style={{ width: '100%' }}
onSubmit={e => {e.preventDefault();
submitForm(LoginMutation);}}>
...
//This line can be anywhere in the return. Even at the end is fine
{submitted && <Redirect to='/panel'/>}
<Container />
);

How do you access the correct object in a dynamic array using a textfields name and value?

I have a dynamic array of values that i want to be able to change as I type and submit textfields. How do i properly change the value of the variable in the correct object of the array by submitting the textfields?
I have tried relating the key of the exercise to the values i need but its not working.
this is the parent function
export default class AddWorkoutForm extends Component {
state = {
step: 1,
name: '',
duration: 0,
date: '',
exerciselist: [
{
id: uuid.v4(),
exerciseName: '',
numberOfSets: 0,
weight: 0,
reps: 0
}
],
}
// Generate Exercise objects in exerciselist
addExercises = () => {
this.setState({
exerciselist: [
...this.state.exerciselist,
{
id: uuid.v4(),
exerciseName: '',
numberOfSets: 0,
weight: 0,
reps: 0
}
]
})
}
// Remove exercise object in exerciselist
removeExercises = (id) => {
if (this.state.exerciselist.length > 1) {
this.setState({
exerciselist: [...this.state.exerciselist.filter(exercise => exercise.id !== id)]
})
}
}
// Proceed to next step
nextStep = () => {
const { step } = this.state;
this.setState({
step: step + 1
});
};
// Go back to prev step
prevStep = () => {
const { step } = this.state;
this.setState({
step: step - 1
});
};
// Handle fields change
handleChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
render() {
const { step } = this.state;
const { name, duration, date, exerciselist } = this.state;
const values = { name, duration, date, exerciselist };
switch (step) {
case 1:
return (
<AddWorkout
nextStep={this.nextStep}
handleChange={this.handleChange}
values={values}
/>
);
case 2:
return (
<AddExercise
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange={this.handleChange}
addExercises={this.addExercises}
removeExercises={this.removeExercises}
values={values}
/>
);
case 3:
return (
<AddWorkoutConfirm
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange={this.handleChange}
values={values}
/>
);
}
}
}
This is the code for mapping the fields for each object in the dynamic array:
export default class AddExercise extends Component {
continue = (e) => {
e.preventDefault();
this.props.nextStep();
}
back = (e) => {
this.props.prevStep();
};
render() {
const { values, handleChange, addExercises, removeExercises } = this.props;
const { exerciselist } = values;
return (
<div style={{ textAlign: "center", height: "100%" }}>
<ClientMenuBar title="Add An Exercise" />
<h2>Enter Your Exercise Details</h2>
<form>
<div style={styles.form}>
{exerciselist.map((exercise) => {
return (
<div style={styles.textfieldWrapper} key={exercise.id}>
<TextField
fullWidth
label="Exercise Name"
margin="dense"
name="exerciseName"
onChange={handleChange}
defaultValue={exercise.exerciseName}
/>
<TextField
label="Starting # Of Reps"
margin="dense"
type="number"
style={styles.textfield}
name="reps"
onChange={handleChange}
defaultValue={exercise.reps}
/>
<TextField
label="Starting Weight"
margin="dense"
type="number"
style={styles.textfield}
name="weight"
onChange={handleChange}
defaultValue={exercise.weight}
/>
<TextField
label="# of Sets"
margin="dense"
type="number"
style={styles.textfield}
name="numberOfSets"
onChange={handleChange}
defaultValue={exercise.numberOfSets}
/>
<Button
onClick={() => removeExercises(exercise.id)}
size="small"
disableRipple
fullWidth
>
REMOVE EXERCISE
</Button>
</div>
);
})}
<Button
onClick={addExercises}
size="small"
disableRipple
fullWidth
>
ADD EXERCISE
</Button>
</div>
<div style={styles.buttonWrapper}>
<Button
color="inherit"
variant="contained"
style={styles.button}
size="large"
onClick={this.back}
>
back
</Button>
<Button
color="primary"
variant="contained"
style={styles.button}
size="large"
onClick={this.continue}
>
Next
</Button>
</div>
</form>
</div>
)
}
}
And this is where i am printing out what i have:
export default class AddWorkoutConfirm extends Component {
continue = e => {
e.preventDefault();
// PROCESS FORM //
this.props.nextStep();
};
back = e => {
e.preventDefault();
this.props.prevStep();
};
render() {
const { name, duration, date, exerciselist } = this.props.values;
return (
<div style={{ textAlign: "center", height: "100%" }}>
<ClientMenuBar title="Confirm Your Details" />
<h2>Enter Your Workout Details</h2>
<form style={styles.form}>
<List>
<ListItem>Workout Name: {name}</ListItem>
<ListItem>Estimated Duration Of Workout: {duration} mins</ListItem>
<ListItem>Number Of Exercises: {exerciselist.length}</ListItem>
<ListItem>Date Of Workout: {date}</ListItem>
</List>
<div style={{borderTop:"1px solid gray"}}>
{
exerciselist.map((exercise) => {
return (
<List key={exercise.id}>
<ListItem>Exercise Name: {exercise.exerciseName}</ListItem>
<ListItem>Number of Sets: {exercise.numberOfSets}</ListItem>
<ListItem>Starting Weight: {exercise.weight} lbs</ListItem>
<ListItem>Starting Number Of Reps: {exercise.reps}</ListItem>
</List>
)
})}
</div>
<br />
<div style={styles.buttonWrapper}>
<Button
color="inherit"
variant="contained"
style={styles.button}
size="large"
onClick={this.back}
>
back
</Button>
<Button
color="primary"
variant="contained"
style={styles.button}
size="large"
onClick={this.continue}
>
Confirm
</Button>
</div>
</form>
</div>
)
}
}
It properly prints out the values that are not in an array but i dont know how to use a handleChange for the values in the array. It's not recording what i am typing at all for the dynamic arrays values.
As far as I see handleChange is triggered when there is change event on the TextField. Each TextField component represents an attribute of exercise in the exerciselist that is in your state. Now when user tries to add an exercise, you call the handleChange which sets in the state the value for exerciseName and same for other attributes. But when user clicks on a Button that adds the exercise, you are not constructing a new exercise object and adding it to your exerciselist. Instead your function addExercises below adds a default exercise object which has an auto generated uuid, and empty exerciseName, 0 numberOfSets, weight and reps. You need to pass the exerciseName, numberOfSets, weight, reps to this function that user entered on the form -> construct a new exercise object and then add that object to your exerciseList array in the state.
`
addExercises = () => {
this.setState({
exerciselist: [
...this.state.exerciselist,
{
id: uuid.v4(),
exerciseName: '',
numberOfSets: 0,
weight: 0,
reps: 0
}
]
})
}`
Hope that helps!

Categories

Resources