Set up the development environment by installing Expo CLI (https://reactnative.dev/docs/environment-setup)
yarn global add expo-cli
Create a new project (app):
expo init todo-app
# select "blank (TypeScript)"
Start the development server:
cd todo-app
yarn start
# press w to open the web app
Install a checkbox component:
yarn add react-native-bouncy-checkbox
Then create the following folders & files structure:
./todo-app/
...
models/
todo.model.ts
components/
todo.tsx
todo-list.tsx
menu.tsx
views/
edit-todo.view.tsx
...
Define the data model in the file models/todo.model.ts
:
interface ITodo {
id: string
done: boolean
text: string
color: string
}
export default ITodo
Create the Todo component in components/todo.tsx
:
import React, { useState } from "react";
import { StyleSheet, Text, View } from 'react-native';
import BouncyCheckbox from "react-native-bouncy-checkbox";
import ITodo from "../models/todo.model";
interface ITodoProps {
data: ITodo
}
const Todo = (props: ITodoProps) => {
const [isDone, setDone] = useState(false);
return (
<View style={[styles.container, {backgroundColor: props.data.color}]}>
<BouncyCheckbox
fillColor="black"
unfillColor="#FFFFFF"
iconStyle={{ borderColor: "black" }}
isChecked={isDone}
onPress={setDone}
style={styles.checkbox}
/>
<Text style={styles.text}>{props.data.text}</Text>
</View>
);
}
export default Todo
const styles = StyleSheet.create({
container: {
width: '100%', minHeight: '30px', height: 'auto',
color: 'black',
alignItems: 'center',
justifyContent: 'flex-start',
display: 'flex',
flexDirection: 'row',
marginBottom: 5,
padding: 10,
borderRadius: 5,
},
checkbox: {
width: 40,
minWidth: 40, height: 40,
},
text: {
color: 'black',
width: '100%',
}
});
Then, the Todo List component in components/todo-list.tsx
:
import React, { useState } from "react";
import { StyleSheet, FlatList } from 'react-native';
import ITodo from "../models/todo.model";
import Todo from "./todo";
interface ITodoListProps {
data: ITodo[]
}
const TodoList = (props: ITodoListProps) => {
const [isDone, setDone] = useState(false);
return (
<FlatList
style={styles.container}
data={props.data}
renderItem={
(item: any) => {
return (
<Todo data={item.item} />
)
}
}
keyExtractor={(item, index) => item.id}
/>
);
}
export default TodoList
const styles = StyleSheet.create({
container: {
// height: '100%', maxHeight: '100%',
height: 500,
width: '100%',
flexDirection: 'column',
padding: 10,
overflow: 'scroll',
},
});
And the Main Menu in components/menu.tsx
:
import React, { useState } from "react";
import { Button, StyleSheet, Text, View } from 'react-native';
interface IMenu {
onAddTodo: () => void
}
const Menu = (props: IMenu) => {
return (
<View style={styles.container}>
<Button
title="+ Add ToDo"
onPress={() => props.onAddTodo()}/>
</View>
);
}
export default Menu
const styles = StyleSheet.create({
container: {
width: '100%', minHeight: '30px', height: 'auto',
color: 'black',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
flexDirection: 'row',
padding: 20,
},
});
Create the Add/Edit View in view/edit-todo.view.tsx
:
import React, { useState } from "react"
import { Modal, StyleSheet, TextInput, View, Text, TouchableOpacity, KeyboardAvoidingView } from 'react-native';
import ITodo from "../models/todo.model";
interface IEditTodoProps {
isVisible: boolean
onClose: () => void
onSave: (data: any) => void
data?: ITodo
}
const EditTodoView = (props: IEditTodoProps) => {
const colors = ['#87D3F5', '#BDE991', '#BAAAFB']
const [colorIndex, setColorIndex] = useState(0)
const title = props.data ? 'Edit Todo' : 'Add Todo'
const [text, setText] = useState(props.data?.text || '')
const onSave = () => {
if (text.trim().length === 0) {
props.onClose()
return
}
if (props.data) {
const newData = {
...props.data,
text
}
props.onSave(newData)
} else {
const newData = {
id: 'id-' + Math.floor(Math.random() * 10000000),
text,
done: false,
color: colors[colorIndex],
}
props.onSave(newData)
}
}
return (
<Modal visible={props.isVisible} style={styles.modal}
animationType="slide"
transparent={true}
>
<KeyboardAvoidingView style={styles.container} >
<Text style={styles.title}>{title}</Text>
<View style={styles.content}>
<Text style={styles.label}>ToDo Text:</Text>
<TextInput
style={styles.input}
onChangeText={setText}
value={props.data?.text}
multiline={true}
numberOfLines={10}
// keyboardType="numeric"
/>
<Text style={styles.label}>ToDo Color:</Text>
<View style={styles.colors} >
<View style={[styles.color, {
backgroundColor: colors[0],
borderColor: 'black',
borderWidth: colorIndex === 0 ? 4 : 0
}]}
>
<TouchableOpacity
style={{height: '100%', width:'100%'}}
onPress={() => setColorIndex(0)}>
</TouchableOpacity>
</View>
<View style={[styles.color, {
backgroundColor: colors[1],
borderColor: 'black',
borderWidth: colorIndex === 1 ? 4 : 0}]}
>
<TouchableOpacity
style={{height: '100%', width:'100%'}}
onPress={() => setColorIndex(1)}>
</TouchableOpacity>
</View>
<View style={[styles.color, {
backgroundColor: colors[2],
borderColor: 'black',
borderWidth: colorIndex === 2 ? 4 : 0}]}
>
<TouchableOpacity
style={{height: '100%', width:'100%'}}
onPress={() => setColorIndex(2)}>
</TouchableOpacity>
</View>
</View>
</View>
<View style={styles.menu} >
<TouchableOpacity
style={styles.button}
onPress={() => props.onClose()}
>
<Text style={styles.buttonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.button}
onPress={onSave}
>
<Text style={styles.buttonText}>Save</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</Modal>
)
}
export default EditTodoView
const styles = StyleSheet.create({
modal: {
backgroundColor: 'rgba(0,0,0,0)',
},
container: {
width: '100%',
height: '100%',
paddingTop: 100,
// backgroundColor: '#fff',
backgroundColor: 'rgba(0,0,0,0.7)',
flexDirection: 'column',
},
content: {
backgroundColor: '#fff',
flexDirection: 'column',
},
title: {
fontSize: 24,
fontWeight: 'bold',
padding: 20,
paddingBottom: 0,
backgroundColor: '#fff',
},
menu: {
display: 'flex',
width: '100%', height: 60,
paddingLeft: 30,
paddingTop: 15,
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#fff'
},
input: {
height: 'auto',
margin: 12,
borderWidth: 1,
padding: 10,
},
label: {
padding: 10,
paddingBottom: 0,
},
colors: {
width: '100%',
display: 'flex',
flexDirection: 'row',
padding: 20,
},
color: {
width: 30, height: 30,
marginRight: 20,
borderRadius: 3,
},
button: {
height: 20,
width: 100,
},
buttonText: {
fontSize: 18,
color: '#007fff'
}
});
Update the file App.tsx
with:
import React, { useState } from 'react';
import { StyleSheet, Text, SafeAreaView } from 'react-native';
import Menu from './components/menu';
import TodoList from './components/todo-list';
import ITodo from './models/todo.model';
import EditTodoView from './views/edit-todo.view';
export default function App() {
const [data, setData] = useState<ITodo[]>([])
const [isEditTodoVisible, setIsEditTodoVisible] = useState(false)
const onAddTodo = () => {
setIsEditTodoVisible(true)
}
const onCloseEditTodo = () => {
setIsEditTodoVisible(false)
}
const onSaveTodo = (data: ITodo) => {
setData((d) => [...d, data])
setIsEditTodoVisible(false)
}
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>ToDo</Text>
<TodoList data={data} />
<Menu onAddTodo={onAddTodo}/>
<EditTodoView isVisible={isEditTodoVisible}
onClose={onCloseEditTodo}
onSave={onSaveTodo}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
display: 'flex',
flexDirection: 'column',
backgroundColor: '#fff',
height: '100%',
width: '100%',
},
title: {
fontSize: 24,
fontWeight: 'bold',
padding: 20,
paddingBottom: 0,
},
});
You can find the source code here
Also published here