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>
Related
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.
I have a component in React that has required in its fields. The idea is onClick will trigger a separate function that adds a soccer player to two different stores (similar to a TODO app). However, it appears that the validation does not work -name for example is a required string, but the form seems to add a player even if the string is empty. The same goes for the two number inputs.
I don't want to use onSubmit because this seems to refresh the page every time, which causes me to lose data.
I'm using the react-modal library for my forms. To start with here's a function that opens the modal:
function renderAddButton() {
return (
<Row
horizontal='center'
vertical='center'
onClick={openModal}
>
Add Player
</Row>
);
}
Here's the modal and its hooks:
const [playerName, setPlayerName] = useState('');
const [totalGoals, setTotalGoals] = useState(0);
const [goalPercentage, setGoalPercentage] = useState(0);
function openModal() {
setIsOpen(true);
}
function closeModal() {
setIsOpen(false);
}
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
style={modalStyles}
contentLabel='Player Form Modal'
>
<h3>Create Player</h3>
<form>
<label>Name</label>
<input
type='string'
id='playerNameId'
name='playerName'
defaultValue=''
onChange={(e) => setPlayerName(e.target.value)}
required
/>
<label>Total Goals</label>
<input
type='number'
id='totalGoalsId'
name='totalGoals'
defaultValue='0'
min='0'
onChange={(e) => setTotalGoals(e.target.value)}
required
/>
<label>Goal Percentage</label>
<input
type='number'
id='goalPercentageId'
name='playerGoalPercentage'
defaultValue='0'
min='0'
step ='0.01'
max='1'
onChange={(e) => setGoalPercentage(e.target.value)}
required
/>
<button onClick={(e) => onAddButtonClick(e)}>Submit</button>
</form>
<button onClick={closeModal}>close</button>
</Modal>
And now when this function is triggered, the validations don't seem to work. Empty playerId and totalGoals and goalPercentage seem to go through fine. How do I validate the inputs and stop this function from running if the inputs are empty?
function onAddButtonClick(e) {
e.preventDefault();
setItems((prev) => {
const newItems = [...prev];
const uuid= uuidv4();
newItems.push({
name: playerName,
playerId:uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
playersStore.push({
name: playerName,
playerId:uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
return newItems;
});
}
The required attribute only works with default form actions. You'll need to do your own validation in the handler. You should also explicitly define the button type attribute as well since buttons by default are type="submit".
Create a validation function and pass your state values to it. Return true if input is valid, false otherwise.
const validateInput = ({ goalPercentage, playerName, totalGoals }) => {
if (!playerName.trim()) {
return false;
}
// other validations
return true;
};
Check the input in onAddButtonClick and only update state if input is valid.
function onAddButtonClick(e) {
e.preventDefault();
const validInput = validateInput({ goalPercentage, playerName, totalGoals });
if (!validInput) {
return null;
}
setItems((prev) => {
const newItems = [...prev];
const uuid= uuidv4();
newItems.push({
name: playerName,
playerId: uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
playersStore.push({
name: playerName,
playerId: uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
return newItems;
});
}
Update the button to have an explicit type.
<button
type="button"
onClick={onAddButtonClick}
>
Submit
</button>
you can do something like this
function onAddButtonClick(e) {
e.preventDefault();
if(playerName.trim()===''){ //you can make more validations here
return;
}
//down here use the rest of logic
}
I have an asynchronous Autocomplete component that works fine so far.
Hopefully the simplified code is understandable enough:
export function AsyncAutocomplete<T>(props: AsyncAutocompleteProps<T>) {
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<T[]>();
const onSearch = (search: string) => {
fetchOptions(search).then(setOptions);
};
return (
<Autocomplete<T>
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={(event, value) => {
props.onChange(value as T);
}}
getOptionSelected={props.getOptionSelected}
getOptionLabel={props.getOptionLabel}
options={options}
value={(props.value as NonNullable<T>) || undefined}
renderInput={(params) => (
<TextField
{...params}
onChange={(event) => onSearch(event.currentTarget.value)}
/>
)}
/>
);
}
The component above works easily: when the user clicks on the input, the Autocomplete component displays an empty input field where the user can type in a value to search for. After the input has changed, the options are refetched to show matching results.
Now I want to add support for shortcodes: when the user types qq, the search term should be replaced by something, just like if the user would have typed something himself.
However, I found no way to update the value of the rendered TextField programmatically. Even if I set value directly on the TextField, it won't show my value but only the users input.
So, any ideas how to solve this problem?
Thank you very much.
What I've tried so far was to simply update the input within onKeyUp:
// ...
renderInput={(params) => (
<TextInput
{...params}
label={props.label}
onChange={(event) => onSearchChange(event.currentTarget.value)}
InputProps={{
...params.InputProps,
onKeyUp: (event) => {
const value = event.currentTarget.value;
if(value === 'qq') {
event.currentTarget.value = 'something';
}
},
}}
/>
)}
With the code above I can see the something for a short time, but it gets replaced by the initial user input very soon.
Autocomplete is useful for setting the value of a single-line textbox in one of two types of scenarios: combobox and free solo.
combobox - The value for the textbox must be chosen from a predefined set.
You are using it so it not allowing you to add free text (onblur it replaced)
Answer: To take control of get and set value programmatically.
you need a state variable.
Check here codesandbox code sample taken from official doc
Your code with my comment:-
export function AsyncAutocomplete<T>(props: AsyncAutocompleteProps<T>) {
... //code removed for brevity
//This is a state variable to get and set text value programmatically.
const [value, setValue] = React.useState({name: (props.value as NonNullable<T>) || undefined});
return (
<Autocomplete<T>
... //code removed for brevity
//set value
value={value}
//get value
onChange={(event, newValue) => setValue(newValue)}
renderInput={(params) => (
<TextInput
{...params}
label={props.label}
onChange={(event) => onSearchChange(event.currentTarget.value)}
InputProps={{
...params.InputProps,
onKeyUp: (event) => {
//get value
const value = event.currentTarget.value;
//if qq then set 'something'
if (value === "qq") {
setValue({ name: "something" });
}
//otherwise set user free input text
else {
setValue({ name: value });
}
},
}}
/>
)}
/>
);
}
Introduction
Yesterday I followed an advanced tutorial from Kent C. Dodds where he explained how to connect an input to localstorage which then handles the setting of value, change of values etc and automatically sync with LocalStorage in react.
At the first place this works pretty well for normal components. However, for example the custom checkboxes which I have in my app do not work with the logics. I tried to alter the logics a bit but it seems that I didn't got far with it.
The Problem
Currently my custom checkbox component does not connect / work with the hoc LocalStorageFormControl.
Project info
I have made a CodeSandbox for you to play around with: https://codesandbox.io/s/eager-curie-8sj1x
The project is using standard bootstrap with scss stylings. The CustomCheckbox consists of two elements: the main div and the actual input itself. Currently the matching value in state will trigger className change in one of the elements to allow custom styling.
For any further questions please comment below. Thanks in advance for all the help.
Resources
Kent C. Dodds - Tutorial resource
CodeSandBox Project
The problems were:
The LocalStorageFormControl component didn't update the state when
it gets the initial value from localStorage.
The input didn't update the state onChange as it didn't have onChange
handler.
The CustomCheckboxGroup component didn't have a name prop which is used
as a part of the key in the localStorage
The solution is as following:
App.js
import React, { useEffect, useState } from "react";
// Bootstrap
import { Row, Col, Form } from "react-bootstrap";
import CustomCheckboxGroup from "./CustomCheckboxGroup";
// Function that calls all functions in order to allow the user to provide their own onChange, value etc
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args));
// Connect any <input /> to LocalStorage and let it manage value / onChange
function LocalStorageFormControl({
children,
formControl = React.Children.only(children),
lsKey = `lsfc:${formControl.props.name}`,
updateInitialState
}) {
const [hasChanged, setHasChanged] = useState(false);
const [value, setValue] = useState(() => {
return (
window.localStorage.getItem(lsKey) || formControl.props.defaultValue || ""
);
});
// Let the user control the value if needed
if (
formControl.props.value !== undefined &&
formControl.props.value !== value
) {
setValue(formControl.props.value);
}
useEffect(() => {
if (hasChanged) {
if (value) {
window.localStorage.setItem(lsKey, value);
} else {
window.localStorage.removeItem(lsKey);
}
} else {
if (value) {
// if hasChanged is false and there is value that means there was a value in localStorage
setHasChanged(true);
// update the state
updateInitialState(value);
}
}
}, [value, lsKey, hasChanged, updateInitialState]);
return React.cloneElement(React.Children.only(children), {
onChange: callAll(formControl.props.onChange, e => {
setHasChanged(true);
setValue(e.target.value);
}),
value,
defaultValue: undefined
});
}
const checkboxes = [
{
label: "Dhr",
name: "aanhef-dhr",
stateName: "salutation",
value: "De heer"
},
{
label: "Mevr",
name: "aanhef-mevr",
stateName: "salutation",
value: "Mevrouw"
}
];
export default function App() {
const [state, setState] = useState({});
function handleSubmit(e) {
e.preventDefault();
console.log("Handling submission of the form");
}
function onChange(e, stateName) {
e.persist();
setState(prevState => ({ ...prevState, [stateName]: e.target.value }));
}
// Log the state to the console
console.log(state);
return (
<Row>
<Col xs={12}>
<Form
id="appointment-form"
onSubmit={handleSubmit}
noValidate
style={{ marginBottom: 75 }}
>
<LocalStorageFormControl
updateInitialState={value => {
setState({ ...state, "test-textfield": value });
}}
>
{/* Add onChange handler to update the state with input value*/}
<input
type="text"
name="test-textfield"
onChange={e => {
setState({ ...state, "test-textfield": e.target.value });
}}
/>
</LocalStorageFormControl>
<LocalStorageFormControl
updateInitialState={value => {
setState({ ...state, salutation: value });
}}
>
<CustomCheckboxGroup
checkboxes={checkboxes}
key="salutation"
label="Salutation"
name="salutation"
onChange={(e, stateName) => onChange(e, stateName)}
required={true}
value={state.salutation}
/>
</LocalStorageFormControl>
</Form>
</Col>
</Row>
);
}
CustomCheckboxGroup.js
import React from "react";
// Bootstrap
import { Form, Row, Col } from "react-bootstrap";
export default ({ onChange, value, name, label, className, checkboxes }) => (
<Row>
<Col xs={12}>
<Form.Label>{label}</Form.Label>
</Col>
<Col>
<Form.Group className="d-flex flex-direction-column">
{checkboxes.map((checkbox, key) => {
return (
<div
key={key}
className={
checkbox.value === value
? "appointment_checkbox active mr-2 custom-control custom-checkbox"
: "appointment_checkbox mr-2 custom-control custom-checkbox"
}
>
<input
name={name}
type="checkbox"
value={checkbox.value}
onChange={e => onChange(e, checkbox.stateName)}
checked={value === checkbox.value}
id={"checkbox-" + checkbox.name}
className="custom-control-input"
/>
<label
className="custom-control-label"
htmlFor={"checkbox-" + checkbox.name}
>
{checkbox.label}
</label>
</div>
);
})}
</Form.Group>
</Col>
</Row>
);
I have some advice about your code:
Use radio buttons instead of checkboxes if you allow the user to choose one option only. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio
You can persist the whole state object if you would like to by replacing this:
const [state, setState] = useState({});
with this:
// Get the saved state in local storage if it exists or use an empty object
// You must use JSON.parse to convert the string back to a javascript object
const initialState = localStorage.getItem("form-state")
? JSON.parse(localStorage.getItem("form-state"))
: {};
// Initialize the state with initialState
const [state, setState] = useState(initialState);
// Whenever the state changes save it to local storage
// Notice that local storage accepts only strings so you have to use JSON.stringify
useEffect(() => {
localStorage.setItem("form-state", JSON.stringify(state));
}, [state]);
I'm porting some of my forms I made using SUIR over to a Formik implementation. I have input components that need custom change handlers to perform actions. How can I pass my change handler to the onChange prop so the values are tracked by formik?
I've tried something like this, but with no luck.
onChange={e => setFieldValue(dataSchemaName, e)}
which is mentioned in a different post here.
It's also mentioned here, but I can't quite get my custom change handler to hook up nicely with Formik.
My change handler looks like this
handleSchemaChange = (e, { value }) => {
this.setState({ dataSchemaName: value }, () => {
console.log("Chosen dataSchema ---> ", this.state.dataSchemaName);
//also send a request the selfUri of the selected dataSchema
});
const schema = this.state.dataschemas.find(schema => schema.name === value);
if (schema) {
axios
.get(schema.selfUri)
.then(response => {
console.log(response);
this.setState({
fields: response.data.data.fields,
});
console.log(this.state.fields);
})
.catch(error => console.log(error.response));
}
};
Here's an example of one of my form fields using a dropdown
<Form.Field>
<label style={{ display: "block" }}>
Select a Schema
</label>
<Dropdown
id="dataSchemaName"
multiple={false}
options={dataschemas.map(schema => {
return {
key: schema.id,
text: schema.name,
value: schema.name
};
})}
value={dataSchemaName}
onChange={this.handleSchemaChange}
/>
</Form.Field>
I have a sandbox with the issue
You can pass the setFieldValue function to your function handleSchemaChange and then use it inside.
So, instead of onChange={this.handleSchemaChange} make it
onChange={(e, val) => this.handleSchemaChange(e, val, setFieldValue)}.
And inside your handleSchemaChange function:
handleSchemaChange = (e, { value }, setFieldValue) => {
setFieldValue('dataSchemaName', value);
... the rest of your implementation
}
Here is a sandbox with the solution implemented:
https://codesandbox.io/s/formik-create-query-schema-uq35f