Skip to main content

Delete and complete

We've set up our UI to delete our shopping list items or mark them as done, but we haven't actually hooked this stuff up. Let's do it now.

Delete an item

In the ShoppingListItem, add an onDelete prop that's a function to be called instead of the console.log when the user confirms they want to delete the item.

Update: components/ShoppingListItem.tsx
@@ -6,9 +6,10 @@ import { theme } from "../theme";
type Props = {
name: string;
isCompleted?: boolean;
+ onDelete: () => void;
};

-export function ShoppingListItem({ name, isCompleted }: Props) {
+export function ShoppingListItem({ name, isCompleted, onDelete }: Props) {
const handleDelete = () => {
Alert.alert(
`Are you sure you want to delete ${name}?`,
@@ -16,7 +17,7 @@ export function ShoppingListItem({ name, isCompleted }: Props) {
[
{
text: "Yes",
- onPress: () => console.log("Ok, deleting."),
+ onPress: () => onDelete(),
style: "destructive",
},
{ text: "Cancel", style: "cancel" },

In the index route, add a handleDelete function that removes the selected item from the shopping list.

Update: app/index.tsx
@@ -23,6 +23,11 @@ export default function App() {
}
};

+ const handleDelete = (id: string) => {
+ const newShoppingList = shoppingList.filter((item) => item.id !== id);
+ setShoppingList(newShoppingList);
+ };
+
return (
<FlatList
ListHeaderComponent={
@@ -44,7 +49,12 @@ export default function App() {
style={styles.container}
contentContainerStyle={styles.contentContainer}
stickyHeaderIndices={[0]}
- renderItem={({ item }) => <ShoppingListItem name={item.name} />}
+ renderItem={({ item }) => (
+ <ShoppingListItem
+ name={item.name}
+ onDelete={() => handleDelete(item.id)}
+ />
+ )}
></FlatList>
);
}

Complete (or un-complete) an item

Let's add a completedAt prop to our shopping list items and toggle it from the current timestamp to undefined whenever the shopping list item is tapped.

Let's replace the containing View in ShoppingList with a Pressable, add a new onToggleComplete prop and call it when the item is pressed.

Update: components/ShoppingListItem.tsx
@@ -1,4 +1,11 @@
-import { TouchableOpacity, View, Alert, StyleSheet, Text } from "react-native";
+import {
+ TouchableOpacity,
+ View,
+ Alert,
+ StyleSheet,
+ Text,
+ Pressable,
+} from "react-native";
import AntDesign from "@expo/vector-icons/AntDesign";
import Entypo from "@expo/vector-icons/Entypo";
import { theme } from "../theme";
@@ -7,9 +14,15 @@ type Props = {
name: string;
isCompleted?: boolean;
onDelete: () => void;
+ onToggleComplete: () => void;
};

-export function ShoppingListItem({ name, isCompleted, onDelete }: Props) {
+export function ShoppingListItem({
+ name,
+ isCompleted,
+ onDelete,
+ onToggleComplete,
+}: Props) {
const handleDelete = () => {
Alert.alert(
`Are you sure you want to delete ${name}?`,
@@ -26,11 +39,12 @@ export function ShoppingListItem({ name, isCompleted, onDelete }: Props) {
};

return (
- <View
+ <Pressable
style={[
styles.itemContainer,
isCompleted ? styles.completedContainer : undefined,
]}
+ onPress={onToggleComplete}
>
<View style={styles.row}>
<Entypo
@@ -54,7 +68,7 @@ export function ShoppingListItem({ name, isCompleted, onDelete }: Props) {
color={isCompleted ? theme.colorGrey : theme.colorRed}
/>
</TouchableOpacity>
- </View>
+ </Pressable>
);
}

Now add a onToggleComplete function that sets or unsets the completedAt prop on the shopping list item. We'll need to also pass in the isCompleted prop as true when the date is set, and false otherwise.

Update: app/index.tsx
@@ -6,6 +6,7 @@ import { useState } from "react";
type ShoppingListItemType = {
id: string;
name: string;
+ completedAtTimestamp?: number;
};

export default function App() {
@@ -28,6 +29,22 @@ export default function App() {
setShoppingList(newShoppingList);
};

+ const handleToggleComplete = (id: string) => {
+ const newShoppingList = shoppingList.map((item) => {
+ if (item.id === id) {
+ return {
+ ...item,
+ completedAtTimestamp: item.completedAtTimestamp
+ ? undefined
+ : Date.now(),
+ };
+ } else {
+ return item;
+ }
+ });
+ setShoppingList(newShoppingList);
+ };
+
return (
<FlatList
ListHeaderComponent={
@@ -53,6 +70,8 @@ export default function App() {
<ShoppingListItem
name={item.name}
onDelete={() => handleDelete(item.id)}
+ onToggleComplete={() => handleToggleComplete(item.id)}
+ isCompleted={Boolean(item.completedAtTimestamp)}
/>
)}
></FlatList>

Order by last updated

Finally, let's do some ordering. To keep things simple, we'll write a little utility function for this and order the data just before passing it into the FlatList. The requirements are:

  • display the incomplete items first
  • when a new item is added, it gets put at the top of the incomplete pile
  • when it is marked as done, it goes to the top of the complete pile
  • when a previously completed item is marked incomplete, it goes back to the top of the incomplete pile

Since last interactions are important here, we need an extra prop and that is the lastUpdatedTimestamp.

Let's add that to the shopping list type and make sure we set it whenever we add or edit an item:

Update: app/index.tsx
@@ -7,6 +7,7 @@ type ShoppingListItemType = {
id: string;
name: string;
completedAtTimestamp?: number;
+ lastUpdatedTimestamp: number;
};

export default function App() {
@@ -16,7 +17,11 @@ export default function App() {
const handleSubmit = () => {
if (value) {
const newShoppingList = [
- { id: new Date().toISOString(), name: value },
+ {
+ id: new Date().toISOString(),
+ name: value,
+ lastUpdatedTimestamp: Date.now(),
+ },
...shoppingList,
];
setShoppingList(newShoppingList);
@@ -37,6 +42,7 @@ export default function App() {
completedAtTimestamp: item.completedAtTimestamp
? undefined
: Date.now(),
+ lastUpdatedTimestamp: Date.now(),
};
} else {
return item;

Now we have everything we need to order our items using the JavaScript sort function.

With the sort function you pass in one function, (a, b) => number and you return a positive number if a should be before b, and a negative number if b is before a. That's why using timestamps here is handy since you can subtract them form each other to get the right ordering.

Here's the ordering function:

Ordering function
function orderShoppingList(shoppingList: ShoppingListItemType[]) {
return shoppingList.sort((item1, item2) => {
if (item1.completedAtTimestamp && item2.completedAtTimestamp) {
return item2.completedAtTimestamp - item1.completedAtTimestamp;
}

if (item1.completedAtTimestamp && !item2.completedAtTimestamp) {
return 1;
}

if (!item1.completedAtTimestamp && item2.completedAtTimestamp) {
return -1;
}

if (!item1.completedAtTimestamp && !item2.completedAtTimestamp) {
return item2.lastUpdatedTimestamp - item1.lastUpdatedTimestamp;
}

return 0;
});
}

And we'll sort the array just before passing it to the FlatList

Update: app/index.tsx
@@ -68,7 +68,7 @@ export default function App() {
<Text>Your shopping list is empty</Text>
</View>
}
- data={shoppingList}
+ data={orderShoppingList(shoppingList)}
style={styles.container}
contentContainerStyle={styles.contentContainer}
stickyHeaderIndices={[0]}
@@ -84,6 +84,28 @@ export default function App() {
);
}

+function orderShoppingList(shoppingList: ShoppingListItemType[]) {
+ return shoppingList.sort((item1, item2) => {
+ if (item1.completedAtTimestamp && item2.completedAtTimestamp) {
+ return item2.completedAtTimestamp - item1.completedAtTimestamp;
+ }
+
+ if (item1.completedAtTimestamp && !item2.completedAtTimestamp) {
+ return 1;
+ }
+
+ if (!item1.completedAtTimestamp && item2.completedAtTimestamp) {
+ return -1;
+ }
+
+ if (!item1.completedAtTimestamp && !item2.completedAtTimestamp) {
+ return item2.lastUpdatedTimestamp - item1.lastUpdatedTimestamp;
+ }
+
+ return 0;
+ });
+}
+
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colorWhite,

Checkpoint