[僕のlonicでスマホアプリ開発]#4 残高登録画面作成

# はじめに
前回の[[僕のlonicでスマホアプリ開発]#3 口座登録画面作成](https://qiita.com/drafts/c05ad4c60cd655410716/edit)続きになります。
今回作成するスマホアプリは画面が以下の通りになる想定ですが、
- 残高確認画面
- 残高登録画面
- 口座登録画面
- データインポート/エクスポート画面

今回は`残高登録画面`を実装していきます。

# 残高登録画面開発
## 入力項目
当画面の入力項目は
- 口座情報
- 登録年月日
- 残高額
くらいです。

かつ内容もほとんど前回の単票登録形式の`口座登録画面`とほとんど変わらないです。

## template
```vue:src/views/Balance.vue
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>BALANCE</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">BALANCE</ion-title>
        </ion-toolbar>
      </ion-header>
      <!-- 残高管理No -->
      <ion-item>
        <div id="page-top">
          <ion-input
            label="No"
            label-placement="stacked"
            :disabled="true"
            :value="no"
            @input="no = $event.target.value"
          ></ion-input>
        </div>
      </ion-item>
      <!-- 口座情報 -->
      <ion-item>
        <ion-select
          label="ACCOUNT"
          placeholder="Select ACCOUNT"
          label-placement="stacked"
          interface="action-sheet"
          v-model="accountKey"
        >
          <ion-select-option
            :value="account.key"
            v-for="account in accountList"
            :key="account.key"
          >
            {{ account.bank }}:{{ account.accountHolder }}
          </ion-select-option>
        </ion-select>
      </ion-item>
      <!-- 登録年月日 -->
      <ion-item>
        <ion-input
          label="DATE"
          label-placement="stacked"
          type="date"
          :value="recordDate"
          @input="recordDate = $event.target.value"
        >
        </ion-input>
      </ion-item>
      <!-- 口座残高 -->
      <ion-item>
        <ion-input
          label="BALANCE"
          label-placement="stacked"
          type="number"
          placeholder="1000000"
          :value="balance"
          @input="balance = $event.target.value"
        ></ion-input>
      </ion-item>
      <ion-grid>
        <ion-row>
          <!-- 登録ボタン -->
          <ion-col>
            <ion-button expand="block" color="success" @click="registBalance"
              ><ion-icon :icon="saveOutline" slot="start"></ion-icon
              >REGIST</ion-button
            >
          </ion-col>
        </ion-row>
      </ion-grid>
      <ion-grid v-if="!(no === 'New Balance')">
        <ion-row>
          <!-- 削除ボタン -->
          <ion-col>
            <ion-button expand="block" color="danger" @click="deleteBalance"
              ><ion-icon :icon="trashOutline" slot="start"></ion-icon
              >DELETE</ion-button
            >
          </ion-col>
        </ion-row>
      </ion-grid>
    </ion-content>
  </ion-page>
</template>
```

## script
script部分もこんな感じです。
```vue:src/views/Balance.vue
<script setup>
import {
  IonPage,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
  IonItem,
  IonInput,
  IonCol,
  IonButton,
  IonIcon,
  IonLabel,
  IonSelect,
  IonSelectOption,
  IonGrid,
  IonRow,
  onIonViewWillEnter,
  useIonRouter
} from "@ionic/vue";
import { cashOutline, saveOutline, trashOutline } from "ionicons/icons";
import ExploreContainer from "@/components/ExploreContainer.vue";
import { useRoute } from "vue-router";
import { ref, onMounted } from "vue";
import { Storage } from "@ionic/storage";

/* ******** プロパティ ******** */
let no = ref(); // 管理No
let accountKey = ref(); // 口座キー
let recordDate = ref(); // 登録日
let balance = ref(); // 口座残高
let accountList = ref([]); // 口座名義人リスト
let storage;

const NEW_BALANCE_NO = "New Balance"; // 新規残高情報の場合の管理No
const DATA_TYPE_FOR_ACCOUNT = "accountInfo"; // ストレージのデータタイプ（口座情報）
const KEY_PREFIX_FOR_ACCOUNT = "accountInfo_"; // ストレージのキーの接頭辞（口座情報）
const DATA_TYPE_FOR_BALANCE = "balanceInfo"; // ストレージのデータタイプ（残高情報）
const KEY_PREFIX_FOR_BALANCE = "balanceInfo_"; // ストレージのキーの接頭辞（残高情報）

/* ******** router準備 ******** */
const route = useRoute();
const ionRouter = useIonRouter();

/* ******** ライフサイクルフック ******** */
onMounted(async function () {});

onIonViewWillEnter(async function () {
  // ストレージ準備
  const store = new Storage();
  storage = await store.create();
  // routerからパラメータ受け取り&残高管理Noの設定
  const balanceNoByRouter = Number(route.params["balanceNo"]);
  // 口座情報を取得
  readAllAccount();

  if (balanceNoByRouter === 0) {
    no.value = NEW_BALANCE_NO;
  } else {
    getBalance(balanceNoByRouter);
  }
});

/* ******** メソッド ******** */
/* ----------------------------------
method:口座全件読み込み
detail:口座情報を全件読み込む。
 ---------------------------------- */
const readAllAccount = async function () {
  accountList.value = [];
  storage.forEach((value, key) => {
    let infoJson = JSON.parse(value);
    // データタイプがaccountInfoなら口座情報として読み込む
    if (infoJson.dataType === DATA_TYPE_FOR_ACCOUNT) {
      accountList.value.push({
        key: key,
        no: infoJson.no,
        bank: infoJson.bank,
        accountHolder: infoJson.accountHolder,
      });
    }
  });
};

/*
method:残高登録
detail:残高を登録する。
*/
const registBalance = async function () {
  // 入力値に未入力のものがあれば処理終了
  if (
    checkEmptyValue(no.value) ||
    checkEmptyValue(accountKey.value) ||
    checkEmptyValue(recordDate.value) ||
    checkEmptyValue(balance.value)
  ) {
    return;
  }

  // 残高情報の管理No
  let newNo = 0;

  // 残高情報新規登録かチェック
  let newBalanceFlg = false;
  if (no.value === NEW_BALANCE_NO) {
    newBalanceFlg = true;
  }

  // 残高情報新規登録なら残高管理Noを新たに採番
  if (newBalanceFlg) {
    await storage.forEach((value, key) => {
      // ストレージに保存された各データのnoと突合
      let infoJson = JSON.parse(value);
      if (infoJson.dataType === DATA_TYPE_FOR_BALANCE) {
        let thisBalanceNo = Number(infoJson.no);
        if (newNo <= thisBalanceNo) {
          newNo = Number(thisBalanceNo);
        }
      }
    });
    newNo = newNo + 1;

  } else {
    // 既存の残高情報を削除
    storage.remove(KEY_PREFIX_FOR_BALANCE + String(no.value));
    newNo = no.value;
  }

  // ストレージへの登録
  await storage.set(
    KEY_PREFIX_FOR_BALANCE + newNo,
    JSON.stringify({
      no: newNo,
      accountKey: accountKey.value,
      recordDate: recordDate.value,
      balance: Number(balance.value),
      dataType: DATA_TYPE_FOR_BALANCE
    })
  );

  // 新規登録か既存更新かで処理後の挙動を分岐
  if (newBalanceFlg) {
    // 入力欄のクリア
    no.value = NEW_BALANCE_NO;
    accountKey.value = "";
    recordDate.value = "";
    balance.value = "";
  
  } else {
    ionRouter.back();
  }
};

/* ----------------------------------
method:残高検索
detail:パスパラメータで送られてきた
 ---------------------------------- */
const getBalance = async function (balanceNo) {
  let balanceInfo = await storage.get(
    KEY_PREFIX_FOR_BALANCE + String(balanceNo)
  );
  let balanceInfoJson = JSON.parse(balanceInfo);
  no.value = balanceInfoJson.no;
  accountKey.value = balanceInfoJson.accountKey;
  recordDate.value = balanceInfoJson.recordDate;
  balance.value = balanceInfoJson.balance;
};

/* ----------------------------------
method:残高削除
detail:選択された残高を削除。
 ---------------------------------- */
const deleteBalance = async function (account) {
  if ((no.value === NEW_BALANCE_NO)) {
    return;
  }
  await storage.remove(KEY_PREFIX_FOR_BALANCE + String(no.value));
  ionRouter.back();
};

/* ----------------------------------
method:空文字・NULL・undefined判定
detail:空文字・NULL・undefinedの場合、trueを返す。
 ---------------------------------- */
const checkEmptyValue = function (targetValue) {
  if (targetValue === null || targetValue === "" || targetValue === undefined) {
    return true;
  }
  return false;
};
</script>
```

### つまったポイント
あまり困ったポイントはありませんでしたが、この画面は画面下部のタブから遷移してきたときは、新規登録を行え、後々開発する残高一覧画面から遷移してきたときはその残高情報の更新削除ができるようにという要件で実装予定でしたので、パスパラメータでこの辺りは実現してます。

ルーティングパスで`'balance/:balanceNo'`のように、パスパラメータを受け取れるようにしておいてます。

```javascript:src/router/index.js
import { createRouter, createWebHistory } from '@ionic/vue-router';
import TabsPage from '../views/TabsPage.vue'

const routes = [
  {
    path: '/',
    redirect: '/tabs/view'
  },
  {
    path: '/tabs/',
    component: TabsPage,
    children: [
      {
        path: '',
        redirect: '/tabs/view'
      },
      {
        path: 'view',
        component: () => import('@/views/View.vue')
      },
      {
        path: 'balance/:balanceNo',
        component: () => import('@/views/Balance.vue')
      },
      {
        path: 'account',
        component: () => import('@/views/Account.vue')
      },
      {
        path: 'importExport',
        component: () => import('@/views/importExport.vue')
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router
```

また、画面側ではルーティングで定義しておいたパスパラメータを受け取るように↓のような実装にしています。
`route.params["balanceNo"]`のように

```js:src/views/Balance.vue
/* ******** router準備 ******** */
const route = useRoute();
const ionRouter = useIonRouter();

/* ******** ライフサイクルフック ******** */
onIonViewWillEnter(async function () {
  // ストレージ準備
  const store = new Storage();
  storage = await store.create();
  // routerからパラメータ受け取り&残高管理Noの設定
  const balanceNoByRouter = Number(route.params["balanceNo"]);
  // 口座情報を取得
  readAllAccount();

  if (balanceNoByRouter === 0) {
    no.value = NEW_BALANCE_NO;
  } else {
    getBalance(balanceNoByRouter);
  }
});

```

# ソース全量
<details><summary>Account.vue全量</summary>

```vue:src/views/Account.vue
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>BALANCE</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">BALANCE</ion-title>
        </ion-toolbar>
      </ion-header>
      <!-- 残高管理No -->
      <ion-item>
        <div id="page-top">
          <ion-input
            label="No"
            label-placement="stacked"
            :disabled="true"
            :value="no"
            @input="no = $event.target.value"
          ></ion-input>
        </div>
      </ion-item>
      <!-- 口座情報 -->
      <ion-item>
        <ion-select
          label="ACCOUNT"
          placeholder="Select ACCOUNT"
          label-placement="stacked"
          interface="action-sheet"
          v-model="accountKey"
        >
          <ion-select-option
            :value="account.key"
            v-for="account in accountList"
            :key="account.key"
          >
            {{ account.bank }}:{{ account.accountHolder }}
          </ion-select-option>
        </ion-select>
      </ion-item>
      <!-- 登録年月日 -->
      <ion-item>
        <ion-input
          label="DATE"
          label-placement="stacked"
          type="date"
          :value="recordDate"
          @input="recordDate = $event.target.value"
        >
        </ion-input>
      </ion-item>
      <!-- 口座残高 -->
      <ion-item>
        <ion-input
          label="BALANCE"
          label-placement="stacked"
          type="number"
          placeholder="1000000"
          :value="balance"
          @input="balance = $event.target.value"
        ></ion-input>
      </ion-item>
      <ion-grid>
        <ion-row>
          <!-- 登録ボタン -->
          <ion-col>
            <ion-button expand="block" color="success" @click="registBalance"
              ><ion-icon :icon="saveOutline" slot="start"></ion-icon
              >REGIST</ion-button
            >
          </ion-col>
        </ion-row>
      </ion-grid>
      <ion-grid v-if="!(no === 'New Balance')">
        <ion-row>
          <!-- 削除ボタン -->
          <ion-col>
            <ion-button expand="block" color="danger" @click="deleteBalance"
              ><ion-icon :icon="trashOutline" slot="start"></ion-icon
              >DELETE</ion-button
            >
          </ion-col>
        </ion-row>
      </ion-grid>
    </ion-content>
  </ion-page>
</template>

<script setup>
import {
  IonPage,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
  IonItem,
  IonInput,
  IonCol,
  IonButton,
  IonIcon,
  IonLabel,
  IonSelect,
  IonSelectOption,
  IonGrid,
  IonRow,
  onIonViewWillEnter,
  useIonRouter
} from "@ionic/vue";
import { cashOutline, saveOutline, trashOutline } from "ionicons/icons";
import ExploreContainer from "@/components/ExploreContainer.vue";
import { useRoute } from "vue-router";
import { ref } from "vue";
import { Storage } from "@ionic/storage";

/* ******** プロパティ ******** */
let no = ref(); // 管理No
let accountKey = ref(); // 口座キー
let recordDate = ref(); // 登録日
let balance = ref(); // 口座残高
let accountList = ref([]); // 口座名義人リスト
let storage;

const NEW_BALANCE_NO = "New Balance"; // 新規残高情報の場合の管理No
const DATA_TYPE_FOR_ACCOUNT = "accountInfo"; // ストレージのデータタイプ（口座情報）
const KEY_PREFIX_FOR_ACCOUNT = "accountInfo_"; // ストレージのキーの接頭辞（口座情報）
const DATA_TYPE_FOR_BALANCE = "balanceInfo"; // ストレージのデータタイプ（残高情報）
const KEY_PREFIX_FOR_BALANCE = "balanceInfo_"; // ストレージのキーの接頭辞（残高情報）

/* ******** router準備 ******** */
const route = useRoute();
const ionRouter = useIonRouter();

/* ******** ライフサイクルフック ******** */
onIonViewWillEnter(async function () {
  // ストレージ準備
  const store = new Storage();
  storage = await store.create();
  // routerからパラメータ受け取り&残高管理Noの設定
  const balanceNoByRouter = Number(route.params["balanceNo"]);
  // 口座情報を取得
  readAllAccount();

  if (balanceNoByRouter === 0) {
    no.value = NEW_BALANCE_NO;
  } else {
    getBalance(balanceNoByRouter);
  }
});

/* ******** メソッド ******** */
/* ----------------------------------
method:口座全件読み込み
detail:口座情報を全件読み込む。
 ---------------------------------- */
const readAllAccount = async function () {
  accountList.value = [];
  storage.forEach((value, key) => {
    let infoJson = JSON.parse(value);
    // データタイプがaccountInfoなら口座情報として読み込む
    if (infoJson.dataType === DATA_TYPE_FOR_ACCOUNT) {
      accountList.value.push({
        key: key,
        no: infoJson.no,
        bank: infoJson.bank,
        accountHolder: infoJson.accountHolder,
      });
    }
  });
};

/*
method:残高登録
detail:残高を登録する。
*/
const registBalance = async function () {
  // 入力値に未入力のものがあれば処理終了
  if (
    checkEmptyValue(no.value) ||
    checkEmptyValue(accountKey.value) ||
    checkEmptyValue(recordDate.value) ||
    checkEmptyValue(balance.value)
  ) {
    return;
  }

  // 残高情報の管理No
  let newNo = 0;

  // 残高情報新規登録かチェック
  let newBalanceFlg = false;
  if (no.value === NEW_BALANCE_NO) {
    newBalanceFlg = true;
  }

  // 残高情報新規登録なら残高管理Noを新たに採番
  if (newBalanceFlg) {
    await storage.forEach((value, key) => {
      // ストレージに保存された各データのnoと突合
      let infoJson = JSON.parse(value);
      if (infoJson.dataType === DATA_TYPE_FOR_BALANCE) {
        let thisBalanceNo = Number(infoJson.no);
        if (newNo <= thisBalanceNo) {
          newNo = Number(thisBalanceNo);
        }
      }
    });
    newNo = newNo + 1;

  } else {
    // 既存の残高情報を削除
    storage.remove(KEY_PREFIX_FOR_BALANCE + String(no.value));
    newNo = no.value;
  }

  // ストレージへの登録
  await storage.set(
    KEY_PREFIX_FOR_BALANCE + newNo,
    JSON.stringify({
      no: newNo,
      accountKey: accountKey.value,
      recordDate: recordDate.value,
      balance: Number(balance.value),
      dataType: DATA_TYPE_FOR_BALANCE
    })
  );

  // 新規登録か既存更新かで処理後の挙動を分岐
  if (newBalanceFlg) {
    // 入力欄のクリア
    no.value = NEW_BALANCE_NO;
    accountKey.value = "";
    recordDate.value = "";
    balance.value = "";
  
  } else {
    ionRouter.back();
  }
};

/* ----------------------------------
method:残高検索
detail:パスパラメータで送られてきた
 ---------------------------------- */
const getBalance = async function (balanceNo) {
  let balanceInfo = await storage.get(
    KEY_PREFIX_FOR_BALANCE + String(balanceNo)
  );
  let balanceInfoJson = JSON.parse(balanceInfo);
  no.value = balanceInfoJson.no;
  accountKey.value = balanceInfoJson.accountKey;
  recordDate.value = balanceInfoJson.recordDate;
  balance.value = balanceInfoJson.balance;
};

/* ----------------------------------
method:残高削除
detail:選択された残高を削除。
 ---------------------------------- */
const deleteBalance = async function (account) {
  if ((no.value === NEW_BALANCE_NO)) {
    return;
  }
  await storage.remove(KEY_PREFIX_FOR_BALANCE + String(no.value));
  ionRouter.back();
};

/* ----------------------------------
method:空文字・NULL・undefined判定
detail:空文字・NULL・undefinedの場合、trueを返す。
 ---------------------------------- */
const checkEmptyValue = function (targetValue) {
  if (targetValue === null || targetValue === "" || targetValue === undefined) {
    return true;
  }
  return false;
};
</script>


```
</details>

これで無事に残高登録画面実装完了です。
<img width=300 src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549748/16e0449c-98ca-2b97-7b77-eb5f1343edb6.png>

次は残高確認画面を実装します。