アプリ開発を楽しむ【#11:Togoの追加】
前回までの記事 アプリ開発を楽しむ【#1:アプリの概要】 アプリ開発を楽しむ【#2:環境構築1(React+TypeScript)】 アプリ開発を楽しむ【#3:環境構築2 (ESLint+Prettier)】 アプリ開発を楽しむ【#4:ヘッダー】 アプリ開発を楽しむ【#5:MyTogoList】 アプリ開発を楽しむ【#6:Reduxで状態管理1】 アプリ開発を楽しむ【#7:Reduxで状態管理2】 アプリ開発を楽しむ【#8:Google Map API】 アプリ開発を楽しむ【#9:新規追加モーダルUI(togo)】 アプリ開発を楽しむ【#10:Google Mapから座標を取得】
前回と前々回で、Togoを追加できるフォームをつくってきました。 今回は、MyTogoListに新しいTogoを追加できるようにAddTogoModalを完成させたいと思います。
1.togoの型を修正
frontend/app/src/types/togo.tsを修正します。
// ここまで追加
import type { MapPosition } from './map';
// ここまで追加
export type Togo = {
// ここから修正
id: number | undefined;
// ここまで修正
done: boolean;
location: string;
// ここから修正
position: MapPosition;
// ここまで修正
tag: string;
};
前回、緯度経度の型定義をつくったので、togoにも反映させておきます。 idは、新規登録の際、定義しないことになるので、修正しておきます。
2.MyTogoListの修正
~~~省略~~~
import sampleTogoList from '../../sampleData/togo';
// ここから追加
const initialTogoData = {
id: undefined,
done: false,
location: '',
tag: '',
position: {
lat: 35.6808610662155,
lng: 139.76856460990368,
},
};
ここまで追加
const MyTogoList = () => {
const dispatch: AppDispatch = useDispatch();
~~~省略~~~
return (
<>
<AddTogoModal
// ここから追加
togoData={initialTogoData}
// ここまで追加
isOpenAddTogoModal={isOpenAddTogoModal}
handleCloseAddTogoModal={handleCloseAddTogoModal}
/>
<Typography
component="h2"
variant="h6"
color="primary"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
~~~省略~~~
Togoの初期データをpropsとして、AddTogoModalに渡しています。
Togoのidの型定義にundefinedを追加したので、MyTogoListの次の部分を修正します。
// ここから修正
const handleChangeTogoDone = (id: number | undefined) => {
if (id === undefined) {
return;
}
dispatch(updateTogoDone(id));
};
const handleDeleteTogo = (id: number | undefined) => {
if (id === undefined) {
return;
}
dispatch(deleteTogo(id));
};
// ここまで修正
idが定義されていない場合は、何もしないようにしています。
ついでになりますが、次の部分も修正しておきます。 Togoリストが空の場合に、リストのヘッダーではなく、「登録されているTogoはありません。」と表示するように修正しました。
<Typography
component="h2"
variant="h6"
color="primary"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
My List
</Typography>
// ここから追加
{togoList.length ? (
// ここまで追加
<Table size="small">
<TableHead>
<TableRow>
<TableCell> </TableCell>
<TableCell>場所</TableCell>
<TableCell>タグ</TableCell>
<TableCell>地図</TableCell>
<TableCell>編集</TableCell>
<TableCell>削除</TableCell>
</TableRow>
</TableHead>
<TableBody>
// ここから修正
{togoList.map((item) => (
// ここまで修正
<TableRow key={item.id}>
<TableCell>
<Checkbox checked={item.done} onChange={() => handleChangeTogoDone(item.id)} />
</TableCell>
<TableCell>{item.location}</TableCell>
<TableCell>
<Chip label={item.tag} color="primary" size="small" />
</TableCell>
<TableCell>
<Link
href={`https://maps.google.co.jp/maps?ll=${item.position.lat},${item.position.lng}&z=20`}
underline="none"
target="_blank"
rel="noopener"
>
<LocationOnIcon />
</Link>
</TableCell>
<TableCell>
<ModeEditOutlineIcon sx={{ cursor: 'pointer' }} />
</TableCell>
<TableCell>
<DeleteIcon
sx={{ cursor: 'pointer' }}
onClick={() => handleDeleteTogo(item.id)}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
// ここから追加
) : (
<Typography variant="body1">登録されているToGoはありません</Typography>
)}
// ここまで追加
<Fab
color="primary"
aria-label="add"
size="medium"
sx={{ position: 'absolute', top: 16, right: 16 }}
onClick={handleOpenAddTogoModal}
>
<AddIcon />
</Fab>
</>
3.AddTogoModalの修正
場所とタグ用のステートを用意します。 前回までに、座標の位置情報用のステートを作成していました。同じ様に場所とタグ用のステートをつくります。 また、mapCenterPositionとmapMarkerPositionの初期値の修正も行います。
import type { MapPosition } from '../../types/map';
// ここから追加
import type { Togo } from '../../types/togo';
// ここまで追加
type AddTogoModalProps = {
// ここから追加
togoData: Togo;
// ここまで追加
isOpenAddTogoModal: boolean;
handleCloseAddTogoModal: () => void;
};
const AddTogoModal: React.FC<AddTogoModalProps> = ({
// ここから追加
togoData,
// ここまで追加
isOpenAddTogoModal,
handleCloseAddTogoModal,
}) => {
const mapStyles = {
height: '500px',
width: '100%',
};
const mapOptions = {
gestureHandling: 'cooperative',
zoomControl: false,
scaleControl: false,
streetViewControl: false,
panControl: false,
mapTypeControl: false,
fullscreenControl: false,
};
const [searchLocationKeyword, setSearchLocationKeyword] = useState<string>('');
// ここから修正
const [mapCenterPosition, setMapCenterPosition] = useState<MapPosition>(togoData.position);
// ここまで修正
// ここから追加
const [location, setLocation] = useState<string>(togoData.location);
const [tag, setTag] = useState<string>(togoData.tag);
// ここまで追加
// ここから修正
const [mapMarkerPosition, setMapMarkerPosition] = useState<MapPosition>(togoData.position);
// ここまで修正
// ここから追加
const handleChangeLocation = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocation(e.target.value);
};
const handleChangeTag = (e: React.ChangeEvent<HTMLInputElement>) => {
setTag(e.target.value);
};
const handleChangeMapMarkerPosition = (position: MapPosition) => {
setMapMarkerPosition(position);
};
const handleChangeMapCenterPosition = (position: MapPosition) => {
setMapCenterPosition(position);
};
// ここまで追加
const createMarker = (e: google.maps.MapMouseEvent) => {
const lat = e.latLng?.lat();
const lng = e.latLng?.lng();
if (lat && lng) {
// ここから修正
handleChangeMapMarkerPosition({ lat, lng });
// ここまで修正
}
};
const geocode = () => {
const geocoder = new window.google.maps.Geocoder();
geocoder
.geocode({ address: searchLocationKeyword }, (results, status) => {
if (results && status === google.maps.GeocoderStatus.OK) {
const lat = results[0].geometry?.location.lat();
const lng = results[0].geometry?.location.lng();
// ここから修正
handleChangeMapCenterPosition({ lat, lng });
// ここまで修正
}
})
.catch((err) => console.log(err));
};
const handleSearchLocation = () => {
searchLocation();
};
// ここから追加
const handleAddTogo = () => {
if (!location || !tag) {
return;
}
const addTogoData: Togo = {
id: togoList.length,
done: false,
location,
tag,
position: mapMarkerPosition,
};
console.log(addTogoData);
handleCloseAddTogoModal();
};
// ここまで追加
return (
<Dialog open={isOpenAddTogoModal} onClose={handleCloseAddTogoModal} fullWidth maxWidth="md">
<DialogTitle>あなたが行きたいところを登録しましょう!</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="name"
label="場所"
type="text"
fullWidth
variant="standard"
// ここから追加
value={location}
onChange={handleChangeLocation}
// ここまで修正
/>
<TextField
margin="dense"
id="name"
label="タグ"
type="text"
fullWidth
variant="standard"
// ここから追加
value={tag}
onChange={handleChangeTag}
// ここまで修正
/>
<Paper
component="form"
elevation={2}
sx={{ p: '2px 4px', m: '10px 0px', display: 'flex', alignItems: 'center', width: 400 }}
>
<InputBase
sx={{ ml: 1, flex: 1 }}
placeholder="Search Google Maps"
inputProps={{ 'aria-label': 'search google maps' }}
onChange={(e) => setSearchLocationKeyword(e.target.value)}
onKeyDown={handleKeyPressSearchLocation}
/>
<IconButton sx={{ p: '10px' }} aria-label="search" onClick={handleSearchLocation}>
<SearchIcon />
</IconButton>
</Paper>
<Paper elevation={0} sx={{ mt: 2 }}>
<GMap
mapStyles={mapStyles}
zoom={10}
mapCenterPosition={mapCenterPosition}
mapOptions={mapOptions}
handleClick={(e) => createMarker(e)}
>
{mapMarkerPosition && <Marker position={mapMarkerPosition} />}
</GMap>
</Paper>
</DialogContent>
<DialogActions sx={{ mb: 1, ml: 2, display: 'flex', justifyContent: 'flex-start' }}>
// ここから修正
<Button variant="contained" onClick={handleAddTogo}>
// ここまで修正
登録
</Button>
<Button variant="outlined" onClick={handleCloseAddTogoModal}>
キャンセル
</Button>
</DialogActions>
</Dialog>
);
とりあえずこれで一度動かしてみて、consoleに登録した内容が表示されればOKです。
4.ReduxのStoreのステートを更新する
frontend/app/src/redux/togoSlice.tsを修正して、togoListのステートに新しいTogoを追加するreducerをつくります。
export const togoSlice = createSlice({
name: 'togo',
initialState,
reducers: {
getTogoList(state: TogoState, action: PayloadAction<Togo[]>) {
state.togoList = action.payload;
},
updateTogoDone(state: TogoState, action: PayloadAction<number>) {
state.togoList = state.togoList.map((togo) => {
if (togo.id === action.payload) {
togo.done = !togo.done;
}
return togo;
});
},
deleteTogo(state: TogoState, action: PayloadAction<number>) {
state.togoList = state.togoList.filter((togo) => togo.id !== action.payload);
},
// ここから追加
addTogo(state: TogoState, action: PayloadAction<Togo>) {
state.togoList = [...state.togoList, action.payload];
},
// ここまで追加
},
});
// ここから修正
export const { getTogoList, updateTogoDone, deleteTogo, addTogo } = togoSlice.actions;
// ここまで修正
AddTogoModal.tsxの修正して、ステートを更新できるようにします。
import React, { useState, useEffect } from 'react';
// ここから追加
import { useDispatch, useSelector } from 'react-redux';
// ここまで追加
~~~省略~~~
import GMap from '../common/GMap';
// ここから追加
import { RootState, AppDispatch } from '../../redux/store';
import { addTogo, initialState } from '../../redux/togoSlice';
// ここまで追加
import type { MapPosition } from '../../types/map';
import type { Togo } from '../../types/togo';
~~~省略~~~
const mapOptions = {
gestureHandling: 'cooperative',
zoomControl: false,
scaleControl: false,
streetViewControl: false,
panControl: false,
mapTypeControl: false,
fullscreenControl: false,
};
// ここから追加
const dispatch: AppDispatch = useDispatch();
const { togoList } = useSelector((state: RootState) => state.togo || initialState);
// ここまで追加
const [searchLocationKeyword, setSearchLocationKeyword] = useState<string>('');
const [mapCenterPosition, setMapCenterPosition] = useState<MapPosition>(togoData.position);
~~~省略~~~
const handleAddTogo = () => {
if (!location || !tag) {
return;
}
const addTogoData: Togo = {
// ここから修正
id: togoList.length,
// ここまで修正
done: false,
location,
tag,
position: mapMarkerPosition,
};
// ここから削除
console.log(addTogoData)
// ここまで削除
// ここから追加
dispatch(addTogo(addTogoData));
// ここまで追加
handleCloseAddTogoModal();
};
storeからtogoListを取得しているのは、togoListの配列の要素数を取得して、idに一意の番号を割り当てるためです。実際には、idには値を割り当てず、バックエンドと通信を行います。idは、データベースが自動的に割り当てることになります。 これで、MyTogoListに新しいTogoを追加できるようになりました。
5.フォームの初期化
いまのままですと、新しくTogoを登録し、再びTogoを登録しようとすると、AddTogoModalに前回の値が残ったままになります。これを修正していきます。
AddTogoModal.tsxを修正します。
~~~省略~~~
const [location, setLocation] = useState<string>(togoData.location);
const [tag, setTag] = useState<string>(togoData.tag);
const [mapMarkerPosition, setMapMarkerPosition] = useState<MapPosition>(togoData.position);
// ここから追加
const initializeTogoState = (isOpen: boolean) => {
if (isOpen) {
setLocation(togoData.location);
setTag(togoData.tag);
setMapMarkerPosition(togoData.position);
setMapCenterPosition(togoData.position);
}
};
useEffect(() => {
initializeTogoState(isOpenAddTogoModal);
}, [isOpenAddTogoModal]);
// ここまで追加
const handleChangeLocation = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocation(e.target.value);
};
const handleChangeTag = (e: React.ChangeEvent<HTMLInputElement>) => {
setTag(e.target.value);
};
~~~省略~~~
今回のプロジェクでは、モーダルが開くたびに初期化されるようにしました。
最後ですが、ESLintのrulesの設定に次を追加します。
rules: {
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error'],
'no-param-reassign': 0,
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'react/require-default-props': 'off',
// ここから追加
'react-hooks/exhaustive-deps': 'off',
// ここまで追加
'react/function-component-definition': [2, { namedComponents: 'arrow-function' }],
'react/jsx-filename-extension': [
'error',
{
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
},
今回は以上となります。 次回は、登録したTogoの編集ができるようにしていきたいと思います。
コードはGitHubに置いてありますのでよければ参考にしてください。 mainブランチは常に最新のものになります。 今回の内容はblog_11のブランチを参照してください。 https://github.com/KINE-M/togo_app