はじめに
前回の[僕のlonicでスマホアプリ開発]#2 router設定続きになります。
今回作成するスマホアプリは画面が以下の通りになる想定ですが、
- 残高確認画面
- 残高登録画面
- 口座登録画面
- データインポート/エクスポート画面
操作の順番としては
1. 口座登録画面で口座登録。
2. 残高登録画面で残高登録。
3. 残高確認画面で残高確認。
4. データインポート/エクスポート画面でデータのインポートとエクスポート。
という順番になるので、開発もこの順番でやっていこうと思います。
口座登録画面開発
入力項目
当画面の入力項目は
- 銀行名
- 口座名義人
くらいかなと。
Ionicは準備してくれてる画面コンポーネントもだいぶ充実してるのでこちらから選べばいいですね。
https://ionicframework.com/docs/ja/v6/components
template
template部分の実装は下記の通り。
<template>
<ion-page>
<!-- ヘッダー部 -->
<ion-header>
<ion-toolbar>
<ion-title>ACCOUNT</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="bindingScrollContent" :scroll-events="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">ACCOUNT</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-input
required="true"
label="BANK"
label-placement="stacked"
placeholder="SSK BANK"
color="primary"
:value="bank"
@input="bank = $event.target.value"
></ion-input>
</ion-item>
<!-- 口座名義人名入力欄 -->
<ion-item>
<ion-input
required="true"
label="ACCOUNT HOLDER"
label-placement="stacked"
placeholder="TARO SUZUKI"
color="primary"
:value="accountHolder"
@input="accountHolder = $event.target.value"
></ion-input>
</ion-item>
<ion-grid>
<ion-row>
<!-- 新規口座ボタン -->
<ion-col>
<ion-button
expand="block"
color="medium"
@click="setNewAccountModeOn"
><ion-icon :icon="personAddOutline" slot="start"></ion-icon>NEW
ACCOUNT</ion-button
>
</ion-col>
<!-- 登録ボタン -->
<ion-col>
<ion-button expand="block" color="success" @click="registAccount"
><ion-icon :icon="saveOutline" slot="start"></ion-icon
>REGIST</ion-button
>
</ion-col>
</ion-row>
</ion-grid>
<!-- 口座一覧表示エリア -->
<ion-list v-for="account in accountList" :key="account.key">
<ion-item button color="light" @click.stop="selectAccount(account)">
<ion-label>
<p>{{ account.no }}</p>
<h3>{{ account.bank }}</h3>
<h3>{{ account.accountHolder }}</h3>
</ion-label>
<ion-button
slot="end"
size="middle"
color="danger"
@click.stop="deleteAccount(account)"
>
DELETE
</ion-button>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>

(余談)データバインディング
Vueは2系しか使ったことがなく3系では若干書き方が変わってるのでそのお勉強を軽く。
あとIonicのクセあるポイントなんですが、v-model
だとうまいことデータバインディングできないっぽい。
<template>
<ion-page>
<!-- ヘッダー部 -->
<ion-header>
<ion-toolbar>
<ion-title>ACCOUNT</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">ACCOUNT</ion-title>
</ion-toolbar>
</ion-header>
<!-- メインコンテンツ -->
<!-- 口座管理No -->
<ion-item>
<ion-input label="No" label-placement="stacked" :disabled="true"></ion-input>
</ion-item>
<!-- 銀行名入力欄 -->
<ion-item>
<ion-input required="true" label="BANK" label-placement="stacked"
placeholder="SSK BANK" color="primary" :value="bank" @input="bank = $event.target.value"></ion-input>
</ion-item>
<!-- 口座名義人名入力欄 -->
<ion-item>
<ion-input required="true" label="ACCOUNT HOLDER" label-placement="stacked"
placeholder="TARO SUZUKI" color="primary" :value="accountHolder" @input="accountHolder = $event.target.value"></ion-input>
</ion-item>
<!--test -->
<ion-item>
銀行名:{{bank}} 口座名:{{accountHolder}}
</ion-item>
<ion-grid>
<ion-row>
<!-- 新規口座ボタン -->
<ion-col>
<ion-button expand="block" color="medium"><ion-icon :icon="personAddOutline" slot="start"></ion-icon>NEW ACCOUNT</ion-button>
</ion-col>
<!-- 登録ボタン -->
<ion-col>
<ion-button expand="block" color="success"><ion-icon :icon="saveOutline" slot="start"></ion-icon>REGIST</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
</ion-page>
</template>
<script setup>
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import ExploreContainer from '@/components/ExploreContainer.vue';
import { personAddOutline, saveOutline } from 'ionicons/icons';
import { ref } from 'vue';
/* プロパティ */
let bank = ref(); //銀行名
let accountHolder = ref() // 口座名義人
</script>
:value="accountHolder" @input="accountHolder = $event.target.value"
でデータバインディング問題は解決。
3系の記法の話ですがsetup関数が楽でいいですね。
深く見てないんで、setup関数が書けるとどういう恩恵があるのかは深くは不明。
データ保存
「REGIST」ボタンが押されたときに、ローカルに入力内容を保存する。
調べてみたらIonicでIonic Secure Storageとかいうものを準備してくれてるらしい。
ionic-storage githubをみながらやればまあ間違わないかなと。
僕の作ったソース(後述)ではStorageへの保存とか削除とか一覧取得とかは各所に散らばってますが、事前の準備はonMounted
で。
/* ******** ライフサイクル ******** */
onMounted(async function () {
// ストレージ準備
const store = new Storage();
storage = await store.create();
await readAllAccount();
});
script部分
特に難しいことはしてないです。
<script setup>
<script setup>
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonItem,
IonInput,
IonCol,
IonButton,
IonIcon,
IonGrid,
IonRow,
IonList,
IonLabel,
onIonViewWillEnter,
alertController
} from "@ionic/vue";
import ExploreContainer from "@/components/ExploreContainer.vue";
import { personAddOutline, saveOutline, star } from "ionicons/icons";
import { ref, reactive } from "vue";
import { Storage } from "@ionic/storage";
/* ******** プロパティ ******** */
let no = ref("New Account"); // 管理No
let bank = ref(); // 銀行名
let accountHolder = ref(); // 口座名義人
let accountList = ref([]); // 口座名義人リスト
let storage;
const NEW_ACCOUNT_NO = "New Account"; // 新規残高情報の場合の管理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_"; // ストレージのキーの接頭辞(残高情報)
/* ******** ライフサイクル ******** */
onIonViewWillEnter(async function () {
// ストレージ準備
const store = new Storage();
storage = await store.create();
// アカウント情報読み込み
await readAllAccount();
});
/* ******** メソッド ******** */
/* ----------------------------------
method:新規口座登録モード
detail:全項目をnullに変更。
----------------------------------*/
const setNewAccountModeOn = function () {
no.value = NEW_ACCOUNT_NO;
bank.value = "";
accountHolder.value = "";
};
/* ----------------------------------
method:口座登録
detail:口座情報を登録する。
----------------------------------*/
const registAccount = async function () {
// 銀行もしくは口座名義人が空なら処理終了
if (checkEmptyValue(bank.value) || checkEmptyValue(accountHolder.value)) {
return;
}
// 口座情報の管理No
let newNo = 0;
// 新規アカウントなら新しい管理番号を採番する。
if (no.value === NEW_ACCOUNT_NO) {
await storage.forEach((value, key) => {
// ストレージに保存された各データのnoと突合
let infoJson = JSON.parse(value);
if (infoJson.dataType === DATA_TYPE_FOR_ACCOUNT) {
let thisAccountNo = Number(infoJson.no);
if (newNo <= thisAccountNo) {
newNo = Number(thisAccountNo);
}
}
});
newNo = newNo + 1;
// 既存の口座情報なら一度削除する。
} else {
await storage.remove(KEY_PREFIX + no.value);
newNo = no.value;
}
// ストレージへの登録
await storage.set(
KEY_PREFIX_FOR_ACCOUNT + newNo,
JSON.stringify({
no: newNo,
bank: bank.value,
accountHolder: accountHolder.value,
dataType: DATA_TYPE_FOR_ACCOUNT,
})
);
setNewAccountModeOn();
readAllAccount();
};
/* ----------------------------------
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,
});
}
});
// 配列を管理番号順に並べ替え
accountList.value.sort(function (first, second) {
if (first.no < second.no) {
return -1;
} else if (first > second) {
return 1;
} else {
return 0;
}
});
};
/* ----------------------------------
method:口座選択
detail:選択された口座の情報を画面上部の入力欄に反映する。
---------------------------------- */
const selectAccount = function (account) {
no.value = account.no;
bank.value = account.bank;
accountHolder.value = account.accountHolder;
// 画面最上部へスクロール
clickToTop();
};
/* ----------------------------------
method:口座削除
detail:選択された口座を削除。
---------------------------------- */
const deleteAccount = async function (account) {
// 口座情報を削除
await storage.remove(account.key);
// 口座情報に紐づく残高情報を削除
await storage.forEach( (value, key) => {
let infoJson = JSON.parse(value);
// データタイプがaccountInfoなら口座情報として読み込む
if (infoJson.dataType === DATA_TYPE_FOR_BALANCE) {
if (infoJson.accountKey === account.key) {
storage.remove(key);
}
}
});
readAllAccount();
presentAlert();
};
/* ----------------------------------
method:データ削除完了用アラート表示。
detail:データ削除完了を示すアラートを表示する。
---------------------------------- */
const presentAlert = async () => {
const alert = await alertController.create({
header: "INFOMATION",
subHeader: 'Deleting accout data and relative balance data are finished!',
buttons: ['OK'],
});
await alert.present();
};
/* ----------------------------------
method:空文字・NULL・undefined判定
detail:空文字・NULL・undefinedの場合、trueを返す。
---------------------------------- */
const checkEmptyValue = function (targetValue) {
if (targetValue === null || targetValue === "" || targetValue === undefined) {
return true;
}
return false;
};
/* ----------------------------------
method:画面上部自動スクロール
detail:画面最上部に自動スクロールする。
---------------------------------- */
const clickToTop = () => {
const scrollContent = document.querySelector(
"ion-content.bindingScrollContent"
);
scrollContent.scrollToTop(200);
};
地味に困った(つまずいた)ポイント
入力エリア固定問題
もともとの構想だと下記の画像でいう青枠部分を固定にして、赤枠部分をスクロールできるようにしようと思ったんですが、これが絶妙に難しかったです。というか諦めました。
赤枠の口座情報を押下すると、画面最上部の入力エリアに選択した口座情報を反映して、その状態で「REGIST」ボタンを押すと口座情報が更新できるので、青枠部分は固定するなりしておかないと絶妙に使いずらいです。

青枠部分を固定するようなオプションはIonicには用意されておらず(以前のバージョンではあった)、自力でCSSを書いて固定するしかないです。
ただ、これがフロント雑魚の僕にはなかなか難しくて、固定はできてもコンポーネントが一部消えたり、Ionic側で制御しているスタイルが解除されたり。。。
というわけで最後はあきらめて、赤枠の口座情報を押下したら、画面最上部に自動スクロールするようにしました。
<template>
<ion-page>
<!-- ヘッダー部 -->
<ion-header>
<ion-toolbar>
<ion-title>ACCOUNT</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">ACCOUNT</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>
・
・
・
/* ----------------------------------
method:口座選択
detail:選択された口座の情報を画面上部の入力欄に反映する。
---------------------------------- */
const selectAccount = function (account) {
no.value = account.no;
bank.value = account.bank;
accountHolder.value = account.accountHolder;
// 画面最上部へスクロール
document.getElementById("page-top").scrollIntoView({ // ← ここで自動スクロール
behavior: "smooth",
block: "center",
});
};
script内でリアクティブな変数を更新する
Vueは2系しか書いたことがなくて、今回初めて3系&Option API(script setup
)を使ったんですが、ろくに調べてもいなかったので
let no = ref("New Account"); // 管理No
let bank = ref(); // 銀行名
let accountHolder = ref(); // 口座名義人
let accountList = ref([]); // 口座名義人リスト
let storage;
no = 1 // noのデータバインドが解除される
としたらデータバインディングが解除されて、templateで何も表示されなくなってしまいました。
正確には{バインドしてる変数}.value
と左辺に書かないといけなかったんですね。。。
let no = ref("New Account"); // 管理No
let bank = ref(); // 銀行名
let accountHolder = ref(); // 口座名義人
let accountList = ref([]); // 口座名義人リスト
let storage;
no.value = 1 // こうしないといけなかった
でもまあ、とはいえ口座登録画面はこれで完成です。
最後にソースの全量を載せておきます。
Account.vue全量
<template>
<ion-page>
<!-- ヘッダー部 -->
<ion-header>
<ion-toolbar>
<ion-title>ACCOUNT</ion-title>
</ion-toolbar>
</ion-header>
<ion-content
:fullscreen="true"
class="bindingScrollContent"
:scroll-events="true"
>
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">ACCOUNT</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-input
required="true"
label="BANK"
label-placement="stacked"
placeholder="SSK BANK"
color="primary"
:value="bank"
@input="bank = $event.target.value"
></ion-input>
</ion-item>
<!-- 口座名義人名入力欄 -->
<ion-item>
<ion-input
required="true"
label="ACCOUNT HOLDER"
label-placement="stacked"
placeholder="TARO SUZUKI"
color="primary"
:value="accountHolder"
@input="accountHolder = $event.target.value"
></ion-input>
</ion-item>
<ion-grid>
<ion-row>
<!-- 新規口座ボタン -->
<ion-col>
<ion-button
expand="block"
color="medium"
@click="setNewAccountModeOn"
><ion-icon :icon="personAddOutline" slot="start"></ion-icon>NEW
ACCOUNT</ion-button
>
</ion-col>
<!-- 登録ボタン -->
<ion-col>
<ion-button expand="block" color="success" @click="registAccount"
><ion-icon :icon="saveOutline" slot="start"></ion-icon
>REGIST</ion-button
>
</ion-col>
</ion-row>
</ion-grid>
<!-- 口座一覧表示エリア -->
<ion-list v-for="account in accountList" :key="account.key">
<ion-item button color="light" @click.stop="selectAccount(account)">
<ion-label>
<p>{{ account.no }}</p>
<h3>{{ account.bank }}</h3>
<h3>{{ account.accountHolder }}</h3>
</ion-label>
<ion-button
slot="end"
size="middle"
color="danger"
@click.stop="deleteAccount(account)"
>
DELETE
</ion-button>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup>
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonItem,
IonInput,
IonCol,
IonButton,
IonIcon,
IonGrid,
IonRow,
IonList,
IonLabel,
onIonViewWillEnter,
alertController
} from "@ionic/vue";
import ExploreContainer from "@/components/ExploreContainer.vue";
import { personAddOutline, saveOutline, star } from "ionicons/icons";
import { ref, reactive } from "vue";
import { Storage } from "@ionic/storage";
/* ******** プロパティ ******** */
let no = ref("New Account"); // 管理No
let bank = ref(); // 銀行名
let accountHolder = ref(); // 口座名義人
let accountList = ref([]); // 口座名義人リスト
let storage;
const NEW_ACCOUNT_NO = "New Account"; // 新規残高情報の場合の管理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_"; // ストレージのキーの接頭辞(残高情報)
/* ******** ライフサイクル ******** */
onIonViewWillEnter(async function () {
// ストレージ準備
const store = new Storage();
storage = await store.create();
// アカウント情報読み込み
await readAllAccount();
});
/* ******** メソッド ******** */
/* ----------------------------------
method:新規口座登録モード
detail:全項目をnullに変更。
----------------------------------*/
const setNewAccountModeOn = function () {
no.value = NEW_ACCOUNT_NO;
bank.value = "";
accountHolder.value = "";
};
/* ----------------------------------
method:口座登録
detail:口座情報を登録する。
----------------------------------*/
const registAccount = async function () {
// 銀行もしくは口座名義人が空なら処理終了
if (checkEmptyValue(bank.value) || checkEmptyValue(accountHolder.value)) {
return;
}
// 口座情報の管理No
let newNo = 0;
// 新規アカウントなら新しい管理番号を採番する。
if (no.value === NEW_ACCOUNT_NO) {
await storage.forEach((value, key) => {
// ストレージに保存された各データのnoと突合
let infoJson = JSON.parse(value);
if (infoJson.dataType === DATA_TYPE_FOR_ACCOUNT) {
let thisAccountNo = Number(infoJson.no);
if (newNo <= thisAccountNo) {
newNo = Number(thisAccountNo);
}
}
});
newNo = newNo + 1;
// 既存の口座情報なら一度削除する。
} else {
await storage.remove(KEY_PREFIX + no.value);
newNo = no.value;
}
// ストレージへの登録
await storage.set(
KEY_PREFIX_FOR_ACCOUNT + newNo,
JSON.stringify({
no: newNo,
bank: bank.value,
accountHolder: accountHolder.value,
dataType: DATA_TYPE_FOR_ACCOUNT,
})
);
setNewAccountModeOn();
readAllAccount();
};
/* ----------------------------------
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,
});
}
});
// 配列を管理番号順に並べ替え
accountList.value.sort(function (first, second) {
if (first.no < second.no) {
return -1;
} else if (first > second) {
return 1;
} else {
return 0;
}
});
};
/* ----------------------------------
method:口座選択
detail:選択された口座の情報を画面上部の入力欄に反映する。
---------------------------------- */
const selectAccount = function (account) {
no.value = account.no;
bank.value = account.bank;
accountHolder.value = account.accountHolder;
// 画面最上部へスクロール
clickToTop();
};
/* ----------------------------------
method:口座削除
detail:選択された口座を削除。
---------------------------------- */
const deleteAccount = async function (account) {
// 口座情報を削除
await storage.remove(account.key);
// 口座情報に紐づく残高情報を削除
await storage.forEach( (value, key) => {
let infoJson = JSON.parse(value);
// データタイプがaccountInfoなら口座情報として読み込む
if (infoJson.dataType === DATA_TYPE_FOR_BALANCE) {
if (infoJson.accountKey === account.key) {
storage.remove(key);
}
}
});
readAllAccount();
presentAlert();
};
/* ----------------------------------
method:データ削除完了用アラート表示。
detail:データ削除完了を示すアラートを表示する。
---------------------------------- */
const presentAlert = async () => {
const alert = await alertController.create({
header: "INFOMATION",
subHeader: 'Deleting accout data and relative balance data are finished!',
buttons: ['OK'],
});
await alert.present();
};
/* ----------------------------------
method:空文字・NULL・undefined判定
detail:空文字・NULL・undefinedの場合、trueを返す。
---------------------------------- */
const checkEmptyValue = function (targetValue) {
if (targetValue === null || targetValue === "" || targetValue === undefined) {
return true;
}
return false;
};
/* ----------------------------------
method:画面上部自動スクロール
detail:画面最上部に自動スクロールする。
---------------------------------- */
const clickToTop = () => {
const scrollContent = document.querySelector(
"ion-content.bindingScrollContent"
);
scrollContent.scrollToTop(200);
};
</script>
<style scoped>
</style>
次は、残高登録画面を作っていきましょう。
追記
画面の自動スクロールは↓を参考に実装しなおしました。
https://ionicframework.com/docs/ja/v6/api/content#%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89
(実機だと機能しなかったので、、、だがこれでもある条件下でヘッダ名が画面上部にめり込むというバグが、、、)
/* ----------------------------------
method:画面上部自動スクロール
detail:画面最上部に自動スクロールする。
---------------------------------- */
const clickToTop = () => {
const scrollContent = document.querySelector('ion-content.bindingScrollContent');
scrollContent.scrollToTop(200) // duration: 1000
}