前回までの記事 アプリ開発を楽しむ【#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>&nbsp;</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の両方のコンポーネントで利用していきたいと思います。 共通化するところは、下のイメージですと赤色の□で囲ったところ(場所・タグ・地図)になります。 スクリーンショット 2022-09-12 21.39.38.png

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