アプリ開発を楽しむ【#12:Togoの編集1】
前回までの記事 アプリ開発を楽しむ【#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から座標を取得】 アプリ開発を楽しむ【#11:Togoの追加】
今回から何回かに分けて、登録してあるTogoの編集をできるようにしていきたいと思います。
1.編集するTogoの値を取得する
①編集アイコンをクリックしたときに、選択したTogoの値を取得できるようにします。
frontend/ap/src/components/togo/MyTogoList.tsxを修正します。
~~~省略~~~
import sampleTogoList from '../../sampleData/togo';
// ここから追加
import type { Togo } from '../../types/togo';
// ここまで追加
const initialTogoData = {
id: undefined,
done: false,
location: '',
tag: '',
position: {
lat: 35.6808610662155,
lng: 139.76856460990368,
},
};
~~~省略~~~
const handleCloseAddTogoModal = () => {
setIsOpenAddTogoModal(false);
};
// ここから追加
const handleOpenUpdateTogoModal = (id: number | undefined) => {
if (id === undefined) {
return;
}
const data: Togo[] = togoList.filter((togo) => togo.id === id);
console.log(data[0]);
};
// ここまで追加
return (
<>
<AddTogoModal
togoData={initialTogoData}
isOpenAddTogoModal={isOpenAddTogoModal}
handleCloseAddTogoModal={handleCloseAddTogoModal}
/>
<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' }}
// ここから追加
onClick={() => handleOpenUpdateTogoModal(item.id)}
// ここまで追加
/>
</TableCell>
<TableCell>
<DeleteIcon
sx={{ cursor: 'pointer' }}
onClick={() => handleDeleteTogo(item.id)}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<Typography variant="body1">登録されているToGoはありません</Typography>
)}
~~~省略~~~
編集用モーダルを開いたときに、値を取得するので、handleUpdateTogoModalというメソッド名にしています。 編集ボタンをクリックして、選択したTogoの値がconsoleに表示されればOKです。
②togoDataというステートをつくり、取得した値を編集用モーダルにpropsとして渡せるようにします。 ついでに、UpdateTogoModalを開閉するステートもつくっています。
~~~省略~~~
const { togoList } = useSelector((state: RootState) => state.togo || initialState);
const [isOpenAddTogoModal, setIsOpenAddTogoModal] = useState<boolean>(false);
// ここから追加
const [isOpenUpdateTogoModal, setIsOpenUpdateTogoModal] = useState<boolean>(false);
const [togoData, setTogoData] = useState<Togo>(initialTogoData);
// ここまで追加
useEffect(() => {
dispatch(getTogoList(sampleTogoList));
}, [dispatch]);
~~~省略~~~
const handleOpenUpdateTogoModal = (id: number | undefined) => {
if (id === undefined) {
return;
}
const data: Togo[] = togoList.filter((togo) => togo.id === id);
// ここから修正
setTogoData(data[0]);
setIsOpenUpdateTogoModal(true)
// ここまで修正
};
// ここから追加
const handleCloseUpdateTogoModal = () => {
setIsOpenUpdateTogoModal(false);
};
// ここまで追加
return (
<>
<AddTogoModal
togoData={initTogoData}
isOpenAddTogoModal={isOpenAddTogoModal}
handleCloseAddTogoModal={handleCloseAddTogoModal}
/>
// ここから追加
<UpdateTogoModal
togoData={togoData}
isOpenUpdateTogoModal={isOpenUpdateTogoModal}
handleCloseUpdateTogoModal={handleCloseUpdateTogoModal}
/>
// ここまで追加
<Typography
component="h2"
variant="h6"
color="primary"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
My List
</Typography>
2.Togoの追加用モーダルの修正
編集用モーダルのコンポーネントをつくっていきますが、ほとんど、AddTogoModalコンポーネントと同じになります。
なので、同じところは共通化し、その部分をTogoFormコンポーネントとしてつくって、AddTogoModalとUpdateTogoModalの両方のコンポーネントで利用していきたいと思います。
共通化するところは、下のイメージですと赤色の□で囲ったところ(場所・タグ・地図)になります。
frontend/app/src/components/togo/TogoForm.tsxをつくって、AddTogoModal.tsxの共通部分をTogoForm.tsxに移していきます。 現在のAddTogoModalのコードは以下のようになっています。(移動させる部分と移動させない部分をコメントとして、記載してあります。)
type AddTogoModalProps = {
togoData: Togo;
isOpenAddTogoModal: boolean;
handleCloseAddTogoModal: () => void;
};
const AddTogoModal: React.FC<AddTogoModalProps> = ({
togoData,
isOpenAddTogoModal,
handleCloseAddTogoModal,
}) => {
// ここからTogoFormに移動
const mapStyles = {
height: '500px',
width: '100%',
};
const mapOptions = {
gestureHandling: 'cooperative',
zoomControl: false,
scaleControl: false,
streetViewControl: false,
panControl: false,
mapTypeControl: false,
fullscreenControl: false,
};
// ここまでTogoFormに渡す
const dispatch: AppDispatch = useDispatch();
const { togoList } = useSelector((state: RootState) => state.togo || initialState);
// ここからTogoFormに移動
const [searchLocationKeyword, setSearchLocationKeyword] = useState<string>('');
// ここまでTogoFormに移動
// ここからTogoFormにpropsとして渡す(移動させない)
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);
// ここまでTogoFormにpropsとして渡す
const initializeTogoState = (isOpen: boolean) => {
if (isOpen) {
setLocation(togoData.location);
setTag(togoData.tag);
setMapMarkerPosition(togoData.position);
setMapCenterPosition(togoData.position);
}
};
useEffect(() => {
initializeTogoState(isOpenAddTogoModal);
}, [isOpenAddTogoModal]);
// ここからTogoFormにpropsとして渡す(移動させない)
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);
};
// ここまでTogoFormにpropsとして渡す
// ここからTogoFormに移動
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 searchLocation = () => {
if (!searchLocationKeyword) {
return;
}
geocode();
};
const handleKeyPressSearchLocation = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
searchLocation();
}
};
const handleSearchLocation = () => {
searchLocation();
};
// ここまでTogoFormに移動
const handleAddTogo = () => {
if (!location || !tag) {
return;
}
const addTogoData: Togo = {
id: togoList.length,
done: false,
location,
tag,
position: mapMarkerPosition,
};
dispatch(addTogo(addTogoData));
handleCloseAddTogoModal();
};
return (
<Dialog open={isOpenAddTogoModal} onClose={handleCloseAddTogoModal} fullWidth maxWidth="md">
<DialogTitle>あなたが行きたいところを登録しましょう!</DialogTitle>
// ここからTogoFormに移動
<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>
// ここまでTogoFormに移動
<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>
);
};
export default AddTogoModal;
①frontend/app/src/components/togo/TogoForm.tsxをつくります。 TogoForm.tsxに移動させた結果が下記に記載したコードとなります。 受け取るpropsも定義しています。
import React, { useState } from 'react';
import { Marker } from '@react-google-maps/api';
import { DialogContent, IconButton, InputBase, Paper, TextField } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import GMap from '../common/GMap';
import type { MapPosition } from '../../types/map';
type TogoFormProps = {
location: string;
tag: string;
mapCenterPosition: MapPosition;
mapMarkerPosition: MapPosition;
handleChangeLocation: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleChangeTag: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleChangeMapCenterPosition: (centerPosition: MapPosition) => void;
handleChangeMapMarkerPosition: (markerPosition: MapPosition) => void;
};
const TogoForm: React.FC<TogoFormProps> = ({
location,
tag,
mapCenterPosition,
mapMarkerPosition,
handleChangeLocation,
handleChangeTag,
handleChangeMapMarkerPosition,
handleChangeMapCenterPosition,
}) => {
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 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 searchLocation = () => {
if (!searchLocationKeyword) {
return;
}
geocode();
};
const handleKeyPressSearchLocation = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
searchLocation();
}
};
const handleSearchLocation = () => {
searchLocation();
};
return (
<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>
);
};
export default TogoForm;
②AddTogoModal.tsxを修正します。 TogoFormに移したものを削除し、TogoFormをimportして、必要なpropsを渡しています。
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Button, Dialog, DialogActions, DialogTitle } from '@mui/material';
import TogoForm from './TogoForm';
import { RootState, AppDispatch } from '../../redux/store';
import { addTogo, initialState } from '../../redux/togoSlice';
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 dispatch: AppDispatch = useDispatch();
const { togoList } = useSelector((state: RootState) => state.togo || initialState);
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 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);
};
const handleChangeMapMarkerPosition = (position: MapPosition) => {
setMapMarkerPosition(position);
};
const handleChangeMapCenterPosition = (position: MapPosition) => {
setMapCenterPosition(position);
};
const handleAddTogo = () => {
if (!location || !tag) {
return;
}
const addTogoData: Togo = {
id: togoList.length,
done: false,
location,
tag,
position: mapMarkerPosition,
};
dispatch(addTogo(addTogoData));
handleCloseAddTogoModal();
};
return (
<Dialog open={isOpenAddTogoModal} onClose={handleCloseAddTogoModal} fullWidth maxWidth="md">
<DialogTitle>あなたが行きたいところを登録しましょう!</DialogTitle>
<TogoForm
location={location}
tag={tag}
mapCenterPosition={mapCenterPosition}
mapMarkerPosition={mapMarkerPosition}
handleChangeLocation={handleChangeLocation}
handleChangeTag={handleChangeTag}
handleChangeMapMarkerPosition={handleChangeMapMarkerPosition}
handleChangeMapCenterPosition={handleChangeMapCenterPosition}
/>
<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>
);
};
export default AddTogoModal;
これで、AddTogoModalは完成になります。 今回はここまでです。 次回は、UpdateTogoFormをつくっていきたいと思います。
コードはGitHubに置いてありますのでよければ参考にしてください。 mainブランチは常に最新のものになります。 今回の内容はblog_12のブランチを参照してください。 https://github.com/KINE-M/togo_app