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
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 am using react select async creatable,
the data from api loads correctly and can select it also can create new values
but after selecting a choice or if not, creating a new value, seems like my titleState becomes empty, I tried consoling the state but gives me blank/empty value on console
const [titleState,setTitleState] = useState('')
const loadOptions = (inputValue, callback) => {
axios.post(`sample/api`, {
data: inputValue
})
.then(res => {
callback(res.data.map(i => ({
label: i.title,
value: i.title,
})))
console.log(res)
})
.catch(err => console.log(err))
}
const handleInputChange = (newValue) => {
setTitleState(newValue)
}
const logState = () => {
console.log(titleState) // logs empty in the devtools
}
<AsyncCreatableSelect
menuPortalTarget={document.body}
styles={{ menuPortal: base => ({ ...base, zIndex: 9999 }) }}
loadOptions={loadOptions}
onInputChange={handleInputChange}
placeholder="Title Trainings"
/>
<Button onClick={logState}>Click</Button>
then I have some button when click will console the titleState, but it doesn't log it.
You should use onChange={handleInputChange} instead of onInputChange={handleInputChange}
onChange triggers when a new option is selected / created.
onInputChange triggers when writing a new option.
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>
Using Fluent UI React, I'm displaying some data from an AppSync API in a TextField. I want to be able to show text from the API for a contact form. I then want to edit that text and click a button to post it back to the AppSync API.
If I use the TextField component on its own, I can then use a hook to set a variable to result of an AppSync API call and then have the TextField component read the value coming from the variable I set with the hook. I can then edit that text as I feel like and its fine.
The problem I have is that if I want to take edits to the TextField and set them using my hook I lose focus on the TextField. To do this I am using the onChange property of TextField. I can set the variable fine but I have to keep clicking back in to the input window.
Any thoughts on how I can keep the focus?
import React, { useEffect, useState } from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import * as queries from '../../graphql/queries';
import { Fabric, TextField, Stack } from '#fluentui/react';
const PhoneEntryFromRouter = ({
match: {
params: { phoneBookId },
},
}) => PhoneEntry(phoneBookId);
function PhoneEntry(phoneBookId) {
const [item, setItem] = useState({});
useEffect(() => {
async function fetchData() {
try {
const response = await API.graphql(
graphqlOperation(queries.getPhoneBookEntry, { id: phoneBookId })
);
setItem(response.data.getPhoneBookEntry);
} catch (err) {
console.log(
'Unfortuantely there was an error in getting the data: ' +
JSON.stringify(err)
);
console.log(err);
}
}
fetchData();
}, [phoneBookId]);
const handleChange = (e, value) => {
setItem({ ...item, surname: value });
};
const ContactCard = () => {
return (
<Fabric>
<Stack>
<Stack>
<TextField
label='name'
required
value={item.surname}
onChange={handleChange}
/>
</Stack>
</Stack>
</Fabric>
);
};
if (!item) {
return <div>Sorry, but that log was not found</div>;
}
return (
<div>
<ContactCard />
</div>
);
}
export default PhoneEntryFromRouter;
EDIT
I have changed the handleChange function to make use of prevItem. For this event it does accept the event and a value. If you log that value out it is the current value and seems valid.
Despite the change I am still seeing the loss of focus meaning I can only make a one key stroke edit each time.
setItem((prevItem) => {
return { ...prevItem, surname: e.target.value };
});
};```
I think you want the event.target's value:
const handleChange = e => {
setItem(prevItem => { ...prevItem, surname: e.target.value });
};
You should also notice that in your version of handleChange(), value is undefined (only the event e is being passed as a parameter).
Edit: Now I see that you're setting the value item with data from a fetch response on component mount. Still, the value of item.surname is initially undefined, so I would consider adding a conditional in the value of the <TextField /> component:
value={item.surname || ''}
I'm trying to debounce a component in my webapp. Actually it is a filter for the maxPrice and if the user starts to print, the filter starts to work and all the results disappear until there is a reasonable number behind it.
What I tried so far:
import _ from 'lodash'
class MaxPrice extends Component {
onSet = ({ target: { value }}) => {
if (isNaN(Number(value))) return;
this.setState({ value }, () => {
this.props.updateMaxPrice(value.trim());
});
};
render() {
const updateMaxPrice = _.debounce(e => {
this.onSet(e);
}, 1000);
return (
<div>
<ControlLabel>Preis bis</ControlLabel><br />
<FormControl type="text" className={utilStyles.fullWidth} placeholder="egal"
onChange={updateMaxPrice} value={this.props.maxPrice}
/>
</div>
);
}
}
I'm getting the error
MaxPrice.js:11 Uncaught TypeError: Cannot read property 'value' of null
at MaxPrice._this.onSet (MaxPrice.js:11)
at MaxPrice.js:21
at invokeFunc (lodash.js:10350)
at trailingEdge (lodash.js:10397)
at timerExpired (lodash.js:10385)
In my old version I had onChange={this.onSet} and it worked.
Any idea what might be wrong?
As you mentioned in comments, it's required to use event.persist() to use event object in async way:
https://facebook.github.io/react/docs/events.html
If you want to access the event properties in an asynchronous way, you
should call event.persist() on the event, which will remove the
synthetic event from the pool and allow references to the event to be
retained by user code.
It means such code, for example:
onChange={e => {
e.persist();
updateMaxPrice(e);
}}
Here is my final solution. Thanks to lunochkin!
I had to introduce a second redux variable so that the user see's the values he is entering. The second variable is debounced so that the WepApp waits a bit to update.
class MaxPrice extends Component {
updateMaxPriceRedux = _.debounce((value) => { // this can also dispatch a redux action
this.props.updateMaxPrice(value);
}, 500);
onSet = ({ target: { value }}) => {
console.log(value);
if (isNaN(Number(value))) return;
this.props.updateInternalMaxPrice(value.trim());
this.updateMaxPriceRedux(value.trim());
};
render() {
return (
<div>
<ControlLabel>Preis bis</ControlLabel><br />
<FormControl type="text" className={utilStyles.fullWidth} placeholder="egal"
onChange={e => {
e.persist();
this.onSet(e);
}} value={this.props.internalMaxPrice}
/>
</div>
);
}
}
function mapStateToProps(state) {
return {
maxPrice: state.maxPrice,
internalMaxPrice: state.internalMaxPrice
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({updateMaxPrice:updateMaxPrice,
updateInternalMaxPrice:updateInternalMaxPrice}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(MaxPrice);