I have a Formik form where I take user input input & run a query after submitting.
When I click on the search button, a list is rendered. All list items have a button of their own. When I click on the green add button (from the list items) for the first time, the button does not work. Console log's content is not printed. Instead, the onBlur event of the inputField is triggered. However, if I click on the + button again, then it works and prints no. This problem is visible in simulators/phones, not on the web mode on the sandbox.
export const AddFriendScreen: React.FunctionComponent = () => {
const initialValues: FormValues = {
input: '',
};
const [showFlatList, setShowFlatList] = useState<UsersLazyQueryHookResult>(
'',
);
const handleSubmitForm = (
values: FormValues,
helpers: FormikHelpers<FormValues>,
) => {
loadUsers({
variables: {
where: {
OR: [
{ phoneNumber: newPhoneNumber },],
},
},
});
helpers.resetForm();
};
return (
<SafeAreaView style={styles.safeAreaViewContainer}>
<View style={styles.searchTopContainer}>
<View style={styles.formContainer}>
<Formik
initialValues={initialValues}
onSubmit={handleSubmitForm}
validationSchema={validationSchema}>
{({ handleChange, handleBlur, handleSubmit, values }) => (
<View style={styles.searchFieldContainer}>
<View style={styles.form}>
<FieldInput
handleChange={handleChange}
handleBlur={handleBlur}
value={values.input}
fieldType="input"
icon="user"
placeholderText="E-Mail oder Telefonnummer oder Name"
/>
<ErrorMessage
name="input"
render={(msg) => <ErrorText errorMessage={msg} />}
/>
</View>
<View style={styles.buttonContainer}>
<ActionButton buttonText="Suchen" onPress={handleSubmit} />
</View>
</View>
)}
</Formik>
</View>
<View style={styles.listHolder}>
{data && showFlatList !== null && (
<UsersFoundList
data={data}/>
)}
</View>
</View>
</SafeAreaView>
);
};
Snack Expo:
https://snack.expo.io/#nhammad/jealous-beef-jerky
[![enter image description here][1]][1]
Edit:
Found a workaround but still open to easier solutions. Still haven't figured out what's causing the issue exactly.
However, this causes the formik error to show up immediately after the form is submitted and data is returned/rendered. For instance, the error suggests that the input field should not be empty.
...
Why is the keyboard interfering in this?
Probably because when you call Keyboard.dismiss() you are actually unfocusing the input, which will trigger the validateOnBlur event.
So when you unfocus a field (blur), it will try to validate (validateOnBlur), and because the field is empty, it will show the validation.
The error should only show up when we are trying to resubmit the form without an input.
If the error should only show when you submit the form, you should pass to the Formik component validateOnBlur={false}. With this, it won't show the error message when you call Keyboard.dismiss(), because it will remove the validation on blur.
But it still validates when the input changes, triggering validateOnChange. You can also pass to the Formik component validateOnChange={false} to disable the validation on change and it will only validate when you submit the form (press the button).
Please notice that validateOnChange and validateOnBlur are true by default.
Edit:
You need to add validateOnBlur={false} but instead of calling Keyboard.dismiss() you need to call fieldRef.current.blur(). But for that to work, you need to use React.forwardRef in your input component.
So you need to create the fieldRef
const fieldRef = useRef();
Pass to the component
<FieldInput
ref={fieldRef} // passing the ref
handleChange={handleChange}
handleBlur={handleBlur}
value={values.input}
fieldType="input"
icon="user"
placeholderText="input"
/>
And you need to wrap around your FieldInput with React.forwardRef and pass the ref to the Input component.
// wrapping the component with React.forwardRef
export const FieldInput: React.FunctionComponent<FieldInputProps> = React.forwardRef(({
handleChange,
handleBlur,
fieldType,
placeholderText,
value,
style,
rounded,
//ref,
}, ref) => {
return (
<Item rounded={rounded} style={[styles.inputItem, style]}>
<Input
ref={ref} // passing the ref
autoFocus={true}
autoCapitalize="none"
style={styles.inputField}
placeholder={placeholderText}
keyboardType="default"
onChangeText={handleChange(fieldType)}
onBlur={handleChange(fieldType)}
value={value}
/>
</Item>
);
});
And now in the handleSubmitForm you call fieldRef.current.blur()
const handleSubmitForm = (
values: FormValues,
helpers: FormikHelpers<FormValues>,
) => {
// other logic from your submit
if (fieldRef && fieldRef.current)
fieldRef.current.blur();
};
With this, you will solve the problems of your question and from the comments.
Working example
Explanation about useRef
We need to use the useRef hook so we can get the element of the Input component to be able to call the blur function so don't be focused in the input and can click in the plus button.
We need to use the useRef hook because that is how you create a ref in functional components.
Related
I tried creating a reusable component in React. Which has a textInput and secure text entry handled in the state of the reusable component. But the state is not getting maintained differently when reusing always the last state is updated,
Issue: If i call the reusable const two times on a single screen or on the next screen in stack. The toggle for secure entry keeps changing for the last field loaded and earlier loaded fields state is lost.
i.e., when i click on toggle of Password, text change from hidden to visible or vice-versa happens for Confirm password field.
This is how i call
<View style={styles.inputContainerView}>
<InputTextFieldView
enteredText={passwordEntered}
setEnteredText={setPasswordEntered}
title={Constants.registerPasswordPlaceholder} icon={lockIcon}
isSecureEntry={true}
placeholder={Constants.registerPasswordPlaceholder} />
</View>
<View style={styles.inputContainerView}>
<InputTextFieldView
enteredText={confirmPasswordEntered}
setEnteredText={setConfirmPasswordEntered}
title={Constants.registerConfirmPasswordPlaceholder} icon={lockIcon}
isSecureEntry={true}
placeholder={Constants.registerConfirmPasswordPlaceholder} />
</View>
My component:
const InputTextFieldView = ({ enteredText, setEnteredText, title, icon, isSecureEntry, placeholder }) => {
const [isSecureEntryEnabled, setIsSecureEntryEnabled] = useState(isSecureEntry)
const eyeOpenIcon = require('../../../assets/visibility.png')
const eyeCloseIcon = require('../../../assets/close-eye.png')
useEffect(() => {
console.log('called')
}, [])
toggleSecureTextEntry = () => {
console.log('title', title)
setIsSecureEntryEnabled(!isSecureEntryEnabled)
}
return (
<View style={styles.fieldsContainerView}>
<Text style={styles.titleStyle}>{title}</Text>
<View style={[styles.fieldInputContainerView, {padding: Platform.OS === 'ios' ? 12 : 0}]}>
<Image source={icon} style={styles.fieldIconView} />
<TextInput secureTextEntry={isSecureEntryEnabled} style={{ width: isSecureEntry ? '75%' : '85%' }} onChange={() => setEnteredText()} value={enteredText} placeholder={placeholder} />
{isSecureEntry &&
<TouchableWithoutFeedback onPress={() => toggleSecureTextEntry()}>
<Image source={isSecureEntryEnabled ? eyeOpenIcon : eyeCloseIcon} style={styles.fieldIconView} />
</TouchableWithoutFeedback>
}
</View>
</View>
)
}
I'm guessing that you are using isSecureEntry as the hook to toggle the password fields? If so, it looks like you are passing the same state to both
the password field and the confirm password field. Right now, you essentially have one light switch that controls two different lamps. So you are going to want to have separate separate useState hooks for the password field and confirm password field. Then pass each one to the correct component.
const [passwordSecure, togglePasswordSecure] = useState(true);
const [confirmPasswordSecure, toggleConfirmPasswordSecure] = useState(true);
const togglePasswordField = () => {
togglePasswordSecure(!passwordSecure)
};
const toggleConfirmPasswordField = () => {
toggleConfirmPasswordSecure(!confirmPasswordSecure)
};
Issue was happening due to TouchableWithoutFeedback. Now used TouchableOpacity and it started to work. Not sure why but it may help someone
What I am trying to achieve is focusing on TextInput present in child component when clicked on an icon present in parent file. I have tried using refs and forward refs but that means i have to wrap the component in forwardref which i am trying to avoid.
const InputText = React.forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
My child file looks like this:
export const TextField = ({
label,
placeholder = "",
id = "",
isSecure = false,
disabled = false,
handleBlur,
handleChange,
value,
...props
}: IProps): React.ReactElement => {
const { labelStyle, fieldStyle, status=false, search=false,inputStyle, errorStyle } =
props;
//some statements
<View style={styles.elementsContainer}>
<TextInput //I want to focus on this
.......
.
.
onChangeText={handleChange(props.name)}
onBlur={handleBlur(props.name)}
/>
</View>
);
};
export default TextField;
Below is my parent file where I have an icon and on click of it I trying this textfield to be focused.
return (
<Formik
initialValues={initialValues}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(false);
}}
>
{(formikProps: FormikProps<{ searchText: string }>) => (
<View style={status?styles.highlight:[styles.container,textFieldDimensions]}>
<TouchableOpacity onPress={addressFocus}> //This is the icon on click of it i am trying textfield child component to be focused.
<Icon testID="home" name={IconNames.home} />
</TouchableOpacity>
<View style={styles.textfieldContainer}>
<TextField //CHild component
handleChange={formikProps.handleChange}
status={status}
handleBlur={() => searchFunction(formikProps.values.searchText)}
value={formikProps.values.searchText}
name="searchText"
placeholder={placeholder}
search={search}
label="Search :"
id="searchText"
fieldStyle={[styles.field,fieldStyle]}
inputStyle={styles.input}
labelStyle={[styles.label, labelStyle]}
/>
</View>
</View>
)}
</Formik>
);
So after brainstorming i come to a simple approach which works for me as of now. But i am not crystal clear from ref part how its working.
so what i did is in parent file where i am passing props in child component like this:
const toggle=()=>{
setToggler(!toggler) This function changes state value which is boolean type from false to true and vice versa.
}
<TouchableOpacity onPress={toggle}> //toggle function called on onPress
<Icon testID="home" name={IconNames.home} />
</TouchableOpacity>
<TextField
toggler={toggler} //Now I am passing state toggler into child component, change of this state will re render the component
handleChange={formikProps.handleChange}
status={status}
handleBlur={() => searchFunction(formikProps.values.searchText)}
value={formikProps.values.searchText}
name="searchText"
placeholder={placeholder}
search={search}
label="Search :"
id="searchText"
fieldStyle={[styles.field,fieldStyle]}
inputStyle={styles.input}
labelStyle={[styles.label, labelStyle]}
/>
Now how i am handling refs in child component is that it focus on textinput on click of button.
All this is happening in functional component, for simplicity react rules has not been taken into account only relevant pieces of code is shown
const inputRef = useRef<TextInput>(null);
const onClickFocus = () => {
{props.toggler && inputRef.current?.focus();} //If toggler changes to true then this function will execute and perform inputRef.current?.focus();
}
onClickFocus() //Function is called here and later inputRef is utilized in textinput as ref={inputRef}
<TextInput
ref={inputRef}
placeholder={placeholder}
secureTextEntry={isSecure}
style={search? fieldStyle:[styles.inputBox, inputStyle]}
editable={!disabled}
value={value}
{...field}
onChangeText={handleChange(props.name)}
onBlur={handleBlur(props.name)}
/>
Upon entering my screen, there's no error displayed on the input field.
In my form, I take an input and run a graphql mutation on it. Once it's done, I reset the form. However, after resetting, I start seeing a Formik Error because the field is .required() and currently it's empty.
This error should only be shown when I am trying to submit the form without an input. It shouldn't show after I have submitted it once successfully.
const handleSubmitForm = (
values: FormValues,
helpers: FormikHelpers<FormValues>,
) => {
editLocationName({
variables: {
favouritePlaceId: route.params.id,
input: {customisedName: values.locationName}
},
});
helpers.resetForm();
helpers.setErrors({});
};
.....
<Formik
initialValues={initialValues}
onSubmit={handleSubmitForm}
validationSchema={validationSchema}>
{({ handleChange, handleBlur, handleSubmit, values }) => (
<View style={styles.searchFieldContainer}>
<View style={styles.form}>
<FieldInput
handleChange={handleChange}
handleBlur={handleBlur}
value={values.locationName}
fieldType="locationName"
placeholderText="Neuer Name"
/>
<ErrorMessage
name="locationName"
render={(msg) => <ErrorText errorMessage={msg} />}
/>
</View>
<View style={styles.buttonContainer}>
<ActionButton buttonText="Save" onPress={handleSubmit} />
</View>
</View>
)}
</Formik>
Validation Schema:
const favouriteLocationNameValidationSchema = yup.object().shape({
locationName: yup
.string()
.label('locationName')
.required('Required Field')
});
How can I reset the error message along with the form?
setErrors({}) did not work for me.
Adding it here, because suggestion requires code . Add following code and let me know if it helped.
helpers.resetForm({
errors: {},
touched: {}
});
Remove helpers.setErrors({});
I'm using Formik for validation in a React app.
Validation is working correctly, but my onChange handler does not fire:
<Field
type="text"
name="name"
placeholder="First Name"
component={Input}
onChange={() => console.log("gfdg")}
/>
Link to Sandbox
Why is this?
Inside Input, the way you have ordered the props passed to your input element means your onChange is being overwritten by Formik's onChange. When you create a Field with a custom component (i.e. Input in your case), Formik passes its FieldProps to the component. FieldProps contains a property field that contains various handlers including onChange.
In your Input component you do this (I've removed the irrelevant props):
<input
onChange={onChange}
{...field}
/>
See how your own onChange will just get replaced by Formik's onChange() inside field? To make it clearer ...field is basically causing this to happen:
<input
onChange={onChange}
onChange={field.onChange}
// Other props inside "field".
/>
If you were to reorder those the console message will now appear:
<input
{...field}
onChange={onChange}
/>
However now your input won't work now because you do need to call Formik's onChange to let Formik now when your input changes. If you want both a custom onChange event and for your input to work properly you can do it like this:
import React from "react";
import { color, scale } from "./variables";
const Input = React.forwardRef(
({ onChange, onKeyPress, placeholder, type, label, field, form }, ref) => (
<div style={{ display: "flex", flexDirection: "column" }}>
{label && (
<label style={{ fontWeight: 700, marginBottom: `${scale.s2}rem` }}>
{label}
</label>
)}
<input
{...field}
ref={ref}
style={{
borderRadius: `${scale.s1}rem`,
border: `1px solid ${color.lightGrey}`,
padding: `${scale.s3}rem`,
marginBottom: `${scale.s3}rem`
}}
onChange={changeEvent => {
form.setFieldValue(field.name, changeEvent.target.value);
onChange(changeEvent.target.value);
}}
onKeyPress={onKeyPress}
placeholder={placeholder ? placeholder : "Type something..."}
type={type ? type : "text"}
/>
</div>
)
);
export default Input;
See it here in action.
Although overall I'm not really sure what you're trying to do. Your form is working fine, you probably don't need a custom onChange but maybe you have some specific use case.
Let me first make it clear this answer is just for help purpose and I do know that this question has been accepted but I do have some modification for above answer with my version if the above solution doesn't work for anyone
Here onChangeText will return the value of the quantity field
<Formik
initialValues={{ product_id: '', quantity: '', amount: '' }}
onSubmit={(values, actions) => {
this.submitLogin(values);
}}
//some other elements ....
<Field placeholder='No. of Quantity' name='quantity' component={CustomInputComponent}
onChangeText={value => {
console.warn(value); // will get value of quantity
}}
/>
/>
Outside of your class you can define your component
const CustomInputComponent = ({
onChangeText,
field, // { name, value, onChange, onBlur }
form: { touched, errors, isValid, handleBlur, handleChange, values, setFieldValue }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
...props
}) => {
return (
<Input {...field} {...props} onBlur={handleBlur(field.name)}
onChangeText={value => {
setFieldValue(field.name, value);
onChangeText(value); // calling custom onChangeText
}}
/>
)
}
I'm building a React Native app and using the tomb-form-native library for my forms. In one of my screens, I loop through an array of types and output a form for each type:
{my_types.map(ob =>
<View key={++i}>
<Text>{ob.type} #{ob.num}</Text>
<Form
ref={(c) => {
this.form = {}
this.form[ob.type] = c
}}
type={_formType(this, ob.type)}
options={_formOptions(ob.type)}
value={this.state.value}
onChange={this.onChange.bind(this)}
/>
</View>
)}
<TouchableHighlight style={styles.button} onPress={this.onPress.bind(this)}>
<Text style={styles.buttonText}>Submit</Text>
</TouchableHighlight>
But when I try to get the submitted values in my onPress function, it doesn't work for multiple types. It works for one type if I only call getValue() once:
input = this.form['my_type'].getValue()
console.log(input) // I see in my debugger that it works.
But if I try to get the input for two or more types, I don't see anything in the log...
input = this.form['my_type'].getValue()
console.log(input) // Nothing. It doesn't work.
input2 = this.form['my_other_type'].getValue()
console.log(input2) // Nothing here either.
Is it possible to use the tcomb library to submit multiple forms with one onPress? Maybe it's the way I call my onPress function in the onPress property of TouchableHighlight?
UPDATE
This simplified onPress function suggests my form ref is only working the last time through the loop. If my loop has two items...
onPress() {
let input = this.form[1]
console.log(input) // Undefined.
let input2 = this.form[2]
console.log(input2) // Object.
}
It appears to be possible. If I use an array to track the form refs, it works:
this.form = []
return (
...
{a.map(ob =>
<View key={++i} style={s}>
<Text>{ob.type} #{ob.num}</Text>
<Form
ref={(c) => {
this.form.push(c)
}}
key={i}
type={_formType(this, ob.type)}
options={_formOptions(ob.type)}
value={this.state.value}
onChange={this.onChange.bind(this)}
/>
</View>
)}
<TouchableHighlight style={styles.button} onPress={this.onPress.bind(this)}>
<Text style={styles.buttonText}>Submit</Text>
</TouchableHighlight>
And here is a simplified onPress...
onPress() {
let tF = this.form
tF.forEach(function(f) {
if (f) { // First two times through the loop, f is null, in my application.
console.log(f.getValue()) // It works!
}
})
}