全選功能實作

這篇會示範如何實作購物車app常有的全選功能

之前曾經用錯誤的方式做導致卡住
而且腦筋轉不過來
一時想不到其他變通方式
只好到社團問
一問才發現其實很容易
不過應該很多事情都是這樣啦
不懂的時候就好像很難
會了之後就覺得自己好蠢XD

首先做出選單
假設 MOCK 是我們資料庫的資料,或是從api獲取的資料
data state 是我們的資料變數
之所以先把資料放在 react state 中
是因為傳入 FlatList 的資料必須是能夠變動的
這樣我們只要改變資料
就能動態渲染在FlatList中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

const MOCK = [
{ name: 'Andy' },
{ name: 'Beck' },
{ name: 'Carl' },
]

const RenderData = ({ item }) => {

return (
<TouchableOpacity
onPress={() => { }}
style={[styles.item_btn]}>
<Text>{item.name}</Text>
</TouchableOpacity>
)
}

export default function App() {
return (
<View style={styles.container}>

<View>
<TouchableOpacity
onPress={()=>{}}
style={[styles.selectAll_btn]}>
<Text>全選</Text>
</TouchableOpacity>
</View>

<View style={styles.flatlist_container}>
<FlatList
data={MOCK}
renderItem={(cases) => <RenderData item={cases.item} />}
keyExtractor={(cases, index) => index.toString()}
style={{ width: '100%', }}
/>
</View>

</View >
);
}

左圖是 app 剛啟動的畫面
中間圖是選取單個項目時的畫面
右圖是點擊全選按鈕時的畫面

以下是實作全選功能的流程圖

虛線部分是 app 剛啟動時的狀態,這時data也還是初始值
接下來我會宣告一個selectedItem陣列來放我所選的項目物件

1
const [selectedItems, setSelectedItems] = useState([]);

每當項目被點擊時,用some()檢查
如果selectedItem中有這個物件,就用filter()把那個項目從中清除;
反之如果沒有的話,就複製一份丟進去
這個函數會被丟進每個項目中

1
2
3
4
5
6
const onItemSelect = (item) => {
setSelectedItems(
prev => prev.some((prevItem) => prevItem === item)
? prev.filter((prevItem) => prevItem !== item)
: [...prev, item])
}

接著記得把selectedItem傳入各個項目中
以便讓每個項目判斷自己是否在其中,改變isSelected state,藉此渲染自己的樣式

1
2
3
4
5
6
7
// in <RenderData/>
const [isSelected, setIsSelected] = useState(false)
useEffect(() => {
if (selectedItems.some(selectedItem => selectedItem === item)) {
setIsSelected(true)
} else { setIsSelected(false) }
}, [selectedItems])

選取後可能還會編輯項目,這時只要更新data並清空selectedItem就ok了
我這邊就先不做編輯的部分

接著回到app()
別忘了還沒做全選按鈕的功能
策略很簡單,宣告一個isAllSelected state
如果還沒全選,就把data中的所有項目丟進selectedItems
如果已經全選了就把selectedItems清空
我們就叫他selectAll

1
2
3
4
5
const [isAllSelected, setIsAllSelected] = useState(false); //現在有沒有全選?
const selectAll = () => {
if (isAllSelected) setSelectedItems([])
else setSelectedItems([...MOCK])
}

在useEffect中判斷有沒有全選

1
2
3
4
5
useEffect(() => {
if (selectedItems.length === MOCK.length) {
setIsAllSelected(true)
} else { setIsAllSelected(false) }
}, [selectedItems]) // 每當selectedItems改變,就檢查一次

把前面提到的onItemSelect和剛剛的selectAll畫成流程圖:

最後的成果:

完整程式碼放在這邊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native';

const MOCK = [
{ name: 'Andy' },
{ name: 'Beck' },
{ name: 'Carl' },
]

const RenderData = ({ item, selectedItems, onItemSelect }) => {

const [isSelected, setIsSelected] = useState(false);
useEffect(() => {

if (selectedItems.some(selectedItem => selectedItem === item)) {
setIsSelected(true)
} else { setIsSelected(false) }
}, [selectedItems])

return (
<TouchableOpacity
onPress={() => { onItemSelect() }}
style={[styles.item_btn, { backgroundColor: isSelected ? 'gray' : 'transparent' }]}>
<Text>{item.name}</Text>
</TouchableOpacity>
)
}

export default function App() {
const [selectedItems, setSelectedItems] = useState([]);
const [isAllSelected, setIsAllSelected] = useState(false);
const onItemSelect = (item) => {
setSelectedItems(prev => prev.some((prevItem) => prevItem === item) ? prev.filter((prevItem) => prevItem !== item) : [...prev, item])
}
const selectAll = () => {
if (isAllSelected) {
setSelectedItems([])
} else {
setSelectedItems([...MOCK])
}
}


useEffect(() => {
if (selectedItems.length === MOCK.length) {
setIsAllSelected(true)
} else { setIsAllSelected(false) }
}, [selectedItems])

return (
<View style={styles.container}>

<View>
<TouchableOpacity
onPress={selectAll}
style={[styles.selectAll_btn, { backgroundColor: isAllSelected ? 'gray' : 'transparent' }]}>
<Text>全選</Text>
</TouchableOpacity>
</View>

<View style={styles.flatlist_container}>
<FlatList
data={MOCK}
renderItem={(cases) => <RenderData item={cases.item} selectedItems={selectedItems} onItemSelect={() => { onItemSelect(cases.item) }} />}
keyExtractor={(cases, index) => index.toString()}
style={{ width: '100%', }}
/>
</View>

</View >
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
selectAll_btn: {
borderWidth: 1,
width: 80,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
flatlist_container: {
borderWidth: 1,
width: 200,
height: 150,
alignItems: 'center',
marginTop: 20,
},
item_btn: {
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
});