Auto-indent with React Native TextInput on iOS

Craig Lu
Quant Five
Published in
5 min readJan 20, 2018
www.codepressapp.com

On one of our projects, Codepress, users are able to solve coding challenges on their mobile devices. This allows people to practice for technical interviews on the go at their own convenience. One of the issues that users expressed was that it was a nuisance to enter in four spaces on the keyboard in place of a tab.

Currently we are developing the app with Expo which does not support the use of custom keyboards. Before we decide to eject from Expo and create a custom keyboard that has a tab button we are exploring other options. One of those options is to have auto-indent in React Native’s TextInput component.

Our TextInput component looks like this

<TextInput 
style={styles.code}
multiline={true}
onChangeText={(code) => this.onChangeText(code)}
value={this.state.code}
autoCapitalize='none'
autoCorrect={false}
autoGrow={true}
onKeyPress={this.handleKeyDown}
onSelectionChange={this.getCursor}
/>

To support this auto-indent functionality I used three props of TextInput: onChangeText, onKeyPress, and onSelectionChange

Firstly we need to capture the changes that the user is making in the TextInput component

onChangeText = (code) => {
if (this.state.lastAction !== 'Enter') {
this.setState({
code: code
})
}
}

setState is wrapped in an if statement where the last action does not equal enter because we are implementing auto-indention that is triggered when enter is pressed so we need to capture this case differently which will be explained later.

Now that we are able to capture the changes the user is making to the code in the TextInput component we can start working on auto-indention. The position of the cursor needs to be obtained so that we can correctly auto-indent the next line based on the previous line that the enter key was pressed.

This is done with the onSelectionChange prop where our function looks like this

getCursor = (e) => {
var cursor = e.nativeEvent.selection;
var cursorPosition;
if (cursor.start > cursor.end) {
cursorPosition = cursor.start;
} else {
cursorPosition = cursor.end
}
this.setState({
cursorPosition: cursorPosition
})
}

This function will keep updating the cursor position and save it to state so that we are able to determine the location in the string where enter was pressed.

The next piece of code deals with the onKeyPressed prop. This is the function in its entirety and I will break it down into parts to help understand what is happenning

handleKeyDown = (e) => {
if (e.nativeEvent.key == "Enter") {
var tempCode = this.state.code
var beforeEnter = tempCode.substring(0, this.state.cursorPosition)
var afterEnter = tempCode.substring(this.state.cursorPosition)
var index = beforeEnter.lastIndexOf('\n') + 1
var previousLine = beforeEnter.substring(index)

var tabLevel = 0
if (previousLine.trim().slice(-1) === ':') {
var prevTabLevel = previousLine.search(/\S|$/) / 4
tabLevel = prevTabLevel + 1
} else {
tabLevel = previousLine.search(/\S|$/) / 4
}

tempCode = beforeEnter + '\n'
for (var i = 0; i < tabLevel; i++) {
tempCode = tempCode + ' '
}
tempCode = tempCode + afterEnter this.setState({
code: tempCode,
lastAction: e.nativeEvent.key,
})
} else {
this.setState({
lastAction: e.nativeEvent.key
})
}
}

The first part to this function is the if else statement

if (e.nativeEvent.key == "Enter") {
// Code
} else {
// Code
}

e.nativeEvent.key tells us which key was pressed. Since we are implementing auto-indent the main key we are focused on is "Enter” . The rest of the keys are grouped together.

Next we will move on to the case where enter was pressed

var tempCode = this.state.code
var beforeEnter = tempCode.substring(0, this.state.cursorPosition)
var afterEnter = tempCode.substring(this.state.cursorPosition)
var index = beforeEnter.lastIndexOf('\n') + 1
var previousLine = beforeEnter.substring(index)

These lines of code sets up the variables that the rest of the if case will use. beforeEnter stores the substring before the cursor position where enter was pressed and afterEnter stores the substring after the cursor position where enter was pressed. Index stores the index in the string just after the last new line character. We increase the index by 1 so that the new line character will not be present when we are counting the number of spaces of the previous line. previousLine is the variable that stores the line just above the new line which we are trying to auto-indent (the line that the user’s cursor was on when the enter key was pressed.)

Moving on to the second part of this if statement, the calculations

var tabLevel = 0if (previousLine.trim().slice(-1) === ':') {
var prevTabLevel = previousLine.search(/\S|$/) / 4
tabLevel = prevTabLevel + 1
} else {
tabLevel = previousLine.search(/\S|$/) / 4
}

tempCode = beforeEnter + '\n'
for (var i = 0; i < tabLevel; i++) {
tempCode = tempCode + ' '
}
tempCode = tempCode + afterEnter

This section of code does the calculations that does the auto-indention for us. tabLevel is the degree of tabs/indentions currently defined in our use case as four space characters.

The if statement assumes that the coding language the user is writing in is python but can be adapted for languages. previousLine.trim().slice(-1) === ':' this conditional is looking to see if the previous line ended with a semicolon or not. In python if a line of code ends with a semi colon the next set of lines are indented one level more similar to curly brackets in Javascript. If there was a semicolon we count the tabLevel of that previous line with var prevTabLevel = previousLine.search(/\S|$/) / 4 which will count the number of spaces before a non space character and divide it by 4 giving the previous tab level. Since the previous line had a semicolon at the end we know that the current line needs to be auto-indented by one more level than the previous line so we set the tabLevel to prevTabLevel + 1 .

If the previous line does not have a semicolon at the end of the line then we assume that the tabLevel will be the same as the previous line.

After calculating the tabLevel we reassemble the string of code where a for loop is used to insert the auto-indention based on the tabLevel .

The last part of the conditional where enter is pressed is saving the changes of the auto-indent along with the key that was pressed.

this.setState({
code: tempCode,
lastAction: e.nativeEvent.key,
})

For the case where the enter key was not pressed, the key that was pressed is saved to state

else {
this.setState({
lastAction: e.nativeEvent.key
})
}

With all of these functions in place, auto-indent is complete and will give the user a more satisfactory user experience.

Note: The onKeyPress prop for TextInput fires before onChange callbacks as described in the React Native docs. This affects a few things in the way that the code was written. First, the cursor position available for use in the onKeyPress function will be the cursor position at the time enter was pressed and not the cursor position of the new line. Second, because onKeyPress is fired first, onChangeText will still run after onKeyPress has finished. This is why we save the changes to the string of code in the onKeyPress function as well as stopping the onChangeText function from saving its changes if enter was pressed, otherwise the auto-indent done in onKeyPress will be overwritten by the onChangeText function.

--

--