React Formik change validation when auto submitting - javascript

I have a multistep form with a submit button at the bottom of each section and a clickable list of the sections for navigation. I have implemented an auto submit feature that runs if a user updates information and then navigates away using the menu without clicking the submit button:
const AutoSubmit = () => {
const { dirty, submitForm } = useFormikContext();
React.useEffect(() => {
if (dirty) {
submitForm();
}
}, [dirty, submitForm]);
return null;
};
The issue is that if the Yup validation fails then the submission is obviously blocked and any information on the previous section is lost. Essentially what i'm trying to do is trigger full validation if a user clicks the actual submit button, but do a partial validation (i.e. no required fields) if its an auto submit so their progress can be saved.
Ive tried using setFieldValue to set an "autoSubmit: true" flag in the above component and then changed the Yup Schema to only require fields etc if its not there or false, but it doesn't seem to pick up the value in time for the submission (presumably something to do with it running when the component is unmounting). Anyone got any ideas on how to get this working?
Thanks

So i had a bit of a rethink of this and thought i should post my solution just in case anyone finds it useful. I essentially decided to bypass the Formik validation for the partial submit, and let the backend handle it. As this was going to be a silent submit, the user feedback of the validation wasn't required, and because its assuming some data may be missing etc, it seems like something the backend should be deciding on.
The only other problem was that Formik re-renders every time any of the form data changes. My requirement was to only trigger this partial submit when the user navigated away from the form section, so my example above wasn't suitable as it would fire on every re-render. To get around this i used a ref and put the function outside of Formik:
function FormComponent() {
const formRef = useRef();
useEffect(() => {
return async () => {
const {dirty, isSubmitting, values} = formRef.current;
if (!isSubmitting && dirty) {
try {
await doPartialSubmission(values);
} catch (error) {
// Handle error
}
}
};
}, [formRef])
return (
<Formik
innerRef={formRef}
onSubmit={doFullValidatedSubmit()}
>
//Standard formik stuff...
</Formik>
);
};

Related

Why my data is not loading back when I erase the searchTerm passed to Input field in reactJs?

I am making a search functionality into react that effectively looks for data from json-server for a match. I don't want to provide a debounced search to the input field, rather I want to trigger the search when "Enter" key is pressed. So i used onKeyPress prop from MUI's textfield, where I provided the logic to send the query to the server.
Please acknowledge my code as mentioned below -
imports...
export default function AppSearchBar ( ) {
// local state for searchTerm
const [ searchTerm, setSearchTerm ] = useState<string>('');
// using redux - action
const {loadedBooks} = useAppSelector(state => state.books);
const {loadedGames} = useAppSelector(state => state.games);
// these 'loadedBooks' and 'loadedGames' are of type boolean and are initially false (coming from my slices) and set to true when their requests are getting fulfilled.
const dispatch = useAppDispatch();
// useCallback
const fetchAllCategories = useCallback(() => {
setTimeout( async () => {
await dispatch(fetchBooksAsync( searchTerm )); // this is async thunks i created to fetch books into my bookSlice.ts file
await dispatch(fetchGamesAsync( searchTerm )); // this is async thunks i created to fetch books into my gameSlice.ts file
}, 500);
}, [ searchTerm , dispatch ]);
// effect when searchTerm mounts
/* useEffect(() => {
fetchAllCategories()
}, [ fetchAllCategories ]); */ // dependency as the function itself.
// I want this useEffect, but un-commenting this is not allowing my "handleSearchOnEnter" to come into the picture at all, but, I want that first load of all cars be automatic, and then when I write something to input, on pressing enter it should search, and finally when I wipe input field, it should return me back all the cards.
const handleSearchOnEnter = ( event : any ) => {
if ( event.key === "Enter" ) {
fetchAllCategories(); // this is wrapped inside of useCallBack and effect is produced using useEffect.
}}
return (
<Fragment>
<TextField
value = {searchTerm}
onChange = {( event : any ) => setSearchTerm(event.target.value)}
onKeyPress = { searchTerm !== "" ? handleSearchOnEnter : undefined } />
</Fragment>
)
}
Now, problem statement -
Whenever I load my window, all Books and Games are not loaded at all (if I remove useEffect() from code). They only loads when I press enter. But, I don't want this behaviour.
If I keep useEffect() - hook, then they behaves like debounce search and onChange of my text input field, they return the searched result.
What I want is as follows -
- Whenever I first loads the window, all products get loaded.
- Whenever I write something into the input field, then it shouldn't call (fetchFiltersAsync() - which is the API call for full-text search on Json-Server) until i press Enter key, only When I press enter, it should call the API and fetch the relevant results.
- After that, when I manually remove the searchedItem from input field (wiping it), all of my data should get returned. (i.e display all cards into UI)
What is Happening? -
Whenever My window loads, all of my data/cards are not getting loaded., until I presses enter key...cool
When I type something into input field, it fetches the searched results without even taking "Enter" (because of open useEffect() into the code)
When I remove a term from the input field, my data is not getting loaded automatically back to like as they were into first visit (all cards visible).
All controls are here only (into the code), I have to do something with searchTerm, so whenever searchTerm is not empty, then my handleSearchOnEnter() function should get called.
I mean, it should produce useEffect automatically, but only when a searchTerm is not being provided. If the searchTerm is being provided, then it should trigger handleOnEnterSearch()
I had the same issue that is described in the second Problem I solved it by adding in my project.
<form onSubmit={onKeyDownHandler}>
<TextField>{"Some Code"}</TextField>
</form>;
Also you can create an useState and giving it new Date will refresh your table better.
const onKeyDownHandler = (e) => {
e.preventDefault();
// if (searchTxt.length >= 3 || !searchTxt.length) {
dispatch(setSearchTxt(searchTxtTemp));
setTriggerSearch(new Date().getTime());
// }
};
But the bad sides of this code is when you remove everything from input you need to press enter again to refresh.

Submit and handle two forms with one handleSubmit in react-hook-form

In my react app, I have two address forms on one page that have two save address functions which save the address in the database. There's also one submit button that submits both two fields and navigates to the next page (The plus button in the circle adds the address to the saved addresses list):
What I want to do is to validate the form fields and I'm already doing that by calling two instances of the useForm() hook:
// useForm hook
const {
control: senderControl,
handleSubmit: handleSenderSubmit,
setValue: senderSetValue,
} = useForm({
defaultValues: {
senderName: '',
senderLastName: '',
senderPostalCode: '',
senderPhone: '',
senderAddress: '',
},
});
// useForm hook
const {
control: receiverControl,
handleSubmit: handleReceiverSubmit,
setValue: receiverSetValue,
} = useForm({
defaultValues: {
receiverName: '',
receiverLastName: '',
receiverPhone: '',
receiverPostalCode: '',
receiverAddress: '',
},
});
I've then added the handleSubmit method of these two hooks to the onClick (onPress in RN) of the plus button for each field respectively.
This does indeed validate the forms individually but the problem arises when I'm trying to submit the whole page with the SUBMIT button.
I still want to be able to validate both of these two address fields when pressing the general SUBMIT button but I have no idea how I can validate these two instances with one handleSubmit and get the return data of both fields' values.
EDIT (CustomInput.js):
const CustomInput = ({
control,
name,
rules = {},
placeholder,
secureTextEntry,
keyboardType,
maxLength,
textContentType,
customStyle,
}) => (
<Controller
control={control}
name={name}
rules={rules}
render={({field: {onChange, onBlur, value}, fieldState: {error}}) => (
<View style={customStyle || styles.container}>
<TextInput
value={value}
onBlur={onBlur}
onChangeText={onChange}
placeholder={placeholder}
keyboardType={keyboardType}
maxLength={maxLength}
textContentType={textContentType}
secureTextEntry={secureTextEntry}
style={[
styles.textInput,
{
borderColor: !error
? GENERAL_COLORS.inputBorder
: GENERAL_COLORS.error,
},
]}
/>
{error && <Text style={styles.errMsg}>{error.message || 'error'}</Text>}
</View>
)}
/>
);
Usage:
<CustomInput
control={control}
name="senderPostalCode"
rules={{
required: 'Postal Code is Required',
}}
placeholder="Postal Code"
keyboardType="number-pad"
textContentType="postalCode"
customStyle={{
width: '49%',
marginBottom: hp(1),
}}
/>
Is there even any way this can be possible at all?
Thanks to #TalgatSaribayev 's comment for leading me to this solution.
I didn't need to set any specific validation rules for the address field and in the end I separated the sender and receiver forms into two different pages.
First, I've got to point out that instead of getting the input values of postalCode and address fields with the getValues API, I used the useWatch hook to get the most updated values.
// Watch inputs
const watchedInputs = useWatch({
control,
name: ['senderPostalCode', 'senderAddress'],
});
When I saved the input values with getValues in a variable, I got the previous state instead of the most recent one and the only way to solve that was calling getValues('INPUT_NAME') whenever I wanted to get the most recent one. Even then I needed to call an instance of useWatch without saving it in any variable to keep track of the changes since typing in the input fields wouldn't update the getValues at all. So in the end I resolved to use useWatch and store its values in a variable and use that to access the values of the input fields.
///////////////////////////////////////////////////////////////////////////////////////////////////
As #TalgatSaribayev pointed out, creating just one useForm instance was sufficient enough. All I had to do was to create a function which would set the errors manually and check their validation upon pressing the save address button.
// Check if sender postal code input has error
const senderHasError = () => {
if (!/^\d+$/.test(watchedInputs[0])) {
setError('senderPostalCode', {
type: 'pattern',
message: 'Postal Code must be a number',
});
return true;
}
// Any other rules
if (YOUR_OWN_RULE) {
setError('senderPostalCode', {
type: 'custom',
message: 'CUSTOM_MESSAGE',
});
return true;
}
// Clear error and return false
clearErrors('senderPostalCode');
return false;
};
The problem was that the errors wouldn't get updated (cleared) even when they had passed the validation. As if the invalid input wouldn't attach onChange event listeners to re-validate it. Something that happens as default when you submit the form with the onSubmit mode of useForm. (https://react-hook-form.com/api/useform)
So I resolved to use the trigger API of useForm to manually trigger form validation and listen for the changes on the postalCode field when the save address button is pressed.
First I created a toggle state which changes the trigger state:
// Postal code onSubmit event state
const [triggered, setTriggered] = useState(false);
Then used the trigger API of useForm in a useMemo to trigger the input validation only if the triggered state is set to true and the input field's value has changed:
// Manually trigger input validation if postal code's onSubmit event is true
useMemo(() => {
if (triggered) trigger('senderPostalCode');
}, [watchedInputs[0]]);
I assume the way I triggered the input field with the trigger API works the same way as useForm's mode of onSubmit does it under the hood: Starting the trigger when the user presses the save address button by changing the trigger state with setTrigger.
// Add address to sender favorites
const handleAddSenderFavAddress = () => {
// Trigger on the press event
setTriggered(true);
// Return if sender postal code input has error
if (senderHasError()) return;
// SAVE ADDRESS LOGIC
///////////////////////////////
};
This was the only way I managed to validate separate input fields apart from the general validation that occurs with useForm's handleSubmit function.
I welcome any more answers that might lead to a better solution.
You can use a single instance of useForm hook and register all your fields in both the forms using the same register method.
This way whenever you click on submit, it will fetch the values from all the fields registered in different forms using the same register method.
Have attached a code sandbox link for your reference

How to force a functional component to rerender on submit?

I have form inputs that update their form state(X) with an onChange then i have a form onSubmit submit that has to do 2 things:
update a separate state(Y) using info from state(X)
run an actual function that would use state(Y)
const [ethValue, setEthValue] = React.useState({ethValue: 0});
const submit = (e) => {
setEthValue(() => { return { ethValue: formData.value } })
e.preventDefault();
runContractFunction()
};
It is working, however in order for it to complete the task I have to press Submit button twice, due to asynchronous nature of set functions.
In order for it to finish setting i decided to use re-rendering so it would finish setting the state, but neither of methods i tried, work.
Since I use functional components i cannot use forceUpdate() and i cannot put useEffect inside of my submit function because it would put the useEffect hook outside of the component function body.
I want to make it so you have to press Submit once and not twice for it to work.
How do I solve this problem?
Thanks in advance for your time!
Pass that 'Y' value to the function as argument which uses it as state.
const submit = (e) => {
setEthValue(() => { return { ethValue: formData.value } })
e.preventDefault();
runContractFunction({
ethValue: formData.value
})
};

react-native custom form validation

I created a custom validation method that runs on submitting a form since i found no useful, easy to implement validation library for react native, my validation method returns false and the code keeps executing, it doesn't freeze.
Here is the validation and login method
_validateEmail = (email) => {
let isEmail = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return isEmail.test(String(email).toLowerCase());
}
_login = async () => {
let user = this.state;
console.log('before validate');
await this._validateEmail(user.email);
console.log('after validate');
}
My terminal keeps logging the after validate even if wrote a wrong email address, how can I write a custom validation method that sets the state whether to show or hide error message on every click on the form?
PS: is there a simple validation library available for this?
In general, you don't need to do email validation yourself, this task is already solved in almost every UI Components Library which has a User Input component.
Now, assuming that you are using React Native TextInput Component:
You have to set the textContentType to emailAddress, and it will automatically validate your input for the mentioned content type, like this <TextInput textContentType='emailAddress' />
After, this use the onEndEditing TextInput event to update the state if the input is valid
If you are using the default react-native TextInput then just setting the probs should suffice but if you still want to perform the validation by yourself then. you should do the following.
with respect to your login function
_login = async () => {
let user = this.state;
console.log('before validate');
await this._validateEmail(user.email);
console.log('after validate');
}
It means each time that the login function is called we expect both the before validate and after validate to be printed.
To make sure your terminal does not log the after validate string when the wrong email is entered then you should change to something like this
_login = async () => {
let user = this.state;
console.log('before validate');
let re = await this._validateEmail(user.email);
re && console.log('after validate');
}
So basically you should check for re and if its true then your validation was correct else if its false then your validation failed. In the case where its it false you might want to set some error variables in the state and check so as to give feedback to the user. typically something like
_login = async () => {
let user = this.state;
console.log('before validate');
let re = await this._validateEmail(user.email);
if(!re){
setState({error: "invalid Email"})
//perform what ever login you want here.
}
}
the above will only log after validate when the email is correct
So you can then call your login in your code as onPress={()=>this._login()}

Redux middleware is half working but not fully cancelling action

I have some simple middleware that is kind of working but also not working
basically I have a list of users and I am trying to delete one. and then sync it up with firebase. all is well.
I'm adding some middleware so that when a user deletes one it asks if you are sure? (just using a simple alert for now). if you click cancel, it doesn't delete. if you click ok it does
so far, that is working but because of my action creators it still carrying on and deleting the user. here is some code:
// click to delete user
<button
onClick={() =>
this.props.deleteUserFromStoreThenUpdateFirebase(user)
}
>
calls this method
I think something funky is going on here, basically it shouldn't call the deletedUserFromFirebase method if I hit cancel
export const deleteUserFromStoreThenUpdateFirebase = user => {
return (dispatch, getState) => {
return dispatch(deleteUser(user)).then(() => {
return deleteUserFromFirebase(user);
})
};
};
export const deleteUser = user => {
return async dispatch => {
dispatch({ type: DELETE_USER, user: user, reqConfirm: true });
};
};
middleware:
const confirmMiddleware = store => next => action => {
if(action.reqConfirm){
if(confirm('are you sure?')){
next(action)
}
}
else {
next(action)
}
}
I also think that confirmation actions should be [only] role of the UI.
Redux store can (should?) be treated like API - request (action) > response (changed state). Are you sending request to an API and waiting to confirmation message? It would be at least strange.
But I see even more than some sense in this idea. Centralised confirmation element can have a similiar role to toasts/snackbar messages.
Problem #1: There is a strict separation between UI and store logic - you can't use middleware to show dialog. But you can change state which can used in UI to show confirmation dialog.
Problem #2: There is no simple if/then/return chain of actions, you have to buffer an action and run it after confirmation (receiving 'confirm' action) or drop it on cancel. Yes, it can be done as middleware.
IDEA:
Action dispatched as requiring confirmation is saved in buffer. You can then change action type to CONFIRM_SHOW - state will be changed, props passed, modal dialog can be shown.
On CONFIRM_OK run buffered action, next steps will be common with CONFIRM_CANCEL: clear buffer and hide modal. It could be even one value - buffer can be a modal flag (empty - hide, defined - show) dervied in mapStateToProps.
To be fully usable there should be an option to pass custom confirmation message along with reqConfirm.

Categories