LoginSignup
0
0

More than 1 year has passed since last update.

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

Posted at

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

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

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

UIについて

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

  1. NCMB SDKの読み込み
  2. NCMBのキーの取得
  3. NCMBの初期化
  4. ルーティングの設定
  5. フォームUI(HTML)を配置

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

フォーム画面

image.png

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

使い方

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

必要なライブラリ、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を使ってアプリ開発にチャレンジしてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0