I have the component ExpenseForm
class ExpenseForm extends Component {
state = {
description: '',
amount: '',
note: '',
createdAt: moment(),
calendarFocused: false,
error: ''
};
onInputChange = (e) => {
const prop = e.target.name;
const val = e.target.value;
if(prop === 'amount') {
if(!val || val.match(/^\d{1,}(\.\d{0,2})?$/)) {
this.setState(() => ({ [prop]:val }));
}
} else {
this.setState(() => ({ [prop]:val }));
}
};
onDateChange = (createdAt) => {
if(createdAt) {
this.setState(() => ({createdAt}));
}
};
onFocusChange = ({focused}) => {
this.setState(() => ({calendarFocused: focused}))
};
onFormSubmit = (e) => {
e.preventDefault();
const { description, amount, note, createdAt } = this.state;
if(!description || !amount) {
this.setState(() => ({error: 'Please provide description and amount'}));
} else {
this.setState(() => ({error: ''}));
console.log(this.props.onSubmit) //<<< here i get undefined
this.props.onSubmit({
description,
amount: parseFloat(amount, 10) * 100,
createdAt: createdAt.valueOf(),
note
});
}
};
render() {
console.log(this.props) //<<< here I see the object with the prop onSubmit, where lies the function onEditSubmit
return (
<div>
<h1>Expense Form</h1>
{this.state.error && <p>{this.state.error}</p>}
<form onSubmit={this.onFormSubmit}>
<input
onChange={this.onInputChange}
value={this.state.description}
name="description"
type="text"
placeholder="Description"
autoFocus
/>
<input
onChange={this.onInputChange}
value={this.state.amount}
name="amount"
type="text"
placeholder="Amount" />
<SingleDatePicker
date={this.state.createdAt}
onDateChange={this.onDateChange}
focused={this.state.calendarFocused}
onFocusChange={this.onFocusChange}
numberOfMonths={1}
isOutsideRange={() => false }
/>
<textarea
onChange={this.onInputChange}
value={this.state.note}
name="note"
placeholder="Add a note for your expense (optional)"
></textarea>
<button>Add Expense</button>
</form>
</div>
)
}
}
I use this component in two places
The first one is here
function AddExpensePage({ addExpense, history }) {
const onAddSubmit = (data) => {
addExpense(data);
history.push('/');
};
return (
<div>
<h1>AddExpensePage</h1>
<ExpenseForm
onSubmit={onAddSubmit}
/>
</div>
)
}
And the second one is here
function EditPage(props) {
const onEditSubmit = () => {
console.log('edit submit')
};
return (
<div>
<h1>Edit Expense Page {props.match.params.id}</h1>
<ExpenseForm
onSumbit={onEditSubmit}
/>
</div>
)
}
In the first case everything works fine and I invoke the function transferred via props (onAddSubmit).
In the second one I get the error _this.props.onSubmit is not a function.
When I console.log the props of ExpenseForm I see in the object the function I transferred (onEditSubmit). But when I make console.log before calling this.props.onSubmit I see undefined in my console.
So I can't understand why this's happening.
I think that is just a typo:
onSumbit={onEditSubmit}
Should be
onSubmit={onEditSubmit}
Related
I am having a simple form that has firstName and lastName.
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
For this I am trying to add validation.
The validation rules are,
Both fields should accept only text
First name is required and should have at least 4 characters.
If Last name field has value, then it needs to be at least 3 characters.
Things I have tried to achieve this,
components/utils.js
export function isLettersOnly(string) {
return /^[a-zA-Z]+$/.test(string);
}
components/basic_details.js
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
return;
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
On handle input field, I am making the validation to check whether the input has value but I am unable to get the point how to catch the actual validation error and display below respective input box.
Kindly please help me to display the validation message on the respective fields.
Working example:
I suggest adding an errors property to the form data in form_context:
const [formValue, setFormValue] = useState({
basicDetails: {
firstName: '',
lastName: '',
profileSummary: '',
errors: {},
},
...
});
Add the validation to basic_details subform:
const ErrorText = ({ children }) => (
<div style={{ color: 'red' }}>{children}</div>
);
const BasicDetails = () => {
const [value, setValue] = React.useContext(FormContext);
const { basicDetails } = value;
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: 'Can have only letters.',
},
},
}));
return;
}
switch (name) {
case 'firstName': {
const error = value.length < 4 ? 'Length must be at least 4.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
case 'lastName': {
const error = value.length < 3 ? 'Length must be at least 3.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
default:
// ignore
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
return (
<>
<br />
<br />
<div className="form-group col-sm-6">
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.firstName && (
<ErrorText>{basicDetails.errors.firstName}</ErrorText>
)}
<br />
<br />
<div className="form-group col-sm-4">
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.lastName && (
<ErrorText>{basicDetails.errors.lastName}</ErrorText>
)}
<br />
</>
);
};
Lastly, check the field values and errors to set the disabled attribute on the next button in index.js. The first !(value.basicDetails.firstName && value.basicDetails.lastName) condition handles the initial/empty values state while the second condition handles the error values.
{currentPage === 1 && (
<>
<BasicDetails />
<button
disabled={
!(
value.basicDetails.firstName && value.basicDetails.lastName
) ||
Object.values(value.basicDetails.errors).filter(Boolean).length
}
onClick={next}
>
Next
</button>
</>
)}
This pattern can be repeated for the following steps.
First you must be getting converting controlled component to uncontrolled component error in your console. For controlled component it is always preferred to use state to set value for the input. And with onChange handler you set the state. I will try to put into a single component so you would get the idea and apply your case
import React, {useState} from 'react';
import {isLettersOnly} from './components/utils'; // not sure where it is in your folder structure
const MyInputComponent = ({value, ...props}) => {
const [inputValue, setInputValue] = useState(value || ''); // input value should be empty string or undefined. null will not be accepted.
const [error, setError] = useState(null);
const handleChange = event => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setError('Invalid Input');
}
setInputValue(value);
}
return (
<>
<input
value={inputValue}
onChange={handleChange}
{...props}
/>
{error && (
<span className={"error"}>{error}</span>
)}
</>
)
}
export default MyInputComponent;
This is a very rudimentary component. just to show the concept. You can then import this component as your input field and pass necessary props like name, className etc from parent.
import React from 'react';
import MyInputComponent from 'components/MyInputComponent';
const MyForm = (props) => {
return props.data && props.data.map(data=> (
<MyInputComponent
name="lastName"
className="form-control"
value={data.lastName}
));
}
Here is the code:
import React from 'react';
class EditEntry extends React.Component {
constructor(props) {
super(props);
this.state = {
entry: '',
description: '',
item: [],
error: null
};
this.handleChangeEntry = this.handleChangeEntry.bind(this);
this.handleChangeDescription = this.handleChangeDescription.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
const { match: { params } } = this.props;
const { id } = params;
fetch(`/api/entries/${id}`, {
method: 'GET'
})
.then(response => response.json())
.then(
data => {
this.setState({
item: data
});
},
error => {
this.setState({
error
});
});
}
handleChangeEntry(e) {
this.setState({ entry: e.target.value });
}
handleChangeDescription(e) {
this.setState({ description: e.target.value });
}
handleSubmit(e) {
e.preventDefault();
const { entry, description } = this.state;
const { match: { params } } = this.props;
const { id } = params;
fetch(`/api/entries/${id}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(entry, description)
})
.then(response => {
if (response.ok) {
window.location.assign("/");
} else {
throw new Error('No response.');
}
},
error => {
this.setState({
error
});
});
}
render() {
const { item } = this.state;
const itemEditForm = item.map(i => {
return <div key={i.id}>
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="entry">Entry</label>
<input
id="entry"
name="entry"
type="text"
className="form-control"
required
value={i.entry}
onChange={this.handleChangeEntry} />
</div>
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
id="description"
name="description"
type="text"
className="form-control"
required
rows="5"
value={i.description}
onChange={this.handleChangeDescription} />
</div>
<button type="submit" className="btn btn-secondary btn-sm mb-sm-2">Submit</button>
</form>
</div>
});
return (
<div>
{itemEditForm}
</div>
);
}
}
export default EditEntry;
I can't edit the data contained in:
<input id="entry" (...) />
<textarea id="description" (...) />
There is data, there is a cursor, but these fields are disabled for editing.
I fetch an array with one object from the database (componentDidMount()). I know there is another way to access the data contained in an object. But that way in this project doesn't work. Although in my another project it works. Strange.
You are setting the "entry" and "description" properties in the onChange handler methods. but using the i.entry and i.description in the value property of the input. Please try changing it and maybe if you have multiple items in the item array, you can try something like below to handle state.
Component:
<input
id="name"
name="name"
type="text"
className="form-control"
required
value={i.name}
onChange={(e) => this.handleChangeName(e, i.id)}
/>
JS:
handleChangeName(e, id) {
console.log(e.target.value);
const newState = {
item: this.state.item.map((x) =>
x.id != id ? x : { id: id, name: e.target.value }
)
};
this.setState(newState);
}
Also, please find a working example based on your code on the CodeSandbox Link
Thanks.
So I have a a component that returns a 200 code but for some reason the content does not update at all after I click the submit button. My goal is to update the 4 divs inside the form after submitting the form. The course state contains properties that contain info about each course, those properties are _id, description, estimatedTime, materialsNeeded and title.
Can someone help?
class UpdateCourse extends Component {
constructor(props) {
super(props);
this.state = {
course: []
};
this.handleSubmit = this.handleSubmit.bind(this);
}
change = e => {
this.setState({
[e.target.name]: e.target.value
});
};
handleSubmit = event => {
const {
match: { params }
} = this.props;
event.preventDefault();
const updateCourse = {
title: this.state.course.title,
description: this.state.course.description,
estimatedTime: this.state.course.estimatedTime,
materialsNeeded: this.state.course.materialsNeeded
};
axios({
method: "put",
url: `http://localhost:5000/api/courses/${params.id}`,
auth: {
username: window.localStorage.getItem("Email"),
password: window.localStorage.getItem("Password")
},
data: updateCourse
})
.then(response => {
//if the response came back as 204 then alert the user that the course was successfully updated, if another code came back then redirect them to the error handler
if (response.status === 204) {
alert("The course has been successfully updated!");
this.props.history.push("/");
} else {
throw new Error();
}
})
.catch(err => {
//use a catch method to catch the errors and display them is the status code comes back as 400
console.log("CATCH =", err.response.data.errors);
this.setState({
//if there were errors, then set the errors state in react to the error messages that came from the REST API
errors: err.response.data.errors
});
});
};
componentDidMount() {
const {
match: { params }
} = this.props;
axios
.get(`http://localhost:5000/api/courses/${params.id}`)
.then(results => {
this.setState({
course: results.data
});
});
}
render() {
return (
<div>
<div>
<form onSubmit={this.handleSubmit}>
<div>
<input
id="title"
name="title"
type="text"
className="input-title course--title--input"
placeholder="Course title..."
defaultValue={this.state.course.title}
onChange={e => this.change(e)}
/>
</div>
<div>
<textarea
id="description"
name="description"
placeholder={this.state.course.description}
defaultValue={this.state.course.description}
onChange={e => this.change(e)}
/>{" "}
</div>
<div>
<input
id="estimatedTime"
name="estimatedTime"
type="text"
className="course--time--input"
placeholder="Hours"
defaultValue={this.state.course.estimatedTime}
onChange={e => this.change(e)}
/>
</div>
<div>
<textarea
id="materialsNeeded"
name="materialsNeeded"
placeholder={this.state.course.materialsNeeded}
defaultValue={this.state.course.materialsNeeded}
onChange={e => this.change(e)}
/>
</div>
</form>
</div>
</div>
);
}
}
Please update with this:
constructor(props) {
super(props);
this.state = {
course: {}
};
this.handleSubmit = this.handleSubmit.bind(this);
this.change = this.change.bind(this);
}
change = e => {
const obj = { [e.target.name]: e.target.value };
const course = Object.assign({}, this.state.course, obj);
this.setState({
course
});
};
I have a group of checkboxes, and when each box is checked, each one can have its own lengthType and size values that are stored to state.
I have also made a sandbox: https://codesandbox.io/s/j29yp2j905
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
fields: ["action", "callee", "caller", "duration", "message"],
fieldNames: [],
size: {},
lengthType: {},
maxArrayElements: {}
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
fieldNamesChanged = newFieldNames => {
console.log("newFiledNames", newFieldNames);
this.setState({ fieldNames: newFieldNames });
};
onChange = e => {
e.persist();
const { fieldNames } = this.state;
const lastCheckedFieldName = fieldNames[fieldNames.length - 1];
this.setState(prevState => {
return {
size: {
...prevState.size,
[lastCheckedFieldName]: e.target.value
}
};
});
console.log([e.target.name]);
};
updateLengthType = e => {
e.persist();
const { fieldNames } = this.state;
const lastCheckedFieldName = fieldNames[fieldNames.length - 1];
console.log("e", e);
this.setState(prevState => {
let lengthType = { ...prevState.lengthType };
lengthType[lastCheckedFieldName] = e.target.value;
return {
lengthType
};
});
console.log(this.state.lengthType);
};
onChangeMaxArrayElements = e => {
e.persist();
const { fieldNames } = this.state;
const lastCheckedFieldName = fieldNames[fieldNames.length - 1];
this.setState(prevState => {
return {
maxArrayElements: {
...prevState.maxArrayElements,
[lastCheckedFieldName]: e.target.value
}
};
});
console.log([e.target.name]);
};
handleChange = event => {
const schema = this.state.schemas.find(
schema => schema.name === event.target.value
);
if (schema) {
axios({
method: "get",
url: `${schema.selfUri}`,
headers: { Accept: " " }
})
.then(response => {
console.log(response);
this.setState({
fields: response.data.data.fields,
selectedId: response.data.data.id
});
console.log(this.state.selectedId);
console.log(this.state.fields);
})
.catch(error => console.log(error.response));
}
};
handleSubmit = event => {
event.preventDefault();
const fields = this.state.fieldNames.map(fieldName => ({
name: fieldName,
lengthType: this.state.lengthType,
size: this.state.size,
maxArrayElements: this.state.maxArrayElements
}));
axios({
method: "post",
url: `/some/url`,
headers: {
Accept: " ",
"Content-Type": "application/json"
},
data: JSON.stringify({
name: this.state.qsName,
selectorField: this.state.selectorField,
fields: fields
})
})
.then(response => {
console.log(response);
this.setState({ querySchemaId: response.data.data.id });
})
.catch(error => console.log(error.response));
};
render() {
const { fields, fieldNames } = this.state;
const lastCheckedFieldName = fieldNames[fieldNames.length - 1];
return (
<div className="App">
<h1>Checkbox Group</h1>
<div>
<form onSubmit={this.handleSubmit}>
<fieldset>
<legend>Choose field names</legend>
<br />
<CheckboxGroup
checkboxDepth={5}
name="fieldNames"
value={this.state.fieldNames}
onChange={this.fieldNamesChanged}
>
{fields &&
fields.map(field => {
return (
<label>
<Checkbox value={field} />
{field}
</label>
);
})}
<br />
</CheckboxGroup>
<br />
{lastCheckedFieldName && (
<div>
<label>Length Type:</label>
<select
value={this.state.lengthType[lastCheckedFieldName] || ""}
onChange={this.updateLengthType}
required
>
<option value="">Select Length Type...</option>
<option value="fixed">Fixed</option>
<option value="variable">Variable</option>
</select>
</div>
)}
<br />
{lastCheckedFieldName && (
<div>
<label>Size:</label>
<input
value={this.state.size[lastCheckedFieldName] || 1}
onChange={this.onChange}
type="number"
name="size"
placeholder="1"
min="0"
required
/>
</div>
)}
<br />
{lastCheckedFieldName && (
<div>
<label>MaxArray Elements:</label>
<input
value={
this.state.maxArrayElements[lastCheckedFieldName] || 1
}
onChange={this.onChangeMaxArrayElements}
type="number"
name="maxArrayElements"
placeholder="1"
min="0"
max="100"
required
/>
</div>
)}
</fieldset>
<div className="btn-group">
<span className="input-group-btn">
<button handleSubmit={this.handleSubmit} type="submit">
Submit
</button>
<button
handleCancel={this.handleCancel}
type="reset"
onClick={() => {
alert("Clearing current field values.");
}}
>
Reset
</button>
</span>
</div>
</form>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
When I submit this form data something strange is happening with the state values being sent.
Data I’m sending has become malformed since changing a few things:
"data":
"{\"name\":\"QS7\",
\"selectorField\":\"callee\",
\"fields\":
[{\"name\":\"action\",
\"lengthType\":{\"action\":\"fixed\"},
\"size\":{\"action\":\"3\"},
\"maxArrayElements\":{\"action\":\"4\"}
}]}"
Should look like this
"data":
"{\"name\":\"QS7\",
\"selectorField\":\"callee\",
\"fields\":
[{\"name\":\"action\",
\"lengthType\":\"fixed\",
\"size\":\"3\"},
\"maxArrayElements\":\"4\"}
}]}"
field.name is being added as the first value in an array to each field element for some reason.
You store lengthType, size and maxArrayElements in a form like { fieldName: value } inside your state. So you probably forgot to pick each of them for the current fieldName inside your mapping callback in handleSubmit method. So fields should basically be mapped like this:
const fields = this.state.fieldNames.map(fieldName => ({
name: fieldName,
lengthType: this.state.lengthType[fieldName],
size: this.state.size[fieldName],
maxArrayElements: this.state.maxArrayElements[fieldName]
});
This question has come up quite a few times on here, I know, but none of the solutions have worked for me. So, I'm making a call to the iTunes API with a fetch request, based on user input in a React app. The initial call/submit works just fine. However, when I try to do a second search, I get a this.setState is not a function error as soon as I type in the input box. I've tried binding this in all the methods and in all the ways one can, but no success in getting it to work. Any ideas?
class App extends Component {
constructor(props) {
super(props);
this.state = {
query: '',
artist: null,
albums: []
};
this.handleChange = this.handleChange.bind(this);
this.getSearchResults = this.getSearchResults.bind(this);
}
handleChange(event) {
this.setState({ query: event.target.value });
}
getSearchResults() {
console.log('query!!! ' + this.state.query);
const URL_TEMPLATE = "https://itunes.apple.com/search?entity=album&limit=25&term={query}";
let url = URL_TEMPLATE.replace('{query}', this.state.query);
fetch(url)
.then((response) => {
let data = response.json();
return data;
})
.then((data) => {
console.log(data.results);
this.setState ({
albums: data.results
});
})
.catch((err) => {
console.log(err);
});
this.setState({ query: '' });
}
render() {
return (
<div className="container">
<hr />
<div className="col-lg-6">
<div className="input-group">
<input type="text"
value={this.state.query}
onChange={this.handleChange}
className="form-control" placeholder="Search for..." />
<span className="input-group-btn">
<button
onClick={() => this.getSearchResults()}
className="btn btn-default" type="button">Go
</button>
</span>
</div>
</div>
<hr />
<div>
Albums: {this.state.albums}
</div>
</div>
);
}}
edit: I should mention that it gives me the error on the handleChange() method, since that's what deals with the input.
Here is the complete working code
import React from 'react';
class TestJS extends React.Component {
constructor(props) {
super(props);
this.state = {
query: '',
artist: null,
albums: []
};
this.handleChange = this.handleChange.bind(this);
this.getSearchResults = this.getSearchResults.bind(this);
}
handleChange(event) {
this.setState({ query: event.target.value });
}
getSearchResults() {
console.log('query!!! ' + this.state.query);
const URL_TEMPLATE = "https://itunes.apple.com/search?entity=album&limit=25&term={query}";
let url = URL_TEMPLATE.replace('{query}', this.state.query);
fetch(url)
.then((response) => {
let data = response.json();
return data;
})
.then((data) => {
console.log(data.results);
this.setState({
albums: data.results
});
})
.catch((err) => {
console.log(err);
});
this.setState({ query: '' });
}
render() {
let plus5 = [];
if(!!this.state.albums && this.state.albums.length > 0){
this.state.albums.map((val, i, arr) => {
plus5.push(<div key={i}>{val.artistId}</div>);
});
}
return (
<div className="container">
<hr />
<div className="col-lg-6">
<div className="input-group">
<input type="text"
value={this.state.query}
onChange={(e) => this.handleChange(e)}
className="form-control" placeholder="Search for..." />
<span className="input-group-btn">
<button
onClick={() => this.getSearchResults()}
className="btn btn-default" type="button">Go
</button>
</span>
</div>
</div>
<hr />
<div>
{plus5}
</div>
</div>
);
}
}
export default TestJS;
I've just rendered artistId. You can render anything you want
Thanks!
Does arrow function work?
<input type="text"
value={this.state.query}
onChange={(e) => this.handleChange(e)}
className="form-control" placeholder="Search for..." />
And you cannot call setState() like this:
this.setState = {
albums: data.results
};
please change to :
this.setState({
albums: data.results
});
I think since you are inside the closure, this is not available inside
.then((data) => {
console.log(data.results);
this.setState = {
albums: data.results
};
})
Try assigning let that = this outside and before the fetch call.
And then
.then((data) => {
console.log(data.results);
that.setState = {
albums: data.results
};
})