Progressively focus TextInput react native - javascript

I am currently creating a 4 digit otp (one time password) view for a react native app and i need the TextInput to progressively focus when i type a number on the previous input.
I have been able to make the style change (without focus) but the focus doesn't change with it and when i enter another digit the app crashes.
I want it such that when i type in on the first input, the focus move to the next TextInput and so on and on the last input, it focuses on submit.
How do i go about this?
Below is my code
the component
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, ScrollView, View, Alert, Image } from 'react-native';
import { SimpleLinearGradientButton } from '../../components/Buttons';
import { SimpleCard } from '../../components/Cards';
export default function(props) {
const [otp, setOtp] = useState([]);
const [isFocused, setIsFocused] = useState({ focused: true, index: 0 });
const inputs = Array(4).fill(0);
function renderInputs() {
return inputs.map((input, index) => {
return (
<TextInput
style={
isFocused.focused && index === isFocused.index
? styles.inputFieldsFocused
: styles.inputFields
}
key={index}
keyboardType={'numeric'}
onChange={focus}
></TextInput>
);
});
}
function focus(e) {
setOtp(otp.concat(this.value));
setIsFocused({ focused: true, index: isFocused.index + 1 });
isFocused.index ? e.focus() : null;
}
return (
<ScrollView contentContainerStyle={styles.content}>
<View>
<Image
style={styles.image}
source={require('../../../assets/images/verification.png')}
/>
</View>
<View>
<Text>Verification</Text>
<Text>
Enter the 4 digit sent to your email address
</Text>
</View>
<View>
<SimpleCard>
<>{renderInputs()}</>
</SimpleCard>
<SimpleLinearGradientButton
title="Submit"
/>
</View>
</ScrollView>
);
}
The focus function was meant to focus the next input at that index but this doesn't seem to work but the style changes. How do i go about this? Thanks

I suggest to use single hidden TextInput, and render digits in simple View-s, based on input's value. I see no reason to have different inputs for 4 digit code.
However if you need to use different inputs, you have to control their focus states in imperative way (with refs). See focus() method. Also there are some bugs with this method (https://github.com/facebook/react-native/issues/19366)
See docs here https://facebook.github.io/react-native/docs/textinput

tl;dr Set maxLength of textInput to 1 and on onChangeText callback change to next textInput using refs and focus()
This is a straight-forward implementation to understand easily. You can reduce the code a lot better. And I guess you're supposed to implement Backspace to clear the text and go back to previous textinput. I used onKeyPress prop to achieve that.
import React, { Component } from 'react';
import { Text, View, StyleSheet, TextInput } from 'react-native';
import Constants from 'expo-constants';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
one: '',
two: '',
three: '',
oneFocus: false,
twoFocus: false,
threeFocus: false,
};
}
componentDidMount() {
this.refs.one.focus();
}
handleChangeTextOne = (text) => {
this.setState({ one: text }, () => { if (this.state.one) this.refs.two.focus(); });
}
handleChangeTextTwo = (text) => {
this.setState({ two: text }, () => { if (this.state.two) this.refs.three.focus(); });
}
handleChangeTextThree = (text) => {
this.setState({ three: text });
}
backspace = (id) => {
if (id === 'two') {
if (this.state.two) { this.setState({ two: '' }); } else if (this.state.one) { this.setState({ one: '' }); this.refs.one.focus(); }
} else if (id === 'three') {
if (this.state.three) { this.setState({ three: '' }); } else if (this.state.two) { this.setState({ two: '' }); this.refs.two.focus(); }
}
}
render() {
const { oneFocus, twoFocus, threeFocus } = this.state;
const oneStyle = {
borderBottomColor: oneFocus ? 'red' : 'black',
borderBottomWidth: oneFocus ? 2 : 1,
};
const twoStyle = {
borderBottomColor: twoFocus ? 'red' : 'black',
borderBottomWidth: twoFocus ? 2 : 1,
};
const threeStyle = {
borderBottomColor: threeFocus ? 'red' : 'black',
borderBottomWidth: threeFocus ? 2 : 1,
};
return (
<View style={styles.container}>
<View style={styles.inputcontainer}>
<TextInput
ref='one'
style={[styles.textInput, { ...oneStyle }]}
autoCorrect={false}
autoCapitalize='none'
keyboardType='number-pad'
caretHidden
onFocus={() => this.setState({ oneFocus: true })}
onBlur={() => this.setState({ oneFocus: false })}
maxLength={1}
onChangeText={(text) => { this.handleChangeTextOne(text); }}
value={this.state.one}
/>
<TextInput
ref='two'
onKeyPress={({ nativeEvent }) => (
nativeEvent.key === 'Backspace' ? this.backspace('two') : null
)}
style={[styles.textInput, { ...twoStyle }]}
autoCorrect={false}
autoCapitalize='none'
maxLength={1}
onFocus={() => this.setState({ twoFocus: true })}
onBlur={() => this.setState({ twoFocus: false })}
caretHidden
keyboardType='number-pad'
onChangeText={(text) => { this.handleChangeTextTwo(text); }}
value={this.state.two}
/>
<TextInput
ref='three'
onKeyPress={({ nativeEvent }) => (
nativeEvent.key === 'Backspace' ? this.backspace('three') : null
)}
style={[styles.textInput, { ...threeStyle }]}
autoCorrect={false}
autoCapitalize='none'
onFocus={() => this.setState({ threeFocus: true })}
onBlur={() => this.setState({ threeFocus: false })}
maxLength={1}
caretHidden
keyboardType='number-pad'
onChangeText={(text) => { this.handleChangeTextThree(text); }}
value={this.state.four}
/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
inputcontainer: {
height: '5%',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingHorizontal: '20%',
marginBottom: '2%',
},
textInput: {
fontSize: 22,
textAlign: 'center',
paddingVertical: 0,
paddingHorizontal: 0,
width: '12%',
},
});
Run this in snack: https://snack.expo.io/#legowtham/otp-textinput-example-react-native

Related

how do I build a text input in react native that has a placeholder that changes to a textview on top when clicked

I am a bit new to react native and I have an issue I need help with
how do I build a text input in react native that has a placeholder that changes to a text view on top when clicked?
Similar to the screenshot below
empty text input field looks like this in its default state
text field with data entered
see the empty input text has a placeholder appearing in the middle of the input text field
see the second diagram, the place holder text is moved to the top of the input field once the user starts typing text into the input field
The easiest way is to use react-native-paper package with their text input:
import * as React from 'react';
import { TextInput } from 'react-native-paper';
const MyComponent = () => {
const [text, setText] = React.useState('');
return (
<TextInput
label="Email"
value={text}
onChangeText={text => setText(text)}
/>
);
};
export default MyComponent;
Result:
Here is what I use without any library, working example and if you want to add animation to the fields working example with animation
import React, { Component } from 'react';
import {
View,
StatusBar,
TextInput,
Text,
} from 'react-native';
class FloatingLabelInput extends Component {
state = {
isFocused: false,
};
handleFocus = () => this.setState({ isFocused: true });
handleBlur = () => this.setState({ isFocused: false });
render() {
const { label, ...props } = this.props;
const { isFocused } = this.state;
const labelStyle = {
position: 'absolute',
left: 0,
top: !isFocused ? 18 : 0,
fontSize: !isFocused ? 20 : 14,
color: !isFocused ? '#aaa' : '#000',
};
return (
<View style={{ paddingTop: 18 }}>
<Text style={labelStyle}>
{label}
</Text>
<TextInput
{...props}
style={{ height: 26, fontSize: 20, color: '#000', borderBottomWidth: 1, borderBottomColor: '#555' }}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
blurOnSubmit
/>
</View>
);
}
}
export default class App extends Component {
state = {
value: '',
};
handleTextChange = (newText) => this.setState({ value: newText });
render() {
return (
<View style={{ flex: 1, padding: 30, backgroundColor: '#f5fcff' }}>
<StatusBar hidden />
<FloatingLabelInput
label="Email"
value={this.state.value}
onChangeText={this.handleTextChange}
/>
</View>
);
}
}

React native `input` must be a function received undefined

I have an array which displays a list of jobs with the radio button(each).If I click any radio button I want to get its value so far I'm getting an error input must be a function received undefined. how can I achieve something like this in react native?
Code
import React, {useState} from 'react';
import {View, Text, TextInput, Image,Button,Body,Pressable,ScrollView,Switch} from 'react-native';
export default function Home({ navigation}){
let roles = navigation.getParam('roles')
const [isEnabled, setIsEnabled] = useState(false);
const toggleSwitch = () => setIsEnabled(previousState => !previousState);
const handleClick = (data, e) => {
alert(e.target.value, data);
}
<View>
{
Object.keys(roles).map((key) => {
return (
<View>
<View style={styles.XYQHC}>
<Text>{roles[key]['name']}</Text>
<Switch
trackColor={{ false: "#767577", true: "#81b0ff" }}
thumbColor={isEnabled ? "#f5dd4b" : "#f4f3f4"}
ios_backgroundColor="#3e3e3e"
onValueChange={toggleSwitch}
value={isEnabled}
/>
<input type="radio" name="roles" value={roles[key]['name']} defaultChecked={roles[key]['id'] == 3 ? true : false } onClick={handleClick.bind(this, roles[key]['name'])}/>
</View>
</View>
)
})
}
</View>
};
I had to edit a little bit to make it work and to assume some input data but here is a working example of your code:
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
function RadioButton({ label, onPress, selected, style }) {
return (
<TouchableOpacity
style={[
{
flexDirection: 'row',
alignItems: 'center',
marginLeft: 12,
marginVertical: 20,
},
style,
]}
onPress={onPress}>
<View
style={{
height: 24,
width: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: '#000',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
}}>
{selected ? (
<View
style={{
height: 12,
width: 12,
borderRadius: 6,
backgroundColor: '#000',
}}
/>
) : null}
</View>
<Text>{label}</Text>
</TouchableOpacity>
);
}
export default function Home({ navigation }) {
// let status = navigation.getParam('status');
let status = 2;
// let roles = navigation.getParam('roles');
let roles = [
{
id: 1,
name: 'lawyer',
},
{
id: 2,
name: 'farmer',
},
{
id: 3,
name: 'architect',
},
];
const [isEnabled, setIsEnabled] = useState(false);
const [chosenRole, setChosenRole] = useState(false);
useEffect(() => {
roles.find((role) => {
if (role.id === status) {
setChosenRole(role);
return true;
}
return false;
});
}, []);
const handleClick = (data) => {
console.log(data);
setChosenRole(data);
};
return (
<View style={{ marginTop: 30 }}>
{roles.map((role) => {
return (
<View>
<RadioButton
label={role.name}
selected={role.name === chosenRole.name}
onPress={() => handleClick(role)}
/>
</View>
);
})}
</View>
);
}

React Native Redux causing whole page to render upon calling function

I have an issue with React Native Redux where I am calling a Redux action from the onChangeText function from inside a TextField. Normally, an update like this would only cause the TextField itself to update, however the whole page is being re-rendered upon each call of the function.
Here is the code for the TextField. The method in question is this.props.updateURL(value)
<TextField label='Server'
value={this.props.profile.url}
labelFontSize={textFont(16)}
tintColor='#B3B2B6'
autoCapitalize="none"
autoCorrect={false}
returnKeyType={'next'}
blurOnSubmit={false}
autoFocus={!this.props.profile.edited}
style={styles.inputStyle}
selectionColor={CUSRSOR_COLOR}
editable={!this.props.profile.edited}
onChangeText={async value => {
this.props.updateURL(value)
}}
renderAccessory={()=>{
return(<TouchableOpacity onPress={this.handleScanPress} >
<Image source={ImgScan} style={{height: 25, width: 25, overflow:'visible', marginBottom:20, marginRight: 10}}/>
</TouchableOpacity>);
}}
>
</TextField>
This is for the Redux methods mapStateToProps and mapDispatchToProps:
const mapStateToProps = (state) => {
return {
profile : state.profileReducer,
deviceUid: state.GlobalReducer.deviceUid,
locationsList: state.LogsReducer.locationList,
errorList: state.ErrorsReducer.failedList,
watermarkText: state.GlobalReducer.watermarkText,
alertPresent: state.LogsReducer.alertPresent
}
};
const mapDispatchToProps = (dispatch) => {
return {
updateURL : (url) => dispatch(updateURL(url)),
updateUserName : (userName) => dispatch(updateUserName(userName)),
updatePassword : (password) => dispatch(updatePassword(password)),
updateDeviceName : (deviceName) => dispatch(updateDeviceName(deviceName)),
allFieldsEntered : (url,userName,password) => dispatch(allFieldsEntered(url,userName,password)),
loginDevice: (url,userName,password,deviceName,deviceId,navigation,existingUser) => dispatch(callLoginApi(url,userName,password,deviceName,deviceId,navigation,existingUser)),
updateFailureMessage: (message) => dispatch(updateFailureMessage(message)),
showLoader: () => dispatch(showLoader()),
hideLoader: () => dispatch(hideLoader()),
updateRegistrationSuccess: (value) => dispatch(updateRegistrationSuccess(value)),
checkForInternet: () => dispatch(checkConnectivity()),
getProfileDetails: async(isToken) => await dispatch(fetchProfileDetails(isToken)),
updateLocationSwitch: (value) => dispatch(updateLocationStatus(value)),
updateIsEdit: (value) => dispatch(updateIsEdit(value)),
storeRegistrationFlagInAsync: () => dispatch(storeRegistrationFlagInAsync()),
isDeviceConnected: (value) => dispatch(isDeviceConnected(value)),
requestLocationPermissions: () => dispatch(requestLocationPermissions()),
passwordChanged: (value) => dispatch(updateIsPasswordChanged(value)),
isEditPage: (value) => dispatch(isEditPage(value)),
getDeviceName: () => dispatch(getDeviceName()),
shouldEnableLocation: (value) => dispatch(shouldEnableLocation(value)),
fetchLocationList: () => dispatch(fetchLocations()),
syncRecords: (url,userName,password,locationList,deviceId,isFromError,shouldPresentErrorMsg) => dispatch(syncFailedRecords(url,userName,password,locationList,deviceId,isFromError,shouldPresentErrorMsg)),
fetchFailedRecords: (shouldShowLoader) => dispatch(fetchFailedRecords(shouldShowLoader)),
updateDeviceAction: (action,deviceId,url) => dispatch(performDeviceAction(action,deviceId,url)),
callLogoutApi: (userName,password) => dispatch(callLogoutApi(userName,password)),
syncDeviceActions: () => dispatch(syncDeviceActions()),
syncPendingUpdates: (url,userName,password,deviceId) => dispatch(syncPendingUpdates(url,userName,password,deviceId)),
fetchPendingLocationsList: () => dispatch(fetchPendingLocationsList()),
showPasswordChangeAlert: (alertFlag,navigation) => dispatch(handleUIForPasswordChange(alertFlag,navigation)),
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Profile);
Here are the reducers:
const initialState = {
url: '',
userName : '',
password : '',
deviceName : '',
resourceCode : '',
isDeviceConnected : false,
allFieldsFilled: false,
token: '',
gpsTrackingInterval : 5,
apiPostingInterval : 30,
failureMessage : '',
registered : false,
edited: false,
editPage: false,
isConnected: true,
shouldEnableLocation: false,
resourceDesc: '',
adminEmail: '',
companyName: ''
}
const profileReducer = (state=initialState,action) => {
switch (action.type) {
case actionTypes.UPDATE_URL :
return {
...state,
url: action.payload
}
...
}
This is the Redux action:
export const updateURL = (url) => {
return {
type: actionTypes.UPDATE_URL,
payload: url
};
};
Any help would be much appreciated.
import React, { Component } from 'react';
import {
StyleSheet, Text, View, TextInput, TouchableOpacity, Image,
KeyboardAvoidingView, ScrollView, SafeAreaView, Linking
} from 'react-native';
import { COLOR_BLACK, COLOR_GRAY_9, COLOR_TRANSPARENT, COLOR_GRAY_4 } from '../../../constants/colors';
import ImgProntoLogo from '../../../assets/images/pronto-logo.png';
import ImgPasswordVisibleIcon from '../../../assets/images/visibility.png';
import ImgUrl from '../../../assets/images/url.png';
import ImgBarcode from '../../../assets/images/scan-barcode.png';
import ImgUsernameIcon from '../../../assets/images/username.png';
import ImgPasswordIcon from '../../../assets/images/password.png';
import ImgPasswordHide from '../../../assets/images/password-hide.png';
import GlobalStyleSheet from '../../../constants/styles';
import {
PASSWORD, USERNAME, URL_CONNECT
} from '../../../constants/strings';
import { StackActions, NavigationActions } from 'react-navigation';
import Sync from '../../../utils/Syncing';
import { AsyncStorage } from 'react-native';
export default class Configuration extends Component {
constructor(props) {
super(props)
this.handleOpenURL = this.handleOpenURL.bind(this);
this.handleScanPress = this.handleScanPress.bind(this);
}
async componentWillMount() {
this.props.clearConfiguration();
this.props.checkConnectivity();
Linking.addEventListener('url', this.handleOpenURL);
Sync.removeDataFromUserTable();
Sync.removeDataFromCompanyTable();
}
componentDidMount() {
console.log("COMPONENT MOUNTING");
Linking.getInitialURL().then((url) => {
if (url) {
url = url.substring(18);
this.props.updateURL(url);
}
}).catch(err => console.error('An error occurred', err))
}
componentWillReceiveProps(props) {
console.log("WILL RECEIVE")
}
handleOpenURL = (event) => {
this.props.updateURL(event.url.substring(18))
}
copyRights() {
var currentYear = new Date().getFullYear().toString();
return '© '.concat(currentYear).concat(' Pronto Software Limited.All Rights Reserved.');
}
// Connect the device to the pronto connect
async connectDevice() {
this.props.showLoader();
this.props.resetConnectPasswordVisibility();
await this.props.connectDevice(this.props.configuration.url,
this.props.configuration.username, this.props.configuration.password);
let isDeviceConnected = await AsyncStorage.getItem("isConnected");
if (isDeviceConnected === "true") {
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Register' })],
});
this.props.navigation.dispatch(resetAction);
}
this.props.hideLoader();
}
static navigationOptions = {
header: null
}
handleScanPress() {
const action = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Scanner' })],
});
this.props.navigation.push('Scanner');
}
render() {
return (
<SafeAreaView style={GlobalStyleSheet.container}>
<KeyboardAvoidingView style={GlobalStyleSheet.container} >
<View style={styles.headerLogoView}>
<Image source={ImgProntoLogo} style={styles.logo} />
</View>
<ScrollView keyboardShouldPersistTaps="handled" style={styles.scrollViewStyle}>
<View style={{ flex: 1, height: '100%' }}>
<View style={styles.viewContainer}>
<Text style={[GlobalStyleSheet.textHeading4, GlobalStyleSheet.marginTop10,
GlobalStyleSheet.marginBottom10]}>CONFIGURE DEVICE</Text>
<View style={[GlobalStyleSheet.textInputView, GlobalStyleSheet.marginBottom10]}>
<Image style={GlobalStyleSheet.iconInputBar} source={ImgUrl} />
<TextInput
underlineColorAndroid={COLOR_TRANSPARENT}
style={GlobalStyleSheet.textInput}
autoCapitalize='none'
placeholder={URL_CONNECT}
value={this.props.configuration.url}
onChangeText={(value) => { this.props.updateURL(value) }}
editable={!(this.props.configuration.isDeviceConnected)}
/>
<TouchableOpacity style={[GlobalStyleSheet.passwordTouchable]} onPress={this.handleScanPress}>
<Image style={[GlobalStyleSheet.passwordShowHideIcon]} source={ImgBarcode} />
</TouchableOpacity>
</View>
<View style={[GlobalStyleSheet.textInputView, GlobalStyleSheet.marginBottom10]}>
<Image style={GlobalStyleSheet.iconInputBar} source={ImgUsernameIcon} />
<View style={[GlobalStyleSheet.flexRow, { flex: 1 }]}>
<TextInput
placeholder={USERNAME}
underlineColorAndroid={COLOR_TRANSPARENT}
style={GlobalStyleSheet.textInput}
onChangeText={(value) => { this.props.updateUsername(value) }}
editable={!(this.props.configuration.isDeviceConnected)}
/>
</View>
</View>
<View style={[GlobalStyleSheet.textInputView, GlobalStyleSheet.marginBottom10]} >
<Image style={[GlobalStyleSheet.iconInputBar]} source={ImgPasswordIcon} />
<View style={[GlobalStyleSheet.flexRow, { flex: 1 }]}>
<TextInput
placeholder={PASSWORD}
underlineColorAndroid={COLOR_TRANSPARENT}
contextMenuHidden={true}
style={GlobalStyleSheet.textInput}
autoCapitalize='none'
onChangeText={(value) => { this.props.updatePassword(value) }}
editable={!(this.props.configuration.isDeviceConnected)}
secureTextEntry={!(this.props.configuration.passwordVisibility)} />
<TouchableOpacity style={[GlobalStyleSheet.passwordTouchable]}
onPress={() => {
if (!this.props.configuration.isDeviceConnected) {
this.props.togglePasswordVisibility(this.props.configuration.passwordVisibility)
}
}}>
{
this.props.configuration.passwordVisibility ?
(<Image style={[GlobalStyleSheet.passwordShowHideIcon]} source={ImgPasswordVisibleIcon} />) :
(<Image style={[GlobalStyleSheet.passwordShowHideIcon]} source={ImgPasswordHide} />)
}
</TouchableOpacity>
</View>
</View>
{
this.props.configuration.isDeviceConnected ?
(
<TouchableOpacity style={GlobalStyleSheet.touchableBarProcessed} disabled={true} >
<Text style={GlobalStyleSheet.textTouchableBarProcessed}>CONNECTED</Text>
</TouchableOpacity>
) :
(this.props.configuration.url != "" &&
this.props.configuration.username != "" && this.props.configuration.password != "" ?
(
<TouchableOpacity style={[GlobalStyleSheet.touchableBarEnabled]}
onPress={async () => { await this.connectDevice() }}>
<Text style={GlobalStyleSheet.textTouchableBarEnabled}>CONNECT</Text>
</TouchableOpacity>
) :
(
<TouchableOpacity style={[GlobalStyleSheet.touchableBarDisabled]} disabled={true}>
<Text style={[GlobalStyleSheet.textTouchableBarDisabled]}>CONNECT</Text>
</TouchableOpacity>
)
)
}
</View>
</View>
<View style={styles.copyRightsView}>
<Text style={GlobalStyleSheet.copyRightsText}>{this.copyRights()}</Text>
</View>
</ScrollView>
</KeyboardAvoidingView >
</SafeAreaView >
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
height: '100%',
width: '100%',
backgroundColor: COLOR_GRAY_9,
},
headerLogoView: {
height: 50,
backgroundColor: COLOR_GRAY_9,
elevation: 5,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 2,
shadowOffset: { width: 2, height: 2, },
shadowColor: COLOR_BLACK,
shadowOpacity: 0.1,
},
logo: {
height: 40,
width: '80%',
resizeMode: 'contain',
},
viewContainer: {
marginLeft: 15,
marginRight: 15,
paddingBottom: 10
},
copyRightsView: {
alignItems: 'center',
minHeight: 40,
paddingBottom: 5,
},
scrollViewStyle: {
flex: 1,
height: '100%',
paddingTop: 10
},
});

How can I switch to the second input when first will be done in React Native

I am creating a Taxi Application just like Uber where user enters two locations in given inputs (Pickup and Dropoff), I done with the PickUp when User enters a location it shows the googleautocomplete listview for location and can select the pickup location, but I am stuch with the Dropoff when I write something in the second input it changes the first too or when I click the clear icon it clears text from both inputs. So what do I have to do to solve this problem kindly help please Thanks.
AutoCompeleteInputs.js
import React from 'react';
import { TextInput, View, StyleSheet, Animated, TouchableOpacity } from "react-native";
import AutoCompleteListView from "./AutoCompleteListView";
import Events from "react-native-simple-events";
import debounce from '../utils/debounce'
import fetch from 'react-native-cancelable-fetch';
import MaterialIcons from "react-native-vector-icons/MaterialIcons";
const AUTOCOMPLETE_URL = "https://maps.googleapis.com/maps/api/place/autocomplete/json";
const REVRSE_GEO_CODE_URL = "https://maps.googleapis.com/maps/api/geocode/json";
export default class AutoCompleteInput extends React.Component {
static defaultProps = {
language: 'en'
};
constructor(props) {
super(props);
}
componentWillUnmount() {
this._abortRequest();
}
state = {
predictions: [],
loading: false,
inFocus: false,
};
_abortRequest = () => {
fetch.abort(this);
};
fetchAddressForLocation = (location) => {
this.setState({ loading: true, predictions: [] });
let { latitude, longitude } = location;
fetch(`${REVRSE_GEO_CODE_URL}?key=${API_KEY}&latlng=${latitude},${longitude}`, null, this)
.then(res => res.json())
.then(data => {
this.setState({ loading: false });
let { results } = data;
if (results.length > 0) {
let { formatted_address } = results[0];
this.setState({ text: formatted_address });
}
});
}
_request = (text) => {
this._abortRequest();
if (text.length >= 3) {
fetch(`${AUTOCOMPLETE_URL}?input=${encodeURIComponent(text)}&key=${API_KEY}&language=${this.props.language}`, null, this)
.then(res => res.json())
.then(data => {
let { predictions } = data;
this.setState({ predictions });
});
} else {
this.setState({ predictions: [] });
}
}
_onChangeText = (text) => {
this._request(text);
this.setState({ text });
}
_onFocus = () => {
this._abortRequest();
this.setState({ loading: false, inFocus: true });
Events.trigger('InputFocus');
}
_onBlur = () => {
this.setState({ inFocus: false });
Events.trigger('InputBlur');
}
blur = () => {
this._input.blur();
}
_onPressClear = () => {
this.setState({ text: '', predictions: [] });
}
_getClearButton = () => {
return this.state.inFocus ?
(<TouchableOpacity style={styles.btn} onPress={this._onPressClear}>
<MaterialIcons name={'clear'} size={20} />
</TouchableOpacity>) : null;
}
getAddress = () => {
return this.state.loading ? '' : this.state.text;
}
render() {
return (
<Animated.View style={this.props.style}>
<View style={{ flexDirection: 'column' }}>
<View
style={styles.textInputPickupContainer}
elevation={5}>
<TextInput
ref={input => this._input = input}
value={this.state.loading ? 'Loading...' : this.state.text}
style={styles.textInput}
underlineColorAndroid={'transparent'}
placeholder={'Pickup'}
onFocus={this._onFocus}
onBlur={this._onBlur}
onChangeText={this._onChangeText}
outlineProvider='bounds'
autoCorrect={false}
spellCheck={false} />
{this._getClearButton()}
</View>
<View
style={styles.textInputDropoffContainer}
elevation={5}>
<TextInput
ref={input => this._input = input}
value={this.state.loading ? 'Loading...' : this.state.dropOff}
style={styles.textInput}
underlineColorAndroid={'transparent'}
placeholder={'Dropoff'}
onFocus={this._onFocus}
onBlur={this._onBlur}
onChangeText={(text) => this.setState({dropOff: text})}
outlineProvider='bounds'
autoCorrect={false}
spellCheck={false} />
{this._getClearButton()}
</View>
</View>
<View style={styles.listViewContainer}>
<AutoCompleteListView
predictions={this.state.predictions} />
</View>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
textInputPickupContainer: {
flexDirection: 'row',
height: 40,
zIndex: 10,
paddingLeft: 10,
borderRadius: 5,
backgroundColor: 'white',
shadowOffset: {
width: 0,
height: 2
},
shadowRadius: 2,
alignItems: 'center'
},
textInputDropoffContainer: {
flexDirection: 'row',
height: 40,
zIndex: 10,
paddingLeft: 10,
marginTop: 10,
borderRadius: 5,
backgroundColor: 'white',
shadowOffset: {
width: 0,
height: 2
},
shadowRadius: 2,
alignItems: 'center'
},
textInput: {
flex: 1,
fontSize: 17,
color: '#404752'
},
btn: {
width: 30,
height: 30,
padding: 5,
justifyContent: 'center',
alignItems: 'center'
},
listViewContainer: {
paddingLeft: 3,
paddingRight: 3,
paddingBottom: 3
},
});
SearchRoute.js(Main)
return (
<View style={styles.container}>
<MapView
ref={(mapView) => this._map = mapView}
style={styles.mapView}
region={this.state.region}
showsMyLocationButton={true}
showsUserLocation={false}
onPress={({ nativeEvent }) => this._setRegion(nativeEvent.coordinate)}
onRegionChange={this._onMapRegionChange}
onRegionChangeComplete={this._onMapRegionChangeComplete}
/>
<Entypo name={'location-pin'} size={30}
color={this.props.markerColor}
style={{ backgroundColor: 'transparent' }} />
<View style={styles.fullWidthContainer}>
<AutoCompleteInput
ref={input => this._input = input}
apiKey={API_KEY}
style={[styles.input, { transform: [{ scale: inputScale }] }]}
debounceDuration={this.props.debounceDuration}
/>
</View>
The error is that you are assigning the same value to both TextInputs, so if you change one, the other TextInput will change too to the same value.
On your AutoCompeleteInputs.js change dropOff TextInput's value to
<TextInput
...
value={this.state.loading ? 'Loading...' : this.state.dropOff}
...
/>
So when you change dropOff value, the pickUp point will be the same you entered before, and if you clear one the other will not change.
Also change dropOff onChangeText={this._onChangeText} to onChangeText={(text) => this.setState({dropOff: text}) to ensure that you are storing the correct value.

How to autofocus next TextInput on react-native

I'm trying to create a passcode protected screen. The screen will uses 4 numeric input as the passcode.
The way I'm doing this is create a TextInput Component and call it 4 times in my main screen.
The problem I'm having is the TextInputs will not focus on the next one as I type the value of the previous TextInput.
I'm using refs for all PasscodeTextInput component (I've been informed that it is a legacy method but I do not know any other way, alas).
Tried this method(without creating my own component), no luck too.
METHOD
index.ios.js
import React, { Component } from 'react';
import { AppRegistry, TextInput, View, Text } from 'react-native';
import { PasscodeTextInput } from './common';
export default class ProgressBar extends Component {
render() {
const { centerEverything, container, passcodeContainer, textInputStyle} = styles;
return (
<View style={[centerEverything, container]}>
<View style={[passcodeContainer]}>
<PasscodeTextInput
autoFocus={true}
ref="passcode1"
onSubmitEditing={(event) => { this.refs.passcode2.focus() }} />
<PasscodeTextInput
ref="passcode2"
onSubmitEditing={(event) => { this.refs.passcode3.focus() }} />
<PasscodeTextInput
ref="passcode3"
onSubmitEditing={(event) => { this.refs.passcode4.focus() }}/>
<PasscodeTextInput
ref="passcode4" />
</View>
</View>
);
}
}
const styles = {
centerEverything: {
justifyContent: 'center',
alignItems: 'center',
},
container: {
flex: 1,
backgroundColor: '#E7DDD3',
},
passcodeContainer: {
flexDirection: 'row',
},
}
AppRegistry.registerComponent('ProgressBar', () => ProgressBar);
PasscodeTextInput.js
import React from 'react';
import {
View,
Text,
TextInput,
Dimensions
} from 'react-native';
const deviceWidth = require('Dimensions').get('window').width;
const deviceHeight = require('Dimensions').get('window').height;
const PasscodeTextInput = ({ ref, autoFocus, onSubmitEditing, onChangeText, value}) => {
const { inputStyle, underlineStyle } = styles;
return(
<View>
<TextInput
ref={ref}
autoFocus={autoFocus}
onSubmitEditing={onSubmitEditing}
style={[inputStyle]}
maxLength={1}
keyboardType="numeric"
placeholderTextColor="#212121"
secureTextEntry={true}
onChangeText={onChangeText}
value={value}
/>
<View style={underlineStyle} />
</View>
);
}
const styles = {
inputStyle: {
height: 80,
width: 60,
fontSize: 50,
color: '#212121',
fontSize: 40,
padding: 18,
margin: 10,
marginBottom: 0
},
underlineStyle: {
width: 60,
height: 4,
backgroundColor: '#202020',
marginLeft: 10
}
}
export { PasscodeTextInput };
Update 1
index.ios.js
import React, { Component } from 'react';
import { AppRegistry, TextInput, View, Text } from 'react-native';
import { PasscodeTextInput } from './common';
export default class ProgressBar extends Component {
constructor() {
super()
this.state = {
autoFocus1: true,
autoFocus2: false,
autoFocus3: false,
autoFocus4: false,
}
}
onTextChanged(t) { //callback for immediate state change
if (t == 2) { this.setState({ autoFocus1: false, autoFocus2: true }, () => { console.log(this.state) }) }
if (t == 3) { this.setState({ autoFocus2: false, autoFocus3: true }, () => { console.log(this.state) }) }
if (t == 4) { this.setState({ autoFocus3: false, autoFocus4: true }, () => { console.log(this.state) }) }
}
render() {
const { centerEverything, container, passcodeContainer, testShit, textInputStyle } = styles;
return (
<View style={[centerEverything, container]}>
<View style={[passcodeContainer]}>
<PasscodeTextInput
autoFocus={this.state.autoFocus1}
onChangeText={() => this.onTextChanged(2)} />
<PasscodeTextInput
autoFocus={this.state.autoFocus2}
onChangeText={() => this.onTextChanged(3)} />
<PasscodeTextInput
autoFocus={this.state.autoFocus3}
onChangeText={() => this.onTextChanged(4)} />
<PasscodeTextInput
autoFocus={this.state.autoFocus4} />
</View>
</View>
);
}
}
const styles = {
centerEverything: {
justifyContent: 'center',
alignItems: 'center',
},
container: {
flex: 1,
backgroundColor: '#E7DDD3',
},
passcodeContainer: {
flexDirection: 'row',
},
}
AppRegistry.registerComponent('ProgressBar', () => ProgressBar);
There is a defaultProp for TextInput where one can focus after component mounted.
autoFocus
If true, focuses the input on componentDidMount, the default value is false. for more information please read the related Docs.
UPDATE
After componentDidUpdate it won't work properly. In that case, one can use ref to focus programmatically.
You cannot forward the ref to <TextInput> using that way because ref is one of the special props. Thus, calling this.refs.passcode2 will return you <PasscodeTextInput> instead.
Try change to the following to get the ref from <TextInput>.
PasscodeTextInput.js
const PasscodeTextInput = ({ inputRef, ... }) => {
...
return (
<View>
<TextInput
ref={(r) => { inputRef && inputRef(r) }}
...
/>
</View>
...
);
}
Then, assign the inputRef from <PasscodeTextInput> to a variable and use focus() to switch focus (it is not deprecated as of RN 0.41.2).
index.ios.js
return (
<PasscodeTextInput
autoFocus={true}
onChangeText={(event) => { event && this.passcode2.focus() }} />
<PasscodeTextInput
inputRef={(r) => { this.passcode2 = r }}
onChangeText={(event) => { event && this.passcode3.focus() }} />
<PasscodeTextInput
inputRef={(r) => { this.passcode3 = r }}
onChangeText={(event) => { event && this.passcode4.focus() }} />
<PasscodeTextInput
inputRef={(r) => { this.passcode4 = r }} />
);
P.S: event && this.passcode2.focus() prevents focus is switched when trying to clear the old passcode and enter a new one.
we handled this style of screen with a different approach.
Rather than manage 4 individual TextInputs and handle the navigation of focus across each one (and then back again when the user deletes a character), we have a single TextInput on screen but is invisible (ie. 0px x 0px) wide which has the focus, maxLength and keyboard configuration, etc.
This TextInput takes input from the user but can't actually been seen, as each character is typed in we render the entered text as a series simple View/Text elements, styled much similar to your screen above.
This approach worked well for us with no need to manage what the 'next' or 'previous' TextInput to focus next to.
You can use focus method onChangeText as Jason stated, in addition to that adding maxLength={1} can make you jump to the next input immediately without checking what's added. (just noticed its deprecated, but still this is how I solved my problem, and should do fine until v0.36, and this link explains how you should update the deprecated function).
<TextInput
ref="first"
style={styles.inputMini}
maxLength={1}
keyboardType="numeric"
returnKeyType='next'
blurOnSubmit={false}
placeholderTextColor="gray"
onChangeText={(val) => {
this.refs['second'].focus()
}}
/>
<TextInput
ref="second"
style={styles.inputMini}
maxLength={1}
keyboardType="numeric"
returnKeyType='next'
blurOnSubmit={false}
placeholderTextColor="gray"
onChangeText={(val) => {
this.refs['third'].focus()
}}
/>
...
Please notice that my use of refs are deprecated too, but I've just copied the code since I can guarantee you that was working back then (hopefully works now too).
Finally, the main issue with this type of implementation is, once you try to remove a number with backspace your focus will jump to next one, causing serious UX issues. However, you can listen for backspace key entry and perform something different instead of focusing to next input. So I'll leave a link here for you to further investigate if you choose to use this type of implementation.
Hacky Solution to Previously Described Issue: If you check what's entered in onChangeText prop before doing anything, you can jump to next input if the value is a number, else (that's a backspace), jump back. (Just came up with this idea, I haven't tried it.)
I think the issue is that onSubmitEditing is when you hit the "return" or "enter" key on the regular keyboard... there is not one of those buttons on the keypad.
Assuming you want each input to only have one character, you could look at the onChangeText and then check if text has length 1 and call focus if the length is indeed 1.
<TextInput
ref={input => {
this.nameOrId = input;
}}
/>
<TouchableOpacity
onPress={()=>{
this.nameOrId.focus()
}}
>
<Text>Click</Text>
</TouchableOpacity>
I solve with this code:
const VerifyCode: React.FC = ({ pass, onFinish }) => {
const inputsRef = useRef<Input[] | null[]>([]);
const [active, setActive] = useState<number>(0);
const onKeyPress = ({ nativeEvent }:
NativeSyntheticEvent<TextInputKeyPressEventData>) => {
if (nativeEvent.key === "Backspace") {
if (active !== 0) {
inputsRef.current[active - 1]?.focus();
return setActive(active - 1);
}
} else {
inputsRef.current[active + 1]?.focus();
return setActive(active + 1);
}
return null;
};
return (
<View style={styles.container}>
<StyledInput
onKeyPress={onKeyPress}
autoFocus={active === 0}
ref={(r) => {
inputsRef.current[0] = r;
}}
/>
<StyledInput
onKeyPress={onKeyPress}
autoFocus={active === 1}
ref={(r) => {
inputsRef.current[1] = r;
}}
/>
<StyledInput
onKeyPress={onKeyPress}
autoFocus={active === 2}
ref={(r) => {
inputsRef.current[2] = r;
}}
/>
<StyledInput
onKeyPress={onKeyPress}
autoFocus={active === 3}
ref={(r) => {
inputsRef.current[3] = r;
}}
/>
</View>
);
};
export default VerifyCode;
I put one ref in all the inputs, and when the onKeyPress fire, the function verify if have to go back or go to next input
Solved it by removing autoFocus={true} and setting timeout.
I have a popup as a functional component and using "current.focus()" with Refs like this:
const Popup = ({ placeholder, autoFocus, showStatus, }) => { const inputRef = useRef(null); useEffect(() => {
Platform.OS === 'ios'
? inputRef.current.focus()
: setTimeout(() => inputRef.current.focus(), 40); }, [showStatus]); return (
<View style={styles.inputContainer}>
<TextInput
style={styles.inputText}
defaultValue={placeholder}
ref={inputRef}
/>
</View> };

Categories