I want to be able to move the focus from one input onto the next after the user has entered 2 numbers in the input box. Instead of the user manually clicking the next field I'd like the focus to switch there automatically.
Below is how I've currently implemented my two input fields.
I've tried the things mentioned here using refs but doesn't seem to work at all https://www.codegrepper.com/code-examples/csharp/react+native+textinput+focus
<Controller
control={form.control}
defaultValue={initialMonthValue}
name={monthName}
render={({ onChange, onBlur, value }) =>
<Input
containerStyle={{ width:80 }}
editable={!isSubmitting}
autoCapitalize="none"
numberOfLines={1}
multiline={false}
keyboardType='number-pad'
placeholder="MM"
maxLength={2}
placeholderTextColor={Colors.Grey}
onChangeText={onChange}
onBlur={onBlur}
secureTextEntry={false}
invalid={!!monthError}
value={value} />}
/>
<Slash text="/" />
<Controller
control={form.control}
defaultValue={initialYearValue}
name={yearName}
render={({ onChange, onBlur, value }) =>
<Input
containerStyle={{ width:80 }}
editable={!isSubmitting}
autoCapitalize="none"
numberOfLines={1}
multiline={false}
keyboardType='number-pad'
placeholder="YY"
placeholderTextColor={Colors.Grey}
onChangeText={onChange}
onBlur={onBlur}
secureTextEntry={false}
invalid={!!yearError}
value={value} />}
/>
If I understand what are you looking to implement then this should work for you.
import { useRef, useEffect, useState } from "react";
export default function FormInputs() {
const inputRef = useRef(null);
const [length, setLength] = useState(0);
const handleChange = (e) => {
// Handle type checking here
setLength(length + 1);
};
useEffect(() => {
// if the Ref doesn't exist for some reason then break.
if (!inputRef) return;
const input = inputRef.current;
if (length === 2) input.focus();
}, [length]);
return (
<>
<input onChange={handleChange} />
<input ref={inputRef} />
</>
);
}
https://codesandbox.io/s/wonderful-platform-no55n?file=/src/App.js:77-590
if you would love me to help you implement this on your code then don't hesitate to share the components needed for the code to be functional.
You can use TextInput's focus function. Here is a working example:
import React from 'react';
import {SafeAreaView, StyleSheet, TextInput} from 'react-native';
const App = () => {
const [textInputValue1, setTextInputValue1] = React.useState('');
const textInputRef2 = React.useRef<TextInput | null>();
const [textInputValue2, setTextInputValue2] = React.useState('');
return (
<SafeAreaView>
<TextInput
style={styles.textInput}
value={textInputValue1}
onChangeText={(value) => {
if (value.length >= 2) {
textInputRef2.current?.focus(); // Focus on the other TextInput
}
setTextInputValue1(value);
}}
/>
<TextInput
style={styles.textInput}
ref={(ref) => (textInputRef2.current = ref)}
value={textInputValue2}
onChangeText={(value) => setTextInputValue2(value)}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
textInput: {
backgroundColor: 'gray',
margin: 20,
minHeight: 40,
},
});
export default App;
You can focus other Input's ref when you reach maxLength on first input' maxLength prop's value like this:
import React, {useRef} from 'react';
import {View, TextInput} from 'react-native';
const App = () => {
const secondTextInputRef = useRef(null);
const [firstTextInputValue, setFirstTextInputValue] =
React.useState('');
const [secondTextInputValue, setSecondTextInputValue] =
React.useState('');
const maxInputSize = 3;
const App = () => {
return (
<View>
<TextInput
value={firstTextInputValue}
maxLength={maxInputSize}
onChangeText={(value) => {
setFirstTextInputValue(value);
if (value.length >= maxInputSize) {
secondTextInputRef.current?.focus();
}
}}/>
<TextInput
ref={(ref) => (secondTextInputRef.current = ref)}
value={secondTextInputValue}
onChangeText={(value) => setSecondTextInputValue(value)} />
</View>
);
};
export default App;
Related
I have a form that uses Formik and contains two fields - email and siteID
The email field uses a TextInput whereas siteID comes from my own component called DropDown that uses react-native-dropdown-picker
When my form is submitted I can see the value of email within handleSubmit() but siteID remains as 0 and not the selected value.
How can I access the selected value from my DropDown child component when submitting the parent form?
My form
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.loginContainer}>
<Formik
validationSchema={loginValidationSchema}
initialValues={{
siteID: 0,
email: "",
}}
onSubmit={(values) => handleSubmit(values)}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
isValid,
}) => (
<>
<Text style={styles.label}>Email</Text>
<TextInput
name="email"
style={styles.textInput}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
value={values.email}
keyboardType="email-address"
/>
<DropDown name="siteID" />
</>
)}
</Formik>
</View>
</SafeAreaView>
);
DropDown component
import * as React from 'react';
import {StyleSheet} from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import {usePostRequest} from '../../client';
import {useState, useEffect} from 'react';
const DropDown = () => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickervalue, pickersetValue] = useState(null);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={pickervalue}
items={pickeritems}
setOpen={pickersetOpen}
setValue={pickersetValue}
setItems={pickersetItems}
/>
);
};
Based on the docs, to use your own component, you should wrap your component in a Field as follows:
const DropDownField = <Field name="siteId" component={DropDown} />
When using a Field formik will inject props (name, value, onChange, onBlur) into your component allowing you to control the form state.
So you can then redeclare your component with those props as follows:
import * as React from 'react';
import {StyleSheet} from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import {usePostRequest} from '../../client';
import {useState, useEffect} from 'react';
const DropDown = ({ field: { name, value, onChange }) => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={value}
items={pickeritems}
setOpen={pickersetOpen}
setValue={onChange}
setItems={pickersetItems}
name={name}
/>
);
};
Essentially you wire your component up to the onChange and value props. Since you assigned a name of siteId to the DropDownField component (which you should now use in your form), any time you call onChange site Id will update.
Link to docs
You can use setFieldValue provided by Formik to set a field manually.
A good example of usage is provided in this example
You can pass setFieldValue to your DropDown Component:
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.loginContainer}>
<Formik
validationSchema={loginValidationSchema}
initialValues={{
siteID: 0,
email: "",
}}
onSubmit={(values) => handleSubmit(values)}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
isValid,
setFieldValue,
}) => (
<>
<Text style={styles.label}>Email</Text>
<TextInput
name="email"
style={styles.textInput}
onChangeText={handleChange("email")}
onBlur={handleBlur("email")}
value={values.email}
keyboardType="email-address"
/>
<DropDown name="siteID" setSiteID={setFieldValue} />
</>
)}
</Formik>
</View>
</SafeAreaView>
);
And trigger it from inside of the DropDown Component:
const DropDown = ({ setSiteID }) => {
const [pickeropen, pickersetOpen] = useState(false);
const [pickervalue, pickersetValue] = useState(null);
const [pickeritems, pickersetItems] = useState([]);
const {status: siteStatus, data: siteData} = usePostRequest('/api/sites', {});
useEffect(() => {
console.log('siteData', siteData);
if (siteData.count) {
const sitesList = [];
siteData.results.map((item, key) =>
sitesList.push({label: item.siteName, value: item.siteID})
);
pickersetItems(sitesList);
}
}, [siteData]);
// You can use useEffect or create a custom handleValueChange
// and pass it to DropDownPicker
useEffect(() => {
setSiteID('siteID', pickervalue);
}, [pickersetValue])
return (
<DropDownPicker
style={styles.textInput}
placeholder="Please chose a site"
open={pickeropen}
value={pickervalue}
items={pickeritems}
setOpen={pickersetOpen}
setValue={pickersetValue}
setItems={pickersetItems}
/>
);
};
try this,
get setFieldValue from formik props.
add onChangeDropdown to <Dropdown /> like,
<DropDown name="siteID" onChangeDropdown={(value)=>{
setFieldValue("siteID",value)
}}/>
get the props inside Dropdown component,
const DropDown = (props) => {
add onChangeValue prop to your DropDownPicker
onChangeValue={props.onChangeDropdown}
I have called Controller from react-hook-form in a separate component in my react native app and displaying that component on login screen that is showing this error:
TypeError: undefined is not an object (evaluating 'e.substring')
CustomInput.js
import { View, TextInput, StyleSheet } from 'react-native'
import React from 'react'
import { Controller } from 'react-hook-form';
const CustomInput = ({control, name, placeholder, secureTextEntry}) => {
return (
<View style={styles.container}>
<Controller control={control} name={name} render={({field: {value, onChange, onBlur}, fieldState: {error}}) =>
<TextInput style={styles.input} value={value} onChangeText={onChange} onBlur={onBlur} placeholder={placeholder} secureTextEntry={secureTextEntry} /> } />
</View>
);
};
LoginScreen.js
import CustomInput from '../../components/CustomInput/CustomInput';
import { useNavigation } from '#react-navigation/native';
import {useForm} from 'react-hook-form';
const LoginScreen = () => {
const {control, handleSubmit, formState: { errors } } = useForm();
const navigation = useNavigation();
const onLoginPressed = (data) => {
console.log(data, errors)
};
const onForgotPasswordPressed = () => {
navigation.navigate('ForgotPassword');
}
const onRegisterPressed = () => {
navigation.navigate('Register')
}
return (
<ScrollView contentContainerStyle={{ flexGrow: 1, justifyContent: 'center' }} showsVerticalScrollIndicator={false}>
<View style={styles.root}>
<Image source={Logo} style={[styles.logo, {height : height * 0.2}]} resizeMode={'contain'} />
//CUSTOM INPUT HOLDING THE CONTROLLER
<CustomInput name='username' placeholder='Username' control={control} />
<CustomInput password='password' placeholder='Password' secureTextEntry={true} control={control}/>
<CustomButton text={loading ? 'Loading...' : 'Login Account'} onPress={handleSubmit(onLoginPressed)} />
</View>
</ScrollView>
);
};
However, If I directly add the controller in the Login Screen Like this (React Hooks form showing undefined username in logs) it works fine
I'm building a simple signup form in react native but it seems some states are not working properly. I know about the fact that states update asynchronously but still, I don't understand what is happening here
Signup component
import React, {useEffect, useState} from "react";
import { StyleSheet, View, Pressable, ScrollView} from "react-native";
import {AntDesign} from '#expo/vector-icons'
import Text from '../components/Text'
import Input from "../components/Input";
import Button from "../components/Button";
import Nope from "../utils/validator";
import type { InputState } from "../components/Input";
interface Props {
}
const initialInputState: InputState = {
errorMsg: '',
isValidated: false,
value: ''
}
const SignUp:React.FC<Props> = () => {
const [email, setEmail] = useState(initialInputState)
const [password, setPassword] = useState(initialInputState)
const [confirmPassword, setConfirmPassword] = useState(initialInputState)
const [isValidationTriggered, setIsValidationTriggered] = useState(false)
const [isValidated, setIsValidated] = useState(false)
function triggerValidation(){
setIsValidationTriggered(true)
}
const handleSubmit = () => {
console.log({isValidated, isValidationTriggered})
if(!isValidated){
console.log('Not validated')
!isValidationTriggered && triggerValidation()
return
}
const formData = {
email: email?.value,
password: password?.value,
confirmPassword: confirmPassword?.value
}
console.log({formData})
}
useEffect(() => {
setIsValidated(
(email?.isValidated &&
password?.isValidated &&
confirmPassword?.isValidated) ?? false
)
}, [email, password, confirmPassword])
useEffect(() => {
console.log(isValidated)
}, [isValidated])
return (
<ScrollView keyboardShouldPersistTaps="handled">
<View style={styles.container}>
<View style={styles.header}>
<Text h1 bold>Sign up</Text>
</View>
<View style={styles.form}>
<Input
placeholder="Email or phone number"
validator={Nope.isEmail().isRequired().validate()}
triggerValidation = {isValidationTriggered}
onChangeState={state => setEmail(state)}
/>
<Input
secure
placeholder="Password"
validator={Nope.isMin(4).isRequired().validate()}
triggerValidation = {isValidationTriggered}
onChangeState={state => setPassword(state)}
/>
<Input
secure
placeholder="Confirm password"
validator={Nope.isEqual(password?.value ?? '', 'Passwords should match').validate()}
triggerValidation = {isValidationTriggered}
onChangeState={state => setConfirmPassword(state)}
/>
<Button
fullWidth
style={styles.topSpace}
disabled={isValidationTriggered? !isValidated : false}
onPress={() => handleSubmit()}
>
<Text bold color="#fff">Sign up</Text>
</Button>
<Button secondary fullWidth style={styles.topSpace}>
<AntDesign name="google" size={24} color="#fff" style={{marginRight: 10}} />
<Text bold color="#fff">Sign in with google</Text>
</Button>
</View>
<View style={styles.footer}>
<Text>Already have an account, </Text>
<Pressable>
<Text bold color="#1263f8">Sign in</Text>
</Pressable>
</View>
</View>
</ScrollView>
)
}
Input component
import React, {useEffect, useRef, useState} from "react";
import { StyleSheet, TextInput, View, ViewStyle} from "react-native";
import type {StyleProp, KeyboardTypeOptions} from 'react-native'
import Text from './Text'
import IconButton from "./IconButton";
import { Ionicons } from '#expo/vector-icons'
interface Props {
placeholder?: string,
secure?: boolean
keyboardType?: KeyboardTypeOptions
style?: StyleProp<ViewStyle>
inputStyle?: {}
solid?:boolean
outlined?:boolean
bind?: [string, React.Dispatch<React.SetStateAction<string>>]
validator?: (text: string) => string
triggerValidation?:boolean
onChangeState?: (state: InputState) => void
}
export interface InputState {
value: string
errorMsg: string
isValidated: boolean
}
const Input:React.FC<Props> = ({
placeholder,
keyboardType,
secure,
style,
inputStyle,
solid,
outlined,
bind,
validator,
triggerValidation,
onChangeState
}) => {
let inputRef = useRef<TextInput | null>(null)
const [value, setValue] = bind ? bind : useState('')
const [errorMsg, setErrorMsg] = useState('')
const [startValidation, setStartValidation] = useState(false)
const [showSecureText, setShowSecureText] = useState(false)
function changeTextHandler(text: string){
setValue(text)
}
function blurHandler(){
if(startValidation === false){
setStartValidation(true)
}
}
useEffect(() => {
if(startValidation && validator !== undefined){
setErrorMsg(validator(value))
}
}, [value, startValidation, validator])
useEffect(() => {
if(triggerValidation && !startValidation){
setStartValidation(true)
}
}, [triggerValidation, startValidation])
useEffect(() => {
if(onChangeState !== undefined){
onChangeState({
value,
errorMsg,
isValidated: /^\s*$/.test(errorMsg) && startValidation
})
}
}, [value, errorMsg, startValidation])
return (
<View style={[styles.inputWrapper, style]}>
<TextInput
placeholder={placeholder}
keyboardType={keyboardType}
secureTextEntry={secure && !showSecureText}
style={[
styles.input,
inputStyle,
(outlined || (!outlined && !solid))&& styles.outlined,
solid && styles.solid,
!/^\s*$/.test(errorMsg) && styles.error
]}
value={value}
onChangeText={changeTextHandler}
onBlur={blurHandler}
ref={ref => inputRef.current = ref}
/>
{
secure
&& (
<IconButton
style={styles.toggleBtn}
onPress={() => {
setShowSecureText(v => !v)
inputRef.current?.focus()
}}
>
{
showSecureText
? <Ionicons name="eye-off" size={24} color="#666" />
: <Ionicons name="eye" size={24} color="#666" />
}
</IconButton>
)
}
{
!/^\s*$/.test(errorMsg)
&& <Text color="#f00" sm style={styles.errorMsg}>{errorMsg}</Text>
}
</View>
)
}
When I submit the form after making sure the isValidated = true it still runs the !isValidated branch
I have a text input inside the tab view in which I have used setState for updating the value of the input.
export function Login(){
const [email, setEmail] = useState();
return(
<View>
<TextInput
onChangeText={setEmail}
value={email}
/>
</View>
)
}
Now every time I called setEmail on onChangeText input gets hides. Is there anything using which I can prevent the keyboard from getting close?
You need to use the synthetic event listener and set the state to the desired value:
import React from "react";
export default function App() {
const [email, setEmail] = React.useState('');
return (
<>
<input onChange={(e) => setEmail(e.target.value)} value={email} />
</>
);
}
This happens when you define a nested component as in the code below:
const ListMenu = () => {
const [searchText, setSearchText] = useState("");
const Header = () => {
return <TextInput
placeholder="Search"
onChangeText={text => { setSearchText(text) }}
/>
}
return (<View>
<Header />
</View>
)}
instead make nested component like this:
const ListMenu = () => {
const [searchText, setSearchText] = useState("");
const header = () => {
return <TextInput
placeholder="Search"
onChangeText={text => { setSearchText(text) }} />
}
return (<View> {header()} </View>)
}
For example, in react-native, I can pass a ref to a regular TextInput and then, through this ref, call the methods:
const inputRef = useRef(null);
const myCallback = () => {
inputRef?.current?.focus();
}
<>
<TouchableOpacity onPress={() => myCallback()}>
<Text>
Press here to focus the input!
</Text>
</TouchableOpacity>
<TextInput
ref={inputRef}
{...props} // Doesn't matter, nothing special
>
</>
So, my question is, how can I create methods on my components, so I can call them from outside of component using ref.
Of course, I'm interested in creating a method in a functional component.
You can use useImperativeHandle hook to expose the methods you need to have with the input element.
Try like this.
import React, { useImperativeHandle, forwardRef, useRef } from "react";
import { Button, StyleSheet, View, TextInput } from "react-native";
const MyTextInput = (props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
doFocus: () => {
inputRef.current.focus();
},
doBlur: () => {
inputRef.current.blur();
}
}));
return <TextInput ref={inputRef} style={props.style} />;
};
const MyCustomTextInput = forwardRef(MyTextInput);
const App = () => {
const myInputRef = useRef();
return (
<View style={styles.app}>
<MyCustomTextInput ref={myInputRef} style={styles.input} />
<View style={styles.button}>
<Button
onPress={() => {
myInputRef?.current?.doFocus();
}}
title="focus"
style={styles.button}
/>
</View>
<View style={styles.button}>
<Button
onPress={() => {
myInputRef?.current?.doBlur();
}}
title="blur"
style={styles.button}
/>
</View>
</View>
);
};
Code sandbox => https://codesandbox.io/s/react-native-web-forked-mnwee?file=/src/App.js