NCMBとFramework7を使ったフォームコンポーネントの紹介と使い方

NCMBはモバイルアプリ開発におけるバックエンド機能（認証、データストア、ファイルストア、プッシュ通知など）を提供しています。バックエンドなのでAPIベースで利用するのが基本で、UI（アプリ側）は各自で開発する仕組みになっています。

現在、数多くのアプリが存在し、その中には定番とも言える機能があります。そうした定番機能を各フレームワークごとに実装しておくことで、再利用性高くNCMBが利用できるかと思います。

今回はFramework7で作ったフォームコンポーネントを紹介します。Monacaアプリでも利用可能です。

## UIについて

コンポーネントは1つのHTMLだけで実装されているのが特徴です。そのため、基本的には以下の方法で導入・利用ができます。

1. NCMB SDKの読み込み
1. NCMBのキーの取得
1. NCMBの初期化
1. ルーティングの設定
1. フォームUI（HTML）を配置

用意されている画面（機能）は次の通りです。

### フォーム画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/1fff28aa-3b44-063b-b100-4d887411c926.png)


フォーム画面は文字列、数字、日付、真偽値そして写真のアップロードが可能です。これらはあらかじめ項目を用意しているので、自分でカスタマイズできます。

## 使い方

ではここからは使い方を紹介します。

### 必要なライブラリ、NCMB SDKの読み込み

今回利用しているライブラリは次の通りです。

- NCMBのJavaScript SDK

```html
<script src="js/ncmb.min.js"></script>
```

### 必要なキーの取得

NCMBのアプリケーションキーとクライアントキーを取得します。

- [ニフクラ mobile backend](https://mbaas.nifcloud.com/)

### NCMBの初期化

`www/js/app.js` にてNCMBを初期化します。今回は `www/js/config.json` というファイルにキーを記述しているので、以下のように読み込みを行っています。

```js
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でいうテーブル名に相当）になります。

```js
const routes = [
  {
    path: '/',
    url: './index.html',
  },
  {
    path: '/form/:className',
    componentUrl: './pages/form.html',
  },
  {
    path: '(.*)',
    url: './pages/404.html',
  },
];
```

`www/index.html` で `/form/Hello` を最初に表示します。この `Hello` がデータの追加対象になるクラスです。

```html
<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](https://raw.githubusercontent.com/NCMBMania/framework7_form_component/main/www/pages/form.html) をダウンロードして、`www/pages/form.html` に配置するだけです。

## フォームUIについて

form.htmlの内容です。このコンポーネントは基本的にカスタマイズ前提となっています。デフォルトの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` を使ってください。

```html
<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` を指定してください。

```html
<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` を指定してください。

```html
<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>
```

#### 真偽値

日付で保存する場合にはラジオボタンを使ってください。

```html
<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というフィールド名にアップロードした写真のファイル名が入ります。写真はプレビュー表示もされます。

```html
<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` 関数を編集してください。

```js
// 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を使ってアプリ開発にチャレンジしてください。