React Native TextInput that only accepts numeric characters - react-native

I need to have a React Native TextInput component that will only allow numeric characters (0 - 9) to be entered. I can set the keyboardType to numeric which almost gets me there for input except for the period (.). However this does nothing to stop pasting non-numeric characters into the field.
What I've come up with so far is to use the OnChangeText event to look at the text entered. I remove any non-numeric characters from the text. Then put the text in a state field. Then update the TextInput through it's Value property. Code snippet below.
<TextInput
style={styles.textInput}
keyboardType = 'numeric'
onChangeText = {(text)=> this.onChanged(text)}
value = {this.state.myNumber}
/>
onTextChanged(text) {
// code to remove non-numeric characters from text
this.setState({myNumber: text})
}
This seems to work but it seems like a hack. Is there another way to do this?

Using a RegExp to replace any non digit is faster than using a for loop with a whitelist, like other answers do.
Use this for your onTextChange handler:
onChanged (text) {
this.setState({
mobile: text.replace(/[^0-9]/g, ''),
});
}
Performance test here: https://jsperf.com/removing-non-digit-characters-from-a-string

You can do it like this. It will only accept numeric values, and limit to 10 numbers as your wish.
<TextInput
style={styles.textInput}
keyboardType='numeric'
onChangeText={(text)=> this.onChanged(text)}
value={this.state.myNumber}
maxLength={10} //setting limit of input
/>
You can see the entered value by writing the following code in your page:
{this.state.myNumber}
In the onChanged() function the code look like this:
onChanged(text){
let newText = '';
let numbers = '0123456789';
for (var i=0; i < text.length; i++) {
if(numbers.indexOf(text[i]) > -1 ) {
newText = newText + text[i];
}
else {
// your call back function
alert("please enter numbers only");
}
}
this.setState({ myNumber: newText });
}

That is the correct way to do it till such a component (or attribute on the TextInput) is specifically developed.
The web has the ‘number’ type for the input element, but that is web based and react-native does not use a web view.
You could consider to create that input as a react component on it’s own (maybe call NumberInput): that’ll enable you to reuse it or maybe even open source it since you can create many TextInputs that has different value filters/checkers.
The downside to immediate correction is to ensure correct feedback is given to the user as to prevent confusion as to what happened to his value

React Native TextInput provides keyboardType props with following possible values :
default
number-pad
decimal-pad
numeric
email-address
phone-pad
so for your case you can use keyboardType='number-pad' for accepting only numbers. This doesn't include '.'
so,
<TextInput
style={styles.textInput}
keyboardType = 'number-pad'
onChangeText = {(text)=> this.onChanged(text)}
value = {this.state.myNumber}
/>
is what you have to use in your case.
for more details please refer the official doc link for TextInput :
https://facebook.github.io/react-native/docs/textinput#keyboardtype

First Solution
You can use keyboardType = 'numeric' for numeric keyboard.
<View style={styles.container}>
<Text style={styles.textStyle}>Enter Number</Text>
<TextInput
placeholder={'Enter number here'}
style={styles.paragraph}
keyboardType="numeric"
onChangeText={value => this.onTextChanged(value)}
value={this.state.number}
/>
</View>
In first case punctuation marks are included ex:- . and -
Second Solution
Use regular expression to remove punctuation marks.
onTextChanged(value) {
// code to remove non-numeric characters from text
this.setState({ number: value.replace(/[- #*;,.<>\{\}\[\]\\\/]/gi, '') });
}
Please check snack link
https://snack.expo.dev/#vishaldhanotiya/numeric-keyboard

Only allow numbers using a regular expression
<TextInput
keyboardType = 'numeric'
onChangeText = {(e)=> this.onTextChanged(e)}
value = {this.state.myNumber}
/>
onTextChanged(e) {
if (/^\d+$/.test(e.toString())) {
this.setState({ myNumber: e });
}
}
You might want to have more than one validation
<TextInput
keyboardType = 'numeric'
onChangeText = {(e)=> this.validations(e)}
value = {this.state.myNumber}
/>
numbersOnly(e) {
return /^\d+$/.test(e.toString()) ? true : false
}
notZero(e) {
return /0/.test(parseInt(e)) ? false : true
}
validations(e) {
return this.notZero(e) && this.numbersOnly(e)
? this.setState({ numColumns: parseInt(e) })
: false
}

Function to validate input:
validateInputs(text, type) {
let numreg = /^[0-9]+$/;
if (type == 'username') {
if (numreg.test(text)) {
//test ok
} else {
//test not ok
}
}
}
<TextInput
onChangeText={text => this.validateInputs(text, 'username')}
/>
I hope this is helpful.

const [text, setText] = useState('');
const onChangeText = (text) => {
if (+text) {
setText(text);
}
};
<TextInput
keyboardType="numeric"
value={text}
onChangeText={onChangeText}
/>
This should save from physical keyboards

A kind reminder to those who encountered the problem that "onChangeText" cannot change the TextInput value as expected on iOS: that is actually a bug in ReactNative and had been fixed in version 0.57.1. Refer to: https://github.com/facebook/react-native/issues/18874

if (!/^[0-9]+$/.test('YourString')) {
console.log('Enter Only Number');
} else {
console.log('Success');
}

<TextInput autoCapitalize={'none'} maxLength={10} placeholder='Mobile Number' value={this.state.mobile} onChangeText={(mobile) => this.onChanged(mobile)}/>
and onChanged method :
onChanged(text){
var newText = '';
var numbers = '0123456789';
if(text.length < 1){
this.setState({ mobile: '' });
}
for (var i=0; i < text.length; i++) {
if(numbers.indexOf(text[i]) > -1 ) {
newText = newText + text[i];
}
this.setState({ mobile: newText });
}
}

I had the same problem in iOS, using the onChangeText event to update the value of the text typed by the user I was not being able to update the value of the TextInput, so the user would still see the non numeric characters that he typed.
This was because, when a non numeric character was pressed the state would not change since this.setState would be using the same number (the number that remained after removing the non numeric characters) and then the TextInput would not re render.
The only way I found to solve this was to use the keyPress event which happens before the onChangeText event, and in it, use setState to change the value of the state to another, completely different, forcing the re render when the onChangeText event was called. Not very happy with this but it worked.

Here is my other simple answer to accept only numbers in the text box using Regular Expressions.
onChanged(text){
this.setState({
myNumber: text.replace(/[^0-9]/g, '')
});
}

I wrote this function which I found to be helpful to prevent the user from being able to enter anything other than I was willing to accept. I also used keyboardType="decimal-pad" and my onChangeText={this.decimalTextChange}
decimalTextChange = (distance) =>
{
let decimalRegEx = new RegExp(/^\d*\.?\d*$/)
if (distance.length === 0 || distance === "." || distance[distance.length - 1] === "."
&& decimalRegEx.test(distance)
) {
this.setState({ distance })
} else {
const distanceRegEx = new RegExp(/^\s*-?(\d+(\.\d{ 1, 2 })?|\.\d{ 1, 2 })\s*$/)
if (distanceRegEx.test(distance)) this.setState({ distance })
}
}
The first if block is error handling for the event the user deletes all of the text, or uses a decimal point as the first character, or if they attempt to put in more than one decimal place, the second if block makes sure they can type in as many numbers as they want before the decimal place, but only up to two decimal places after the point.

Using a RegExp to replace any non digit. Take care the next code will give you the first digit he found, so if user paste a paragraph with more than one number (xx.xx) the code will give you the first number. This will help if you want something like price, not a mobile phone.
Use this for your onTextChange handler:
onChanged (text) {
this.setState({
number: text.replace(/[^(((\d)+(\.)\d)|((\d)+))]/g,'_').split("_"))[0],
});
}

You can remove non numeric characters using regex
onTextChanged (text) {
this.setState({
myNumber: text.replace(/\D/g, ''),
});
}

In case anyone is looking for solution that allows to use numeric keyboard but also validates input to allow only one decimal point then below is what I wrote to accomplish that. I also wanted to have comma(,) replaced with dot(.) as that's the way I'm saving it to database but you can remove that - it's optional.
const [decimalInput, setDecimalInput] = useState('')
const validator = /^[+-]?\d*(?:[.,]\d*)?$/;
function onNumberInputChange(text){
if (validator.test(text)){
text = text.replace(",",".") //this is optional
setDecimalInput(text);
}
else{
//this will remove the last character as it didn't succeed validation
setDecimalInput(text.substring(0, text.length - 1));
}
}
and text input component initialization like this:
<TextInput
style={{textAlign: 'center'}}
value = {decimalInput}
onChangeText={(text) => {
setDecimalInput(text);
onNumberInputChange(text);
}}
placeholder={"type decimal value here..."}
keyboardType="numeric"
placeholderTextColor="#ccc">
</TextInput>
Maybe some of you will need same approach.

I've created a component that solves this problem:
https://github.com/amirfl/react-native-num-textinput

For Decimal /Floating point number only try this
onChangeMyFloatNumber(text){
let newText = '';
let numbers = '0123456789.';
for (var i=0; i < text.length; i++) {
if(numbers.indexOf(text[i]) > -1 ) {
newText = newText + text[i];
if(text[i]=="."){
numbers = '0123456789'
}
}
else {
// your call back function
alert("please enter numbers only");
}
}
this.setState({ MyFloatNumber: newText });
}

if you add following lines then it will open number-pads only not characters-pads when you start typing:
<TextInput
placeholder="Enter Mobile No."
onChangeText={setMobileNumber}
value={mobileNumber}
keyboardType="number-pad"
maxLength={12}
/>

i think,
onChangeText((val:string) =>
isNaN(Number(val))
? val.substr(0, val.length - 1)
: val
)
// 0 > true
// 0. > true
// 0.3 > true
// - > false
// . > false
// 0.4- > false
// -1.3 > true
this code will be check "is this a not a number?" and, if last char is wrong delete last word.

If you are using hooks:
<TextInput
onChangeText={text=> setMyNumber(text?.replace(/[^0-9]/g, ''))}
....
/>
assuming the text is initialise using useState hook
const [myNumber,setMyNumber]=React.useState('');

change your key board type to numeric
<TextInput
onChangeText={this.onChangeText}
keyboardType={"numeric"}
/>
then replace dot with space
onChangeText=(value)=>{
let text = value.replace(".", '');
if (isNaN(text)) {
// Its not a number
return
}
console.log("text",text)
}

Related

How to create v-autocomplete which first shows items startsWith and then shows items indexOf

I would like to create a vuetify autocomplete with a custom filter that first shows the hits that start with the searchtext and then show the hits that not start with the searchtext, but have the searchtext somewhere in the middle.
I now have a custom filter like this, but this filter is not prioritizing words that start with the searchtext:
customFilter(item, queryText) {
const textOne = item.description.toLowerCase();
const textTwo = item.code.toLowerCase();
const searchText = queryText.toLowerCase();
return (
textOne.indexOf(searchText) > -1 || textTwo.indexOf(searchText) > -1
);
}
},
Codepen (Type 'ma' for example)
I believe you need to sort it manually, filter returns only true/false whether item is a match.
// https://stackoverflow.com/a/36114029/1981247
var _sortByTerm = function (data, term) {
return data.sort(function (a, b) {
// cast to lowercase
return a.toLowerCase().indexOf(term) < b.toLowerCase().indexOf(term) ? -1 : 1;
});
};
Then pass sorted array to items prop
<v-autocomplete :items="computedItems"
...
computed: {
computedItems() {
return _sortByTerm(this.states, this.search.toLowerCase())
},
},
Note this is just to get you started, and you might need to change code a bit according to the data (and filters) you are using, e.g. _sortByTerm works only on array of strings, but in the link there is a solution for sorting arrays of objects also.

How can I parse user input decimals based on the current locale in React Native, using a dot or comma for the decimal separator?

I have a number input that pops up a "numeric" text input in React Native.
In many locales this brings up a numeric pad with a comma for decimal separation, instead of a dot. This results in inputs like "100,1" instead of "100.1".
JavaScript's Number(value) only works with dot decimals, not commas. How can I determine the user's current format in order to properly parse the input?
This function will parse a decimal input based on the current locale, using react-native-localize:
import { getNumberFormatSettings } from "react-native-localize";
export function parseLocaleNumber(stringNumber: string) {
const { decimalSeparator, groupingSeparator } = getNumberFormatSettings();
return Number(
stringNumber
.replace(new RegExp(`\\${groupingSeparator}`, "g"), "")
.replace(new RegExp(`\\${decimalSeparator}`), "."),
);
}
For good measure, this complementary function provides toFixed functionality based on locale:
export function toFixedLocale(value: number, numDigits: number) {
const standardFixedString = value.toFixed(numDigits);
const { decimalSeparator } = getNumberFormatSettings();
if (decimalSeparator === ",") {
return standardFixedString.replace(".", ",");
} else {
return standardFixedString; // Locale matches JavaScript default
}
}
(parseLocaleNumber based on https://stackoverflow.com/a/42213804/152711)

How to select efficiently from a long list of options in react-select

My use case is to allow the user to select a ticker from a long list of about 8000 companies. I fetch all the companies when the component mounts, so I don't really need the async feature of react-select. The problem really is displaying and scrolling through the 8000 items (as described in several open issues like this one).
My thought is why display 8000 entries when the user can't do anything meaningful with such a big list anyway. Instead why not show a maximum of 5 matches. As the user types more, the matches keep getting better. Specifically:
When the input is blank, show no options
When the input is a single character, there will still be hundreds of matches, but show only the first 5
As the user keeps on typing, the number of matches will reduce, but still limited to 5. However they will be more relavant.
I am not seeing this solution mentioned anywhere, so was wondering if it makes sense. Also wanted to find out what's the best way to implement it with react-select. I have tried the following two approaches - can you think of a better way:
Approach 1: Use Async React Select
Although I don't need async fetching, I can use this feature to filter down the options. It seems to work very well:
const filterCompanies = (value: string) => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
let count = 0;
return inputLength === 0
? []
: companies.filter(company => {
const keep =
count < 5 &&
(company.ticker.toLowerCase().indexOf(inputValue) >= 0 ||
company.name.toLowerCase().indexOf(inputValue) >= 0);
if (keep) {
count += 1;
}
return keep;
});
};
const promiseOptions = (inputValue: string) =>
Promise.resolve(filterCompanies(inputValue));
return (
<AsyncSelect<Company>
loadOptions={promiseOptions}
value={selectedCompany}
getOptionLabel={option => `${option.ticker} - ${option.name}`}
getOptionValue={option => option.ticker}
isClearable={true}
isSearchable={true}
onChange={handleChange}
/>
);
Approach 2: Use filterOption
Here I am using the filterOption to directly filter down the list. However it does not work very well - the filterOption function is very myopic - it gets only one candidate option at a time and needs to decide if that matches or not. Using this approach I cannot tell whether I have crossed the limit of showing 5 options or not. Net result: with blank input I am showing all 8000 options, as user starts typing, the number of options is reduced but still pretty large - so the sluggishness is still there. I would have thought that filterOption would be the more direct approach for my use case but it turns out that it is not as good as the async approach. Am I missing something?
const filterOption = (candidate: Option, input: string) => {
const { ticker, name } = candidate.data;
const inputVal = input.toLowerCase();
return (
ticker.toLowerCase().indexOf(inputVal) >= 0 ||
name.toLowerCase().indexOf(inputVal) >= 0
);
};
return (
<ReactSelect
options={companies}
value={selectedCompany}
filterOption={filterOption}
getOptionLabel={option => `${option.ticker} - ${option.name}`}
getOptionValue={option => option.ticker}
isClearable={true}
isSearchable={true}
onChange={handleChange}
/>
);
you can try using react-window to replace the menulist component
ref : https://github.com/JedWatson/react-select/issues/3128#issuecomment-431397942

Using symbols in JSX and React Native

I would like to use this symbol in my React Native project.
I tried using the Unicode encoding like this:
var arrow = "U+0279C";
And in the JSX:
<Text>
{arrow}
</Text>
However, this just displays the encoding literally: U+0279C.
So any idea how can I use a symbol in JSX?
You should use the HTML code for the symbol.
<Text>
➜
</Text>
As described in notes below.. (important, quotes don't seem to work)...
Just to clarify: <Text>➜</Text> will work, but <Text>{ '➜' }</Text> will not.
Use this functions for symbols in this format: & # 1 7 4 ;
/**
* replaces /$#d\+/ symbol with actual symbols in the given string
*
* Returns given string with symbol code replaced with actual symbol
*
* #param {string} name
*/
export function convertSymbolsFromCode(name = '') {
let final = null;
if (name) {
const val = name.match(/&#\d+;/) ? name.match(/&#\d+;/)[0] : false; // need to check whether it is an actual symbol code
if (val) {
const num = val.match(/\d+;/) ? val.match(/\d+;/)[0] : false; // if symbol, then get numeric code
if (num) {
final = num.replace(/;/g, '');
}
}
if (final) {
name = name.replace(/&#\d+;/g, String.fromCharCode(final));
}
}
return name;
}
USAGE
<Text>
{convertSymbolsFromCode(this.state.unicode)}
</Text>
The provided answer did not work for me, since I was dynamically retrieving unicode hex codes from an API. I had to pass them as JavaScript into the react-native jsx code.
The following answer worked for me:
Concatenate unicode and variable
I used String.fromCodePoint(parseInt(unicode, 16)) and it worked.
Example:
const unicode = unicodeHexValueFromApi //This value equals "05D2"
return(<Text>{String.fromCodePoint(parseInt(unicode, 16))}</Text>)
<Text>{"Any character"}</Text>
This worked for me.

When try to type on text input nothing is typed (its credit card expiry input)

I am trying to build payment screen. And after writing this code, in expiry date input is not possible to type anything. when you type on keyboard numbers nothing is typed in text input. Also there is no any error on the screen. I write 2 code blocks. First one is the function for the card number and 2nd block of functions is for expiry date input. Those functions I found on the other question answers. Here is the code:
constructor() {
super()
this.state = {
isReady: false
}
}
componentDidMount() {
this.setState({
isReady: true
})
}
onChange(text) {
let newText = '';
let numbers = '0123456789';
for (var i = 0; i < text.length; i++) {
if ( numbers.indexOf(text[i]) > -1 ) {
newText = newText + text[i];
}
}
this.setState({myNumber: newText})
}
formatFunction(cardExpiry = ""){
//expiryDate will be in the format MMYY, so don't make it smart just format according to these requirements, if the input has less than 2 character return it otherwise append `/` character between 2nd and 3rd letter of the input.
if(cardExpiry.length < 2){
return cardExpiry;
}
else{
return cardExpiry.substr(0, 2) + "/" + (cardExpiry.substr(2) || "")
}
}
inputToValue(inputText){
//if the input has more than 5 characters don't set the state
if(inputText.length < 6){
const tokens = inputText.split("/");
// don't set the state if there is more than one "/" character in the given input
if(tokens.length < 3){
const month = Number(tokens[1]);
const year = Number(tokens[2]);
//don't set the state if the first two letter is not a valid month
if(month >= 1 && month <= 12){
let cardExpiry = month + "";
//I used lodash for padding the month and year with zero
if(month > 1 || tokens.length === 2){
// user entered 2 for the month so pad it automatically or entered "1/" convert it to 01 automatically
cardExpiry = _.padStart(month, 2, "0");
}
//disregard changes for invalid years
if(year > 1 && year <= 99){
cardExpiry += year;
}
this.setState({cardExpiry});
}
}
}
}
render (){
let {cardExpiry} = this.state;
return (
<Image style={styles.image} source={require('../img/cover.jpg')}
>
<Content style={styles.content}>
<Form>
<Item >
<Icon active name='card'/>
<Input keyboardType='numeric' maxLength={16} placeholder='Card Number'
onChangeText = {(text)=> this.onChange(text)}
value = {this.state.myNumber}/>
</Item>
<Grid>
<Row>
<Col>
<Item style={{ marginBottom:10}}>
<Icon active name='calendar' />
<Input keyboardType='numeric' placeholder='MM/YY'
value = {this.formatFunction(cardExpiry)}
onChangeText={this.inputToValue.bind(this)}/>
</Item>
</Col>
<Col>
<Item style={{ marginBottom:10}}>
<Icon active name='lock' />
<Input maxLength={3} secureTextEntry={true} placeholder='CVV'/>
</Item>
</Col>
</Row>
</Grid>
How can this issue be solved?
If you look at this line:
//if the input has more than 5 characters don't set the state
if(inputText.length < 6){
you will have your answer ;) Your input's value comes from state, but you do not update it so it will not get updated. As a test try to paste correct string into it and that should work.
To deal with this you have two options:
1) Always update cardExpiry state but set additional isExpiryValid flag in your state and use it to display errors etc.
2) Add additional state value like cardExpiryInputValue which will be updated on change text and your input will get that value, but set cardExpiry only when value is valid and then you can use it as a model value. (although this solution seems overcomplicated for validation purposes)