Override/Disable iOS Smart Punctuation in TextInput - ios

React Native 0.59.9 with device running iOS 11, and Smart Punctuation enabled. This iOS feature automatically converts entered text into more visually-pleasing notations.
Examples:
double hyphen -- gets autoconverted to an emdash — (unicode 8212)
quotation mark " gets autoconverted to a curly quote “(unicode 8220)
etc.
Disabling Smart Punctuation (via Settings > General > Keyboard) is unfortunately not an option.
I render a basic TextInput component almost exactly as per the RN docs, with the only exception that I'm using my own onChangeText handler function to see the effect on the entered text:
import React, { Component } from 'react';
import { TextInput } from 'react-native';
function handleTextChange(value) {
// check value here after entering a '-' in the rendered TextInput.
// when initial value is set to '', received value is ascii 45
// when initial value is set to '-', received value is unicode 8212
}
export default function UselessTextInput() {
const [value, onChangeText] = React.useState('-'); // change this to '' to see the difference
return (
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(text) => handleTextChange(text)}
value={value}
/>
);
}
Setting autoCorrect={false} and/or autoCompleteType='off' on the <TextInput> have no effect on the entered text.
Question
Is there a way to override this auto-correct behaviour so as to not change the user's inputted data?
I see that someone asked Facebook RN this question via a Github issue, but got no response.

it's not always an option but, adding:
keyboardType={'ascii-capable'}
to the TextInput fixed it for me

Indeed, this is an issue with the TextInput's iOS implementation, but I can provide a workaround for you. The trick is to check the TextInput's value for special characters and then replace those characters appropriately. See example below, where I replace every "—" with "--".
Code
const UselessTextInput = () => {
const [value, setText] = React.useState('-'); // change this to '' to see the difference
function handleTextChange (value) {
var val = value;
// check if value contains special characters
if (value.includes("—")){
//replace values appropriately
val = value.replace("—", "--");
}
//set new text
setText(val);
}
return (
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(text) => handleTextChange(text)}
value={value}
/>
);
}
Working Example:
https://snack.expo.io/rJkj95ePB

Related

Cannot turn autoCorrect to {false} in react native TextInput

On my TextInput change text, I detect whether the user pushed the # button for mentions.
onChangeText(text){
const suggestTrigger = text.match(/\B#[A-Za-z0-9]*$/i) //grab "#" trigger
const searchQuery = (suggestTrigger && suggestTrigger.length > 0) ? suggestTrigger[0] : null;
this.setState({
searchQuery: searchQuery
})
}
Then, in my render, I do:
<TextInput
autoCapitalize={this.state.searchQuery ? "none" : "sentences"}
autoCorrect={this.state.searchQuery ? false : true}
onChangeText={this.onChangeText}
/>
However, even when I do this, the autoCorrect does not turn off.
I still see this:
This is causing problems because oftentimes the system replaces the entire mention with a different word altogether.
How can I turn autoCorrect and autoCapitalize off when the user pushes the # button?
'
I have even tried rendering an entirely different , but the behavior remains.
TL;DR: you should close and re-launch your keyboard after the TextInput autoCorrect toggling value.
Buddy, this is not your fault, I had the same issue on autoFocus of react native TextInput component. I set a state name for the TextInput editable prop and then with the pressing pencil button I change the editable props. The designer told me after the TextInput made editable the cursor should be focused, so I use the isEditable state for autoFocus prop, see below:
state = {
isEditable: false
};
handleEdit = () => {
const { isEditable } = this.state;
this.setState({ isEditable: !isEditable });
};
<TextInput
autoFocus={isEditable}
editable={isEditable}
style={styles.textNameEditable}
defaultValue={text}
numberOfLines={1}
/>
Then I press the edit button and it turns to:
But it is not focused and the Keyboard didn't launch, I sought and found this link, the TextInput does not change/update some of its props after componentDidMount. ☹️. Also, it is not different in iOS or Android, both of them has this issue, I used ref to focus on this TextInput after the isEditable state made true. see following code:
<TextInput
editable={isEditable}
style={styles.textNameEditable}
defaultValue={text}
numberOfLines={1}
ref={input => {
this.input = input;
}}
/>
componentDidUpdate() {
const { isEditable } = this.state;
if (isEditable) {
this.input.focus();
}
}
And your case:
Absolutely you can not use ref because the autoCorrect should render with react native and it is not like focus() and blur() so JavaScript cannot access to it.
I make a test shape for your case, I create another button like a star for toggling autoCorrect value alongside my edit button. the filled star means autoCorrect is true and the lined star means autoCorrect is false, now see the test area code and view:
state = {
isEditable: false,
correct: true
};
handleCorrect = () => {
const { correct } = this.state;
this.setState({ correct: !correct });
};
<TextInput
autoCorrect={correct}
editable={isEditable}
style={styles.textNameEditable}
defaultValue={text}
numberOfLines={1}
ref={input => {
this.input = input;
}}
/>
In the above photo, it is so clear the autoCorrect rendered as true, so it is enabled:
When I write a wrong Persian word the auto-correction show its suggestion, now time to press the star button:
Wow, the autoCorrection is false in the above situation but still we see the auto-correction of the cellphone. it is just like autoFocus it is rendered in the first render and after it, the TextInput could not change/update its props. suddenly I press edit button:
And I press the edit button again, so surely, you realized the autoCorrect is false now, ok now see what I saw:
The autoCorrect remained false by my double pressing edit button and now the auto-correction of device disappears completely. I don't know it is a bug or my bad understanding but I realized in this test area, for update autoCorrect value, we should do something after changing its value to close the iPhone keyboard and then re-launch it. the main thing that TextInput has issued is the launched keyboard.
For my test, when I pressed the edit button the editable prop of the TextInput is changed to false and the keyboard is closed, so when I pressed the edit button again, the TextInput get focused and keyboard re-launched with new autoCorrect value. This is The Secret.
Solution:
You should do something, to close and open again the iOS keyboard with the new autoCorrect value. for the test case that I wrote for your question, I decided to do a hybrid innovative solution, using ref and the callback of setState:
handleCorrect = () => {
const { correct } = this.state;
this.input.blur(); //-- this line close the keyboard
this.setState({ correct: !correct },
() => {
setTimeout(() => this.input.focus(), 50);
//-- above line re-launch keyboard after 50 milliseconds
//-- this 50 milliseconds is for waiting to closing keyborad finish
}
);
};
<TextInput
autoCorrect={correct}
editable={isEditable}
style={styles.textNameEditable}
defaultValue={text}
numberOfLines={1}
ref={input => {
this.input = input;
}}
/>
And after pressing the star button the keyboard close and re-launch and the auto-correction disappear completely.
Note: obviously, I summarized some other codes like destructuring and writing class or extends and etc for better human readability.
The problem isn't in your code completely(except Regex part which didn't work in my device) but how the React Native renders Keyboard.
I created a sample that along with your initial code also changes backgroundColor of the screen.
You will find that color changes instantly when '#' is entered whereas the keyboard doesn't.
Unless you reload the keyboard. For this, if you un-comment the code it dismisses keyboard once and once you tap back on textInput the new Keyboard without autoCorrect and autoCapitalize is shown.
state = {
searchQuery: null,
isFocused: true,
}
constructor(props) {
super(props);
this.onChangeText = this.onChangeText.bind(this);
}
onChangeText = (val) => {
const suggestTrigger = val.match(/#[A-Za-z0-9]*$/i) //grab "#" trigger
const searchQuery = (suggestTrigger && suggestTrigger.length > 0) ? suggestTrigger[0] : null;
// Un comment this to reload
// if(searchQuery && this.state.isFocused) {
// this.setState({
// isFocused: false
// });
// Keyboard.dismiss();
// // this.myTextInput.focus()
// }
this.setState({
searchQuery: searchQuery
})
}
render() {
const { searchQuery } = this.state
return (
<View style={[styles.container,
{
backgroundColor: searchQuery ? "red": "green"}
]}>
<TextInput
style={{width: 300, height: 50, borderWidth: 1}}
ref={(ref)=>{this.myTextInput = ref}}
autoCapitalize={searchQuery ? "none" : "sentences"}
autoCorrect={searchQuery ? false : true}
onChangeText={this.onChangeText}
/>
</View>
);
}
Since now we know the main cause of error may be a better solution can be reached.
I have 2 suggestions:
First, have you tried using the autoCorrect fallback?
spellCheck={this.state.searchQuery ? false : true}
Second, have you tried with native code (Obj-C / Swift)?
import { Platform, TextInput, View } from 'react-native';
import { iOSAutoCorrect } from './your-native-code';
const shouldWork = Platform.OS === 'ios' ? <iOSAutoCorrect /> : <TextInput
autoCapitalize={this.state.searchQuery ? "none" : "sentences"}
autoCorrect={this.state.searchQuery ? false : true}
onChangeText={this.onChangeText} />
return (<View>{shouldWork}</View>);
In iOSAutoCorrect you should use a UITextView. Then set its proper value depending on your condition:
textField.autocorrectionType = UITextAutocorrectionTypeNo; // or UITextAutocorrectionTypeYes
I have free-coded, thus the code is untested and might contain bugs.

How to set height of React Native Webview? [duplicate]

Hi I know it's a known issue about the auto height of webview in react native,
and I have tried all the possibles solutions I've found on the internet such as :
https://gist.github.com/epeli/10c77c1710dd137a1335
https://github.com/danrigsby/react-native-web-container/blob/master/index.js
and all the solutions suggested in:
React native: Is it possible to have the height of a html content in a webview?
But unfortunately none of these seems to work for me,
I understand that the workaround they all suggest is to set the title to the height, but in my case it seems that the title always stays the same which is :
"text/html ...." and the rest of my html.
I get the html content from an API, it comes without a body, head or html tags, I've also tried adding these tags manually to the html and nothing seems to work.
I would love to hear if anyone else had that problem and how did it get fixed.
I wrap WebView inside a View, and set the height from the View.
<View style={{ height: 200 }}>
<WebView
automaticallyAdjustContentInsets={false}
source={{uri: 'https://player.vimeo.com/video/24156534?title=0&byline=0&portrait=0'}}
/>
</View>
I just follow this guide: https://github.com/react-native-community/react-native-webview/blob/master/docs/Guide.md#communicating-between-js-and-native and succeeded in my work. Here is solution:
1. Define script to send document height to native env after loaded website.
2. Handle onMesssage of webview component and reset Height via state.
const webViewScript = `
setTimeout(function() {
window.postMessage(document.documentElement.scrollHeight);
}, 500);
true; // note: this is required, or you'll sometimes get silent failures
`;
...
constructor(props) {
super(props);
this.state = {
webheight:100,
}
...
<WebView style={{height: this.state.webheight}}
automaticallyAdjustContentInsets={false}
scrollEnabled={false}
source={{uri: "http://<your url>"}}
onMessage={event => {
this.setState({webheight: parseInt(event.nativeEvent.data)});
}}
javaScriptEnabled={true}
injectedJavaScript ={webViewScript}
domStorageEnabled={true}
></WebView>
Hope that help!
A reliable implementation of this behavior is with useAutoheight hook from #formidable-webview/webshell library.
The latter allows to inject "features" into WebViews, e.g. scripts and behaviors.
In this example, we will use 3 features + the aforementioned hook:
HandleHTMLDimensionsFeature which is required by useAutoheight hook to get document size updates;
ForceResponsiveViewportFeature to work around mobile virtual viewport;
ForceElementSizeFeature to work around cyclic size constraints
This component should work with any webpage.
import React from 'react';
import makeWebshell, {
HandleHTMLDimensionsFeature,
ForceResponsiveViewportFeature,
ForceElementSizeFeature,
useAutoheight
} from '#formidable-webview/webshell';
import WebView from 'react-native-webview';
const Webshell = makeWebshell(
WebView,
new HandleHTMLDimensionsFeature(),
new ForceResponsiveViewportFeature({ maxScale: 1 }),
new ForceElementSizeFeature({
target: 'body',
heightValue: 'auto',
widthValue: 'auto'
})
);
export default function ResilientAutoheightWebView(props) {
const { autoheightWebshellProps } = useAutoheight({
webshellProps: props
});
return <Webshell {...autoheightWebshellProps} />;
}
More resources:
Try this on Expo
Full guide here.
Using postMessage and onMessage like below worked for me perfectly.
Credit to iamdhj
onWebViewMessage = (event: WebViewMessageEvent) => {
this.setState({webViewHeight: Number(event.nativeEvent.data)})
}
render() {
return (
<ScrollView>
<WebView
style={{ height: this.state.webViewHeight }}
source={{html: '...'}}
onMessage={this.onWebViewMessage}
injectedJavaScript='window.ReactNativeWebView.postMessage(document.body.scrollHeight)'
/>
</ScrollView>
)
}
The WebView has default styles. If you want to set height, you also need to add flex: 0, as stated in the documentation:
Please note that there are default styles (example: you need to add flex: 0 to the style if you want to use height property).
I made a little component to make this functionality reusable if it helps anyone!
import React, { useState } from "react";
import WebView from "react-native-webview";
const DynamicHeightWebView = (props) => {
const [height, setHeight] = useState(0);
const webViewScript = `
setTimeout(function() {
window.ReactNativeWebView.postMessage(document.documentElement.scrollHeight);
}, 500);
true; // note: this is required, or you'll sometimes get silent failures
`;
return <WebView
{...props}
style={{
...props.style,
height: height,
}}
automaticallyAdjustContentInsets={false}
scrollEnabled={false}
onMessage={event => {
setHeight(parseInt(event.nativeEvent.data));
}}
javaScriptEnabled={true}
injectedJavaScript ={webViewScript}
domStorageEnabled={true}
useWebKit={true}
/>
}
export default DynamicHeightWebView;
Apparently the problem was I had javaScriptEnabled={false}.
After enabling it everything worked.
I waste whole day to fix the height issue but in the end I had to shift to another library
This one is easy and good
https://github.com/archriss/react-native-render-html
You can get the content height by injecting the JS code as suggested by #ken-ratanachai-s. Although, You will experience certain irregularities in some devices (Extra height after the content). This is becuase the javascript returns the content height in pixels, but we need to use display points in react native. To fix this, Divide the height from javascript with the pixel ratio as follows.
import { WebView, PixelRatio } from 'react-native'
const [webviewHeight, setWebviewHeight] = useState(0)
const onProductDetailsWebViewMessage = event => {
setWebviewHeight(Number(event.nativeEvent.data)/PixelRatio.get())
}
return <WebView
originWhitelist={['*']}
style={{ height: productDetailsWebviewHeight }}
onMessage={onProductDetailsWebViewMessage}
injectedJavaScript='window.ReactNativeWebView.postMessage(document.body.scrollHeight)'
source={{ html: "..." }}
/>
Pixel ratio ref.: https://reactnative.dev/docs/pixelratio
Courtesy: https://stackoverflow.com/a/65976827/5321660
use package react-native-autoheight-webview
I recommend react-native-autoheight-webview.
it perfect work for me.
https://github.com/iou90/react-native-autoheight-webview
UPDATE:
Best answer is #Ken Ratanachai S.'s answer.
https://stackoverflow.com/a/65976827/9757656

Picker placement is wrong on react-native view

I'm following this Udemy react-native course and in one of the examples he uses a picker to select data in the screen. Back when he tried it it seems like it was working but now I get a weird result when I try to render it.
If I follow his code exactly the picker shows after all the other items, after making some changes I get it to show kind of at the right place but it is now squished, which is still not correct:
I am definitely doing something wrong here in terms of how to render it, here's the code (full example on github):
import React from 'react';
import {Picker, Text, StyleSheet, View} from 'react-native';
import {connect} from 'react-redux';
import {Card, CardSection, Input, Button} from "./common";
import {employeeUpdate} from "../actions";
class EmployeeCreate extends React.Component {
updateEmployee(name, value) {
this.props.employeeUpdate({prop: name, value: value})
}
renderPickerItems() {
return ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
.map((item) => <Picker.Item key={item} label={item} value={item}/>);
}
render() {
return (
<Card>
<CardSection>
<Input
label="Name"
placeholder="Your Name"
value={this.props.name}
onChangeText={this.updateEmployee.bind(this, 'name')}
/>
</CardSection>
<CardSection>
<Input
label="Phone"
placeholder="555-555-5555"
keyboardType="phone-pad"
value={this.props.phone}
onChangeText={this.updateEmployee.bind(this, 'phone')}
/>
</CardSection>
<CardSection style={{flex: 1}}>
<View style={styles.shiftContainerStyle}>
<Text style={styles.pickerTextStyle}>Shift</Text>
<Picker
style={styles.pickerStyle}
selectedValue={this.props.shift}
onValueChange={this.updateEmployee.bind(this, 'shift')}
>
{this.renderPickerItems()}
</Picker>
</View>
</CardSection>
<CardSection>
<Button>
Create
</Button>
</CardSection>
</Card>
);
}
}
const styles = StyleSheet.create({
pickerTextStyle: {
fontSize: 18,
lineHeight: 23,
flex: 1,
},
pickerStyle: {
flex: 2,
},
shiftContainerStyle: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
}
});
const mapStateToProps = state => {
const {name, phone, shift} = state.employeeForm;
return {
name,
phone,
shift,
};
};
export default connect(mapStateToProps, {employeeUpdate})(EmployeeCreate);
Any idea what I could do to render this correctly?
You need to remove style={{flex: 1}} from this line in your code:
<CardSection style={{flex: 1}}>
The reason being that your parent container, Card, doesn't have any flex or width/height values defined. If flex is left undefined, the default is flex: 0. If you look at the docs for flex, you'll see that:
When flex is 0, the component is sized according to width and height and it is inflexible.
Combine that with having no width/height defined and you get this behavior on rendering your CardSections:
The three CardSections (input, input, button) will take up the default width and height based on their children. That is the default styling for the Inputs and Button.
The CardSection with style={{flex: 1}} will calculate its width and height based on the remaining space taken up by the parent container(s) per the definition of flex: 1:
When flex is a positive number, it makes the component flexible and it will be sized proportional to its flex value. So a component with flex set to 2 will take twice the space as a component with flex set to 1.
The parent container, Card, in this case has no extra space left. So what happens is that this CardSection ends up with 0 height. Hence the strange overflow rendering you're seeing.
Once you remove style={{flex: 1}}, the width and height of the CardSection will be defined by it's child components which, like Input and Button, do have a styles and default styles.
Whether or not this is correct behavior per the Yoga spec (Yoga is what React Native uses for layout) is up for debate and has tripped up others before. Definitely look over that first StackOverflow answer I linked to as it has far more detail and explanation on gotchas than any of the documentation on React Native wrt flex.

React native: webview height

Hi I know it's a known issue about the auto height of webview in react native,
and I have tried all the possibles solutions I've found on the internet such as :
https://gist.github.com/epeli/10c77c1710dd137a1335
https://github.com/danrigsby/react-native-web-container/blob/master/index.js
and all the solutions suggested in:
React native: Is it possible to have the height of a html content in a webview?
But unfortunately none of these seems to work for me,
I understand that the workaround they all suggest is to set the title to the height, but in my case it seems that the title always stays the same which is :
"text/html ...." and the rest of my html.
I get the html content from an API, it comes without a body, head or html tags, I've also tried adding these tags manually to the html and nothing seems to work.
I would love to hear if anyone else had that problem and how did it get fixed.
I wrap WebView inside a View, and set the height from the View.
<View style={{ height: 200 }}>
<WebView
automaticallyAdjustContentInsets={false}
source={{uri: 'https://player.vimeo.com/video/24156534?title=0&byline=0&portrait=0'}}
/>
</View>
I just follow this guide: https://github.com/react-native-community/react-native-webview/blob/master/docs/Guide.md#communicating-between-js-and-native and succeeded in my work. Here is solution:
1. Define script to send document height to native env after loaded website.
2. Handle onMesssage of webview component and reset Height via state.
const webViewScript = `
setTimeout(function() {
window.postMessage(document.documentElement.scrollHeight);
}, 500);
true; // note: this is required, or you'll sometimes get silent failures
`;
...
constructor(props) {
super(props);
this.state = {
webheight:100,
}
...
<WebView style={{height: this.state.webheight}}
automaticallyAdjustContentInsets={false}
scrollEnabled={false}
source={{uri: "http://<your url>"}}
onMessage={event => {
this.setState({webheight: parseInt(event.nativeEvent.data)});
}}
javaScriptEnabled={true}
injectedJavaScript ={webViewScript}
domStorageEnabled={true}
></WebView>
Hope that help!
A reliable implementation of this behavior is with useAutoheight hook from #formidable-webview/webshell library.
The latter allows to inject "features" into WebViews, e.g. scripts and behaviors.
In this example, we will use 3 features + the aforementioned hook:
HandleHTMLDimensionsFeature which is required by useAutoheight hook to get document size updates;
ForceResponsiveViewportFeature to work around mobile virtual viewport;
ForceElementSizeFeature to work around cyclic size constraints
This component should work with any webpage.
import React from 'react';
import makeWebshell, {
HandleHTMLDimensionsFeature,
ForceResponsiveViewportFeature,
ForceElementSizeFeature,
useAutoheight
} from '#formidable-webview/webshell';
import WebView from 'react-native-webview';
const Webshell = makeWebshell(
WebView,
new HandleHTMLDimensionsFeature(),
new ForceResponsiveViewportFeature({ maxScale: 1 }),
new ForceElementSizeFeature({
target: 'body',
heightValue: 'auto',
widthValue: 'auto'
})
);
export default function ResilientAutoheightWebView(props) {
const { autoheightWebshellProps } = useAutoheight({
webshellProps: props
});
return <Webshell {...autoheightWebshellProps} />;
}
More resources:
Try this on Expo
Full guide here.
Using postMessage and onMessage like below worked for me perfectly.
Credit to iamdhj
onWebViewMessage = (event: WebViewMessageEvent) => {
this.setState({webViewHeight: Number(event.nativeEvent.data)})
}
render() {
return (
<ScrollView>
<WebView
style={{ height: this.state.webViewHeight }}
source={{html: '...'}}
onMessage={this.onWebViewMessage}
injectedJavaScript='window.ReactNativeWebView.postMessage(document.body.scrollHeight)'
/>
</ScrollView>
)
}
The WebView has default styles. If you want to set height, you also need to add flex: 0, as stated in the documentation:
Please note that there are default styles (example: you need to add flex: 0 to the style if you want to use height property).
I made a little component to make this functionality reusable if it helps anyone!
import React, { useState } from "react";
import WebView from "react-native-webview";
const DynamicHeightWebView = (props) => {
const [height, setHeight] = useState(0);
const webViewScript = `
setTimeout(function() {
window.ReactNativeWebView.postMessage(document.documentElement.scrollHeight);
}, 500);
true; // note: this is required, or you'll sometimes get silent failures
`;
return <WebView
{...props}
style={{
...props.style,
height: height,
}}
automaticallyAdjustContentInsets={false}
scrollEnabled={false}
onMessage={event => {
setHeight(parseInt(event.nativeEvent.data));
}}
javaScriptEnabled={true}
injectedJavaScript ={webViewScript}
domStorageEnabled={true}
useWebKit={true}
/>
}
export default DynamicHeightWebView;
Apparently the problem was I had javaScriptEnabled={false}.
After enabling it everything worked.
I waste whole day to fix the height issue but in the end I had to shift to another library
This one is easy and good
https://github.com/archriss/react-native-render-html
You can get the content height by injecting the JS code as suggested by #ken-ratanachai-s. Although, You will experience certain irregularities in some devices (Extra height after the content). This is becuase the javascript returns the content height in pixels, but we need to use display points in react native. To fix this, Divide the height from javascript with the pixel ratio as follows.
import { WebView, PixelRatio } from 'react-native'
const [webviewHeight, setWebviewHeight] = useState(0)
const onProductDetailsWebViewMessage = event => {
setWebviewHeight(Number(event.nativeEvent.data)/PixelRatio.get())
}
return <WebView
originWhitelist={['*']}
style={{ height: productDetailsWebviewHeight }}
onMessage={onProductDetailsWebViewMessage}
injectedJavaScript='window.ReactNativeWebView.postMessage(document.body.scrollHeight)'
source={{ html: "..." }}
/>
Pixel ratio ref.: https://reactnative.dev/docs/pixelratio
Courtesy: https://stackoverflow.com/a/65976827/5321660
use package react-native-autoheight-webview
I recommend react-native-autoheight-webview.
it perfect work for me.
https://github.com/iou90/react-native-autoheight-webview
UPDATE:
Best answer is #Ken Ratanachai S.'s answer.
https://stackoverflow.com/a/65976827/9757656

making a multiline, expanding TextInput with React-Native

I'm working on a react-native app and need a TextInput that has similar functionality to the textview in the "messages" app on iOS—it should start out as one line and then gracefully expand to more lines until some limit (like 5 lines of text) and then start scrolling along to latest line as needed.
Took a look at the SlackTextViewController but a) seems like it has a lot of stuff that I don't want and b) I'd like to try to keep as much code in React (and out of objective-C/swift) as possible.
Edit: Just want to emphasize that I would prefer REACT (JAVASCRIPT) code, as stated above, rather than Objective-C or Swift.
I tried two different ways to do this today. Neither are the best but I thought I'd record my efforts in case they are helpful. They both definitely had the effect you are looking for, though sometime delayed with all the async communication.
1) Offscreen Text height
So just under the TextInput, I added a regular Text field with the same font and padding and such. I registered the onChange listener on the input and called setState({text: event.nativeEvent.text}). The Text filed got its value from the state. Both had onLayout Listeners. Basically, the goal was to get the height for the TextInput from the (unrestricted) Text. Then I hid the Text way offscreen
https://gist.github.com/bleonard/f7d748e89ad2a485ec34
2) Native Module
Really, I just needed the height of the content in the real UITextView. So I added a category to RCTUIManager as there are several methods there already that are helpful. I got rid of the hidden Text view. So onChange, I ask for the height and use that in the same way via the state.
https://gist.github.com/bleonard/6770fbfe0394a34c864b
3) Github PR
What I really hope is that this PR gets accepted. It looks to do something like this automatically.
https://github.com/facebook/react-native/pull/1229
Adding multiline={true} to a TextInput will allow scrolling if the amount of text exceeds the available space. You can then change the height of the TextInput by accessing the nativeEvent.contentSize.height of the event from the onChange prop.
class Comment extends Component {
state = {
text: '',
height: 25
}
onTextChange(event) {
const { contentSize, text } = event.nativeEvent;
this.setState({
text: text,
height: contentSize.height > 100 ? 100 : contentSize.height
});
}
render() {
return (
<TextInput
multiline
style={{ height: this.state.height }}
onChange={this.onTextChange.bind(this)}
value={this.state.text}
/>
);
}
}
As of Oct'17 there is a nice component from Wix to do this:
https://github.com/wix/react-native-autogrow-textinput
The usage can be very simple:
<AutoGrowingTextInput
style={styles.textInput}
placeholder="Enter text"
value={this.state.text}
onChangeText={this._handleChangeText}
/>
And there are some extra props like minHeight and maxHeight for example.
I'm using it on RN 0.47.2
Implement the UITextView's delegate method textViewDidChange and play with the rect
- (void)textViewDidChange:(UITextView *)textView {
CGSize constraintSize = CGSizeMake(textView.frame.size.width, MAXFLOAT);
CGRect textRect = [textView.text boundingRectWithSize:constraintSize
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:textView.font}
context:nil];
NSLog(#"Frame:%#", NSStringFromCGRect(textRect));
CGRect newRect = textView.frame;
newRect.size.height = textRect.size.height;
textView.frame = newRect;
}
#Groovietunes got almost all of it correct but as of 26-06-19 things have change a bit from his initial answer.
For one, the onChange prop on textinputs no longer returns the contentSize object, hence event.nativeEvent.contentSize will return undefined.
The fix for this is to use the onContentSizeChange prop. But there's a catch; onContentSizeChange runs only once when the component mounts thus making it impossible to dynamically get the height as it changes.
To fix this we need to make sure the value prop is set after onContentSizeChange in the hierarchy of props in order to keep obtaining the content size as the textinput grows.
At the end of it all i had a component that looked like this
<TextInput
placeholder={this.props.placeholder}
autoFocus={this.props.autoFocus}
style={[
styles.inputFlat,
{ height: this.state.height > 40 ? this.state.height : 40 }
]}
ref="inputFlat"
onFocus={() => {
this.toggleInput();
this.toggleButton();
}}
multiline={this.props.multiline}
onBlur={() => {
this.toggleInput();
this.toggleButton();
}}
onChangeText={this.props.onChangeText}
onContentSizeChange={event => {
const { contentSize } = event.nativeEvent;
this.setState({
height: contentSize.height
});
}}
value={this.state.value}
/>
if for some reason it seems broken as at the time of viewing this answer, refer to this documentation for any updates
Another solution is to check '\n' symbols and set the numberOfLines property.
Works for me.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {TextInput} from 'react-native';
export default class TextInputAutogrow extends Component {
constructor(props) {
super(props);
this._ref = null;
this.bindRef = this.bindRef.bind(this);
this.onChangeText = this.onChangeText.bind(this);
this.state = {
numberOfLines: this.getNumberOfLines()
};
}
bindRef(c) {
this._ref = c;
this.props.innerRef && this.props.innerRef(c);
}
getText() {
return typeof this.props.value === 'string' ?
this.props.value :
(
typeof this.props.defaultValue === 'string' ?
this.props.defaultValue :
''
);
}
getNumberOfLines(value) {
if (value === undefined) {
value = this.getText();
}
return Math.max(this.props.numberOfLines, value.split('\n').length - 1) + 1;
}
onChangeText(value) {
this.setState({numberOfLines: this.getNumberOfLines(value)})
}
render() {
return (
<TextInput
{...this.props}
ref={this.bindRef}
numberOfLines={this.state.numberOfLines}
onChangeText={this.onChangeText}
/>
)
}
}
TextInputAutogrow.propTypes = {
...TextInput.propTypes,
innerRef: PropTypes.func,
};
TextInputAutogrow.defaultProps = {
numberOfLines: 4,
};
For anyone wants an easy solution, using new versions of RN, currently using 0.63.4,
the team added autoGrow to the TextInput if it's multiline, which is the case.
Don't use height (StyeSheet property) for the component or the parent View/Views
Use minHeight, maxHeight instead, and if multiline is enabled, the TextInput will grow till reaching the maxHeight, then will be scrollable.
no calculation nor inner state needs to ba maintained.
Same thing apply for minWidth, maxWidth if you need to expand widly.
Since react-native-autogrow-textinput seems to be unmaintained, I would recommend using native-base <Input/> as it has properties that enables multiline and auto-height as follows:
import { Input } from 'native-base'
<Input
height={'auto'}
multiline={true}
// ...
/>

Resources