The second goal for building out an MVP was as follows:
- Our users should be able to input their own tasks using a text field and the native keyboard
To successfully create this input, we have to break down the problem into some necessary requirements:
- We need to have an input field that will spring up our keyboard to type with
- The keyboard should hide itself when we tap outside of it
- When we successfully add a task, it needs to be added to dataSource in TasksList, which is stored in its state
- The list of tasks needs to be stored locally in the application so that a state reset doesn't delete the entire list of tasks we've created
- There're also a couple of forks in the road we should address:
- What happens when the user hits return on the keyboard? Does that automatically create a task? Alternatively, do we implement and support a line break?
- Is there a dedicated Add this task button?
- Does the successful act of adding a task cause the keyboard to go away, requiring the user to tap on the input field again? Alternatively, do we allow the user to keep adding tasks until they tap outside the keyboard?
- How many characters do we support? How long is too long for a task? What kind of feedback is presented to the user of our software if they exceed that limit?
This is a lot to take in, so let's take it one step at a time! I will propose that we ignore the big decisions for now and have the simple act of having an input on the screen, and then having that input be added to our list of tasks.
Since input should be saved to state and then rendered in the ListView, it makes sense for the input component to be a sibling of the ListView, allowing them to share the same state.
Architecturally, this is how the TasksList component will look:
|TasksList
|__TextInput
|__ListView
|____RowData
|____RowData
|____...
|____RowData
React Native has a TextInput component in its API that fulfills our need for a keyboard input. Its code is customizable and will allow us to take input and add it to our list of tasks.
This TextInput component can accept a multitude of props. I have listed the ones we will use here, but the documentation for React Native will provide much more depth:
- autoCorrect: This is a Boolean that turns autocorrection on and off. It is set to true by default
- onChangeText: This is a callback that is fired when the input field's text changes. The value of the component is passed as an argument to the callback
- onSubmitEditing: This is a callback that is fired when a single-line input's submit button is pressed
- returnKeyType: This sets the title of the return key to one of many different strings; done, go, next, search, and send are the five that work across both the platforms
We can break down the task at hand into a couple of bite-sized steps:
- Update container styling in index.ios.js so that its contents take up the entire screen and not just the center
- Add a TextInput component to our TasksList component's render method
- Create a submit handler for the TextInput component that will take the value of the text field and add it to ListView
- Clear the contents of the TextInput once submitted, leaving a blank field for the next task to be added
Take some time to try and add this first feature into our app! In the next section, I will share some screenshots of my results and break down the code I wrote for it.
Here's a screen to show how my input looks at this stage:
It meets the four basic requirements listed in the preceding section: the contents aren't centered on the screen, a TextInput component is rendered at the top, the submit handler takes the value of the TextInput component and adds it to the ListView, and the contents of the TextInput are emptied once that happens.
Let's look at the code to see how I tackled it--yours may be different!:
// Tasks/index.ios.js
import React, { Component } from 'react';
import {
AppRegistry,
View
} from 'react-native';
import TasksList from './app/components/TasksList';
export default class Tasks extends Component {
render() {
return (
<View>
<TasksList />
</View>
);
}
}
AppRegistry.registerComponent('Tasks', () => Tasks);
This is the updated styling for TasksList:
// Tasks/app/components/TasksList/styles.js
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1
}
});
export default styles;
What I did here was remove the justifyContent and alignItems properties of the container so that items weren't constrained to just the center of the display.
Moving on to the TasksList component, I made a couple of major changes:
// Tasks/app/components/TasksList/index.js
import React, { Component } from 'react';
import {
ListView,
Text,
TextInput,
View
} from 'react-native';
import styles from './styles';
export default class TasksList extends Component {
constructor (props) {
super (props);
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
ds: new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
}),
listOfTasks: [],
text: ''
};
}
The constructor now saves three things to state: our local instance of ListView.DataSource, an empty string to keep track of the value of TextInput, and an array to store the list of tasks.
The render function creates a reference to a dataSource that we will use for our ListView component, cloning the listOfTasks array stored in state. Once again, the ListView just presents plain text:
render () {
const dataSource =
this.state.ds.cloneWithRows(this.state.listOfTasks);
The TextInput component has a couple of options. It binds the value of its input field to the text value of our state, changing it repeatedly as the field is edited. On submitting it by pressing the done key on the keyboard, it fires a callback called _addTask:
return (
<View style={ styles.container }>
<TextInput
autoCorrect={ false }
onChangeText={ (text) => this._changeTextInputValue(text) }
onSubmitEditing={ () => this._addTask() }
returnKeyType={ 'done' }
style={ styles.textInput }
value={ this.state.text }
/>
It renders a ListView component with the _renderRowData method being responsible for returning each individual row of the component:
<ListView
dataSource={ dataSource }
enableEmptySections={ true }
renderRow={ (rowData) => this._renderRowData(rowData) }
/>
</View>
);
}
I like to start the name of methods that I personally create in a React component with an underscore so that I can visually distinguish them from the default life cycle methods.
The _addTask method uses the array spread operator introduced in ES6 to create a new array and copy over an existing array's values, adding the newest task to the list at the end. Then, we assign it to the listOfTasks property in state. Remember that we have to treat our component state as an immutable object and simply pushing to it will be an anti-pattern:
_addTask () {
const listOfTasks = [...this.state.listOfTasks, this.state.text];
this.setState({
listOfTasks
});
this._changeTextInputValue(''
}
Finally, we call _changeTextInputValue so that the TextInput box is emptied:
_changeTextInputValue (text) {
this.setState({
text
});
}
_renderRowData (rowData) {
return (
<Text>{ rowData }</Text>
)
}
}
For now, just returning the name of the to-do list item is fine.
When setting the listOfTasks property in the _addTask method and the text property in _changeTextInputValue, I'm using a new notation feature of ES6, called shorthand property names, to assign a value to a key with the same name as the value. This is the same as if I were to write as follows:
this.setState({
listOfTasks: listOfTasks,
text: text
})
Moving on, you might note that, as you refresh the application, you lose your state! This is impractical for a to-do list app, since we should never expect the user to re-enter the same list whenever they re-open the app. What we want is to store this list of tasks locally in the device so that we can access it whenever needed. This is where AsyncStorage comes into play.