action.ts:
export const fetchField = (dispatch) => {
console.log("!!!")
const Form = new Service();
Form
.getProduct()
.then((spec: Spec) => {
dispatch({
type: ACTIONS.SPEC.SHOW,
spec : specification,
});
})
.catch((err) => {});
};
appReducer:
export interface FormsState {
products: Array<Specification>
}
let initialState: FormsState = {
products: []
};
export let appReducer = (
state: FormsState = initialState,
action
) => {
switch (action.type) {
case ACTIONS.SPEC.SHOW:
return Object.assign({}, state, {
products: [...action.products],
});
default:
return state;
}
};
App.tsx:
const mapStateToProps = (state: FormsState) => {
return state;
};
const mapDispatchToProps = dispatch => {
return {
fetchField: () => fetchField(dispatch),
};
}
interface Props{
fetchField: Function;
details: Array<Specification>
}
componentDidMount() {
this.props.fetchSwaggerField();
}
render(){
<TextInput
invalidText="A valid value is required"
labelText="API title"
type = "text"
value={this.props.fetchField.length}
name="title"
/>
}
I am trying to get the value in text input from the redux api call, and expecting the value of api call in text field. Once I will be getting its value. I want to edit it value, and save its new value so that whenever i come back to the form, the new value should be retained
Here is the answer for your new issue to add/delete fields
You can use FieldArray. The FieldArray component is how you render an array of fields (ref)
A sample code with add/delete:
// renderSubFields.js
const renderSubFields = (member, index, fields) => (
<li key={index}>
<button
type="button"
title="Remove Member"
onClick={() => fields.remove(index)}
/>
<h4>Member #{index + 1}</h4>
<Field
name={`${member}.firstName`}
type="text"
component={renderField}
label="First Name"
/>
<Field
name={`${member}.lastName`}
type="text"
component={renderField}
label="Last Name"
/>
</li>
);
const renderMembers = ({ fields }) => (
<ul>
<button type="button" onClick={() => fields.push({})}>
Add Member
</button>
{fields.map(renderSubFields)}
</ul>
);
Then in your wizard page
<FieldArray name="members" component={renderMembers} />
Here is a demo: https://codesandbox.io/s/redux-form-wizard-example-m26bk?file=/WizardFormFirstPage.js:586-641
Related
For learning purpose,
I am trying prevent re-render on <InputWithLable /> component whenever i Dismiss a search result (see deploy in Full code)
I have use React.memo but it still re-render. So I think maybe its props is the culprit. I use React.useCallback to handleSearch prop, but it doesn't work.
Full code
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import React from 'react';
const API_ENDPOINT = 'https://hn.algolia.com/api/v1/search?query=';
const useSemiPersistentState = (key, initialState) => {
const [value, setValue] = React.useState(
localStorage.getItem(key) || initialState
);
React.useEffect(() => {
localStorage.setItem(key, value);
}, [value, key]);
return [value, setValue];
};
function storiesReducer(prevState, action) {
switch (action.type) {
case "SET":
return { ...prevState, data: action.data, isLoading: false, isError: false };
case "REMOVE":
return {
...prevState,
data: prevState.data.filter(
story => action.data.objectID !== story.objectID
)
}
case "ERROR":
return { ...prevState, isLoading: false, isError: true };
default:
throw new Error();
}
}
const App = () => {
const [searchTerm, setSearchTerm] = useSemiPersistentState(
'search',
'Google'
);
const [stories, dispatchStories] = React.useReducer(storiesReducer, { data: [], isLoading: true, isError: false });
const [url, setUrl] = React.useState("");
const handleFetchStories = React.useCallback(() => {
fetch(url)
.then((response) => response.json())
.then((result) => {
console.log(result);
dispatchStories({ type: "SET", data: result.hits })
})
.catch(err => dispatchStories({ type: "ERROR", data: err }))
}, [url])
React.useEffect(() => {
handleFetchStories();
}, [handleFetchStories])
const handleRemoveStory = React.useCallback(
(item) => {
dispatchStories({ type: "REMOVE", data: item });
},
[], // chi render 1 lan vi props khong thay doi
)
const handleSearch = React.useCallback(
(e) => {
setSearchTerm(e.target.value);
},
[],
)
// Chuc nang filter la cua server (vd: database)
// const searchedStories = stories.data ? stories.data.filter(story =>
// story.title.toLowerCase().includes(searchTerm.toLowerCase())
// ) : null; // nghich cai nay!
console.log('App render');
return (
<div>
<h1>My Hacker Stories</h1>
<InputWithLabel
id="search"
value={searchTerm}
isFocused
onInputChange={handleSearch}
>
<strong>Search:</strong>
</InputWithLabel>
<button onClick={() => setUrl(API_ENDPOINT + searchTerm)}>Search!</button>
<hr />
{stories.isError && <h4>ERROR!</h4>}
{stories.isLoading ? <i>Loading...</i>
: <List list={stories.data} onRemoveItem={handleRemoveStory} />}
</div>
);
};
const InputWithLabel = React.memo(
({
id,
value,
type = 'text',
onInputChange,
isFocused,
children,
}) => {
const inputRef = React.useRef();
React.useEffect(() => {
if (isFocused) {
inputRef.current.focus();
}
}, [isFocused]);
console.log('Search render')
return (
<>
<label htmlFor={id}>{children}</label>
<input
ref={inputRef}
id={id}
type={type}
value={value}
onChange={onInputChange}
/>
</>
);
}
);
// Prevent default React render mechanism: Parent rerender -> Child rerender
const List = React.memo(
({ list, onRemoveItem }) =>
console.log('List render') || list.map(item => (
<Item
key={item.objectID}
item={item}
onRemoveItem={onRemoveItem}
/>
))
);
const Item = ({ item, onRemoveItem }) => (
<div>
<span>
<a href={item.url}>{item.title}</a>
</span>
<span>{item.author}</span>
<span>{item.num_comments}</span>
<span>{item.points}</span>
<span>
<button type="button" onClick={() => onRemoveItem(item)}>
Dismiss
</button>
</span>
</div>
);
export default App;
You should not be looking at how many times a component's render function gets called; React is free to call it as many times as it likes (and indeed, in strict mode, it calls them twice to help you not make mistakes).
But to answer your question (with the actual code that uses children):
<InputWithLabel>
<strong>Search:</strong>
</InputWithLabel>
compiles down to
React.createElement(InputWithLabel, null,
React.createElement("strong", null, "Search:"))
the identity of the children prop (the <strong /> element) changes for each render of the parent component since React.createElement() returns new objects for each invocation. Since that identity changes, React.memo does nothing.
If you wanted to (but please don't), you could do
const child = React.useMemo(() => <strong>Search:</strong>);
// ...
<InputWithLabel>{child}</InputWithLabel>
but doing that for all of your markup leads to nigh-unreadable code.
I'm developing a ToDo app with react-redux toolkit.I can read/display, insert and delete successfully but not update because the UI is not displaying changes made to to-dos, even though i see the changes in Redux ToolsDev.
I think <ToDoList/> component in charge of displaying all to-dos using an array from the state is not re-rendering because is not detecting changes in the array itself since the array item (todo) is the only one updated.
Code:
toDoSlice
const initialState = {
toDos: [
{ id: nanoid(), text: "First ToDo" },
{ id: nanoid(), text: "Second ToDo" },
],
text: "",
};
const toDoSlice = createSlice({
name: "toDo",
initialState,
reducers: {
saveChanges: (state, action) => {
state.toDos[action.payload].text = state.text;
// updating other state values...
},
},
});
export const selectToDos = (state) => state.todo.toDos;
export const selectText = (state) => state.todo.text;
export const selectEditStatus = (state) => state.todo.editStatus;
export const { saveChanges } = toDoSlice.actions;
export default toDoSlice.reducer;
<ToDoList/> display todos from array
export default function ToDoList({ toDos }) {
return (
<form className="p-3" style={{ backgroundColor: "#F0E8E5" }}>
<h2>List</h2>
{toDos.map((todo) => (
<ToDo key={todo.id} {...todo} />
))}
</form>
);
}
<ToDo/> display todo using an an id and text
export default function ToDo({ id, text }) {
const dispatch = useDispatch();
return (
<>
<div className="input-group mt-3">
<input type="text" className="form-control" defaultValue={text} />
<button
type="button"
className="btn btn-warning"
onClick={() => dispatch(editToDo(id))}
>
Update
</button>
<button
type="button"
className="btn btn-danger"
onClick={() => dispatch(deleteTodo(id))}
>
Delete
</button>
</div>
</>
);
}
If you're seeing the component update in the dev tools but it's not re-rendering, then the problem is likely the way the store has been wired up to the ToDoList component.
Here's a StackBlitz showing the code all wired together - I created a simple "host" component which uses your selectToDos selector to wire up the store to the ToDoList -
function ToDoListHost() {
const toDos = useSelector(selectToDos);
return (
<ToDoList toDos={toDos} />
);
}
I also changed the ToDo component a little bit so that each update is immediately propagated to the store by hooking up the onChange event in the input field:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { editToDo, deleteToDo } from './store';
export default function ToDo({ id, text }) {
const dispatch = useDispatch();
const handleChange = event =>
dispatch(editToDo({ id, text: event.target.value }));
return (
<>
<div className="input-group mt-3">
<input
type="text"
className="form-control"
value={text}
onChange={handleChange}
/>
<button
type="button"
className="btn btn-danger"
onClick={() => dispatch(deleteToDo(id))}
>
Delete
</button>
</div>
</>
);
}
I've implemented user list and can delete users dispatching action deleteUser().
Now I add user but once I click add button the data is not mapped in the list.
this is a reducer:
case ADD_USERS:
const newId = state.users[state.users.length-1] + 1
return {
...state,
users: [
...state.users,
{
id: newId,
name: action.payload
}
],
loading: false
}
initial state consists of 2 objects and loading key.
The action function is simple:
export function addUser (name) {
return {
type: ADD_USERS,
payload: name
}
and the component is there:
const mapStateToProps = (state) => ({ users: state.users });
const mapDispatchToProps = (dispatch) => {
return {
deleteUser: id => {
dispatch(deleteUser(id))
},
addUser: name => {
dispatch(addUsers(name))
}
}
};
const Users = (props) => {
const { users } = props.users;
useEffect(() => {
getUsers();
}, []);
return (
<>
<input type='text' placeholder='name..'/>
<button onClick={() => props.addUser(name)}>add</button>
<h2>Users</h2>
{users.map((user) => {
return (
<div className="d-flex justify-content-between align-items-center mb-1">
<li>{user.name}</li>
<button onClick={() => props.deleteUser(user.id)}>x</button>
</div>
);
})}
</>
);
};
}
I consider getUsers don't work or I can be wrong. cause I map state to props and display the data inside {user.name}
I think it should work same with getUsers()
Maybe this is not the only one issue, but at least this looks strange to me:
const { users } = props.users;
Because, with the line above you are creating a constant with value from props.users.users. You have not shown how you use the Users component and what it gets from outside, but this looks at least strange to me.
<button onClick={() => props.addUser(name)}>add</button>
Your button calls addUser with a variable name, but that variable doesn't exist!
You need to change your input into a controlled component so that you can call addUser with the name from the input field.
const [name, setName] = useState("");
return (
<>
<input
type="text"
placeholder="name.."
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button onClick={() => props.addUser(name)}>add</button>
...
I have a React Form app with name and description fields.
The form data is held in a local state object using Hooks:
const [data,setData] = useState({name: '', description: ''}).
The <Form /> element creates inputs and passes their value using <Field initialValue ={data.name} />
Within the <Field /> element, this initialValue is passed to the state, which controls the input value (updated onChange):
const [value,setValue] = useState(initialValue).
But if I reset the data object (see handleResetClick function), the inputs don't clear (even though the data object clears). What am I doing wrong? I thought that changing the data would cause a re-render and re-pass initialValue, resetting the input.
Codepen example here - when I type in the inputs, the data object updates, but when I click Clear, the inputs don't empty.
function Form() {
const [data, setData] = React.useState({name: '', description: ''});
React.useEffect(() => {
console.log(data);
},[data]);
const onSubmit = (e) => {
// not relevant to example
e.preventDefault();
return;
}
const handleResetClick = () => {
console.log('reset click');
setData({name: '', description: ''})
}
const onChange = (name, value) => {
const tmpData = data;
tmpData[name] = value;
setData({
...tmpData
});
}
return (
<form onSubmit={onSubmit}>
<Field onChange={onChange} initialValue={data.name} name="name" label="Name" />
<Field onChange={onChange} initialValue={data.description} name="description" label="Description" />
<button type="submit" className="button is-link">Submit</button>
<button onClick={handleResetClick} className="button is-link is-light">Clear</button>
</form>
)
}
function Field(props) {
const {name, label, initialValue, onChange} = props;
const [value, setValue] = React.useState(initialValue);
return (
<div>
<div className="field">
<label className="label">{label}</label>
<div className="control">
<input
name={name}
className="input"
type="text"
value={value}
onChange={e => {
setValue(e.target.value)
onChange(name, e.target.value)
}}
/>
</div>
</div>
</div>
)
}
class App extends React.Component {
render() {
return (
<div className="container">
<Form />
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
)
On handleResetClick you change the data state of Form, but it doesn't affect its children.
Try adding a listener for initialValue change with useEffect:
function Field(props) {
const { name, label, initialValue, onChange } = props;
const [value, setValue] = React.useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return ...
}
You may be better off having Field as a controlled component (ie it's state is managed by the parent component rather than maintaining its own state). In this example I've swapped in value instead of initialValue and simply passed that down as props to the field. onChange then calls the parent method and updates the state there (which is automatically passed back down to the field when it renders):
const { useState, useEffect } = React;
function Form() {
const [data, setData] = React.useState({
name: '',
description: ''
});
useEffect(() => {
console.log(data);
}, [data]);
const onSubmit = (e) => {
e.preventDefault();
return;
}
const handleResetClick = () => {
setData({name: '', description: ''})
}
const onChange = (e) => {
const { target: { name, value } } = e;
setData(data => ({ ...data, [name]: value }));
}
return (
<form onSubmit={onSubmit}>
<Field onChange={onChange} value={data.name} name="name" label="Name" />
<Field onChange={onChange} value={data.description} name="description" label="Description" />
<button type="submit" className="button is-link">Submit</button>
<button onClick={handleResetClick} className="button is-link is-light">Clear</button>
</form>
)
}
function Field(props) {
const {name, label, value, onChange} = props;
return (
<div>
<div className="field">
<label className="label">{label}</label>
<div className="control">
<input
name={name}
className="input"
type="text"
value={value}
onChange={onChange}
/>
</div>
</div>
</div>
)
}
function App() {
return (
<div className="container">
<Form />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
<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="root"></div>
My Component has form input fields. These made use of a useState hook with their value and setValue for each input field. I want to optimize my component so the input fields made use of the same custom Hook which I called useFormInput
Inspired by Dan Abramov https://youtu.be/dpw9EHDh2bM see at 49:42
This works perfectly. However now I want to update the username after a new exercise is created. This is in the onSubmit method. But I'm not sure how to do this. Before I refactored I could use setUserName(), but now username is set by the generic custom hook function useFormInput
the username has an onChange method, so I thought I can maybe use this. However this uses the e.target.value because it is used for an input field.
Component:
I commented out the setUserName(''), here I want to update the username
const CreateExercise = () => {
const inputEl = useRef(null)
const username = useFormInput('')
const description = useFormInput('')
const duration = useFormInput(0)
const date = useFormInput(new Date())
const [users, setUsers] = useState([])
useEffect(() => {
axios
.get('http://localhost:5000/users/')
.then(res => {
if (res.data.length > 0) {
setUsers(res.data.map(user => user.username))
}
})
.catch(err => console.log(err))
}, [])
const onSubmit = e => {
e.preventDefault()
const exercise = {
username: username.value,
description: description.value,
duration: duration.value,
date: date.value
}
axios
.post('http://localhost:5000/exercises/add', exercise)
.then(res => console.log(res.data))
debugger
// setUsername('')
window.location = '/'
}
custom Hook useFormInput:
const useFormInput = initialValue => {
const [value, setValue] = useState(initialValue)
const handleChange = e => {
const newValue = e.target ? e.target.value : e
setValue(newValue)
}
return {
value,
onChange: handleChange
}
}
I expect the value in the state of username is updated to an empty string ' '
Complete code is on my repo on https://github.com/jeltehomminga/mern-tracker
Instead of trying to maintain more than 1 state, I'd recommend combining all state into one object. Then you can move everything into your custom hook. In addition, always make sure you handle and communicate any errors to the user.
Working example:
State as an object
hooks/useFormHandler (the API defined below is an object with functions to mimic API calls -- you'll replace this with real API calls. Also, if you wanted to make this hook reusable for other form components, then you'll need to remove the useEffect and handleSubmit functions from the custom hook and place them inside the specified functional component instead)
import { useCallback, useEffect, useState } from "react";
import API from "../../API";
// create a custom useFormHandler hook that returns initial values,
// a handleChange function to update the field values and a handleSubmit
// function to handle form submissions.
const useFormHandler = initialState => {
const [values, setValues] = useState(initialState);
// on initial load this will attempt to fetch users and set them to state
// otherwise, if it fails, it'll set an error to state.
useEffect(() => {
API.get("http://localhost:5000/users/")
.then(res => {
if (res.data.length > 0) {
setValues(prevState => ({
...prevState,
users: res.data.map(({ username }) => username)
}));
} else {
setValues(prevState => ({
...prevState,
error: "Unable to locate users."
}));
}
})
.catch(err =>
setValues(prevState => ({ ...prevState, error: err.toString() }))
);
}, []);
// the handleChange function will first deconstruct e.target.name and
// e.target.value, then in the setValues callback function, it'll
// spread out any previous state before updating the changed field via
// [name] (e.target.name) and updating it with "value" (e.target.value)
const handleChange = useCallback(
({ target: { name, value } }) =>
setValues(prevState => ({ ...prevState, error: "", [name]: value })),
[]
);
// the handleSubmit function will send a request to the API, if it
// succeeds, it'll print a message and reset the form values, otherwise,
// if it fails, it'll set an error to state.
const handleSubmit = useCallback(
e => {
e.preventDefault();
const exercise = {
username: values.username,
description: values.description,
duration: values.duration,
date: values.date
};
// if any fields are empty, display an error
const emptyFields = Object.keys(exercise).some(field => !values[field]);
if (emptyFields) {
setValues(prevState => ({
...prevState,
error: "Please fill out all fields!"
}));
return;
}
API.post("http://localhost:5000/exercises/add", exercise)
.then(res => {
alert(JSON.stringify(res.message, null, 4));
setValues(prevState => ({ ...prevState, ...initialState }));
})
.catch(err =>
setValues(prevState => ({ ...prevState, error: err.toString() }))
);
},
[initialState, setValues, values]
);
return {
handleChange,
handleSubmit,
values
};
};
export default useFormHandler;
components/CreateExerciseForm
import isEmpty from "lodash/isEmpty";
import React, { Fragment } from "react";
import { FaCalendarPlus } from "react-icons/fa";
import Spinner from "react-spinkit";
import Button from "../Button";
import Input from "../Input";
import Select from "../Select";
import useFormHandler from "../../hooks/useFormHandler";
const fields = [
{ type: "text", name: "description", placeholder: "Exercise Description" },
{ type: "number", name: "duration", placeholder: "Duration (in minutes)" },
{
type: "date",
name: "date",
placeholder: "Date"
}
];
// utilize the custom useFormHandler hook within a functional component and
// pass it an object with some initial state.
const CreateExerciseForm = () => {
const { values, handleChange, handleSubmit } = useFormHandler({
username: "",
description: "",
duration: "",
date: "",
error: ""
});
// the below will show a spinner if "values.users" hasn't been fulfilled yet
// else, it'll show the form fields. in addition, if there's ever a
// "values.error", it'll be displayed to the user.
return (
<form
style={{ width: 500, margin: "0 auto", textAlign: "center" }}
onSubmit={handleSubmit}
>
{isEmpty(values.users) ? (
<Spinner name="line-scale" />
) : (
<Fragment>
<Select
name="username"
placeholder="Select a user..."
handleChange={handleChange}
value={values.username}
selectOptions={values.users}
style={{ width: "100%" }}
/>
{fields.map(({ name, type, placeholder }) => (
<Input
key={name}
type={type}
name={name}
placeholder={placeholder}
onChange={handleChange}
value={values[name]}
/>
))}
<Button type="submit">
<FaCalendarPlus style={{ position: "relative", top: 2 }} />
Create Exercise
</Button>
</Fragment>
)}
{values.error && <p>{values.error}</p>}
</form>
);
};
export default CreateExerciseForm;
State as independent data types
Or, if you insist on using separated states, then create a resetValue function in the useFormInput hook:
const useFormInput = initialValue => {
// initialize state from "initialValue"
const [value, setValue] = useState(initialValue)
// handle changes to the "value" state via updating it
// with e.target.value
const handleChange = useCallback(({ target: { value } => {
setValue(value)
}, []);
// reset the value back to initialValue
const resetValue = useCallback(() => {
setValue(initialValue);
}, []);
return {
value,
handleChange,
resetValue
}
}
Then, destructure properties for the username (and other states, if needed):
const CreateExercise = () => {
// use ES6 destructure and aliasing to extract and rename the
// "value" (as username), "handleChange" function (as
// handleUsernameChange) and "resetValue" function (as resetUsername)
const {
value: username,
handleChange: handleUsernameChange,
resetValue: resetUsername
} = useFormInput('')
...other form state
...useEffect(() => {}, [])
const handleSubmit = useCallback(e => {
e.preventDefault();
const exercise = {
username: username,
description: description,
duration: duration,
date: date
};
axios
.post('http://localhost:5000/exercises/add', exercise)
.then(res => {
console.log(res.data)
// only reset the username if the exercise was successfully
// created
resetUsername();
})
.catch(err => console.log(err.toString());
}, [date, description, duration, resetUsername, username]);
return ( ...form )
}
I took a look and did a PR - Formik implementation w/validation.
Here is the PR - https://github.com/jeltehomminga/mern-tracker/pull/1
UI View
<>
<h3>Create New Exercise Log</h3>
<pre>{JSON.stringify({ formData }, null, 2)}</pre>
<ExerciseForm {...{ users }} onChange={data => setFormData(data)} />
</>
CreateExercise Form
import React from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import DatePicker from "react-datepicker";
import cx from "classnames";
const requiredMessage = "Required";
const exerciseFormSchema = Yup.object().shape({
username: Yup.string().required(requiredMessage),
description: Yup.string()
.min(2, "Too Short!")
.required(requiredMessage),
duration: Yup.number()
.integer()
.min(1, "Min minutes!")
.max(60, "Max minutes!")
.required(requiredMessage),
date: Yup.string().required(requiredMessage)
});
const ExerciseForm = ({ users = [], onChange }) => {
return (
<Formik
initialValues={{
username: "",
description: "",
duration: "",
date: ""
}}
validationSchema={exerciseFormSchema}
onSubmit={values => onChange(values)}
>
{({
values,
touched,
errors,
handleChange,
handleBlur,
isSubmitting,
setFieldValue
}) => {
const getProps = name => ({
name,
value: values[name],
onChange: handleChange,
onBlur: handleBlur,
className: cx("form-control", {
"is-invalid": errors[name]
})
});
return isSubmitting ? (
// Replace this with whatever you want...
<p>Thanks for the Exercise!</p>
) : (
<Form>
<FormControl label="Username">
<>
<select {...getProps("username")}>
<>
<option value="default">Select user...</option>
{users.map(person => (
<option key={person} value={person.toLowerCase()}>
{person}
</option>
))}
</>
</select>
<FormErrorMessage {...{ errors }} name="username" />
</>
</FormControl>
<FormControl label="Description">
<>
<Field {...getProps("description")} />
<FormErrorMessage {...{ errors }} name="description" />
</>
</FormControl>
<FormControl label="Duration in minutes">
<>
<Field {...getProps("duration")} type="number" />
<FormErrorMessage {...{ errors }} name="duration" />
</>
</FormControl>
<FormControl label="Date">
<>
{/* Was present before refactor */}
<div>
<DatePicker
{...getProps("date")}
selected={values.date}
minDate={new Date()}
onChange={date => setFieldValue("date", date)}
/>
<FormErrorMessage {...{ errors }} name="date" />
</div>
</>
</FormControl>
<button type="submit" className="btn btn-primary">
Create Exercise log
</button>
</Form>
);
}}
</Formik>
);
};
export default ExerciseForm;
// Created to manage label and parent className
const FormControl = ({ label, children }) => (
<div className="form-group">
<label>{label}:</label>
{children}
</div>
);
const FormErrorMessage = ({ name, errors }) => {
const error = errors && errors[name];
return error ? (
<div
class="invalid-feedback"
// Add inline style override as error message cannot sit as sibling to datePicker (bootstrap css)
style={{ display: "block" }}
>
{error}
</div>
) : null;
};