NCMBはモバイルアプリ開発におけるバックエンド機能(認証、データストア、ファイルストア、プッシュ通知など)を提供しています。バックエンドなのでAPIベースで利用するのが基本で、UI(アプリ側)は各自で開発する仕組みになっています。
現在、数多くのアプリが存在し、その中には定番とも言える機能があります。そうした定番機能を各フレームワークごとに実装しておくことで、再利用性高くNCMBが利用できるかと思います。
今回はFramework7で作ったフォームコンポーネントを紹介します。Monacaアプリでも利用可能です。
UIについて
コンポーネントは1つのHTMLだけで実装されているのが特徴です。そのため、基本的には以下の方法で導入・利用ができます。
- NCMB SDKの読み込み
- NCMBのキーの取得
- NCMBの初期化
- ルーティングの設定
- フォームUI(HTML)を配置
用意されている画面(機能)は次の通りです。
フォーム画面
フォーム画面は文字列、数字、日付、真偽値そして写真のアップロードが可能です。これらはあらかじめ項目を用意しているので、自分でカスタマイズできます。
使い方
ではここからは使い方を紹介します。
必要なライブラリ、NCMB SDKの読み込み
今回利用しているライブラリは次の通りです。
- NCMBのJavaScript SDK
<script src="js/ncmb.min.js"></script>
必要なキーの取得
NCMBのアプリケーションキーとクライアントキーを取得します。
NCMBの初期化
www/js/app.js
にてNCMBを初期化します。今回は www/js/config.json
というファイルにキーを記述しているので、以下のように読み込みを行っています。
const $ = Dom7;
(async () => {
const device = Framework7.getDevice();
// 設定ファイルの読み込み
const config = await (await fetch('./js/config.json')).json();
// NCMBの初期化
window.ncmb = new NCMB(config.applicationKey, config.clientKey);
// Framework7の初期化
window.app = new Framework7({
name: 'NCMB Notice',
theme: 'auto',
el: '#app',
id: 'com.nifcloud.mbaas.map',
store: store,
routes: routes,
input: {
scrollIntoViewOnFocus: device.cordova && !device.electron,
scrollIntoViewCentered: device.cordova && !device.electron,
},
statusbar: {
iosOverlaysWebView: true,
androidOverlaysWebView: false,
},
on: {
init: function () {
if (this.device.cordova) {
cordovaApp.init(this);
}
},
},
});
})();
ルーティングの設定
今回は最低限のルーティングを設定しています( www/js/routes.js
)。 /form/(クラス名)
というルーティングでフォームを表示します。クラス名はデータを登録するクラス名(DBでいうテーブル名に相当)になります。
const routes = [
{
path: '/',
url: './index.html',
},
{
path: '/form/:className',
componentUrl: './pages/form.html',
},
{
path: '(.*)',
url: './pages/404.html',
},
];
www/index.html
で /form/Hello
を最初に表示します。この Hello
がデータの追加対象になるクラスです。
<div id="app">
<!-- Your main view, should have "view-main" class -->
<div class="view view-init safe-areas" data-url="/form/Hello">
</div>
</div>
フォームUI(HTML)を配置
後は form.html をダウンロードして、www/pages/form.html
に配置するだけです。
フォームUIについて
form.htmlの内容です。このコンポーネントは基本的にカスタマイズ前提となっています。デフォルトのHTMLはサンプルと考えてください。
<template>
<div class="page">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner sliding">
<div class="title">フォーム</div>
</div>
</div>
<div class="page-content">
<form>
<div class="list">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">テキスト</div>
<div class="item-input-wrap">
<input type="text" name="text" placeholder="テキストデータ" value="テキスト" />
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">数値</div>
<div class="item-input-wrap">
<input type="number" name="number" placeholder="0" value="100" />
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">日付</div>
<div class="item-input-wrap">
<input type="date" name="date" placeholder="2022-01-01" value="2022-01-15" />
</div>
</div>
</li>
<li>
<div class="block-title">チェックボックス</div>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="boolean" value="true" checked="checked" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">True</div>
</div>
</label>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="boolean" value="false" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">False</div>
</div>
</label>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">写真</div>
<div class="item-input-wrap">
<i class="f7-icons size-150" @click=${openFilePicker}>camera</i>
<img width="150px" height="150px" style="object-fit: cover; display: none;" @click=${openFilePicker} />
<span style="display: none;">
<input type="file" name="photo" accept="image/*" @change=${selectPhoto} />
</span>
</div>
</div>
</li>
<li>
<a href="#" class="item-link list-button login-button" @click=${save}>データを作成する</a>
</li>
</ul>
</div>
</form>
</div>
</div>
</template>
<style>
.size-150 {
font-size: 150px;
}
</style>
<script>
export default function (props, {$f7, $f7router, $update }) {
// 登録対象のクラス名
const { className } = props;
// データの保存処理
const save = async (e) => {
$f7.dialog.preloader(); // 処理中ダイアログの表示
try {
// フォームからオブジェクトに変換
const params = formToObject($(e.target).parents('form')[0]);
// データストアの新規データ生成
const obj = new (ncmb.DataStore(className));
// データを登録
for (const key in params) {
obj.set(key, params[key]);
}
// ACLを設定
obj.set('acl', getAcl());
// 保存実行
await obj.save();
// 処理中ダイアログを閉じる
$f7.dialog.close();
// アラート表示
app.dialog.alert('データを登録しました', '新規保存');
} catch (e) {
// 処理中ダイアログを閉じる
$f7.dialog.close();
// アラート表示
app.dialog.alert(`データの登録に失敗しました<br />${e.message}`, 'エラー');
}
};
// フォームのデータをオブジェクトに変換する関数
const formToObject = (form) => {
const obj = {};
Array.prototype.slice.call(form.elements).forEach(ele => {
const { name } = ele;
switch (ele.type) {
case 'text': // テキストの場合
obj[name] = ele.value;
break;
case 'number': // 数値の場合
if (ele.value !== '') {
obj[name] = parseFloat(ele.value);
}
break;
case 'date': // 日付の場合
if (ele.value !== '') {
obj[name] = new Date(ele.value);
}
break;
case 'radio': // ラジオの場合
if (ele.checked) {
obj[name] = ele.value === 'true';
}
break;
case 'file': // ファイルの場合
const file = ele.files[0];
if (!file) break;
// 拡張子を取得
const ext = file.name.replace(/.*\.(.*)$/, "$1");
// ランダムな文字列を作成
const random = Math.random().toString(32).substring(2);
// ファイル名を作成
const fileName = `${(new Date).getTime()}-${random}.${ext}`;
// アップロードを実行
ncmb.File.upload(fileName, file, getAcl());
obj[name] = fileName;
break;
}
});
return obj;
}
// ACLを生成する関数(ここはアプリごとに変更してください)
const getAcl = () => {
const acl = new ncmb.Acl;
const user = ncmb.User.getCurrentUser();
if (user) {
// 認証している場合はその人だけが読み書きを可能に
acl
.setUserReadAccess(user, true)
.setUserWriteAccess(user, true);
} else {
// 認証していない場合は読み込み権限のみ
acl
.setPublicReadAccess(true);
}
return acl;
};
// ファイル選択ダイアログを開く
const openFilePicker = (e) => {
$(e.target).parents('div').find('[type="file"]').click();
};
// 写真を選択した後の処理
const selectPhoto = async (e) => {
const file = await loadPhoto(e);
// 元々のカメラアイコンを非表示に
const div = $(e.target).parents('div');
div.find('.f7-icons').hide();
// 画像をプレビュー表示
div.find('img').attr('src', file).show();
};
// 選択した写真を読み込む関数
const loadPhoto = (e) => {
return new Promise((res, rej) => {
const file = e.target.files[0];
const reader = new FileReader;
reader.addEventListener("load", () => {
res(reader.result);
}, false);
reader.readAsDataURL(file);
});
};
return $render;
}
</script>
カスタマイズポイント
入力項目について
すべての入力項目は name がデータストアのフィールド名になります。
文字列
文字列型の場合は type=text
を使ってください。
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">テキスト</div>
<div class="item-input-wrap">
<input type="text" name="text" placeholder="テキストデータ" value="テキスト" />
</div>
</div>
</li>
数値型
数値で保存する場合には type=number
を指定してください。
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">数値</div>
<div class="item-input-wrap">
<input type="number" name="number" placeholder="0" value="100" />
</div>
</div>
</li>
日付型
日付で保存する場合には type=date
を指定してください。
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">日付</div>
<div class="item-input-wrap">
<input type="date" name="date" placeholder="2022-01-01" value="2022-01-15" />
</div>
</div>
</li>
真偽値
日付で保存する場合にはラジオボタンを使ってください。
<li>
<div class="block-title">チェックボックス</div>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="boolean" value="true" checked="checked" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">True</div>
</div>
</label>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="boolean" value="false" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">False</div>
</div>
</label>
</li>
ファイルアップロード
ファイルは基本的に写真を想定しています。以下のHTMLで、photoというフィールド名にアップロードした写真のファイル名が入ります。写真はプレビュー表示もされます。
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">写真</div>
<div class="item-input-wrap">
<i class="f7-icons size-150" @click=${openFilePicker}>camera</i>
<img width="150px" height="150px" style="object-fit: cover; display: none;" @click=${openFilePicker} />
<span style="display: none;">
<input type="file" name="photo" accept="image/*" @change=${selectPhoto} />
</span>
</div>
</div>
</li>
アクセス権限について
デフォルトでは認証している場合は認証しているユーザーのみ読み書き可能、認証していない場合は読み込みのみ可能なデータとして保存されます。変更する場合は getAcl
関数を編集してください。
// ACLを生成する関数(ここはアプリごとに変更してください)
const getAcl = () => {
const acl = new ncmb.Acl;
const user = ncmb.User.getCurrentUser();
if (user) {
// 認証している場合はその人だけが読み書きを可能に
acl
.setUserReadAccess(user, true)
.setUserWriteAccess(user, true);
} else {
// 認証していない場合は読み込み権限のみ
acl
.setPublicReadAccess(true);
}
return acl;
};
保存後の処理
現在は保存した際にアラートを表示するのみとなっています。前の画面に戻る場合には $f7router.back()
を使ってください。
まとめ
今回のフォームコンポーネントを使えば、データの登録が簡単にできます。さらにリスト・詳細コンポーネントと組み合わせることで、登録した後の一覧表示や詳細表示も簡単に実現できるでしょう。
ぜひNCMBとFramework7を使ってアプリ開発にチャレンジしてください。