fellow Stackers! I probably have a simple question, but can't seem to find the answer...
What I want to achieve:
I have this kind of commenting logic. Basically, when a person comments without any status change a button calls postComment and all works fine. Now when a user comments & selects to change status it presses on Menu.Item (ref antd) which would send the element key for me to grab and work around some logic.
const onMenuClick = (e) => {
postComment(e);
};
<Menu onClick={onMenuClick}>
<Menu.Item key="Done">
<div>
<Row>
<Col md={2}>
<CheckCircleOutlined style={{ color: "limegreen", fontSize: '1.5em' }} className='mt-2 mr-2' />
</Col>
<Col md={20} className="ml-2">
<Row md={24}><Col className="font-weight-semibold"> Comment & Done</Col></Row>
<Row md={24}><Col className="text-muted">Comment & calculation status is done.</Col></Row>
</Col>
</Row>
</div>
</Menu.Item>
Above code does work, but for certain reasons (or my lack of knowledge) but It will jump over the usual HTML submit rules and now the function will only check for validation inside.
So what do I want? I want to use the Menu something like this:
<Menu onClick={(e) => formComment.submit(e)}>
This would submit a form and pass the Menu.Item key which I could use.
My postComment function:
const postComment = async (e) => {
console.log(e);
setCommentsLoading(true)
const formData = new FormData();
const { id } = props.match.params;
let comment;
formComment.validateFields().then(async (values) => {
//Upload and set Documents for post. This fires before validation, which is not ideal.
if (fileList.length > 0) {
fileList.forEach(file => {
formData.append('files', file);
});
await commentService.upload(formData).then((res) => { //Await only works after Async, so cant put it below validateFields()
res.data.forEach((element) => {
documents.push(element.id);
setDocuments(documents)
});
}).catch(error => {
message.error(error);
}).finally(() => {
setDocuments(documents)
}
)
} //This basically uploads and then does everything else.
console.log(values.comment)
//Checks if status should be changed.
if (e.key !== undefined) {
let variable = commentsVariable + 'Status';
let put = {
[variable]: e.key,
};
if (fileList.length <= 0 && e.key === "Done") {
message.error('Should have attachments.')
mainService.get(id).then(res => {
})
setCommentsLoading(false)
return
} else {
//No idea how to update status in the view.js, needs some sort of a trigger/callback.
mainService.put(put, id).then(res => { })
.catch(err => {
console.log(err)
return
})
}
}
if(values.comment === undefined) {
comment = `This was aut-generated comment.`;
} else {
comment = values.comment;
}
let post = {
comment: comment,
[commentsVariable]: id,
attachments: documents,
user: random(1, 4),
};
commentService.post(post).then((res) => {
mainService.get(id).then(res => {
setComments(res.data.data.attributes.comments.data)
message.success('Comment successfully posted!')
formComment.resetFields()
setFileList([])
setDocuments([])
})
});
}).catch(error => {
error.errorFields.forEach(item => {
message.error(item.errors)
setFileList([])
setDocuments([])
})
})
setCommentsLoading(false);
};
My form looks like this (I won't include Form.Items).
<Form
name="commentForm"
layout={"vertical"}
form={formComment}
onFinish={(e) => postComment(e)}
className="ant-advanced-search-form">
So in the end, I just want a proper HTML rule check before the function fires, no matter if I press the "Comment" button or press the ones with the status update.
So I kind of worked around it. Basically I just added a hidden field:
<Form.Item name="key" className={"mb-1"} hidden>
<Input/>
</Form.Item>
Then on Menu i addeda function that has onClick trigger, which sets the value to whatever key is there and submits:
const onMenuClick = (e) => {
console.log(e.key)
formComment.setFieldsValue({
key: String(e.key)
})
formComment.submit()
};
I'm pretty sure this is a dirty workaround, but hey, it works for a Junior dev :D Either way, I'll try to test it without hidden field where it just adds a new value.
Related
I am trying to create the functionality, where an API fetches data and displays them in a list. When the user types in the input, the data that matches each key stroke is then displayed. The functionality works, but here is the bug I am running into: I only want to initially display 3 items from the api, then when the user clicks the "view more" button, it displays the entire list. However, if the user doesn't press the "view more" button and they type in the input, it only filters through the 3 items that are initially displayed, and doesn't filter through the entire api data.
const [apiData, setApiData] = useState([]);
const [viewAllRoles, setViewAllRoles] = useState(false);
const [filterData, setFilterData] = useState("");
const searchValue = useRef("");
useEffect(() => {
axios
.get("https://advisoryalpha.github.io/skill-assessments/identities.json")
.then((res) => {
setApiData(res.data);
})
.catch((error) => {
alert(error.message);
});
}, []);
return (
const viewRolesHandler = () => {
setViewAllRoles(!viewAllRoles);
};
<>
<input
type="text"
ref={searchValue}
value={filterData}
onChange={(e) => setFilterData(e.target.value)}
>
</input>
<ul>
<ShowDataLength
apiData={apiData}
filterData={filterData}
viewAllRoles={viewAllRoles}
/>
<button
id="view-all__btn"
style={{ marginTop: "10px", marginBottom: "5px" }}
onClick={viewRolesHandler}
>
{!viewAllRoles ? "View all" : "View less"}
</button>
</ul>
)
</>
ShowDataLength component:
const ShowDataLength = ({
apiData,
filterData,
setName,
setRole,
viewAllRoles,
}) => {
function dataHandler() {
return !viewAllRoles ? apiData.slice(0, 3) : apiData;
}
return dataHandler()
.filter(
(el) =>
el.name.toLowerCase().includes(filterData.toLowerCase()) ||
filterData === ""
)
.map((el, index) => (
<li
key={index}
onClick={() => {
setName(el.name);
setRole(el.role);
}}
>
<h4>{el.name}</h4>
<h5>{el.role}</h5>
</li>
));
};
hope you're well. I think you not get filtered items. So first make some changes in input.
onChange={(e) => setFilterData(e.target.value.toLowerCase())}
As this helps you to change lowercase during input.
Next make axios response easy like (advice, if you don't use it then no worry)-
const respones = await axios...(here is your code to fetch api)..
this will save data in response.
response&&response?.filter((search)=>(search.includes(FilterData)))
this code first check is there any data in response. if data inside response then it will apply filter. above response is data from api. you can change with your component that shows api data or for data for filtration.
To make above code more accurate then,
response&& response?.filter((filter)=>{
if(filterData.length>0){
return filter?.includes(filterData)
}else{
return filter
}
})
This code will always send data else it's available or not.
Why I add toLowerCase() in input? because ReactJs behave annoying when using tolowercase in filter that's why i prefer to use tolowercase when sending data to state.
If you still face any issue letme i will help you.
Thanks
This is because you are returning a filter from your dataHandler function
function dataHandler() {
return !viewAllRoles ? apiData.slice(0, 3) : apiData;
}
// since viewAllRoles is false, it only grabs the first 3 from your list and filters them
return dataHandler()
.filter(
(el) =>
el.name.toLowerCase().includes(filterData.toLowerCase()) ||
filterData === ""
)
Instead, you should be able to just modify your dataHandler function and have the results you expect
function dataHandler() {
// If the user has typed a query, return all elements to filter through all
if(filterData) return apiData;
// Otherwise, return 3
return !viewAllRoles ? apiData.slice(0, 3) : apiData;
}
I am trying to create a real time search and real time populating users.
Here is how my page look like:
Right now my search function is I have to click search icon to make it appear the result so I want to change that to real time search. When I type the name in input it will auto starting appear the user card for me
Here is my code in SearchForMembers.js:
const SearchForMembers = ({ teamId, fetchTeamData }) => {
// State
const [userResults, setUserResults] = useState([]);
const [userSkillResults, setUsersSkillsResults] = useState([]);
const [showResultsList, setShowResultsList] = useState(false);
const [showResultsMsg, toggleResultsMsg] = useState(false);
// Submit search query
const searchForTerm = async (term) => {
try {
if (term !== undefined) {
// Clear results
setUserResults([]);
setUsersSkillsResults([]);
// Perform search
const res = await axios.get(`/api/v1/search/users/${term}`);
// Check response
// Show message if no results were found
if (res.data[0].length === 0 && res.data[1].length === 0) {
toggleResultsMsg(true);
} else {
toggleResultsMsg(false);
// Set users results
setUserResults(res.data[0]);
// Set skills results
setUsersSkillsResults(res.data[1]);
}
}
} catch (err) {
throw new Error(err);
}
};
useEffect(() => {
if (userResults.length > 0 || userSkillResults.length > 0) {
setShowResultsList(true);
} else {
setShowResultsList(false);
}
}, [userResults, userSkillResults]);
return (
<div className="container--search_for_members">
{/* Search bar */}
<SearchBar
linkToPage={false}
searchForTerm={searchForTerm}
placeHolderText="Search for a name, skill or affiliation"
/>
{/* Results list */}
{showResultsList && (
<SearchForMembersResults
usersFound={userResults}
userSkillResults={userSkillResults}
teamId={teamId}
fetchTeamData={fetchTeamData}
/>
)}
{showResultsMsg && (
<>
<p className="text--message-small">No results were found</p>
<AddNewProfile
teamId={teamId}
fetchTeamData={fetchTeamData}
variant="not in table"
/>
</>
)}
</div>
);
};
And here is my code in SearchBar,js :
const SearchBar = ({
linktoPage,
searchForTerm,
placeHolderText = "Search members or teams",
}) => {
// Search state
const [searchTerm, setSearchTerm] = useState("");
const handleChange = (event) => {
// Update state with input text
event.preventDefault();
setSearchTerm(event.target.value);
};
const handleSubmit = async (event) => {
try {
event.preventDefault();
if (linktoPage) {
// Go to page and pass query
goToPage();
} else {
// Don't change pages, but pass term to search method
searchForTerm(searchTerm);
}
} catch (err) {
throw new Error(err);
}
};
const goToPage = () => {
// Go to search page and pass along the search term.
Router.push({
pathname: "/search",
query: { term: `${searchTerm}` },
});
};
return (
<form className="form--search_wrapper" method="GET" onSubmit={handleSubmit}>
<input
className="input input--search_input"
type="search"
name="q"
placeholder={placeHolderText}
aria-label="Search bar"
onInput={handleChange}
pattern="^[a-zA-Z0-9 ]+"
required
/>
<Button className="input input--search" style={{ color: "white", backgroundColor: "#00B790" }} type="submit" >
<SearchRoundedIcon />
</Button>
</form>
);
};
I read about the live search with axios. Here is the link: https://www.digitalocean.com/community/tutorials/react-live-search-with-axios
How can I use .filter in my code ?
You can directly call the body of your handleSubmit function in the handleChange function, you probably want to debounce it so you don't call your api for every keystroke though
const handleChange = (event) => {
// Update state with input text
event.preventDefault();
setSearchTerm(event.target.value);
try {
if (linktoPage) {
// Go to page and pass query
goToPage();
} else {
// Don't change pages, but pass term to search method
searchForTerm(event.target.value);
}
} catch (err) {
throw new Error(err);
}
};
I have form validation set up on the backend. I want to display this validation to the user on the front end. I have a component that contains a popup modal. Inside that component, I am trying to catch errors inside my useEffect and my onSubmit function. And am trying to display those errors on the modal when the user tries to submit an incorrect value, for instance the user enters a password that isn't long enough.
What I am doing now, is not even catching the errors nor displaying them. If I leave an input blank it should show that error message. I am currently displaying errors via the UI from the UpdateValidation(). That will be removed once I am able to display the errors via the backend.
function EditModal({ close, companyId, employeeId }) {
const { formValidation, formData, setFormData, firstNameError, lastNameError, emailError } = UpdateValidation();
const isVisible = !!userId;
const [errors, setErrors] = useState([])
const handleChange = (e: any) => {
setFormData({
...formData,
[e.target.name]: e.target.value.trim(),
});
};
useEffect(
() => {
if (employeeId !== null) {
axios.get(`${process.env.PUBLIC_URL}/api/company/${companyId}/employees/${employeeId}`)
.then(res => {
setFormData(res.data);
})
.catch(error => {
if (error.response) {
setErrors(error.response.data.errors);
}
})
}
}, [employeeId, setFormData, companyId])
const onSubmit = async (event: any) => {
event.preventDefault();
console.log(formData);
const isValid = formValidation();
if (isValid) {
await axios.put(`${process.env.PUBLIC_URL}/api/company/${companyId}/employees/${employeesId}`, formData)
.then((res) => {
setFormData(res.data);
})
.catch((error) => {
debugger;
if (error.response) {
setErrors(error.response.data.errors);
}
});
close();
}
};
Removing some of this for context, this is part of my modal
return (
<SprkModal
title="Update user:"
isVisible={isVisible}
confirmText="Submit"
cancelText="Cancel"
cancelClick={() => {
close();
}}
confirmClick={onSubmit}
>
<div className="modal">
{errors.length > 0 && (
<div className="errors">
<p>{errors}</p>
</div>
)}
<label>First Name: </label>
I am a new developer so any help will greatly be appreciated. I am not sure what I am doing wrong. I know the backend is set up correctly as I tested it with postman.
EDITOne reason this wasn't working was because of my if(isValid). Since my catch was within that block it wasn't getting hit because it wasn't valid so it would never hit the catch. I have since removed the isValid and am using a try catch.
try {
await axios.put(`${process.env.PUBLIC_URL}/api/company/${companyId}/employees`, formData)
.then((res) => {
setFormData(res.data);
close();
})
}
catch (error) {
debugger;
if (error.response) {
setErrors(error.response.data.errors);
console.log(error.response.status);
}
}
I can now see the errors in the console. But I am still not showing the errors on the UI. I thought that this setErrors(error.response.data.errors); would set the errors and I could retrieve them in the UI by doing this
<div className="modal">
{
errors.length > 0 && (
<div className="errors">
<p>{errors}</p>
</div>
)}
<label>First Name: </label>
But it doesn't hit the errors.length....I tried to console.log(errors) and got an empty array. When I check the react components I do see that the state is being set. I'm not sure how to display it on the UI.
Have you tried logging response.status in the console?
example:
if(response) {
console.log(response.status)
}
This doesn't solve your technical problem but might solve the actual problem which amounts to "how can i give feedback to my users". Are you using any view libraries?
Most libraries have components or functions that will do that for you.
In Ant Design they call it a 'message' -> https://ant.design/components/message/
In Material Ui they call it a 'snackbar' -> https://material-ui.com/components/snackbars/
Had to change this
{
errors.length > 0 && (
<div className="errors">
<p>{errors}</p>
</div>
)}
To this:
{Object.entries(errors).length > 0 &&
Object.entries(errors).map(([key, value]) => {
return <div key={value[0]}>{value[0]}</div>;
})}
And now it is working!
I am trying to get form steps to be easily navigable. Users must be allowed to go back by clicking on previous form steps, and go forward if target step and everything in between is filled in and valid.
I got it somewhat working by using something like this but the problem with this one is that validateFields() will only check current step's form. So I can fill in, let's say the first step and jump forward 8 steps because validateFields() only validates the current one and thinks everything is all good.
form.validateFields()
.then(() => {
setCurrentStep(step);
})
.catch((err) => {
console.log(err);
return;
});
So I am trying to get validateFields() to accept an array which contains every form field name that needs to be checked. However, I could not find any documentation for such implementation and the one below always results in resolve.
const handleStepChange = (step) => {
// let user go back to previous steps
if (step <= currentStep) {
setCurrentStep(step);
return;
}
// let user go forward if everything is valid
// if not, force user to use "next step" button
form.validateFields(FORM_FIELDS) // FORM_FIELDS is an array that keeps field names
.then(() => {
setCurrentStep(step);
})
.catch((err) => {
console.log(err);
return;
});
};
And this is how I roughly put together everything:
const [form] = Form.useForm();
const [currentStep, setCurrentStep] = useState(0);
const initialSteps = {
form,
step: (val) => setCurrentStep(val),
nextStep: () => setCurrentStep((prev) => prev + 1),
};
const [steps] = useState([
{
title: 'Personal',
content: <PersonalForm {...initialSteps} />,
},
{
title: 'Details',
content: <DetailsForm {...initialSteps} />,
},
{
title: 'Contact',
content: <ContactForm {...initialSteps} />,
}])
return <Steps
size="small"
current={currentStep}
direction="vertical"
onChange={(current) => handleStepChange(current)}
>
{steps.map((item, index) => (
<Step status={item.status} key={item.title} title={item.title} />
))}
</Steps>
How can I validate make sure to validate each and every form field be it unmounted, unfilled, untouched etc?
Edit:
I also tried tapping in to each step's form obj individually, expecting that each form obj would hold it's own form fields but that did not work either
const handleStepChange = (step) => {
// let user go back to previous steps
if (step <= currentStep) {
setCurrentStep(step);
return;
}
// let user go forward if target step and steps in between are filled in
const validationFields = [];
const stepsInbetween = [];
for (let i = 0; i < step - currentStep; i++) {
const stepCursor = steps[currentStep + i];
stepsInbetween.push(stepCursor);
const stepCursorFields = STEP_FIELDS[stepCursor.content.type.name];
validationFields.push(...stepCursorFields);
}
let isValid = true;
stepsInbetween.forEach((s) => {
s.content.props.form
.validateFields()
.then(() => {})
.catch(() => (isValid = false));
});
if (isValid) setCurrentStep(step);
};
You should use the Form element instead of putting together each field. Ant Design's Form already has built-in data collection, verification and performs validation on every field whether they're touched or not.
This is a skeleton form that implements Form. You will want to wrap each field with Form.Item and then in the Form.Item pass in an object as rules with required=True being one of the entry. Sounds more complicated than it should so here's a snippet:
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item
name="note"
label="Note"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="gender"
label="Gender"
rules={[
{
required: true,
},
]}
>
<Select
placeholder="Select a option and change input text above"
onChange={onGenderChange}
allowClear
>
<Option value="male">male</Option>
<Option value="female">female</Option>
<Option value="other">other</Option>
</Select>
</Form.Item>
Any field wrapped by <Form.Item /> with required: true in its rule will be checked and validated. You can also use set up rules to be more complex depending on each field's requirement. An example:
<Form.Item name={['user', 'age']} label="Age" rules={[{ type: 'number', min: 0, max: 99 }]}>
<InputNumber />
</Form.Item>
<Form.Item name={['user', 'email']} label="Email" rules={[{ type: 'email' }]}>
<Input />
</Form.Item>
From its documentation,
Form will collect and validate form data automatically.
So you will save yourself a ton of custom code just by relying on the Form component to handle validation for you based on rules you specify on each Form.Item.
EDIT 1
Based on additional information from the comments, since you've mentioned you already use <Form.Item>, this would help enforce the validation is run when user navigate to other pages through the useEffect() hook. If currentStep is updated, which it is (through setCurrentStep), then run the code within the useEffect() body.
const MultiStepForm = () => {
const [form] = Form.useForm();
const [currentStep, setCurrentStep] = useState(1);
useEffect(() => {
form.validateFields()
.then(() => {
// do whatever you need to
})
.catch((err) => {
console.log(err);
});
}, [currentStep]);
const handleStepChange = (step) => {
// other operations here
...
setCurrentStep(step);
};
...
}
This is because the validateFields method of the form object only validates the fields that are currently rendered in the DOM.
The following is a workaround:
Render them all but only make the selected one visible
const formList: any = [
<FirstPartOfForm isVisible={currentStep === 0} />,
<SecondPartOfForm isVisible={currentStep === 1} />,
<ThirdPartOfForm isVisible={currentStep === 2} />
];
const formElements = formList.map((Component: any, index: number) => {
return <div key={index}>{Component}</div>;
});
...
<Form ...props>
<div>{formElements}</div>
</Form>
Then in the components that are the parts of your form:
<div style={{ visibility: isVisible ? "visible" : "hidden", height: isVisible ? "auto" : 0 }}>
<Form.Item>...</Form.Item>
<Form.Item>...</Form.Item>
</div>
I'm using React 16.13 and Bootstrap 4. I have the following form container ...
const FormContainer = (props) => {
...
const handleFormSubmit = (e) => {
e.preventDefault();
CoopService.save(coop, setErrors, function(data) {
const result = data;
history.push({
pathname: "/" + result.id + "/people",
state: { coop: result, message: "Success" },
});
window.scrollTo(0, 0);
});
};
return (
<div>
<form className="container-fluid" onSubmit={handleFormSubmit}>
<FormGroup controlId="formBasicText">
...
{/* Web site of the cooperative */}
<Button
action={handleFormSubmit}
type={"primary"}
title={"Submit"}
style={buttonStyle}
/>{" "}
{/*Submit */}
</FormGroup>
</form>
</div>
);
Is there a standard way to disable the submit button to prevent a duplicate form submission? The catch is if there are errors in the form that come back from the server, I would like the button to be enabled again. Below is the "CoopService.save" I referenced above ...
...
save(coop, setErrors, callback) {
// Make a copy of the object in order to remove unneeded properties
coop.addresses[0].raw = coop.addresses[0].formatted;
const NC = JSON.parse(JSON.stringify(coop));
delete NC.addresses[0].country;
const body = JSON.stringify(NC);
const url = coop.id
? REACT_APP_PROXY + "/coops/" + coop.id + "/"
: REACT_APP_PROXY + "/coops/";
const method = coop.id ? "PUT" : "POST";
fetch(url, {
method: method,
body: body,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw response;
}
})
.then((data) => {
callback(data);
})
.catch((err) => {
console.log("errors ...");
err.text().then((errorMessage) => {
console.log(JSON.parse(errorMessage));
setErrors(JSON.parse(errorMessage));
});
});
}
Not sure if it's relevant, but here is my Button component. Willing to change it or the above to help with implementing a standard, out-of-the-box way to solve this.
import React from "react";
const Button = (props) => {
return (
<button
style={props.style}
className={
props.type === "primary" ? "btn btn-primary" : "btn btn-secondary"
}
onClick={props.action}
>
{props.title}
</button>
);
};
export default Button;
Greg already mentioned this link to show you how you can use component state to store whether or not the button is disabled.
However the latest version of React uses functional components with hooks rather than this.state or this.setState(...). Here's how you might go about that:
import { useState } from 'react';
const FormContainer = (props) => {
...
const [buttonDisabled, setButtonDisabled] = useState(false);
...
const handleFormSubmit = (e) => {
setButtonDisabled(true); // <-- disable the button here
e.preventDefault();
CoopService.save(coop, (errors) => {setButtonDisabled(false); setErrors(errors);}, function(data) {
const result = data;
history.push({
pathname: "/" + result.id + "/people",
state: { coop: result, message: "Success" },
});
window.scrollTo(0, 0);
});
};
return (
...
<Button
action={handleFormSubmit}
disabled={buttonDisabled} // <-- pass in the boolean
type={"primary"}
title={"Submit"}
style={buttonStyle}
/>
...
);
const Button = (props) => {
return (
<button
disabled={props.disabled} // <-- make sure to add it to your Button component
style={props.style}
className={
props.type === "primary" ? "btn btn-primary" : "btn btn-secondary"
}
onClick={props.action}
>
{props.title}
</button>
);
};
I wrote some messy inline code to replace your setErrors function, but chances are you probably want to add setButtonDisabled(false); to the setErrors function wherever you originally defined it, rather than calling it from an anonymous function like I did; so keep that in mind.
More info about the useState hook can be found here. Let me know if that answers your question.
As others have said, disabling the button is a perfect solution. But I dislike the button changing its visuals while submitting.
You should instead set the button's css property pointer-events: none which will turn off all events from being emitted. You can remove that property once submit is done or failed.