I looked at some of the other approaches to resolve this, but couldn't figure this out. I'm using this open source code and trying to integrate it into mine. Seems very straight forward. This is what mine looks like.
const New = ({ inputs, title }) => {
const [values, setValues] = useState({
firstName: "",lastName: "",email: "",password: "",confirmPassword: "",});
const onChange = (e) => {setValues({ ...values, [e.target.id]: e.target.value })};
const handleRegister = async (e) => {e.preventDefault()};
return (
<div className="new">
<div className="newContainer">
<Navbar />
<div className="bottom">
<div className="right">
<form onSubmit={handleRegister}>
{inputs.map((input) => (
<div className="formInput" key={input.id}>
<FormInput
{...input}
key={input.id}
onChange={onChange}
value={values[input.name]}
/>
</div>
))}
<button type="submit">Create Account</button>
</form>
</div>
</div>
</div>
</div>
);
}
I'm not sure why it won't let you type, but if you look at the code I gave you, you might be able to apply it to your own code and get it to work. Other suggestions are also valid in the comments.
const { useEffect, useState } = React;
const App = () => {
const inputs = [
{
id: "firstname",
name: "firstname"
},
{
id: "lastname",
name: "lastname"
},
{
id: "email",
name: "email"
},
{
id: "password",
name: "password"
},
{
id: "confirmpassword",
name: "confirmpassword"
}
];
const [values, setValues] = useState({
firstname: "",
lastname: "",
email: "",
password: "",
confirmpassword: ""
});
const onChange = (e) => {
setValues({ ...values, [e.target.id]: e.target.value });
};
useEffect(() => {
console.log(values);
}, [values]);
return (
<div className="new">
<div className="newContainer">
<div className="bottom">
<div className="right">
<form>
{inputs.map((input) => (
<div className="formInput" key={input.id}>
<input
type="text"
{...input}
key={input.id}
onChange={onChange}
value={values[input.name]}
/>
</div>
))}
<button type="submit">Create Account</button>
</form>
</div>
</div>
</div>
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Related
I'm doing application with friends. I'm using react-hook-form. The api gets only null values. It's a very crucial element of our application. Please help, here is my code:
const NewItemBar = ({ isVisible, handleClose }) => {
const { register, handleSubmit, watch, errors } = useForm();
const onSubmit = ({ name, data, description }) => {
api
.post(endpoints.createTask, {
name,
data,
description,
})
.then((response) => {
console.log(response);
})
.catch((err) => console.log(err));
};
return (
<StyledWrapper handleClose={handleClose} isVisible={isVisible}>
<StyledHeading big>Dodaj zadanie do wykonania</StyledHeading>
<StyledForm onSubmit={handleSubmit(onSubmit)}>
<StyledInput
placeholder="nazwa zadania"
type="text"
name="name"
{...register('name', { required: 'Required' })}
/>
<StyledInput
placeholder="data wykonania"
type="text"
name="data"
{...register('data', { required: 'Required' })}
/>
<StyledTextArea
type="text"
placeholder="opis"
name="description"
as="textarea"
{...register('description', { required: 'Required' })}
/>
<StyledButton type="submit">Zapisz</StyledButton>
</StyledForm>
</StyledWrapper>
);
};
In case of custom components you should create wrapper using Controller or pass a function with forwardRef.
https://react-hook-form.com/get-started#IntegratingwithUIlibraries
forwardRef might be useful when you want your component to manage inner state on it's own.
For example:
const PhoneInput = forwardRef(
(
{
id,
name,
label,
placeholder,
errorMsg,
onSubmit,
onChange,
onBlur,
disabled,
},
ref
) => {
const [value, setValue] = useState("");
const _onChange = (value) => {
setValue(value);
onChange(value);
};
const classes = (className) =>
[
className,
disabled ? "disabled" : "",
errorMsg ? "is-invalid" : "",
].join(" ");
console.log(value);
return (
<div className={classes("f-form-group phone-input")}>
<div className="phone-input-wrap">
<div className="inputs">
<label htmlFor={name}>{label}</label>
<NumberFormat
className={classes("f-form-control")}
name={name}
id={id}
type="tel"
format="+7 (###) ### ##-##"
mask="_"
placeholder={placeholder}
value={value}
onChange={(e) => _onChange(e.target.value)}
onBlur={onBlur}
getInputRef={ref}
/>
</div>
<button
type="button"
className="btn btn-primary"
onClick={() => onSubmit(value)}
>
Подтвердить
</button>
</div>
<div className="invalid-feedback">{errorMsg}</div>
</div>
);
}
);
I'm trying to add an extra input field if user clicks add input button and store those values as an object:
const {useState } = React;
const InviteUser = () => {
const [userDetails, setUserDetails] = useState([
{
email: "",
role: ""
}
]);
const handleCancel = () => {
setUserDetails([
{
email: "",
role: ""
}
]);
};
const handleChange = (e, index) => {
const { name, value } = e.target;
let newUserDetails = [...userDetails];
newUserDetails[index] = { ...userDetails[index], [name]: value };
setUserDetails({ newUserDetails });
};
const addInput = () => {
setUserDetails([...userDetails, { email: "", role: "" }]);
};
return (
userDetails.length >= 0 && (
<div>
{[...userDetails].map((el, index) => (
<form key={index + el} name={el}>
<div>
<input
type="email"
name="email"
required
onChange={(event) => handleChange(event, index)}
value={el.email}
style={{ marginTop: 17 }}
placeholder="Enter Email Address"
/>
</div>
<div>
<input
type="text"
name="roles"
required
value={el.role}
style={{ marginTop: 17 }}
placeholder="Role"
onChange={(event) => handleChange(event, index)}
/>
</div>
</form>
))}
<div>
<button label="Invite Another?" onClick={addInput}>
Add input
</button>
</div>
<div>
<button>Invite</button>
<button onClick={() => handleCancel()}>Cancel</button>
</div>
</div>
)
);
};
ReactDOM.render(
<InviteUser/>,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
A state is an array of objects that holds email and role:
const [userDetails, setUserDetails] = useState([
{
email: "",
role: ""
}
]);
handleChange function which updates the state by pushing an object (of users inputs values from each individual row) into an array:
const handleChange = (e, index) => {
const { name, value } = e.target;
let newUserDetails = [...userDetails];
newUserDetails[index] = { ...userDetails[index], [name]: value };
setUserDetails({ newUserDetails });
};
and addInput function adds new input fields row:
const addInput = () => {
setUserDetails([...userDetails, { email: "", role: "" }]);
};
And return JSX:
return (
userDetails.length >= 0 && (
<InputsContainer>
{[...userDetails].map((el, index) => (
<form key={index + el} name={el} onSubmit={handleSubmit}>
<EmailContainer>
<input
type="email"
name="email"
required
onChange={(event) => handleChange(event, index)}
value={el.email}
style={{ marginTop: 17 }}
placeholder="Enter Email Address"
/>
</EmailContainer>
<RolesContainer>
<input
type="text"
name="roles"
required
value={el.role}
style={{ marginTop: 17 }}
placeholder="Role"
onChange={(event) => handleChange(event, index)}
/>
</RolesContainer>
</form>
))}
<InviteButtonContainer>
<button label="Invite Another?" onClick={addInput}>
Add input
</button>
</InviteButtonContainer>
<SubmitButtonsContainer>
<button onClick={(event) => handleSubmit(event)}>Invite</button>
<button onClick={() => handleCancel()}>Cancel</button>
</SubmitButtonsContainer>
</InputsContainer>
)
);
What am I doing wrong here?
You've got two problems:
In setUserDetails you return the array wrapped in an object, instead of just returning the array.
In the roles field you take the value from el.role insted of el.roles.
In addition, since you update the current state, based on a previous one, it's better to use functional updates.
const {useState } = React;
const createNewEntry = () => ({ email: "", role: "" });
const InviteUser = () => {
const [userDetails, setUserDetails] = useState(() => [createNewEntry()]);
const handleCancel = () => {
setUserDetails([createNewEntry()]);
};
const handleChange = (e, index) => {
const { name, value } = e.target;
setUserDetails(userDetails => {
const newUserDetails = [...userDetails];
newUserDetails[index] = { ...userDetails[index], [name]: value };
return newUserDetails;
});
};
const addInput = () => {
setUserDetails([...userDetails, createNewEntry()]);
};
return (
userDetails.length >= 0 && (
<div>
{[...userDetails].map((el, index) => (
<form key={index + el} name={el}>
<div>
<input
type="email"
name="email"
required
onChange={(event) => handleChange(event, index)}
value={el.email}
style={{ marginTop: 17 }}
placeholder="Enter Email Address"
/>
</div>
<div>
<input
type="text"
name="roles"
required
value={el.roles}
style={{ marginTop: 17 }}
placeholder="Role"
onChange={(event) => handleChange(event, index)}
/>
</div>
</form>
))}
<div>
<button label="Invite Another?" onClick={addInput}>
Add input
</button>
</div>
<div>
<button>Invite</button>
<button onClick={() => handleCancel()}>Cancel</button>
</div>
</div>
)
);
};
ReactDOM.render(
<InviteUser/>,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
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
I have a form made with formik, in which one of the information entered is the contact, however the user can have more than one contact.
I want to add a button that when clicking, new fields for entering more contacts appear.
I've tried some logic, but to no avail
const { getFieldProps, touched, errors, isValid, handleSubmit } = useFormik({
initialValues: {
phone_name: '',
phone_ddd: '',
phone_number: '',
phone_observation: ''
},
onSubmit: (values, actions) => {
saveOrganization({
variables: {
name: values.phone_name,
ddd: values.phone_ddd,
number: values.phone_number,
observation: values.phone_observation,
}
})
})
return (
<TextField
margin="dense"
id="phone_name"
label={<IntlMessages id="name" />}
fullWidth
{...getFieldProps("phone_name")}
/>
{touched.phone_name && errors.phone_name ? (
<small>{errors.phone_name}</small>
) : null}
<InputMask
mask="99"
disabled={false}
maskChar=" "
{...getFieldProps("phone_ddd")}
>
{() =>
<TextField
label={<IntlMessages id='ddd' />}
fullWidth
{...getFieldProps("phone_ddd")}
/>
}
</InputMask>
{touched.phone_ddd && errors.phone_ddd ? (
<small>{errors.phone_ddd}</small>
) : null}
<InputMask
mask="999999999"
disabled={false}
maskChar=" "
{...getFieldProps("phone_number")}
>
{() =>
<TextField
label={<IntlMessages id='number' />}
fullWidth
{...getFieldProps("phone_number")}
/>
}
</InputMask>
{touched.phone_number && errors.phone_number ? (
<small>{errors.phone_number}</small>
) : null}
I want to add a button and new inputs appear
This is my approach. First I declare initialValues to have field 'contact' as array like this
useFormik({
initialValues: {
contact: [
{
name: "",
age: "",
},
],
},
});
Then I create a function for adding new field
const handleNewField = () => {
formik.setFieldValue("contact", [
...formik.values.contact,
{ name: "", age: "" },
]);
};
And in the render just map it out from array like this
<form onSubmit={formik.handleSubmit}>
{formik.values.contact.map((contact, index) => (
<div key={index}>
<label>Name</label>
<input {...formik.getFieldProps(`contact[${index}].name`)} />
<label>Age</label>
<input {...formik.getFieldProps(`contact[${index}].age`)} />
</div>
))}
<button type="submit">Submit</button>
<button type="button" onClick={handleNewField}>
New Field
</button>
</form>
I have try this and this is work just fine for me. If you have any question feel free to ask me I'll try my best to answer
Final code look like this
import React from "react";
import { useFormik } from "formik";
function App() {
const formik = useFormik({
initialValues: {
contact: [
{
name: "",
age: "",
},
],
},
onSubmit: (values) => {
console.log(values);
},
});
const handleNewField = () => {
formik.setFieldValue("contact", [
...formik.values.contact,
{ name: "", age: "" },
]);
};
return (
<div>
<form onSubmit={formik.handleSubmit}>
{formik.values.contact.map((contact, index) => (
<div key={index}>
<label>Name</label>
<input {...formik.getFieldProps(`contact[${index}].name`)} />
<label>Age</label>
<input {...formik.getFieldProps(`contact[${index}].age`)} />
</div>
))}
<button type="submit">Submit</button>
<button type="button" onClick={handleNewField}>
New Field
</button>
</form>
</div>
);
}
export default App;
This is my auth context
import { createContext } from "react";
const AuthContext = createContext();
export default AuthContext;
This is my auth reducer
import { AUTH_ERROR, LOGIN_SUCCESS } from "../types";
export default (state, action) => {
switch (action.type) {
case LOGIN_SUCCESS:
localStorage.setItem("token", action.payload.data.token);
return {
...state,
isAuthenticated: true,
userName: action.payload.data.name,
avatar: action.payload.data.avatar,
token: action.payload.data.token,
error: null,
};
case AUTH_ERROR:
localStorage.removeItem("token");
return {
...state,
isAuthenticated: false,
userName: null,
avatar: null,
error: action.payload.message,
};
default:
return state;
}
};
Here is my auth state
import React, { useReducer } from "react";
import axios from "axios";
import AuthContext from "./AuthContext";
import AuthReducer from "./AuthReducer";
import { AUTH_ERROR, LOGIN_SUCCESS } from "../types";
const AuthState = ({ children }) => {
const initialState = {
isAuthenticated: false,
userName: null,
avatar: null,
token: null,
error: null,
};
const [state, dispatch] = useReducer(AuthReducer, initialState);
const login = async (formData) => {
const header = {
headers: {
"Content-Type": "application/json",
},
};
try {
const res = await axios.post("/api/tenant/auth/login", formData, header);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data,
});
} catch (err) {
dispatch({
type: AUTH_ERROR,
payload: err.response.data,
});
}
};
return (
<AuthContext.Provider
value={{
token: state.token,
isAuthenticated: state.isAuthenticated,
userName: state.userName,
avatar: state.avatar,
error: state.error,
login,
}}
>
{children}
</AuthContext.Provider>
);
};
export default AuthState;
In my app.js I have wrapped around the components like this;
function App() {
return (
<AuthState>
<AlertState>
<SettingsState>
<Alert />
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/login" exact component={Login} />
<Route path="/dashboard" exact component={Dashboard} />
<Route path="/main-settings" exact component={MainSettings} />
</Switch>
</Router>
</SettingsState>
</AlertState>
</AuthState>
);
}
Then in my login component, I tried to use the data from the auth context provider like this;
const Login = ({ history }) => {
const { login, error, userName, isAuthenticated } = useContext(AuthContext);
const { setAlert } = useContext(AlertContext);
return (
<Formik
initialValues={{ email: "", password: "" }}
validationSchema={Yup.object({
email: Yup.string().email("Invalid email address").required("Required"),
password: Yup.string().required("Required"),
})}
onSubmit={async (values, { resetForm, setSubmitting }) => {
const { email, password } = values;
await login({
email,
password,
});
console.log(error, userName, isAuthenticated);
}}
>
{(formik) => (
<>
<div className="container">
<div className="row justify-content-center">
<div className="col-md-8 col-lg-6 col-xl-5">
<div className="card mt-5 shadow">
<div className="card-body p-4" id="error-body">
<div className="text-center mb-4">
<h4 className="text-uppercase mt-0">Sign In</h4>
</div>
<form id="login-form" onSubmit={formik.handleSubmit}>
<div className="form-group">
<label htmlFor="email" className="float-left">
Email <span className="text-danger">*</span>
</label>
<input
type="text"
className="form-control"
id="email"
placeholder=""
name="email"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
/>
{formik.touched.email && formik.errors.email ? (
<div className="text-danger">
{formik.errors.email}
</div>
) : null}
</div>
<div className="form-group">
<label htmlFor="password" className="float-left">
Password <span className="text-danger">*</span>
</label>
<input
type="password"
className="form-control"
id="password"
placeholder="Password"
name="password"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.password}
/>
{formik.touched.password && formik.errors.password ? (
<div className="text-danger">
{formik.errors.password}
</div>
) : null}
</div>
<div className="form-group">
<button
className="btn btn-primary btn-block"
type="submit"
style={{
backgroundColor: "#007bff",
borderColor: "#007bff",
}}
>
Log In
</button>
</div>
<hr />
<div className="form-group mb-3 d-flex justify-content-between">
<div className="custom-control custom-checkbox">
<input
type="checkbox"
className="custom-control-input"
id="rememberMe"
value="remember"
/>
<label
className="custom-control-label"
htmlFor="rememberMe"
>
Remember Me
</label>
</div>
<h6>Forgot Password?</h6>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</>
)}
</Formik>
);
};
After submission, I check in the developer tools and I see the context provider has been updated but whenever I use the useContext to access the data, I get the initial data in the state from the console. I think somewhere my component is not being rerendered after making the initial request and updating the state. So, on a subsequent request that is when I get the updated value. What am I doing wrong and how do I, therefore, get the updated value on my first request?
Try moving the console.log(error, userName, isAuthenticated); out from the onSubmit function and place it under this line : const { setAlert } = useContext(AlertContext); , do you now see the updated context values?