前回は、AWS AppSync を、フロントエンドと統合しました。

今回は、フロントエンドでデータ一覧の画面表示、作成、削除、更新します。

GraphQL で取得したデータ一覧を画面に表示する

まずは、ブラウザに GraphQL で取得したデータ一覧を表示させます。

image2

データの型を作成しましょう。

type Book = {
  id: string
  title: string
  author: string
  category: {
    name: string
  }
  isRead: boolean
  createdAt: string
}

mapを使って、booksのデータを表示させます。

isReadtrueの場合、『済』、falseの場合、『未』を表示するようにします。

bookに、先程作成したBook型を指定しましょう。

<TableBody>
  {books.map((book: Book) => (
    <TableRow key={book.id}>
      <TableCell>{book.id}</TableCell>
      <TableCell>{book.title}</TableCell>
      <TableCell>{book.author}</TableCell>
      <TableCell>{book.category.name}</TableCell>
      {book.isRead ? <TableCell>済</TableCell> : <TableCell>未</TableCell>}
      <TableCell>{`${book.createdAt}`}</TableCell>
    </TableRow>
  ))}
</TableBody>

ブラウザで確認すると、

image3

データを表示することができました。

console.log で確認すると、book データを全て取得しています。

image4

category の id や updatedAt などは必要ないので、取得しないようにしましょう。

src/graphql フォルダの queries.ts を開きます。

listBooksitemsの内容を必要なデータだけ取得するよう、修正しましょう。

export const listBooks = /* GraphQL */ `
  query ListBooks(
    $filter: ModelBookFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listBooks(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        title
        author
        category {
          name
        }
        isRead
        createdAt
      }
      nextToken
    }
  }
`

保存して、console.log で確認すると、

image5

必要なデータのみ取得することができました。

データを作成し、AWS に登録する

次は、データを作成してみます。

読書リスト画面から、『新規作成』ボタンをクリックすると、モーダルが開き、データを作成できるようにします。

components フォルダに、ModalBook.tsx と、AddBook.tsx を作成します。

UI は、MUI( https://mui.com/components/modal/ 、 https://mui.com/components/text-fields/)を参考にしました。

AddBook.tsx

import React from "react"
import Box from "@mui/material/Box"
import TextField from "@mui/material/TextField"
import FormControlLabel from "@mui/material/FormControlLabel"
import {
  Button,
  FormControl,
  FormLabel,
  MenuItem,
  Paper,
  Radio,
  RadioGroup,
  styled,
} from "@mui/material"

export default function AddBook() {
  const [title, setTitle] = useState("")
  const [author, setAuthor] = useState("")
  const [categoryId, setCategoryId] = useState(0)
  const [isRead, setIsRead] = useState(false)

  const handleChangeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(event.target.value as string)
  }

  const handleChangeAuthor = (event: React.ChangeEvent<HTMLInputElement>) => {
    setAuthor(event.target.value as string)
  }

  const handleChangeCategory = (event: React.ChangeEvent<HTMLInputElement>) => {
    setCategoryId(event.target.value as unknown as number)
  }

  const handleChangeIsRead = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value === "true"
    setIsRead(value)
  }

  const handleClick = () => {}

  return (
    <Box
      component="form"
      sx={{
        "& > :not(style)": { m: 1, width: "200ch" },
      }}
      noValidate
      autoComplete="off"
    >
      <CContainer>
        <CPaper>
          <TextField
            id="title"
            label="タイトル"
            variant="outlined"
            value={title}
            onChange={handleChangeTitle}
            margin="normal"
            fullWidth
          />
          <TextField
            id="author"
            label="著者"
            variant="outlined"
            value={author}
            onChange={handleChangeAuthor}
            margin="normal"
            fullWidth
          />
          <TextField
            id="select-Category"
            select
            label="カテゴリ"
            variant="outlined"
            value={categoryId}
            onChange={handleChangeCategory}
            margin="normal"
            fullWidth
          ></TextField>
          <Box>
            <FormControl component="fieldset">
              <FormLabel component="legend">読了</FormLabel>
              <RadioGroup
                aria-label="gender"
                name="controlled-radio-buttons-group"
                value={isRead}
                onChange={handleChangeIsRead}
                row
              >
                <FormControlLabel value="true" control={<Radio />} label="済" />
                <FormControlLabel
                  value="false"
                  control={<Radio />}
                  label="未"
                />
              </RadioGroup>
            </FormControl>
          </Box>
          <Button variant="contained" onClick={handleClick}>
            新規追加
          </Button>
        </CPaper>
      </CContainer>
    </Box>
  )
}

const CContainer = styled(Box)({
  display: "flex",
  flexDirection: "column",
  jusitifyConteng: "center",
  alignItems: "center",
  position: "absolute",
  top: "50%",
  left: "50%",
  transform: "translateX(-50%) translateY(-50%)",
})

const CPaper = styled(Paper)({
  padding: "2rem",
})

ModalBook.tsx

import * as React from "react"
import Button from "@mui/material/Button"
import Modal from "@mui/material/Modal"
import AddBook from "./AddBook"

export default function BasicModal() {
  const [open, setOpen] = React.useState(false)
  const handleOpen = () => setOpen(true)
  const handleClose = () => setOpen(false)

  return (
    <div>
      <Button onClick={handleOpen} variant="contained">
        新規作成
      </Button>
      <Modal
        open={open}
        onClose={handleClose}
        aria-labelledby="modal-modal-title"
        aria-describedby="modal-modal-description"
      >
        <AddBook />
      </Modal>
    </div>
  )
}

読書リスト画面で新規作成ボタンを追加します。

import ModalBook from "../components/ModalBook"
import { Box } from "@mui/material"
<CBox>
  <Title>読書リスト</Title>
  <ModalBook />
</CBox>
const CBox = styled(Box)({
  display: "flex",
  justifyContent: "space-between",
})

image6

AddBook.tsx で、カテゴリを選択できるようにします。

Book.tsx と同様に、ams-amplifyawsExprtsをインポートして、AWS AppSyncと連携します。

import Amplify, { API, graphqlOperation } from "aws-amplify"

import awsExports from "../aws-exports"

Amplify.configure(awsExports)

カテゴリを取得できるようにクエリをインポートしましょう。

import { listCategories } from "../graphql/queries"

useStatecategoriesの状態を管理します。

fetchCategoriesを作成し、useEffectで呼び出すようにします。

useEffect(() => {
  fetchCategories()
}, [])

const fetchCategories = async () => {
  try {
    const categoryData: any = await API.graphql(
      graphqlOperation(listCategories)
    )
    const categories = categoryData.data.listCategories.items
    setCategories(categories)
  } catch (err) {
    console.log("error fetching categories")
  }
}

category の型を指定しましょう。

type TypeCategory = {
  id: string
  name: string
}

リストとして、表示できるようにします。

<TextField
  id="select-Category"
  select
  label="カテゴリ"
  variant="outlined"
  value={categoryId}
  onChange={handleChangeCategory}
  margin="normal"
  fullWidth
>
  {categories.map((category: TypeCategory) => (
    <MenuItem key={category.id} value={category.id}>
      {category.name}
    </MenuItem>
  ))}
</TextField>

ブラウザで確認すると、

image7

フォームが作成でき、カテゴリの選択ができました。

『新規追加』ボタンをクリックすると、データを登録できるようにします。

src/grqphql フォルダの mutations.ts から、createBookをインポートします。

import { createBook } from "../graphql/mutations"

『新規追加』をクリックした後のアクションは、handleClickで操作します。

まずは、JavaScript の不要な挙動を防ぐために、event.preventDefaultを指定しましょう。

const handleClick = async (event: React.MouseEvent) => {
  event.preventDefault()
}

mutations.ts を見てみると、データを作成する場所は、input になっています。

image8

input に title、author、categoryId、isRead が入るようにします。

const handleClick = async (event: React.MouseEvent) => {
  event.preventDefault()

  const input = {
    title,
    author,
    categoryId,
    isRead,
  }
}

APIgraphqlOperationを使って、createBookinputを登録できるようにします。

const handleClick = async (event: React.MouseEvent) => {
  event.preventDefault()

  const input = {
    title,
    author,
    categoryId,
    isRead,
  }

  await API.graphql(graphqlOperation(createBook, { input }))
}

では、データを登録してみます。

image9

『新規追加』ボタンをクリックして、モーダルを閉じると、

image10

新規追加したデータが登録できていました。

特定のデータを取得する

次は、詳細画面へ遷移し、特定のデータを表示させます。

まずは、詳細画面を作成します。

BookDetail.tsx

import * as React from "react"
import { useParams } from "react-router-dom"
import Card from "@mui/material/Card"
import CardContent from "@mui/material/CardContent"
import Typography from "@mui/material/Typography"

import { Button } from "@mui/material"
import DeleteIcon from "@mui/icons-material/Delete"

export default function BookDetail() {
  const { id } = useParams<{ id?: string | undefined }>()

  return (
    <Card sx={{ minWidth: 275 }} variant="outlined">
      <CardContent>
        <Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
          ID:{}
        </Typography>
        <Typography variant="h5" component="div">
          タイトル:{}
        </Typography>
        <Typography sx={{ mb: 1.5 }} color="text.secondary">
          著者:{}
        </Typography>
        <Typography variant="body2" sx={{ mb: 1.5 }}>
          カテゴリ:{}
        </Typography>
        {/* { ? (
          <Typography variant="body2" sx={{ mb: 1.5 }}>
            読了:済
          </Typography>
        ) : (
          <Typography variant="body2" sx={{ mb: 1.5 }}>
            読了:未
          </Typography>
        )} */}
        <Typography variant="body2" sx={{ mb: 1.5 }}>
          作成日:
          {}
        </Typography>
        <Button
          variant="contained"
          color="error"
          startIcon={<DeleteIcon />}
          onClick={() => {}}
        >
          削除
        </Button>
      </CardContent>
    </Card>
  )
}

読書リスト画面で、『詳細画面』ボタンを作成します。

Book.tsx

<TableCell>
  <Link href={`/book/${book.id}`} underline="hover">
    詳細画面
  </Link>
</TableCell>

App.tsx で Route を設定しましょう。

<Route exact path="/book">
  <Dashboard>
    <Book />
  </Dashboard>
</Route>
<Route path="/book/:id">
  <Dashboard>
    <BookDetail />
  </Dashboard>
</Route>

一度、ブラウザで画面遷移を確認します。

image11

『詳細画面』をクリックすると、

BookDetail 画面へ遷移することができました。

ここから、GraphQL で、特定のデータを取得します。

book の型を設定します。

type Book = {
  id: string
  title: string
  author: string
  category: {
    name: string
  }
  isRead: boolean
  createdAt: string
}

book を useState で管理します。

const [book, setBook] = useState<Book | null>(null)

Book.tsx と同様に、ams-amplifyawsExprtsをインポートして、AWS AppSyncと連携します。

import Amplify, { API, graphqlOperation } from "aws-amplify"

import awsExports from "../aws-exports"

Amplify.configure(awsExports)

getBook をインポートします。

import { getBook } from "../graphql/queries"

fetchBook を作成します。

src/graphql の queries.ts にあるgetBookを確認すると、idを指定することで特定のデータを取得できるようです。

image12

graphqlOperationには、getBookidを指定します。

const fetchBook = async () => {
  try {
    const bookData: any = await API.graphql(graphqlOperation(getBook, { id }))
    const book = bookData.data.getBook
    setBook(book)
  } catch (err) {
    console.log("error fetching books")
  }
}

useEffectを設定します。

useEffect(() => {
  fetchBook()
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

CardContentの中身を設定しましょう。

<Card sx={{ minWidth: 275 }} variant="outlined">
  <CardContent>
    <Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
      ID:{book?.id}
    </Typography>
    <Typography variant="h5" component="div">
      タイトル:{book?.title}
    </Typography>
    <Typography sx={{ mb: 1.5 }} color="text.secondary">
      著者:{book?.author}
    </Typography>
    <Typography variant="body2" sx={{ mb: 1.5 }}>
      カテゴリ:{book?.category.name}
    </Typography>
    {book?.isRead ? (
      <Typography variant="body2" sx={{ mb: 1.5 }}>
        読了:済
      </Typography>
    ) : (
      <Typography variant="body2" sx={{ mb: 1.5 }}>
        読了:未
      </Typography>
    )}
    <Typography variant="body2" sx={{ mb: 1.5 }}>
      作成日:
      {book?.createdAt}
    </Typography>
    <Button
      variant="contained"
      color="error"
      startIcon={<DeleteIcon />}
      onClick={() => {}}
    >
      削除
    </Button>
  </CardContent>
</Card>

ブラウザで確認すると、

image13

id のデータを表示することができました。

データを削除する

次は、データを削除します。

削除ボタンのonClickhandleDeleteを指定しましょう。

<Button
  variant="contained"
  color="error"
  startIcon={<DeleteIcon />}
  onClick={handleDelete}
>
  削除
</Button>

handleDelete を作成します。

src/graphql の mutations.ts にあるdeleteBookを確認します。

inputidを指定するとよさそうです。

image14

handleDeleteinputに id指定します。

const handleDelete = async (event: React.MouseEvent) => {
  event.preventDefault()

  const input = { id }
}

graphqlOperationdeleteBookinputを指定します。

const handleDelete = async (event: React.MouseEvent) => {
  event.preventDefault()

  const input = { id }

  await API.graphql(graphqlOperation(deleteBook, { input }))
}

ブラウザで確認しましょう。

image15

読書リスト画面に戻ってみると、

image16

データが削除されていました。

データを更新する

最後に、データを更新します。

読書リストの読了を『済』や『未』へ変更できるようにします。

image17

まずは、『済』と『未』にボタンを設定します。

引数は、book.id と、『済』をクリックした時は false、『未』をクリックした時は true とします。

{
  book.isRead ? (
    <TableCell>
      <Button size="small" onClick={() => handleUpdate(book.id, false)}>
        済
      </Button>
    </TableCell>
  ) : (
    <TableCell>
      <Button size="small" onClick={() => handleUpdate(book.id, true)}>
        未
      </Button>
    </TableCell>
  )
}

handleUpdateを作成します。

src/graphql フォルダの mutations.ts を確認すると、input でデータを変更することができそうです。

image18

handleUpdate に、先程指定した引数を設定します。

input には、どの id を更新するかを指定するために id と、 更新したい内容の isRead を指定します。

graphqlOperationは、updateBookinputを指定しましょう。

const handleUpdate = async (id: string, isRead: boolean) => {
  const input = { id, isRead }

  await API.graphql(graphqlOperation(updateBook, { input }))
}

では、ブラウザで確認します。

image19

読了の『済』をクリックして、リロードすると、

image20

『済』から『未』へ変更することができました。

image21

console.logisReadも、true から false へ更新されました。

ブログ一覧