Problem with nested objects array formik and handleChange in childComponent - javascript

I have a problem passing value to handleChange during formik validation.So I created a component whose quantity is added depending on the number of numChild. And I would like when he clicks the icon he can add any number of skills to the skills table.Can someone guide me how to do it correctly? Thanks for the tips
//Parent component
state = {
numChildren: 0 //
};
addComponent = () => {
this.setState({
numChildren: this.state.numChildren + 1
});
};
render()
{
const children = []; // in loop i try created components
//in this place i try set props to child component but i do something wrong and i get error
for (var i = 0; i < this.state.numChildren; i += 1) {
children.push(<SkillComponent key={i} name1={values.skills.basicAttack.name}
name2={values.skills.specialAttack.name}
damage={values.skills.damage}
handleChange={handleChange}/>);
}
return(
<Formik
initialValues={{
name: '',
// here I try to add as many elements as the user creates skills by SkillComponent with this structure
/*
this my single skill
{
basicAttack: {
name: ''
},
specialAttack: {
name: ''
},
damage: 0
}
*/
skills: [
{
basicAttack: {
name: ''
},
specialAttack: {
name: ''
},
damage: 0
}
]
}}
validationSchema={validationSchema}
onSubmit={(values) => {
console.log("Working")
}}
/>
{({
values
handleChange,
handleBlur,
handleSubmit,
isSubmitting
}) => (
<Form onSubmit={handleSubmit}>
//some code
...
<FontAwesomeIcon
icon={faPlus}
onClick={this.addComponent}// If the user clicks I will increase the numChild
/>
{children}
</Form>
)
</Formik>
And child component
class SkillComponent extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<Form.Group controlId='formBasicEmail'>
<Form.Control
type='text'
name='name'
value={this.props.name1}
handleChage={this.props.handleChange}
/>
</Form.Group>
<Form.Group controlId='formBasicEmail'>
<Form.Control
type='text'
name='name'
value={this.props.name2}
handleChage={this.props.handleChange}
/>
</Form.Group>
<Form.Group controlId='formBasicEmail'>
<Form.Control
type='text'
name='name'
value={this.props.damage}
handleChage={this.props.handleChange}
/>
</Form.Group>
</div>
);
}
}
export default SkillComponent;
Thanks for all the help

First of all you're trying to access values variable in for loop which is not accessible there. In your case I would start with some generateChildren method which will be responsible for repeating your children components:
getChildren = (values, handleChange) =>
[...Array(this.state.numChildren)].map(num =>
<SkillComponent
key={num}
name1={values.skills.basicAttack.name}
name2={values.skills.specialAttack.name}
damage={values.skills.damage}
handleChange={handleChange}
/>
)
render() {
<Formik>
{({ values, handleChange, handleBlur, handleSubmit, isSubmitting }) => (
<Form onSubmit={handleSubmit}>
<FontAwesomeIcon icon={faPlus} onClick={this.addComponent} />
{this.getChildren(values, handleChange)}
</Form>
)}
</Formik>
}

Related

Formik field values arn't being passed from React Context

I have a Formik form that is using a progressive stepper and have multiple fields across different components, thus requiring the need to store the values in React Context. However none of the field values are being passed, so when I click submit, all values are empty strings and the validation fails. You can see on each Formik Field i am setting the value as {state.[field]}, which comes from the Context, so I believe something is going wrong here. Can anyone see what I'm doing wrong?
Thanks a lot
Here is my parent component
const AddSongPage = () => {
const { state, dispatch } = useUploadFormContext();
const initialValues = {
name: "",
};
const { mutate: handleCreateTrack } = useCreateSyncTrackMutation(
gqlClient,
{}
);
const handleSubmit = (values: any) => {
handleCreateTrack(
{
...values,
},
{
onSuccess() {
console.log("Track added succesfully");
},
}
);
};
const validate = Yup.object({
name: Yup.string().required("Song name is required"),
description: Yup.string().optional(),
});
return (
<Layout headerBg="brand.blue">
<Formik
onSubmit={(values) => handleSubmit(values)}
initialValues={initialValues}
validationSchema={validate}
>
<Form>
<Box> {state.activeStep === 1 && <Step1 />}</Box>
<Box> {state.activeStep === 2 && <Step2 />}</Box>
<Box> {state.activeStep === 3 && <Step3 />}</Box>
<Button type={"submit"}>Submit</Button>
</Form>
</Formik>
</Layout>
);
};
Here is step 1
const Step1 = () => {
const { state, dispatch } = useUploadFormContext();
const onInputChange = (e: FormEvent<HTMLInputElement>) => {
const inputName = e.currentTarget.name;
dispatch({
type: "SET_UPLOAD_FORM",
payload: {
[inputName]: e.currentTarget.value,
},
});
};
return (
<Stack spacing={4}>
<Field name={"name"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.name}
/>
</FormControl>
)}
</Field>
<Field name={"description"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.description}
/>
</FormControl>
)}
</Field>
</Stack>
);
};
export default Step1;

How to use Formik with dynamicly generated lists (e.g. via AJAX or setState)?

I have a dynamic array of form fields, whose values are fetched via REST API. On the page, there is also a dropdown, that, when changed, shows a different array of fields. I fetch all of these fields/values during the componentDidMount life cycle hook and filter the list to show the relevant data.
The Formik docs mention FieldArrays as a means to handle an array of fields. However, their example shows a static list of objects as its initialValues -- but I don't see how dynamically generated lists. In fact, since I'm fetching initialValues via AJAX, it's initially an empty array -- so nothing is rendered even after getting the data.
This is simplified version of my code:
const MyComponent = class extends Component {
componentDidMount() {
// data structure: [{Name: '', Id: '', Foo: '', Bar: ''}, ...]
axios
.get('/user')
.then((res) => {
this.setState({
userData: res.data
});
});
}
render() {
return (
<div>
<Formik
initialValues={{
users: this.state.userData
}}
render={({values}) => (
<Form>
<FieldArray
name="users"
render={arrayHelpers => (
<ul>
{
values.users.map((user, index) => {
return (
<li key={user.Id}>
<div>{user.Name}</div>
<Field name={`user[${index}].Foo`} type="text" defaultValue={user.Foo} />
<Field name={`user[${index}].Bar`} type="text" defaultValue={user.Bar} />
</li>);
}
}
</ul>
)}
/>
</Form>
)}
/>
</div>
);
}
}
You can do this via setting enableReinitialize true. According to doc it will do this:
Default is false. Control whether Formik should reset the form if initialValues changes (using deep equality).
I created complete codesanbox where your incoming data is async and when you push the data its also async. check this:
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const InviteFriends = () => {
const [initialValues, setInitialValues] = React.useState({
friends: []
});
useEffect(() => {
const initialValues = {
friends: [
{
name: "",
email: ""
}
]
};
const timer = setTimeout(() => {
setInitialValues(initialValues);
}, 1000);
return () => {
timer && clearTimeout(timer);
};
}, []);
return (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
enableReinitialize={true}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
alert(JSON.stringify(values, null, 2));
}}
>
{({ values }) => (
<Form>
<FieldArray name="friends">
{({ insert, remove, push }) => (
<div>
{console.log("Values", values, initialValues)}
{values.friends.length > 0 &&
values.friends.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`friends.${index}.name`}>Name</label>
<Field
name={`friends.${index}.name`}
placeholder="Jane Doe"
type="text"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<label htmlFor={`friends.${index}.email`}>
Email
</label>
<Field
name={`friends.${index}.email`}
placeholder="jane#acme.com"
type="email"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
>
X
</button>
</div>
</div>
))}
<button
type="button"
className="secondary"
onClick={async () => {
await new Promise((r) =>
setTimeout(() => {
push({ name: "", email: "" });
r();
}, 500)
);
}}
>
Add Friend
</button>
</div>
)}
</FieldArray>
<button type="submit">Invite</button>
</Form>
)}
</Formik>
</div>
);
};
ReactDOM.render(<InviteFriends />, document.getElementById("root"));
Here is the demo: https://codesandbox.io/s/react-formik-async-l2cc5?file=/index.js

handleSubmit with Formik using TypeScript

I have successfully written a Login component in React using plain Javascript. I would now like to convert this component to TypeScript. I have defined some types and also threw in some "any" just to initially compile. Unfortunately, the onClick parameter in my submit button throws the following error:
TS2322: Type '(e?: FormEvent | undefined) => void' is not assignable to type '(event: MouseEvent<HTMLElement, MouseEvent>)
=> void'.
Here is the relevant code:
class Login extends React.Component<LoginProps, LoginState> {
constructor(props) {
super(props);
this.login = this.login.bind(this);
}
async login(values) {
const user = {
email: values.email,
password: values.password,
};
const query = `mutation userLogin(
$user: UserLoginInputs!
) {
userLogin(
user: $user
) {
token
}
}`;
const data: any = await graphQLFetch(query, { user });
if (data && data.userLogin.token) {
const decoded: any = jwt.decode(data.userLogin.token);
const { onUserChange } = this.props;
onUserChange({ loggedIn: true, givenName: decoded.givenName });
const { history } = this.props;
history.push('/');
}
}
render() {
return (
<Formik
onSubmit={this.login}
validationSchema={loginSchema}
initialValues={{
email: '',
password: '',
}}
>
{({
handleSubmit,
handleChange,
values,
}) => (
<Card>
<Card.Body>
<Form>
<Form.Group>
<Form.Label>E-mail</Form.Label>
<Form.Control
name="email"
value={values.email}
onChange={handleChange}
/>
</Form.Group>
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
name="password"
value={values.password}
onChange={handleChange}
/>
</Form.Group>
</Form>
<Button
type="button"
variant="primary"
onClick={handleSubmit}
>
Submit
</Button>
</Card.Body>
</Card>
)}
</Formik>
);
}
}
I'm new to TypeScript and don't fully understand why an error occurs regarding e versus event when the login function does not explicitly reference either of those. Can someone please help me assign types to my handleSubmit function (aka login)?
I hope that example could help you to resolve your issue
import { Field, Form, Formik } from 'formik';
import * as React from 'react';
import './style.css';
interface MyFormValues {
firstName: string;
}
export default function App() {
const initialValues: MyFormValues = { firstName: '' };
const getSubmitHandler =
() =>
(values, formik) => {
console.log({ values, formik });
alert(JSON.stringify(values, null, 2));
formik.setSubmitting(false);
};
return (
<div>
<Formik
initialValues={initialValues}
onSubmit={getSubmitHandler()}
>
{(formik) => (
<Form>
<label htmlFor="firstName">First Name</label>
<Field id="firstName" name="firstName" placeholder="First Name" />
<button
type="button"
onClick={(event) =>
getSubmitHandler()(formik.values, formik)
}
>
Submit
</button>
</Form>
)}
</Formik>
</div>
);
}

How to pass state from child component to parent in React.JS?

I have a form that has 10+ input fields that update the state of the class. To make things look cleaner I moved all input fields with labels into a separate component so I could re-use it for each input instead. This component takes 2 parameters and serves as a child in my main class.
child component:
const Input = ({ name, placeholder }) => {
return (
<div className="wrapper">
<Row className="at_centre">
<Col sm="2" style={{ marginTop: "0.5%" }}><Form.Label>{ name }</Form.Label></Col>
<Col sm="5"><Form.Control placeholder={ placeholder }/></Col>
</Row>
</div>
)
}
parent:
state = { name: '', description: '' }
handleSubmit = (e) => {
e.preventDefault()
console.log(this.state);
}
render(){
return(
<Form style={{ marginBottom: "5%", padding: 10 }} onSubmit={ this.handleSubmit } >
<Input name="Name: " placeholder="How is it called?" onChange={ (event) => this.setState({name: event.target.value}) }/>
<Input name="Description: " placeholder="Please describe how does it look like?" onChange={ (event) => this.setState({description: event.target.value}) }/>
<Button variant="outline-success" size="lg" type="submit" >SUBMIT</Button>
</Form>
)
}
After I did that I can't find the way how to update the state from my child components when the text is changed. All my attempts to do so either crashed the website or did nothing. I am still new to React.js so any feedback is appreciated.
Pass onChange event to your child component and wire it with Form.Control control.
Your Input component will be,
const Input = ({ name, placeholder, onChange }) => {
return (
<div className="wrapper">
<Row className="at_centre">
<Col sm="2" style={{ marginTop: "0.5%" }}>
<Form.Label>{name}</Form.Label>
</Col>
<Col sm="5">
<Form.Control onChange={onChange} placeholder={placeholder} />
</Col>
</Row>
</div>
);
};
And your Parent component is,
class Parent extends React.Component {
state = { name: "", description: "" };
handleSubmit = e => {
e.preventDefault();
console.log(this.state);
};
render() {
return (
<Form
style={{ marginBottom: "5%", padding: 10 }}
onSubmit={this.handleSubmit}
>
<Input
name="Name: "
placeholder="How is it called?"
onChange={event => this.setState({ name: event.target.value })}
/>
<Input
name="Description: "
placeholder="Please describe how does it look like?"
onChange={event => this.setState({ description: event.target.value })}
/>
<Button variant="outline-success" size="lg" type="submit">
SUBMIT
</Button>
</Form>
);
}
}
Working Codesandbox here.
In React, properties flow from the parent component to the child component, so you cannot directly "pass" the state from the child to the parent.
What you can do however is to have the parent pass a callback function to the child that will be called to update the parent's state.
Here is an example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
}
updateName(name) {
if (name === this.state.name) return;
this.setState({ name });
}
render() {
return (
<div>
<p>The name is {this.state.name}</p>
<ChildComponent handleNameUpdate={name => this.updateName(name)} />
</div>
);
}
}
class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
}
handleInputChange(e) {
this.setState({ name: e.target.value });
this.props.handleNameUpdate(e.target.value)
}
render() {
return <input type="text" value={this.state.name} onChange={e => this.handleInputChange(e)} />;
}
}
You have to build what is known as a controlled component.
const Input = ({ label, name, onChange, placeholder }) => (
<div className="wrapper">
<Row className="at_centre">
<Col sm="2" style={{ marginTop: "0.5%" }}>
<Form.Label>{ label }</Form.Label></Col>
<Col sm="5">
<Form.Control name={ name }
value={ value }
placeholder={ placeholder }
onChange={ onChange }
/>
</Col>
</Row>
</div>
)
And in your parent,
state = { name: '', description: '' }
handleChange = ({ target: { name, value } }) => this.setState({ [name]: value })
render() {
const { name, description } = this.state
<Form style={{ marginBottom: "5%", padding: 10 }} onSubmit={ this.handleSubmit } >
<Input label="Name: " name="name" value={name} onChange={handleChange}/>
<Input label="Description: " description="description" value={description} onChange={handleChange}/>
<Button variant="outline-success" size="lg" type="submit" >SUBMIT</Button>
</Form>
}
Advice
Try to avoid manufacturing lambda methods inside the render function as much as possible and have a class property as a lambda method so that lambdas do not need to be manufactured on every render cycle.

React - TypeError: _this.setState is not a function

I am playing with React and trying to save the text that user type to the input to the state. I have added to the textarea an onChange attribute for setting the state.
However, when I start typing, I see error in the console stating TypeError: _this.setState is not a function.
I've tried different ways of trying to fix it, but still don't have it.
const NewItemForm = props => (
<Form onSubmit={props.send_form}>
<Form.Group>
<TextArea
placeholder='Name your first item here'
name='item_msg'
onChange={e => this.setState({ item_msg: e.target.value })} />
<Form.Button primary content='Create Item' />
</Form.Group>
</Form>
)
class App extends Component {
constructor () {
super();
this.state = {
item_msg: ''
}
}
handleSubmit(e){
e.preventDefault();
console.log(this.state.item_msg);
}
render() {
return (
<div className="App">
<MainHeaderr />
<Container>
<NewItemForm send_form={this.handleSubmit.bind(this)} />
</Container>
</div>
);
}
}
export default App;
Functional components do not have lifecycle methods and... state :)
const NewItemForm = props => (
<Form onSubmit={props.send_form}>
<Form.Group>
<TextArea
placeholder='Name your first item here'
name='item_msg'
onChange={e => this.setState({ item_msg: e.target.value })} />
<Form.Button primary content='Create Item' />
</Form.Group>
</Form>
)
This won't work:
onChange={e => this.setState({ item_msg: e.target.value })} />
What you need is to pass callback:
const NewItemForm = props => (
<Form onSubmit={props.send_form}>
<Form.Group>
<TextArea
placeholder='Name your first item here'
name='item_msg'
onChange={props.onInputChange} />
<Form.Button primary content='Create Item' />
</Form.Group>
</Form>
)
class App extends Component {
constructor () {
super();
this.state = {
item_msg: ''
}
this.handleSubmit = this.handleSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
handleSubmit(e){
e.preventDefault();
console.log(this.state.item_msg);
}
handleInputChange(e) {
this.setState({ item_msg: e.target.value })
}
render() {
return (
<div className="App">
<MainHeaderr />
<Container>
<NewItemForm send_form={this.handleSubmit} onInputChange={this.handleInputChange} />
</Container>
</div>
);
}
}
I get where you are coming from, but NewItemForm will get transpiled to React Element so this will reference that Element, not the App component.
React without JSX
Functional components are stateless so you can't call setState within them. You can pass a callback from your parent component that sets state in the parent component as follows:
handleChange = e => this.setState({ item_msg: e.target.value });
<NewItemForm onChange={this.handleChange} />
And then in your NewItemForm component:
<TextArea
placeholder='Name your first item here'
name='item_msg'
onChange={props.onChange}
/>
NewItemForm is function component and function comopent does not have lifecycle method use class component.
You need to either use arrow function or bind the function in constructor like below
constructor(props) {
super(props);
this.state = { date: new Date() };
this.tick = this.tick.bind(this);
}
setInterval(()=>this.tick, 1000);
or Use arrow function
setInterval(()=>this.setState({
date: new Date(),
}), 1000);

Categories

Resources