Update another component when Formik form changes - javascript

There is a basic Formik form:
<Formik
initialValues={{ email: '', color: 'red', firstName: '' }}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
render={props => (
<form onSubmit={props.handleSubmit}>
<Field type="email" name="email" placeholder="Email" />
<div>other inputs ... </div>
<button type="submit">Submit</button>
</form>
)}
/>
When any input in it changes (not submits, but changes) - I need to update another component that is outside of <Formik />. The "outside" component should receive all form data.
Is there some way to do it without adding separate change handler for each individual input of a form? Or the solution is to try to insert "outside" component inside <Formik />?

Formik provides values object which you can make use of to get values outside.
const App = () => {
const initialValues = { email: '', color: 'red', firstName: '' }
const [formValues, setformValues] = useState(initialValues);
const getFormData = values => {
// access values here
};
return (
<div>
<h1>Formik take values outside</h1>
<Formik
initialValues={initialValues}
...
>
{props => {
setformValues(props.values); // store values in state 'formValues'
getFormData(props.values); // or use any function to get values like this
return (
<form onSubmit={props.handleSubmit}>
...
Working demo in codesandbox here

export const LoginForm: React.FC<Values> = () => {
const initialValues = { user: "", password: "" };
const [formValues, setformValues] = React.useState(initialValues);
return (
<div>{formValues.user}</div>
<Formik
initialValues={initialValues}
validationSchema={ValidationSchema}
onSubmit={(values, { setSubmitting, resetForm }) => {
setTimeout(() => {
//alert(JSON.stringify(values, null, 2));
resetForm();
setSubmitting(false);
setformValues(values);
}, 500);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
}) => {
return (
<>
<TextField
label="Usuario"
name="user"
value={values.user}
onChange={handleChange}
onBlur={handleBlur}
fullWidth
color={touched.user && errors.user ? "primary" : "secondary"}
/>
<Error touched={touched.user} message={errors.user} />
</>
<div className="pane-form__submit">
<Button
className={classes.customHoverFocus}
variant="contained"
type="submit"
disabled={isSubmitting}
label="CONTINUAR"
>Continuar</Button>
</div>
</Form>
)
}}
</Formik>
</>
);
};

Related

Formik re-renders on react state change

I'm using a custom component in formik field, and need to call a function upon text change inside function (in order to get all matching hints related to input)
<Field
id="assignees"
name="assignees"
component={({
field, // { name, value, onChange, onBlur }
form, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
...props
}) => {
return (
<input
key="assignees"
className="input-fields"
placeholder="Type names of assignees to start getting suggestions"
onChange={(e) => {
form.setFieldValue(field.name, e.target.value);
getMatching(field.value);
}}
{...props}
{...form}
// {...field}
/>
);
}}
/>
The matching function that's being called in upper code is as below. It updates a state and that state is then being showed inside span in a formik field.
const getMatching = (keyword) => {
for (const item of users) {
if (keyword !== "" && item.name.startsWith(keyword)) {
setMatching(item.name);
return;
} else {
setMatching(undefined);
}
}
};
Expected Behaviour
Formik input doesn't lose focus and retains all the input texts I added upon state change or rerender
Actual Behaviour
Input component loses focus and field is reset whenever a state is set by getMatching function
EDIT- CODE SANDBOX LINK
https://codesandbox.io/s/wizardly-merkle-w191l
A few things:
no need to store matching in state, it can be derived from looking at the assignee textbox value and the list of users
the assignee textbox (I'm calling it searchAssignee) and the list of selected assignees (assignees) are essentially two different inputs, so must be separate Formik inputs
separating the two above inputs fixes the focus issue
The whole idea of formik is that it removes the need to store input values in state. I removed the unnecessary state and separated the above inputs.
Working Example
const getMatching = (users, keyword) => {
if (!keyword) return null;
const match = users.find(({ name }) => name.startsWith(keyword));
if (!match) return null;
return match.name;
};
const FormContainer = ({ users }) => {
return (
<Formik
initialValues={{
summary: "",
description: "",
searchAssignee: "",
assignees: []
}}
onSubmit={async (values) => {
console.log(values);
}}
>
<Form>
<Row>
<Col>
<div className="textcolor">Summary</div>
<Field
id="summary"
name="summary"
placeholder="Add a short summary for your task"
component={({
field, // { name, value, onChange, onBlur }
form, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
...props
}) => {
return (
<input
className="input-fields"
{...props}
// {...form}
{...field}
/>
);
}}
/>
</Col>
</Row>
<Row>
<Col>
<div className="textcolor">Description</div>
<Field
id="description"
name="description"
placeholder="Description"
component={({
field, // { name, value, onChange, onBlur }
form, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
...props
}) => {
return (
<ReactQuill
theme="snow"
value={field.value}
onChange={field.onChange(field.name)}
style={{ minHeight: "5rem" }}
{...field}
{...props}
/>
);
}}
/>
</Col>
</Row>
<Row>
<Col>
<div className="textcolor">Assignees</div>
<Col className="assignee-wrapper d-flex">
<div className="d-flex">
<Field
id="assignees"
name="assignees"
component={({
field, // { name, value, onChange, onBlur }
form, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
...props
}) => (
<>
{field.value.map((item) => (
<div
className="assignee-tag d-flex"
onClick={() => {
const newAssignees = field.value.filter(
(val) => val !== item
);
form.setFieldValue(field.name, newAssignees);
}}
>
<div>{item}</div>
<GrFormClose />
</div>
))}
</>
)}
/>
</div>
<div className="d-flex col">
<Field
id="searchAssignee"
name="searchAssignee"
placeholder="Type names of assignees to start getting suggestions"
component={({
field, // { name, value, onChange, onBlur }
form, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
...props
}) => {
const suggestion = getMatching(users, field.value);
return (
<>
<input
className="input-fields"
{...props}
{...form}
{...field}
/>
{suggestion && (
<span
className="text-nowrap"
onClick={() => {
if (!form.values.assignees.includes(suggestion)) {
form.setFieldValue("assignees", [
...form.values.assignees,
suggestion
]);
}
}}
>
{suggestion}
</span>
)}
</>
);
}}
/>
</div>
</Col>
</Col>
</Row>
<div className="mt-2 float-end">
<button className="btn btn-dark btn-sm" type="submit">
Add
</button>
<button className="btn btn-light btn-sm ms-2" type="reset">
Cancel
</button>
</div>
</Form>
</Formik>
);
};
const App = ({ isModalVisible, setModalVisible }) => {
const [users, setUsers] = useState([
{ name: "Hello World" },
{ name: "Foo Bar" }
]);
return (
<Modal show={true}>
<Modal.Body>
<Row>
<div>
<GrFormClose
className="float-end"
onClick={() => setModalVisible(false)}
style={{ cursor: "pointer" }}
/>
</div>
<div>
<FormContainer
users={users}
/>
</div>
</Row>
</Modal.Body>
</Modal>
);
};

How to define setFieldValue in React

I'm using Formik for validating the fields before creating an entity. There is a Select which opens a dropdown and the user must choose one option from there.
I get an error saying that setFieldValue is not defined but where I've researched before I didn't find any definition of this method, it is just used like that so I don't know why is this happening.
This is my code:
import React from 'react';
import { Formik, Form, Field } from 'formik';
import { Button, Label, Grid } from 'semantic-ui-react';
import * as Yup from 'yup';
class CreateCompanyForm extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
industry: '',
};
}
onChange = (e, { name, value }) => {
this.setState({ [name]: value });
};
handleSubmit = values => {
// ...
};
render() {
const nameOptions = [
{ text: 'Services', value: 'Services' },
{ text: 'Retail', value: 'Retail' },
];
const initialValues = {
industry: '',
};
const requiredErrorMessage = 'This field is required';
const validationSchema = Yup.object({
industry: Yup.string().required(requiredErrorMessage),
});
return (
<div>
<div>
<Button type="submit" form="amazing">
Create company
</Button>
</div>
<Formik
htmlFor="amazing"
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={values => this.handleSubmit(values)}>
{({ errors, touched }) => (
<Form id="amazing">
<Grid>
<Grid.Column>
<Label>Industry</Label>
<Field
name="industry"
as={Select}
options={nameOptions}
placeholder="select an industry"
onChange={e => setFieldValue('industry', e.target.value)} // here it is
/>
<div>
{touched.industry && errors.industry
? errors.industry
: null}
</div>
</Grid.Column>
</Grid>
</Form>
)}
</Formik>
</div>
);
}
}
The error says: 'setFieldValue' is not defined no-undef - it is from ESLint. How can be this solved?
setFieldValue is accessible as one of the props in Formik.
I tried it on CodeSandbox, apparently, the first parameter in the onChange prop returns the event handler while the second one returns the value selected onChange={(e, selected) => setFieldValue("industry", selected.value) }
<Formik
htmlFor="amazing"
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={(values) => this.handleSubmit(values)}
>
{({ errors, touched, setFieldValue }) => (
//define setFieldValue
<Form id="amazing">
<Grid>
<Grid.Column>
<Label>Industry</Label>
<Field
id="industry" // remove onBlur warning
name="industry"
as={Select}
options={nameOptions}
placeholder="select an industry"
onChange={(e, selected) =>
setFieldValue("industry", selected.value)
}
/>
<div>
{touched.industry && errors.industry
? errors.industry
: null}
</div>
</Grid.Column>
</Grid>
</Form>
)}
</Formik>
try calling it using this structure
onChange = {v => formik.setFieldValue('field', v)}
<Formik
htmlFor="amazing"
initialValues={initialValues}
value={{industry:this.state.industry}}
validationSchema={validationSchema}
onSubmit={(values) => this.handleSubmit(values)}
>
{({ errors, touched }) => (
<Form id="amazing">
<Grid>
<Grid.Column>
<Label>Industry</Label>
<Field
name="industry"
as={Select}
options={nameOptions}
placeholder="select an industry"
onChange={(e) =>
this.setState({
industry: e.target.value,
})
} // here it is
/>
<div>
{touched.industry && errors.industry ? errors.industry : null}
</div>
</Grid.Column>
</Grid>
</Form>
)}
</Formik>;
Replace your onChange by onChange={this.onChange('industry', e.target.value)}
and in your constructor add this line this.onChange = this.onChange.bind(this).
your onChange methode will be:
onChange(e, { name, value }){
this.setState({ [name]: value });
}

Auto focus not working on react native formik

I am using formik and react-native-formik to create form. I am using my custom component for textinput in formik form.
My custom component code :
// import statements ...
class FormInput extends Component {
focus = () => { this.textInputField.focus(); }
render() {
const {props} = this;
return (
<TextInput
{...this.props}
placeholder={props.placeholder}
ref={(ref) => { this.textInputField = ref }}
style={{ height: 50, borderWidth: 1, margin: 5 }}
/>
)
}
}
export default FormInput
Then I create Form from below code :
const FormInput2 = compose(
handleTextInput,
withNextInputAutoFocusInput
)(FormInput);
const Form = withNextInputAutoFocusForm(View);
Then I have created original form as below :
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
keyboardShouldPersistTaps="handled"
>
<Formik
initialValues={{ firstName: '', email: '', password: '' }}
onSubmit={values => { }}
validationSchema={signUpValidationSchema}
render={({ values, handleChange, errors, setFieldTouched, touched, isValid, handleSubmit }) => (
<Form>
<FormInput2
icon="user"
placeholder="First Name"
value={values.firstName}
onChangeText={handleChange('firstName')}
onBlur={() => setFieldTouched('firstName')}
/>
{touched.firstName && errors.firstName && (
<CText style={{ fontSize: 10, color: 'red' }}>{errors.firstName}</CText>
)}
{/*
...
...
Others Fiels
*/}
<Button
title="SIGN UP"
disabled={!isValid}
color={Colors.primary}
onPress={handleSubmit}
/>
</Form>
)}
/>
</ScrollView>
But, I am getting done on each and every field keyboard return type. If I press done then form submit method fires.
Can anyone help me how can I implement auto focus ?
I have also tried exporting my custom component as below, but it is also not working :
export default withFormikControl(FormInput)
If anyone is also having the same error then here is the solution...
Custom component should have name property and the order of the component should be the same as initialValues.
Means if initialValues as below :
initialValues={{ firstName: '', lastName: '', email: '', phone: '', password: '', confirmPassword: '' }}
Then first should be firstName field then lastName, email and so on...
So I added name prop in custom component and worked autofocus.
<FormInput2
icon="user"
placeholder="First Name"
value={values.firstName}
label="First Name"
name="firstName" // added this
type="name"
onChangeText={handleChange('firstName')}
onBlur={() => setFieldTouched('firstName')}
/>
{
touched.firstName && errors.firstName && (
<CText style={{ fontSize: 10, color: 'red' }}>{errors.firstName}</CText>
)
}
<FormInput2
label="Last Name"
name="lastName" // added this
type="name"
icon="user"
placeholder="Last Name"
value={values.lastName}
onChangeText={handleChange('lastName')}
onBlur={() => setFieldTouched('lastName')}
/>
{
touched.lastName && errors.lastName && (
<CText style={{ fontSize: 10, color: 'red' }}>{errors.lastName}</CText>
)
}

Testing a login component with Jest

I'm not great at testing, and new to Jest and Enzyme. I have a Login component that consists of two TextInput components for username and password and a Button component. I am testing each component individually.
I would just like to test that a username and password was returned by onLogin.
Here is the component:
export const onLogin = (user, password) => {
console.log('User', user, 'Password', password)
return [user, password];
};
function Login() {
const [user, setUser] = useState("");
const [password, setPassword] = useState("");
return (
<LoginWrapper>
<Branding brand={brand.brandName} />
<FormWrapper onSubmit={(e) => { e.preventDefault(); onLogin(user, password) }}>
<Stack>
<TextInput
className="username"
type="text"
label="Username"
onChange={e => setUser(e.target.value)}
/>
</Stack>
<Stack>
<TextInput
className="password"
type="password"
label="Password"
onChange={e => {setPassword(e.target.value); console.log('user', user)}}
/>
</Stack>
<Stack padding="0" align="right">
<Button type="submit">Login</Button>
</Stack>
</FormWrapper>
</LoginWrapper>
);
}
export default Login;
My test:
describe("<Login />", () => {
it("renders text input correctly", () => {
const tree = renderer.create(<ThemeProvider theme={themes.default}><Login /></ThemeProvider>).toJSON();
expect(tree).toMatchSnapshot();
});
it("calls onLogin when button clicked", () => {
const onSubmitMock = jest.fn();
const component = Enzyme.mount(
<ThemeProvider theme={themes.default}><Login onSubmit={onSubmitMock} /></ThemeProvider>
);
component.find("input.username").simulate('change', { target: { value: 'myUser' } })
component.find("input.password").simulate('change', { target: { value: 'myPassword' } })
component.find("form").simulate("submit");
console.log("onClickMock.mock", onSubmitMock.mock)
expect(onSubmitMock).toBeCalled()
});
});
Results:
Expected mock function to have been called, but it was not called.
Your testing approach is correct except for:
In your test, you are mocking a callback function and passing it as a property onSubmit to your component. Then you need to call this function from your component when you submit the form.
Instead, you are calling the onLogin function on your component that does not have any
repercussion.
In order to fix this, declare the properties on your component function as a parameter, and call props.onSubmit on your form submit.
function Login(props) {
const [user, setUser] = useState("");
const [password, setPassword] = useState("");
return (
<LoginWrapper>
<Branding brand={brand.brandName} />
<FormWrapper onSubmit={(e) => { e.preventDefault(); props.onSubmit(user, password) }}>
<Stack>
<TextInput
className="username"
type="text"
label="Username"
onChange={(e) => setUser(e.target.value)}
/>
</Stack>
<Stack>
<TextInput
className="password"
type="password"
label="Password"
onChange={(e) => { setPassword(e.target.value) }}
/>
</Stack>
<Stack padding="0" align="right">
<Button type="submit">Login</Button>
</Stack>
</FormWrapper>
</LoginWrapper>
);
}

redux form data is not preserved

I have used redux-form for the form. I have developed a wizard considering an example from the documentation. The wizard is working but when i go to step 2 i.e SyncAccount component which has the form and fill that form and hit the next button to go to another step and hit the back button to go back to the SyncAccount component then the form is cleared. I have used destroyOnUnmount but it is not solving my issue either. What have i missed?
only second page i.e SyncAccount has only form. First and third do not have.
here is the code
AccountSteps.js
const mapDispatchToProps = dispatch => ({
getUser: () => dispatch(getCurrentUser()),
postWizard: (userObj, wizardType) =>
dispatch(postWizardData(userObj, wizardType)),
updateWizard: (userObj, wizardType) =>
dispatch(updateWizardData(userObj, wizardType)),
});
const mapStateToProps = state => ({
userData: state.userReducer,
wizard: state.wizardReducer,
});
class AccountSteps extends React.Component<{
user: Object,
wizard: Object,
postWizard: Function,
updateWizard: Function,
getUser: Function
}> {
constructor(props) {
super(props);
this.state = {
page: 0,
steps: [
{ steps: 0, label: 'Privacy' },
{ steps: 1, label: 'Sync Your Account' },
{ steps: 2, label: 'Install Extension' },
],
};
}
componentDidMount() {
this.props.getUser();
}
static getDerivedStateFromProps(nextProps, prevState) {
const { wizard } = nextProps;
if (!isEmpty(wizard) && wizard.page !== prevState.page) {
return {
page: wizard.page,
};
}
return null;
}
nextPage = (userObj, type) => {
this.props.postWizard(userObj, type);
};
previousPage = (wizardData) => {
console.log('wizardData', wizardData);
this.props.updateWizard(wizardData);
};
render() {
const { page, steps } = this.state;
return (
<Wrapper>
<CardWrapper>
<Stepper activeStep={page} alternativeLabel>
{steps.map(step => (
<Step key={step.steps}>
<StepLabel>{step.label}</StepLabel>
</Step>
))}
</Stepper>
{page === 0 && (
<Privacy
{...this.props}
activeStep={page}
back={this.previousPage}
next={this.nextPage}
/>
)}
{page === 1 && (
<SyncAccount
{...this.props}
activeStep={page}
back={this.previousPage}
next={this.nextPage}
/>
)}
{page === 2 && (
<Extension
{...this.props}
activeStep={page}
back={this.previousPage}
next={this.nextPage}
/>
)}
</CardWrapper>
</Wrapper>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AccountSteps);
SyncAccount.js
const SyncAccount = ({
user,
emailProvider,
handleProviderChange,
handleChange,
...props
}: {
user: Object,
emailProvider: string,
handleProviderChange: Function,
handleChange: Function
}) => {
console.log('props in Sync', user.email);
return (
<SyncWrapper>
<IconsWrapper>
<CustomSync style={{ fontSize: 30 }} color="action" />
<Mail style={{ fontSize: 50 }} color="secondary" />
</IconsWrapper>
<p>Please enter your email address which you want to sync with us.</p>
<FormWrapper>
<span>Email Provider: </span>
<RadioGroup
aria-label="emailProvider"
name="provider"
style={{ display: 'flex', flexDirection: 'row' }}
value={user.provider}
onChange={handleChange}
>
<FormControlLabel
value="google"
control={<Radio color="primary" />}
label="Google"
/>
<FormControlLabel
value="imap"
control={<Radio color="primary" />}
label="IMAP"
/>
</RadioGroup>
<StyledField
label="Email"
id="email"
name="email"
type="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
component={GTextField}
required
/>
<StyledField
label="Password"
id="password"
name="password"
type="password"
value={user.password}
onChange={handleChange}
placeholder="Password"
component={GPasswordField}
required
/>
<StyledField
label="Job title"
id="jobTitle"
name="job_title"
value={user.job_title}
onChange={handleChange}
placeholder="Job Title"
component={GAutoCompleteField}
required
/>
<Footer
{...props}
userObj={user}
type="sync"
wizardData={{ step_index: 0, wizard_name: 'privacy' }}
disabled={user && (!user.email || !user.password || !user.job_title)}
/>
</FormWrapper>
</SyncWrapper>
);
};
export default enhance(SyncAccount);
enhance.js
const requiredFields = {
email: 'Email',
password: 'Password',
job_title: 'Job Title',
};
const withReduxForm = reduxForm({
form: 'syncAccount',
fields: requiredFields,
destroyOnUnmount: false,
forceUnregisterOnUnmount: true,
validate,
});
const mapStateToProps = state => ({
wizard: state.wizardReducer,
});
const enhance = compose(
connect(
mapStateToProps,
null,
),
withReduxForm,
withState('user', 'updateUser', {
email: '',
password: '',
job_title: '',
provider: 'google',
wizard_name: 'extension',
step_index: 2,
}),
withHandlers({
handleChange: props => (ev) => {
if (ev.target) {
return props.updateUser({
...props.user,
[ev.target.name]: ev.target.value,
});
}
return props.updateUser({
...props.user,
job_title: ev.name,
});
},
}),
);
export default enhance;

Categories