【コピペでOK】Vue3×FastAPI×MySQL×DockerでWebアプリを作ってみた

# 目次
[1.はじめに](#1-はじめに)
[2.ゴール](#2-ゴール)
[3.DB構築](#3-db構築)
[4.Backend構築](#4-backend構築)
[5.Frontend構築](#5-frontend構築)
[6.動作確認](#6-動作確認)

# 1. はじめに

よくあるWebアプリケーションの超簡略版を作ってみた。
「ちゃちゃっと作って遊びたい」「コピペで作ってみてから学びたい」人向けの簡単な内容を掲載する。

|対象|使うもの
|---|---
|Front-end| Vue3/Element Plus
|Back-end| FastAPI(python)
|Database| MySQL


frontendはVue3を使うことにした。Composition APIなどが優秀等々話は聞くが、もともとVuejsが好きという個人的な理由により採用。
わざわざVueを使うまでもない内容かもしれないが、***とりあえず触れてみる事***は非常に大事だと思うため使う。

CSSフレームワークをElement Plusを使用。Vue3に対応しているCSSフレームワークは少ない印象。洗礼されたデザインなのでおすすめ。


backendはFastAPIを、DBはMySQLを使用。 FastAPIとmysqlについて本記事より詳しい内容・手順を[こちら](https://qiita.com/yota_dev/items/8309dd294975b15c22d5 "FastAPI * MySQL * DockerでCRUDを行うAPIをサクッと作ってみた（初学者向け）")で掲載しているため、是非一読して頂きたい。


https://qiita.com/yota_dev/items/8309dd294975b15c22d5


# 2. ゴール

「*よくあるWebアプリケーションの超簡略版を作ってみた。*」と冒頭に記載したが、具体的にはCRUD操作が可能な簡単な**データ管理アプリ**を作る。

データ管理アプリは下記を実現する
- ユーザはデータを閲覧する
- ユーザはデータを登録（追加）する
- ユーザはデータを削除する
- ユーザはデータを更新する

シンプルCRUD。

最終的な生成物は下記。
- build済み静的コンテンツ
  - AWS S3などにポイっとおけば使える状態のfrontend
- API Docker image
  - AWS ECSなどにポポイッとデプロイすれば良い状態のbackendのdocker image
- DB Docker image
  - ローカルで検証するためのDB Conatiner用Docker image。front/bankをCloudにデプロイする場合は Managedサービスを使う方があるある（かもしれない）


# 3. DB構築
最初にDBを作る。Frontendから作りたい気持ちもわかるが、個人的にはデータ管理を行うためデータ層から順に着手した方が理解しやすかった。

まず全体の作業ディレクトリ(`sample_app`)とDB用のディレクトリ(`db`)を作成し、`db`ディレクトリ配下に下記2ファイルを新規作成する。

```
.
└── sample_app/
    └── db/
        ├── init.sql
        └── Dockerfile
```


## 3-1. initialze sql
DBコンテナを立ち上げた際に、テーブル作成や初期データ投入を行うために`init.sql`を作成しておく。

```sql:init.sql
USE sample_db;

DROP TABLE IF EXISTS item;
CREATE TABLE item
(
  item_id SERIAL PRIMARY KEY,
  name VARCHAR(50),
  price INT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO item (name, price) VALUES ('apple', 100);
INSERT INTO item (name, price) VALUES ('orange', 200);
```

## 3-2. MySQL Container

続いてDockerfileについて。

```Dockerfile:Dockerfile
FROM mysql:latest

ENV MYSQL_DATABASE sample_db
ENV MYSQL_USER sample_user
ENV MYSQL_PASSWORD sample_password
ENV TZ "Asia/Tokyo"
ENV MYSQL_ROOT_PASSWORD p@assw0rd

COPY ./init.sql /docker-entrypoint-initdb.d/init.sql
```

では早速imageをbuild。

```sh
cd db # Dockerfileのあるディレクトリまで移動
docker build -t sample-mysql:1.0.0 .
```

次にコンテナも立ち上げてみる。

```sh
docker run -d --name sample-mysql-container -p 3306:3306 sample-mysql:1.0.0
```

確認

```sh
docker ps -f "name=sample-mysql-container" 
CONTAINER ID   IMAGE                COMMAND                  CREATED         STATUS         PORTS                                                  NAMES
9373afb82b08   sample-mysql:1.0.0   "docker-entrypoint.s…"   5 seconds ago   Up 4 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   sample-mysql-container
```

いた。次にコンテナ内に入り、`init.sql`内で記載したinsert通りにデータが存在するか確認する。

1. コンテナ内に入る。
    ```sh
    docker exec -it $(docker ps -q -f "name=sample-mysql-container") /bin/bash
    ```
2. dbに接続。
    ```sh
    mysql sample_db -h localhost -u sample_user -psample_password
    ```
3. レコードを確認
    ```sql
    mysql> select * from item;
    +---------+--------+-------+---------------------+---------------------+
    | item_id | name   | price | created_at          | updated_at          |
    +---------+--------+-------+---------------------+---------------------+
    |       1 | apple  |   100 | 2022-05-19 22:16:31 | 2022-05-19 22:16:31 |
    |       2 | orange |   200 | 2022-05-19 22:16:31 | 2022-05-19 22:16:31 |
    +---------+--------+-------+---------------------+---------------------+
    2 rows in set (0.00 sec)
    ```

無事構築が完了したので、`exit`でmysql/containerのコンソールを抜けておこう。

後続の構築が完了した後、`sample-mysql-container`に接続するため立ち上げっぱなしでOK。


# 4. Backend構築

続いてBackendを構築する。

API用のディレクトリ(`backend`)を作成し、`backend`ディレクトリ配下にAPIを構築する、

```
.
└── sample_app/
    ├── db/
    │   ├── init.sql
    │   └── Dockerfile
    └── backend/
        ├── Dockerfile
        ├── .env
        └── app/
            ├── requirements.txt
            └── main.py
```

## 4-1. requirements.txt

パッケージ一括管理用に`requirements.txt`を(使用する内容は少数だが)作成しておく。

```text:requirements.txt
sqlalchemy==1.4.31
mysqlclient==2.1.0
```

## 4-2. app

早速コーディングしてみる。DBの接続先情報は環境変数から取得できるようにしておく。

```python:main.py
import os
from fastapi import FastAPI, Depends, Query, status
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy import Column, TIMESTAMP, Integer, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import FetchedValue


ROOT_PATH="/api"

app = FastAPI()

SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}:{}/{}'.format(
        os.environ.get("DB_USER"), 
        os.environ.get("DB_PASSWORD"),
        os.environ.get("DB_HOST"), 
        os.environ.get("DB_PORT"), 
        os.environ.get("DB_NAME")
    )

engine = create_engine(SQLALCHEMY_DATABASE_URI)
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def session():
    db = Session()
    try:
        yield db
    finally:
        db.close()

Base = declarative_base(bind=engine)

# Entity Item
class Item(Base):
    __tablename__ = "item"
    __table_args__ = {"autoload": True}
    item_id = Column(Integer, primary_key = True, nullable=False)
    name = Column(String(50), nullable=False)
    price = Column(Integer, nullable=False)
    created_at = Column(TIMESTAMP, FetchedValue())
    updated_at = Column(TIMESTAMP, FetchedValue())

# Request Body
class ItemRequest(BaseModel):
    name: str = Query(..., max_length=50)
    price: float

# GetItemByName
@app.get('%s/item' % ROOT_PATH)
def get_items(db: Session = Depends(session)):
    result_set = db.query(Item).all()    
    response_body = jsonable_encoder({"list": result_set})
    return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)

# CreateItem
@app.post('%s/item' % ROOT_PATH)
def create_item(request: ItemRequest, db: Session = Depends(session)):
    item = Item(
                name = request.name,
                price = request.price
            )
    db.add(item)
    db.commit()
    response_body = jsonable_encoder({"item_id" : item.item_id})
    return JSONResponse(status_code=status.HTTP_200_OK, content=response_body)

# UpdateItem
@app.put('%s/item/{id}' % ROOT_PATH)
def update_item(id: int, request: ItemRequest, db: Session = Depends(session)):
    item = db.query(Item).filter(Item.item_id == id).first()
    if item is None:
            return JSONResponse(status_code=status.HTTP_404_NOT_FOUND)
    item.name = request.name
    item.price = request.price
    db.commit()
    return JSONResponse(status_code=status.HTTP_200_OK)

# DeleteItem
@app.delete('%s/item/{id}' % ROOT_PATH)
def delete_item(id: int, db: Session = Depends(session)):
    db.query(Item).filter(Item.item_id == id).delete()
    db.commit()
    return JSONResponse(status_code=status.HTTP_200_OK)
```

## 4-3. 環境変数

次にローカルでの稼働確認時にコンテナに渡す環境変数を定義する。

```env:.env
DB_HOST=host.docker.internal
DB_PORT=3306
DB_NAME=sample_db
DB_USER=sample_user
DB_PASSWORD=sample_password
```


:::note warn
注意 ※1
本記事ではdocker-composeを使用しないため無理にHOST指定を`host.docker.internal`としているが、いつまでサポートされているか不明なため可能であればcomposeした方が良い。

*[公式](http://docs.docker.jp/v19.03/docker-for-mac/networking.html "コンテナからホスト上のサービスに対して接続したい")より引用*
>ホストの IP アドレスは変動します（あるいは、ネットワークへの接続がありません）。18.03 よりも前は、特定の DNS 名 host.docker.internal での接続を推奨していました。これはホスト上で内部の IP アドレスで名前解決します。これは開発用途であり、Docker Desktop forMac 外の本番環境では動作しません。

:::


## 4-4. Api Container
次にDockerfile。

```Dockerfile:Dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim

COPY ./app /app

RUN apt-get update && \
    apt-get -y install gcc libmariadb-dev && \
    pip install --no-cache-dir --upgrade -r /app/requirements.txt
```


では早速imageをbuild。

```sh
cd ../backend # Dockerfileのあるディレクトリまで移動
docker build -t sample-backend:1.0.0 . 
```

次にコンテナを立ち上げるが、立ち上げの際に環境変数定義ファイルを渡す。

```sh
docker run -d --env-file=.env --name sample-backend-container -p 80:80 sample-backend:1.0.0

docker ps -f "name=sample-backend-container" 
CONTAINER ID   IMAGE                  COMMAND       CREATED         STATUS         PORTS                               NAMES
db4751373a62   sample-backend:1.0.0   "/start.sh"   3 seconds ago   Up 2 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   sample-backend-container
```

無事コンテナの立ち上げが完了したので、ログを確認して動いているか見てみる。

```sh
docker logs --tail=-1  $(docker ps -q -f "name=sample-backend-container") 
#省略
[2022-05-19 13:19:25 +0000] [1] [INFO] Listening at: http://0.0.0.0:80 (1)
#省略
```

動いてそう。念のためcurlで呼び出せるか確認してみる。

```sh
curl -s -X GET 'http://localhost:80/api/item' | jq

{
  "list": [
    {
      "created_at": "2022-05-19T22:16:31",
      "item_id": 1,
      "updated_at": "2022-05-19T22:16:31",
      "price": 100,
      "name": "apple"
    },
    {
      "created_at": "2022-05-19T22:16:31",
      "item_id": 2,
      "updated_at": "2022-05-19T22:16:31",
      "price": 200,
      "name": "orange"
    }
  ]
}
```

とれた。**本来ならばfrontやdbと結合する前にUTを行うべき**だが、本記事は*ちゃちゃっと作って遊びたい人向け*なため行わない。



# 5. Frontend構築

続いてFrontendを構築する。

Frontendはコンテナとして立ち上げない想定なので、専用のディレクトリを手動では作成せず、npmに任せてみる。


本記事では下記実行環境で開発を進めている。

```sh
node -v
v17.6.0

npm -v
8.9.0
```


では早速構築してみる。

```sh
cd ../ # sample_appへ移動
npm init vue@latest # project作成

Vue.js - The Progressive JavaScript Framework

✔ Project name: … frontend
✔ Add TypeScript? … Yes # Yesを選択
# 以降は本記事では全てNoで問題なし。
✔ Add JSX Support? … No 
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add Cypress for both Unit and End-to-End testing? … No
✔ Add ESLint for code quality? … No
```

projectが作成されたので構築を進める。


一旦作成されたprojectを立ち上げてみる。

```sh
cd frontend
npm install
npm run dev
```

下記のような表示がでればOK.(viteで立ち上がってて嬉しい。)
```sh
vite v2.9.8 dev server running at:

> Local: http://localhost:3000/
> Network: use `--host` to expose

ready in 287ms.
```

ではwebブラウザで`http://localhost:3000/`アクセスしてみる。

![template.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527084/5a1eb51f-467d-fbf0-c6b2-b233c114ed50.png "template")


:::note
作成されたtemplate projectではダークモードに対応しており、OSの設定や作業時間によっては配色が異なる。
鬱陶しさを感じた場合は、OSのダークモードを切るか `frontend/src/assets/base.css`の `@media (prefers-color-scheme: dark)`オブジェクトをごっそりコメントアウトしてしまっても良いと思う。
:::


ctrl+cなどで停止できるが、停止せずともファイルに更新があれば自動で反映される。pcが重い場合や複数コンソールを開くのが嫌な場合はctrl+cなどで停止しておこう。


現在のファイル構成を書きに記す。frontendのモジュールはファイル数が多いため割愛。

```
.
└── sample_app/
    ├── db/
    │   ├── init.sql
    │   └── Dockerfile
    ├── backend/
    │   ├── Dockerfile
    │   ├── .env
    │   └── app/
    │       ├── requirements.txt
    │       └── main.py
    └── frontend/
        ├── 割愛
        └── src/
            ├── assets/
            │     ├── 割愛
            │     └── base.css
            ├── components/
            │     └── 割愛（vueが気になる方は是非中身も見て欲しい）
            └── main.ts
```



次にelement-plusの導入

```sh
npm install element-plus # element-plusとを導入
npm install axios # api call用にとaxiosを導入
npm install vue-router@latest # vue-routerの導入
jq .dependencies package.json # jqが無ければcatでもOK
{
  "axios": "^0.27.2",
  "element-plus": "^2.1.11",
  "vue": "^3.2.33",
  "vue-router": "^4.0.15"
}
```


次に、使用できるように`src/main.ts`を編集する。

```ts:main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/router';
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(ElementPlus)
app.use(router)
app.mount('#app')
```

他にも追加する場合は同様にimportとして`.use(xxx)`すれば良いが、広げすぎると*パパッと*ではなくなるため詳しくは割愛。

backendを呼び出すためのファイルを作成する。

```
src/
└── service/
    ├── httpclient.ts
    └── DataApiService.ts
```


api呼び出しの機能はaxiosを使い、共通化してhttpclient.tsとして作成しておく。

```ts:httpclient.ts
import type { AxiosInstance } from "axios";
import axios from "axios";

const apiClient: AxiosInstance = axios.create({});

export default apiClient;
```

わざわざ別定義するまでもないかもしれないが、拡張性を考慮して定義しておく。


次に、実際のcall部分を定義する。

```ts:DataApiService.ts
import http from "@/service/httpclient";

const APPLICATION_JSON : string = "application/json";

class DataApiService {
  getAll(): Promise<any> {
    return http.get("/api/item");
  }

  create(name: string, price: number): Promise<any> {
    return http.post(
        `/api/item`, 
        { 
            name: `${name}`,
            price: `${price}`
        }
    );
  }

  update(id: number, name: string, price: number): Promise<any> {
    return http.put(
        `/api/item/${id}`,
        { 
            name: `${name}`,
            price: `${price}`
        }
    );
  }

  delete(id: number): Promise<any> {
    return http.delete(`/api/item/${id}`);
  }
}

export default new DataApiService();
```

backendに用意した4つのAPIを呼び出す単純なクラスを作成した。

続いてroutingを行う定義体を`src`直下に`router.ts`として作成する。

```ts:router.ts
import { createRouter,createWebHistory } from 'vue-router';
import List from './components/ListPage.vue';
import Add from './components/AddPage.vue';
import Edit from './components/EditPage.vue';
 
const routes = [
    { path: '/', name: 'list', component: List },
    { path: '/add', name: 'add', component: Add },
    { path: '/edit', name: 'edit', component: Edit, props: true },
]
 
const router = createRouter({
    history: createWebHistory(), // HTML5 History モード
    routes,
})
 
export default router;
```



さらに`App.vue`を編集し、routingされたcomponentを表示する`router-view`を追加する。

```vue:App.vue
<template>
  <el-main>
    <router-view></router-view>
  </el-main>
</template>

<style>
@import './assets/base.css';
#app {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0rem;
  font-weight: normal;
}

@media (min-width: 1024px) {
  #app {
    padding: 0 2rem;
  }
}

.dialog-footer {
     text-align: center;
}
.dialog-footer button {
    width: 100px;
    margin: 0 0 10px 0;
}

@media (min-width: 1024px) {
  .dialog-footer button{
      width: 100px;
      margin: 0 10px 0 0;
      
  }
}
</style>
```

とてもシンプル。フロントが専門ではない私はこれぐらいシンプルな方が理解しやすくvueは好き。

`router-view`に込められるComponentを3つ定義する。

```
src/
└── components/
    ├── ListPage.vue
    ├── AddPage.vue
    └── EditPage.vue
```

まずはListPage.vueについて。こちらは単にDBをselectして一覧表示するというもの。

```vue:ListPage.vue

<script lang='ts' setup>
import { 
  ref,
} from 'vue'

import { 
    useRouter,
} from 'vue-router'

import {
  Edit,
  Delete,
} from '@element-plus/icons-vue'

import { 
  ElMessage, 
  ElMessageBox,
} from 'element-plus'

import DataApiService from "@/service/DataApiService";

const router = useRouter()

const isLoading = ref(false)
const data = ref([])

const initilize = async () => {
    console.log('initilize')
    return new Promise(async (resolve, reject) => {  
      isLoading.value = true;
      await DataApiService.getAll()
        .then((response) => {
          data.value = response.data.list
          resolve(200);
        })
        .catch((error) => {
          console.log(error)
          data.value = [];
          reject(500);
        })
        .finally(() => {
          isLoading.value = false;
        })
  });
    
}

const deleteData = (index: number) => {
 console.log( data.value[index])
  ElMessageBox.confirm(
    '本当に削除しますか?',
    '確認',
    {
      confirmButtonText: '削除する',
      cancelButtonText: 'キャンセル',
      type: 'warning',
      center: true,
    }
  )
    .then(() => {
      console.log('start api call');
      DataApiService.delete(data.value[index]["item_id"])
        .then(() => {
          initilize();
          ElMessage({
            type: 'success',
            message: '削除が完了しました。',
          })
        })
        .catch((error) => {
          console.log('error_%s',error);
           ElMessage({
            type: 'error',
            message: '削除に失敗しました。',
          })
        });  
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '中止しました。',
      })
    })
}

const editData = (index: number) => {
    router.push({ name:'edit',params: { 
            id: data.value[index]["item_id"],
            name: data.value[index]["name"],
            price: data.value[index]["price"]
        }
    })
}

const addData = () => {
    router.push({name:'add'})
}


initilize()
</script>

<template>
 <el-table :data='data' style='width: 100%' max-height='500' v-loading.fullscreen.lock="isLoading">
    <el-table-column fixed prop='item_id' label='ID' width='50' />
    <el-table-column prop='name' label='名称' width='100' />
    <el-table-column prop='price' label='価格' width='100' />
    <el-table-column prop='created_at' label='作成日時' width='200' />
    <el-table-column prop='updated_at' label='更新日時' min-width='200' />
    <el-table-column fixed='right' label='操作' width='120'>
      <template #default='scope'>
        <el-button type='primary' :icon='Edit' circle @click.prevent='editData(scope.$index)'/>
        <el-button type='danger' :icon='Delete' circle @click.prevent='deleteData(scope.$index)'/>
      </template>
    </el-table-column>
  </el-table>
  <el-button class='mt-4' style='width: 100%' @click='addData'>Add Item</el-button>
</template>
```

長くなってしまった。関数群は別ファイルに出したいところだが、componentごとに紐づいたものは1ファイルに纏めておきたかったので上記のようなボリュームになってしまった。

初期表示時に、`initilize`が呼び出され、DBから情報を取得するAPIをcall（`DataApiService.getAll()`）をしている。

`<template>`については、element plusの要素を置いているだけ。

`Add Item`ボタンを押下すると発火される関数は単純で`router.push`を呼び、nameが`add`として定義された*画面*に遷移する。

```ts
router.push({name:'add'})
```

`Edit`ボタンを押下した場合は同じくrouter.pushしてname:`edit`に遷移しているが、選択した項目の情報を渡している。

```ts
router.push({ name:'edit',params: { 
        id: data.value[index]["item_id"],
        name: data.value[index]["name"],
        price: data.value[index]["price"]
    }
})
```

`Delete`ボタンを押下すると、確認popupを挟んでからDeleteのAPI Call（`DataApiService.delete()`）をし、正常終了後に再び`initilize`を呼ぶ。

続いてListPageにて`Add Item`を押下し遷移する先（`AddPage.vue`）を定義する。

```vue:AddPage.vue
<script lang="ts" setup>
import { 
    reactive,
} from 'vue';

import { 
  ElMessage, 
  ElMessageBox,
} from 'element-plus'

import { 
    useRouter,
} from 'vue-router'

import DataApiService from "@/service/DataApiService";

const router = useRouter()

const formLabelWidth = '120px'
const form = reactive({
  name: '',
  price: 0,
})

const cancel = () => {
 router.push({name:'list'})
}

const add = (index: number) => {
  ElMessageBox.confirm(
    '本当に追加しますか?',
    '確認',
    {
      confirmButtonText: '追加する',
      cancelButtonText: 'キャンセル',
      type: 'warning',
      center: true,
    }
  )
    .then(() => {
      console.log('start api call');
      DataApiService.create(form.name,form.price)
        .then(() => {
          router.push({name:'list'})
          ElMessage({
            type: 'success',
            message: '追加が完了しました。',
          })
        })
        .catch((error) => {
          console.log('error_%s',error);
           ElMessage({
            type: 'error',
            message: '追加に失敗しました。',
          })
        });  
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '中止しました。',
      })
    })
}
</script>

<template>
    <el-form :model="form">
        <el-form-item label="ID" :label-width="formLabelWidth">
            <el-input :disabled="true" placeholder="自動採番"/>
        </el-form-item>
        <el-form-item label="名称" :label-width="formLabelWidth">
            <el-input v-model="form.name" autocomplete="off" />
        </el-form-item>
        <el-form-item label="価格" :label-width="formLabelWidth">
            <el-input v-model="form.price" autocomplete="off" />
        </el-form-item>
    </el-form>
    <div class="dialog-footer">
        <el-button @click="cancel">Cancel</el-button>
        <el-button type="primary" @click="add">Confirm</el-button>
    </div>
</template>
```


`template`については特筆すべきことはなく、element plusの要素をペタペタ置いただけ。

`Confirm`ボタンを押下すると関数`add`が呼び出され、deleteと同様に確認popupを挟みAPIcall（`DataApiService.create()`）を呼び出す。
完了後は、name:listつまり一覧表示画面（`ListPage.vue`）に遷移するようにした。

続いてListPageにて`Edit`を押下し遷移する先（`EditPage.vue`）を定義する。


```vue:EditPage.vue
<script lang="ts" setup>
import { 
    reactive,
} from 'vue';

import { 
  ElMessage, 
  ElMessageBox,
} from 'element-plus'

import { 
    useRouter,
} from 'vue-router'

import DataApiService from "@/service/DataApiService";

const router = useRouter()

const formLabelWidth = '120px'
const form = reactive({
  id: 0,
  name: '',
  price: 0,
})

const cancel = () => {
 router.push({name:'list'})
}

const edit = () => {
  ElMessageBox.confirm(
    '本当に更新しますか?',
    '確認',
    {
      confirmButtonText: '更新する',
      cancelButtonText: 'キャンセル',
      type: 'warning',
      center: true,
    }
  )
    .then(() => {
      console.log('start api call');
      DataApiService.update(form.id,form.name,form.price)
        .then(() => {
          router.push({name:'list'})
          ElMessage({
            type: 'success',
            message: '更新が完了しました。',
          })
        })
        .catch((error) => {
          console.log('error_%s',error);
           ElMessage({
            type: 'error',
            message: '更新に失敗しました。',
          })
        });  
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '中止しました。',
      })
    })
}




const props = defineProps({
  id: Number,
  name: String,
  price: Number
})

const initilize = () => {
    if(props.id === undefined) {
        router.push({name:'list'})
        ElMessage({
            type: 'error',
            message: '情報の取得に失敗しました。',
        })
    } else {
        form.id = props.id;
        form.name = props.name == undefined ? '' : props.name;
        form.price = props.price == undefined ? 0 : props.price;
    }
}

initilize();
</script>


<template>
    <el-form :model="form">
        <el-form-item label="ID" :label-width="formLabelWidth">
            <el-input v-model="form.id" :disabled="true"/>
        </el-form-item>
        <el-form-item label="名称" :label-width="formLabelWidth">
            <el-input v-model="form.name" autocomplete="off" />
        </el-form-item>
        <el-form-item label="価格" :label-width="formLabelWidth">
            <el-input v-model="form.price" autocomplete="off" />
        </el-form-item>
    </el-form>
    <div class="dialog-footer">
        <el-button @click="cancel">Cancel</el-button>
        <el-button type="primary" @click="edit">Confirm</el-button>
    </div>
</template>
```


基本的には`AddPage.vue`と同じである。違いは遷移時に値を受け渡している箇所だ。


前述の`router.ts`で定義したがpropsプロパティをtrueと設定する必要がある。

```ts:router.ts
{ path: '/edit', name: 'edit', component: Edit, props: true }
```

さらに、値を受け取る側である`EditPage.vue`では `defineProps`を定義する。

```ts
const props = defineProps({
  id: Number,
  name: String,
  price: Number
})
```

以上で完成。

# 6. 動作確認

では早速動かしてみる。

```sh
npm run dev  
```

最終的な完成物・動作画面はこちら。
![qiita_webapp_sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527084/9df09688-8ff5-628b-51d2-b4607db59e34.gif)



説明は掻い摘んだがコピペで作成可能な量で、とりあえず動かすものを手に入れてから遊びたい方は是非参考にしていただければと思う。

以上。