Using setState in onFocus removes the focus - javascript

I found that the convention to changing css of TextInput is by changing the component's state using onFocus and onBlur props.
I learned it from this post.
However, though successful I had to do some ugly workaround:
I moved the state management to my form component instead of putting it in my TextInput wrapper
I had to implement a hack componentDidUpdate() to refocus the TextInput, something like this
componentDidUpdate() {
this.props.isActive ? setTimeout(() => this.input.focus(), 100) : null
}
Now everything is working as expected, except that the keyboard would flicker upon moving to the next TextInput by using onSubmitEditing props.
Here's my code snippet related to this case:
in my form component I instantiate the input wrapper component like so within a loop, i.e. fields.map()
<AuthTextInput
ref={ref => inputRefs[key] = ref}
key={key}
label={label}
onFocus={() => this.setState({ currentInput: key })}
isActive={this.state.currentInput === key}
onSubmitEditing={() => (idx + 1) < fields.length && fields[idx + 1].type !== 'selection' ? inputRefs[fields[idx + 1].key].focus() : this.setState({ currentInput: null })}
blurOnSubmit={(idx + 1) === fields.length || fields[idx + 1].type === 'selection'}
returnKeyType={(idx + 1) === fields.length || fields[idx + 1].type === 'selection' ? 'done' : 'next'}
autoCapitalize={autocaps}
secureTextEntry={secureTextEntry}
placeholder={placeholder}
keyboardType={keyboard}
containerStyle={[{ width: orientation() === 'landscape' ? 0.5 * windowWidth() : windowWidth() * 0.7, height: normalize(70), marginVertical: normalize(10) }]}
leftIcon={<Image style={{ width: normalize(50), height: normalize(50), marginTop: 25 }} source={fieldIcon} />}
onChangeText={(text) => this.onChange(key, text)}
value={this.state[key] ? this.state[key]['value'] : ''}
error={this.state.error[key]}
/>
The content of AuthTextInput is like so:
import React, { Component } from 'react';
import { View, StyleSheet, TextInput, Text } from 'react-native';
import { Input } from 'react-native-elements';
import { isEmpty } from '../utils/validate';
import { windowWidth, fontSize, fontFamily, normalize, color } from '../theme/baseTheme';
import IconWrapper from './IconWrapper';
const styles = StyleSheet.create({
container: {
flex: 1
},
inputContainer: {
borderBottomWidth: 0,
},
inputStyle: {
fontSize: fontSize.regular + 2,
fontFamily: fontFamily.bold,
paddingLeft: normalize(15),
borderBottomWidth: 1
},
errorStyle: {
color: color.red,
fontSize: fontSize.small - 4,
fontFamily: fontFamily.bold,
fontWeight: 'bold',
marginLeft: normalize(75)
},
focusedContainer: {
borderWidth: 1,
borderColor: color.light_blue,
borderRadius: 8
}
});
class AuthTextInput extends Component {
constructor(props) {
super(props);
this.state = {
secureText: this.props.secureTextEntry,
}
}
componentDidUpdate() {
this.props.isActive ? setTimeout(() => this.input.focus(), 100) : null
}
focus() {
this.input.focus();
}
render() {
const { secureTextEntry, value, containerStyle, isActive } = this.props;
return (
<Input
{...this.props}
ref={ref => this.input = ref}
disableFullscreenUI={true}
secureTextEntry={this.state.secureText}
containerStyle={[styles.container, containerStyle, isActive ? styles.focusedContainer : null]}
inputContainerStyle={styles.inputContainer}
inputStyle={styles.inputStyle}
rightIcon={
secureTextEntry && value !== '' ?
this.state.secureText ?
<IconWrapper name="visibility" size={20} color={color.light_grey} style={{ justifyContent: 'center' }} onPress={() => this.setState({ secureText: false })} />
:
<IconWrapper name="visibility-off" size={20} color={color.light_grey} style={{ justifyContent: 'center' }} onPress={() => this.setState({ secureText: true })} />
:
null
}
errorMessage={!isEmpty(this.props.error) ? this.props.error : null}
errorStyle={[styles.errorStyle, this.props.errorStyle]}
/>
);
}
}
export default AuthTextInput;
The problem I found mainly lies in the first snippet, where I wrote onFocus={() => this.setState({ currentInput: key })} which re-renders the form component and somehow removing the focus. Hence, the refocusing in AuthTextInput's componentDidUpdate.
I thought when my form component re-renders, all the old AuthTextInput are getting destroyed, so I tried doing autoFocus={this.props.isActive} too in AuthTextInput, but it wasn't successful because componentDidMount itself was never called, which implies they didn't get destroyed, just updated.
This made me wonder, if it didn't get destroyed and remade, what made the focus went away?
I mean, I did the same thing to set the value by doing this onChangeText={(text) => this.onChange(key, text)} and calling setState there. and in that case, the component didn't lose focus.
Anyhow, I would love to know if anyone can either:
show me what made the focus go away OR
using my workaround above to refocus after setting the form state, prevent keyboard from flickering (dismissing and reappearing within a short interval).
Thanks in advance!
UPDATE:
I found that in my TextInput wrapper, when it is updated the FIRST TIME using setState from onFocus callback, it always calls onBlur right away, which is weird and I couldn't find anything that calls onBlur the first time both in mine or the library react-native-element's code.
UPDATE2:
Turns out even when I already disabled onBlur from being called and updating the component once again right after onFocus, the input still loses focus, and I have no idea what's causing it. I checked both the form's and the input's component didupdate and they didn't fire, odd...
So I guess I just have to find out what's stealing the focus when my onFocus updates the input state
FINAL UPDATE:
INTERESTING FIND!!! isActive ? styles.focusedContainer : null this is the culprit, for some reason this is triggering blur() event. None of the answers below can recreate this because none of them modifies the css styling of this component.
I think this happens because containerStyleProps is passed as the parent's View component props to the actual TextInput component by react-native-elements. This causes rerendering at that level causing TextInput to rerender too.
But problem persist, how should I go about solving this issue if simply updating my style triggers the TextInput rerendering? Is there any way to tap into the TextInput's shouldUpdateComponent() hook?
Thanks again for any opinion posted here that helped me gain this insight

I tried to implement the skeleton of the problem. In the below example the input does not loses the focus, works as expected.
class App extends React.Component {
state = {
isFocus: 'no focus'
}
handleFocus = () => {
this.setState({ isFocus: 'got focus' })
}
handleBlur = () => {
this.setState({ isFocus: 'no focus' })
}
render() {
return <input onFocus={this.handleFocus} onBlur={this.handleBlur} value={this.state.isFocus} />;
}
}
ReactDOM.render(<App />, document.getElementById('root'))
input:focus {
box-shadow: 5px 5px 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

I had the same problem, specifically on Android - iOS seems to be unaffected at the time of writing this.
onBlur gets called when the TextInput or it's container is rendering differently based on State variables... as you noted, in your case it was the following:
isActive ? styles.focusedContainer : null
To get around this and keep your setState in onFocus and onBlur, and still style your component beautifully based on state, I managed to fix this issue by separating focus styling from the input, and make it styled on a sibling view instead of a parent view or the input view itself:
<View>
<View style={ isActive ? styles.focused : styles.unfocused } />
<Input/> // <-- do not use state to style this.
</View>
This will avoid the onBlur getting called, however, you will obviously need to correctly position the "un/focus styled" View behind the Input view to line them up.
For this, use styles for position='absolute', width='100%', height='100%'
E.g:
const styles = StyleSheet.create({
inputView: {
width='100%',
height='100%'
}
focusView: {
position: 'absolute', // <--- this is the key
width: '100%',
height: '100%'
}
}

Related

Checkboxes getting checked but no value registered because of virtual keyboard using React Native. How can I fix this?

I have the following SearchField component in which the user can search for a task or check one of the predefined ones using a checkbox. When you focus on a SearchField it opens the virtual keyboard. When you press anywhere else the virtual keyboard is closed. The problem arrises when the user presses on a checkbox. It closes the keyboard, but because it did that the checkbox gets checked but no actual value gets registered. So when you select two tasks it only registers the last one, but both of them got checked.
What would be a good fix to this? I was thinking I could maybe disable the checkbox component when the keyboard is opened but I'm not sure on how to do this. I'd like to fix the ListItem (checkbox) component itself as this SearchField component is used multiple times throughout the app.
Edit: The ListItem components onPress method is not actually fired when the checkbox is checked while the keyboard is open.
This is the code for the SearchField:
<SearchField
onConfirm={(items) => setFieldValue('task_blueprint_ids', items)}
placeholder="Zoeken"
selectedValues={values.task_blueprint_ids}
items={taskBlueprintOptions}
value={values.task_blueprint_ids}
containerStyle={{ flex: 1, padding: 5, marginTop: 5 }}
error={errors && errors.task_blueprint_ids}
/>
And this is my ListItem component:
interface Props<T> {
value: boolean;
onPress: (item: { label: string; value: T }) => void;
item: { label: string; value: T };
}
export const ListItem = <T extends number | string>({ value, onPress, item }: Props<T>) => {
const [keyboardVisible, setKeyboardVisible] = useState(true);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true);
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false);
});
return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
};
}, []);
return (
<TouchableOpacity onPress={() => onPress(item)} style={{ zIndex: 1 }}>
<View style={styles.listItem}>
<CheckBox
value={value}
boxType="square"
style={
Platform.OS === 'ios' && {
width: 15,
height: 15,
marginRight: 5,
}
}
onAnimationType="bounce"
offAnimationType="bounce"
tintColor={Colors.main}
onTintColor={Colors.main}
onCheckColor="#fff"
onFillColor={Colors.main}
tintColors={{ true: Colors.main, false: Colors.main }}
lineWidth={1}
/>
<Text style={{ fontSize: 16 }}>{item.label}</Text>
</View>
</TouchableOpacity>
);
};
export default ListItem;
Apparently this is an IOS specific issue:
https://github.com/react-native-checkbox/react-native-checkbox/issues/103
I fixed it by putting the onChange event on the Checkbox itself and the TouchableOpacity around the Text component instead of around both of them.

Only last function create effect when calling same function at a time with different parameter in react useEffect

I have created a component called "CommonStyleGenerator", to generate simple style object with keys like height, width and bgColor. I have created a text fields in component and whenever text change in any of text field I am calling onStyleChange with changed field key and it's value to store changed value in parent component.
Here is the CommonStyleGenerator.js component code :
import React, { useEffect, useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';
const CommonStyleGenerator = ({ style = {
height: "100%",
width: "100%",
bgColor: "#ffffff"
}, onStyleChange }) => {
useEffect(() => {
onStyleChange("height", "100%");
onStyleChange("width", "100%");
onStyleChange("bgColor", "#ffffff"); //Only this getting effect.
}, []);
return (
<View>
<TextInput
value={style?.height}
placeholder="height"
onChangeText={(text) => onStyleChange("height", text)}
style={styles.textField}
/>
<TextInput
value={style?.width}
placeholder="width"
onChangeText={(text) => onStyleChange("width", text)}
style={styles.textField}
/>
<TextInput
value={style?.bgColor}
placeholder="background color"
onChangeText={(text) => onStyleChange("bgColor", text)}
style={styles.textField}
/>
</View>
)
}
const styles = StyleSheet.create({
textField: {
borderWidth: 1,
marginVertical: 10
}
})
export default CommonStyleGenerator;
And here is the code of how I called the component in my App.js :
import React, { useEffect, useState } from 'react';
import { View, Button, Text } from 'react-native';
import CommonStyleGenerator from './components/CommonStyleGenerator';
const App = () => {
const [commonStyle, setCommonStyle] = useState(null);
return (
<View style={{flex: 1, alignItems: 'center', padding: 20}}>
<CommonStyleGenerator
style={commonStyle}
onStyleChange={(key, value) => setCommonStyle({
...commonStyle,
[key]: value
})}
/>
<Text>{JSON.stringify(commonStyle, null, 2)}</Text>
</View>
)
}
export default App;
Now, what I want is that on load CommonStyleGenerator component, generate default style if user don't change any textfield. So I called onStyleChange function on useEffect for each key. But for only last key(bgColor) function is called.
Is anyone know how I can solve this issue ?
snack.expo.io link
The commonStyle in state when the three initial onStyleChanges are called is null. Each time it's called, the new state is set with an object with a single key. The synchronous call of an onStyleChange after a previous onStyleChange hasn't updated the commonStyle variable outside yet.
Your current code is like doing:
onStyleChange({ height: '100%' });
onStyleChange({ width: '100%' });
onStyleChange({ bgColor: '#ffffff' });
so only the last object passed appears to be in state on the next render.
Use a callback instead, when setting:
onStyleChange={(key, value) => setCommonStyle(commonStyle => ({
...commonStyle,
[key]: value
}))}

How to implement onclick in ResultCard and swap the div with another component

I want to display the details of what's user clicks in the ResultCard.
I want to replace the divs contents (currently displayed results) with rendered html based on the result found in my elastic search cluster(res) when user click the url in the resultcard.
I tried adding onclick properties but nothing happens. Reactivesearch documentation don't list this attribute.
Of course, I could pass argument in the url properties of ResultCard and redirect user to another page but page would be reloaded completely (with the menus defined in index.js and the footer)
I think creating parent component with state mirroring the currently displayed children component in the div is the way to go.
But, how to run a javascript for setting the state when user click in the resultcard?
import React, { Component } from 'react';
import { ReactiveBase, CategorySearch, SingleRange, ResultCard } from '#appbaseio/reactivesearch';
class App extends Component {
render() {
return (
<ReactiveBase
app="artists"
url="https://admin:xxxxxxx#node1.searchevolution.com:9200"
type="_doc">
<div style={{ display: "flex", "flexDirection": "row" }}>
<div style={{ display: "flex", "flexDirection": "column", "width": "40%" }}>
<CategorySearch
componentId="searchbox"
dataField="nom"
categoryField="occupations.keyword"
type="artists"
placeholder="Search for Artists, Painter, Sculptor, Photographs"
style={{
padding: "5px",
"marginTop": "10px"
}}
/>
</div>
<ResultCard
componentId="result"
dataField="nom"
title="Results"
from={0}
size={6}
pagination={true}
onclick="alert('Message à afficher');"
react={{
and: ["searchbox"]
}}
onData={(res) => {
return {
image: "data:image/png;base64,iVBORw0KGgoA",
title: res.occupations,
description: res.nom,
url: "/details/" + res
}
}}
style={{
"width": "60%",
"textAlign": "center"
}}
/>
</div>
</ReactiveBase>
);
}
}
export default App;
Expected result is to change the div content with the rendered html from another component (not still coded).
The handler should be called onClick instead of onclick (Even though it looks like HTML, this is JSX, so handlers need to be camelCase).
Also, your code will not call alert unless you put it in curly braces (which tells JSX to execute code). One more thing: you want to wrap it in a function, otherwise the alert will be called when the component mounts, and not on a click.
onClick={() => alert('Message à afficher')}
EDIT: I think I misunderstood your question. If I'm understanding correctly, you're right and you want to handle the click in the App component. Something like this:
class App extends Component {
state = {
showResultCard = true,
}
handleClick = () => {
this.setState({ showResultCard: false });
}
render() {
<ReactiveBase>
...
{this.state.showResultCard ? (
<ResultCard onClick={this.handleClick} ... />
) : (
<OtherComponent ... />
)}
</ReactiveBase>
}
}
finally got it working by using the base component ReactiveList instead ReactiveCard.
ReactiveCard onData callback function is an object with image, title, description and url fields. No way, to return something else. No way to use onClick property.
So, better use the base component and do some html and css myself
import React, { Component } from 'react';
import { ReactiveBase, CategorySearch, SingleRange, ResultCard, ReactiveList} from '#appbaseio/reactivesearch';
class App extends Component {
constructor(props) {
super(props);
this.state = {
showResultCard : true,
};
}
handleClick = () => {
this.state.showResultCard ?
this.setState({ showResultCard: false }) : this.setState({ showResultCard: true });
}
render() {
return (
<ReactiveBase
app="artists"
url="https://admin:xxxxxxxxxx#node1.searchevolution.com:9200"
type="_doc">
{this.state.showResultCard ? (
<div style={{ display: "flex", "flexDirection": "row" }}>
<div style={{ display: "flex", "flexDirection": "column", "width": "40%" }}>
<CategorySearch
componentId="searchbox"
dataField="nom"
categoryField="occupations.keyword"
type="artists"
placeholder="Search for Artists, Painter, Sculptor, Photographs"
style={{
padding: "5px",
"marginTop": "10px"
}}
/>
</div>
<ReactiveList
componentId="result"
dataField="nom"
className="result-list-container"
size={5}
onData={(res) => <div>{res.nom}<button onClick={this.handleClick}>tttt</button></div>}
pagination
URLParams
react={{
and: ['searchbox'],
}}
/>
</div> ) : (<button onClick={this.handleClick}>back</button>)}
</ReactiveBase>
);
}
}
export default App;

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> };

ReactNative ActivityIndicator not showing when animating property initiate false

I'm playing with react native and got a strange behaviour.
When I try to show a ActitvityIndicator for Android setting its animating property to true with a showProgress variable in the state it doesn't work if the variable is started as false.
In the sample below if the ActivityIndicator animating property start as true, then the buttons make the ActivityIndicator hide or appear correctly.
import React, { Component } from 'react';
import {
Text,
View,
StyleSheet,
TextInput,
TouchableHighlight,
ActivityIndicator
} from 'react-native';
export class Login extends Component {
constructor(props) {
super(props);
this.state = {
showProgress: true
};
}
render() {
return (
<View>
<TouchableHighlight onPress={this.progressOff.bind(this)}>
<Text>progressOff</Text>
</TouchableHighlight>
<TouchableHighlight onPress={this.progressOn.bind(this)}>
<Text>progressOn</Text>
</TouchableHighlight>
<ActivityIndicator animating={this.state.showProgress} size="large"/>
</View>
);
}
progressOff() {
this.setState({showProgress: false});
}
progressOn() {
this.setState({showProgress: true});
}
}
But if i use the code below, with the animating property starting as false, then the button to make the ActivityIndicator appear doesn't work:
import React, { Component } from 'react';
import {
Text,
View,
StyleSheet,
TextInput,
TouchableHighlight,
ActivityIndicator
} from 'react-native';
export class Login extends Component {
constructor(props) {
super(props);
this.state = {
showProgress: false
};
}
render() {
return (
<View>
<TouchableHighlight onPress={this.progressOff.bind(this)}>
<Text>progressOff</Text>
</TouchableHighlight>
<TouchableHighlight onPress={this.progressOn.bind(this)}>
<Text>progressOn</Text>
</TouchableHighlight>
<ActivityIndicator animating={this.state.showProgress} size="large"/>
</View>
);
}
progressOff() {
this.setState({showProgress: false});
}
progressOn() {
this.setState({showProgress: true});
}
}
What am I missing here?
This appears to be a bug in React Native. The code with initial state being showProgress: false works on iOS but not on Android.
I've opened an issue on github if you want to follow the progression:
https://github.com/facebook/react-native/issues/9023
Option 1
A workaround I've used is to use the showProgress variable to render a completely different view with the ActivityIndicator:
render() {
if (this.state.showProgress) {
return this.renderLoadingView();
} else {
return this.renderMainView();
}
}
Option 2
You can also set the opacity of the ActivityIndicator according to the state:
render() {
return (
<View>
<TouchableHighlight onPress={this.progressOff.bind(this)}>
<Text>progressOff</Text>
</TouchableHighlight>
<TouchableHighlight onPress={this.progressOn.bind(this)}>
<Text>progressOn</Text>
</TouchableHighlight>
<ActivityIndicator style={{opacity: this.state.showProgress ? 1.0 : 0.0}} animating={true} size="large"/>
</View>
);
}
However the spinner animation doesn't always start at the same position when using this method.
This is a bug of React-Native for component Activity Indicator.
I am not sure that fb has already solved it but you can try this
constructor(props) {
super(props);
this.state = {
opacity: 0
};
}
to show it use this.setState({opacity:1}) and to hide again this.setState({opacity:0}) in your called functions
and in the render where you are using activity indicator
<ActivityIndicator
animating={true}
color="#ffffff"
style={{height: 80, marginTop: 10, opacity: this.state.opacity }}
size="large"/>
If in your project you can use third party components, I recommend the use of react-native-loading-spinner-overlay
Solved easily our problems, beacause this component use a similar way to show or hide the Activity with the property visible.
Another way I found effective to work around that problem without much code is:
{ this.state.showProgress &&
<ActivityIndicator animating={true} size="large"/>
}
I tried a different approach which I think that it is a more "react way" to solve problems. So, the problems with the opacity solution is that If you just set it to 0, it still will be a animation, so it is not the best solution thinking in your app performance.
I created a separated component that I called <Loading/>, here is the code:
import { ActivityIndicator } from "react-native"
import React from "react"
import PropTypes from "prop-types"
const Loading = (props) =>
props.animating
? <ActivityIndicator style={props.style}
importantForAccessibility='auto' size={props.size}
color={props.size} /> : null
Loading.propTypes = {
animating: PropTypes.bool.isRequired,
style: PropTypes.oneOfType([PropTypes.style, PropTypes.object]),
}
export default Loading
Usage:
<Loading animating={true} importantForAccessibility='auto' size="large" color="#A02BFF" style={styles.loading} />
That way it will avoid to create a animation when it is not a necessity, you will create separated component that can be removed easily at the point that the ActivityIndicator issue becomes solved in the future by replacing it to the original ActivityIndicator native component.
The only problem I had with this, was that in Android it wasn't visible because of the background I had on my screen. I fixed by only changing the color prop to something I knew should stand out in the background:
<ActivityIndicator color={theme.secondary.color} />
i got this problem all by a mistake. i did not put ActivityIndeicator in the center of a view. so it positioned on top of a view, which is covered by a natigation bar. code below is correct. hope this can help u.
<View style={{alignItems: 'center', justifyContent: 'center', flex: 1, backgroundColor: 'white'}}>
<ActivityIndicator
animating={true}
style={
{
alignItems: 'center',
justifyContent: 'center',
opacity: this.state.loading ? 1 : 0
}}
size="large"
/>
</View>
A quick fix Use conditional rendering.. Keep animating : {true} and just Visible and invisible view.
Checkout :
https://kylewbanks.com/blog/how-to-conditionally-render-a-component-in-react-native
In my case, for react native version 0.59.10 , the size property type is different for Android and iOS, so for that I had to make a Platform check as following and it worked.
<ActivityIndicator
size={Platform.OS === "ios" ? 0 : "large"} //This platform check worked.
color={props.color}
animating={props.animating}
style={props.style}
/>
The transition of animating from false to true is too slow on Android. But you can force a re-render using the key prop:
<ActivityIndicator
key={`${props.animating}`}
animating={props.animating}
/>
When props.animating changes from false to true, they key also changes. This forces a re-render, meaning that a new component is rendered with animating = true, which will instantly start your spinner.
If you are testing it on Android one of the reason could be the color property.
Be sure to give the ActivityIndicator a color. For example:
<ActivityIndicator size="large" color="#0000ff" />
This solution work perfectly for me in Android.
Hope this will help you.
import {ActivityIndicator} from 'react-native';
const [opacity, setOpacity] = useState(0)
const onLoadStart = () => {
setOpacity(1);
};
const onLoad = () => {
setOpacity(0);
};
const onBuffer = ({isBuffering}) => {
setOpacity(isBuffering ? 1 : 0);
};
return(
<View>
<Video
video={{uri: props.videoSource}}
autoplay={false}
customStyles={{
seekBarProgress: {
backgroundColor: theme.color.primary,
},
seekBarKnob: {
backgroundColor: theme.color.primary,
},
}}
ref={ref => (player = ref)}
onBuffer={onBuffer}
onLoadStart={onLoadStart}
onLoad={onLoad}
/>
<ActivityIndicator
animating
size="large"
color={color.primarylight}
style={{
opacity: opacity,
position: 'absolute',
top: 70,
left: 70,
right: 70,
// height: 50,
}}
/>
</View>
)

Categories