8

In React Native using <TextInput/>, I am trying to make / appear only when the <TextInput/> is focused, and would stay there if another input is entered in. Currently, the format is MM/YY, so when the user types the third digit, it would go after the /, and if the user were to press back, it would delete the digit before the /.

So what would be the right approach to implementing previously mentioned? Thank you and will be sure to accept the answer.

I tried the following but getting an error with length, and this is only adding / after two digits have been entered:

  _changeCardExpiry(value) {
    if (value.indexOf('.') >= 0 || value.length > 5) {
      return;
    }

    if (value.length === 2 && this.state.cardExpiry.length === 1) {
      value += '/'
    }

    //then update state cardExpiry
  }

  ...

  <TextInput
    onChangeText={this._changeCardExpiry.bind(this)}
    placeholder='MM/YY'
    value={cardExpiry}
  />
  • What is the error with length? – PaulBGD Oct 28 '16 at 23:37
  • @PaulBGD I get an error at `if (text.length === 2 && this.state.cardExpiry.length === 1)` saying `Cannot read property 'length' of undefined.` –  Nov 02 '16 at 22:08

4 Answers4

5

In formattion you actually need 3 functions one is for formatting the actual value, second is for converting formatted value back to actual value and the third is needed for checking whether the entered input so far can be valid or not. For example for a date input entered letter inputs should be disregarded, but at the same time 99 should be disregarded as it is not a valid input for a month. So for your specific case the following structure should work for you (in this example inputToValue function both checks whether the entered input is valid and set state according to it):

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 <TextInput
       value = {this.formatFunction(cardExpiry)}
       onChangeText={this.inputToValue.bind(this)}/>;
}
cubbuk
  • 7,800
  • 4
  • 35
  • 62
2

you can use this func from onChangeText;

Don't forget to bind method inside constructor ;

this.fixCardText = this.fixCardText.bind(this)

fixCardText(text){
  if(text.length == 2 && this.state.text.length == 1){
    text += '/'
  }else if(text.length == 2 && this.state.text.length == 3){
    text = text.substring(0 , text.length-1)
  }
  this.setState({text:text}) 
}

your text input should be like;

<TextInput
  value = {this.state.text}
  onChangeText={(text)=>{this.fixCardText(text)}}
/>
Shubham Kumar
  • 295
  • 3
  • 13
2

Not complete solution, but it solves similar problem - masking bluetooth address
AB
AB:C
AB:CD:EF:GH:IJ:KL

/*
Usage:
import { TextInput } from '../utils/input'
const MaskedTextInput = withMask(TextInput)
<MaskedTextInput
  placeholder="Printer address"
  value={ printerId }
  onChange={this.handlePrinterAddressChange}
/>
*/

import React, { Component } from 'react'
import { View } from 'react-native'

export const withMask = (WrappedComponent) => class Wrapper extends Component {

  constructor(props) {
    super()
    this.state = { value: props.value }
  }

  onChange(event) {
    let value = event.nativeEvent.text
    const rawValue = event.nativeEvent.text.replace(/:/g, '')

    if (rawValue) {
      value = rawValue.match(/.{1,2}/g).join(':').toUpperCase()
    }

    this.setState({value})

    if (this.props.onChange) {
      event.nativeEvent.text = value
      this.props.onChange(event)
    }
  }

  render() {
    return <WrappedComponent 
      {...this.props} 
      onChange={(event) => this.onChange(event)}
      value={this.state.value}
    />
  }

}
0

You can use 2 TextInputs instead on one to keep it simple.

here's a way of doing that.

<TextInput
            onChangeText={() => alert('update state with Expiry Month')}
            placeholder="MM/"
            value={expiryMonth}
          />
          {expiryMonth.length > 0 && <Text> / </Text>}
          <TextInput
            onChangeText={() => alert('update state with Expiry Year or do whatever you need with whole Expiry Date')}
            placeholder="YY"
            value={expiryYear}
          />