Monacaを使って何かアプリを作ってみたいと思っても、いざとなるとアイデアが出てこないかもしれません。また、最初から大きなアプリを作ろうと思うと、何から手を付けて良いのか分からないことでしょう。

そこで、この記事では手順を踏んで簡単なアプリを開発してみます。最初の一歩として、ぜひチャレンジしてみてください。

今回はNCMB(ニフクラmobile backend)と組み合わせて、掲示板アプリを開発します。記事は全部で2回に分けており、後半となる今回はスレッドの作成と閲覧、コメントの投稿などを開発します。

※仕様と認証を説明した前半はこちらです。
掲示板アプリを作ってみよう【その1:画面の説明と認証まで】

※ サンプルアプリのソースコードは、こちらです。
https://github.com/monaca-samples/forum-app

スレッドの作成

前回は認証まで完了しましたので、今回はスレッド一覧画面 list.html を開発します。

まず画面初期化時にイベントリスナーを追加します。

// 画面初期化時の処理
ons.getScriptPage().onInit = function() {
  // プラスボタンを押した時の処理
  document.querySelector('#open').onclick = openDialog.bind(this);
  // スレッド追加ボタンを押した時の処理
  document.querySelector('#add').onclick = addThread.bind(this);
}

次に画面が表示された際のイベント処理です。ここではスレッド一覧を読み込みます。

// 画面表示時の処理
ons.getScriptPage().onShow = function() {
  // スレッド一覧を読み込む
  showThread.bind(this)();
}

スレッド一覧の表示について

showThread の実装についてです。まずスレッドの一覧を読み込みます。

this.threads = await getThread();

getThreadはNCMBのThreadクラスからデータを取得します。

// スレッド一覧をNCMBから取得する処理
async function getThread() {
  const Thread = ncmb.DataStore('Thread');
  return await Thread
    .order('createDate', true)
    .limit(100)
    .fetchAll();
}

そして取得したスレッドを ons-list の中に出力します。

// スレッド表示対象のDOM
const dom = this.querySelector('#threads');
const html = [];
this.threads.forEach(thread => {
  // 一覧用のDOMを準備
  html.push(`
    <ons-list-item modifier="chevron" tappable data-object-id="${thread.objectId}">
      <div class="left">
        <img id="img-${thread.objectId}" data-name="${thread.get('image')}" class="square-image list-item__thumbnail" src="http://placehold.jp/30x30.png">
      </div>
      <div class="center">
        <span class="list-item__title">
          ${thread.get('title')} ${deletable(thread)}
        </span>
        <span class="list-item__subtitle">
          ${thread.get('body')}
        </span>
      </div>
    </ons-list-item>
  `);
});
// DOMを追加
dom.innerHTML = html.join('');

ユーザに削除権限があるかどうか判定してHTMLを返す deletable は、スレッド詳細画面でも使えるので js/app.js に記述してあります。

// 削除可能か判定する関数
function deletable(obj) {
  let bol = false;
  // 管理者であれば常に削除可能
  if (window.admin) {
    bol = true;
  } else {
    // 管理者でない場合は自分に削除権限があるかチェック
    const user = ncmb.User.getCurrentUser();
    bol = user && obj.acl[user.objectId] && obj.acl[user.objectId].write;
  }
  // 削除可能な場合はゴミ箱アイコンを表示
  return bol ? <ons-icon data-object-id="${obj.objectId}" class="delete" icon="fa-trash"></ons-icon> : '';
}

HTMLを描画したら、イベントリスナーを追加します。

addEvent.bind(this)();

今回は以下のイベントを追加しています。

  • スレッドをタップしたらスレッド詳細画面に遷移する
  • 画像をNCMBから読み込んで表示する
  • 削除アイコンをタップしたらスレッドを削除する
// スレッドのイベント設定
function addEvent() {
  // スレッドをタップした際のイベント
  this.querySelectorAll('ons-list-item').forEach(d => {
    d.onclick = () => {
      document.querySelector('#nav').pushPage('thread.html', { data: {
        thread: this.threads.filter(t => t.objectId === d.dataset.objectId)[0]
      }});
    }
  });
  // 画像を読み込む
  this.querySelectorAll('ons-list-item img').forEach(d => {
    loadImage.bind(this)(d.dataset.name, #${d.getAttribute('id')});
  });
  // 削除アイコンをタップした際のイベント
  this.querySelectorAll('.delete').forEach(d => {
    d.onclick = () => {
      deleteThread.bind(this)(d);
    }
  });
}

画像を読み込む処理はスレッド詳細でも利用するので js/app.js に定義してあります。

function loadImage(name, className) {
  if (!this.querySelector(className)) return;
  ncmb.File.download(name, 'blob')
    .then(blob => {
      const fileReader = new FileReader();
      fileReader.onload = () => {
        this.querySelector(className).src = fileReader.result;
      }
      fileReader.readAsDataURL(blob) ;
    })
}

ここまでで画面の初期表示が完了します。

イベント処理

スレッド一覧におけるイベント処理は次の通りです。

  • ツールバーにある + ボタンをタップ
  • スレッド追加ボタンを押した時のイベント
  • スレッドをタップした際のイベント
  • 削除アイコンをタップした際のイベント

ツールバーにある + ボタンをタップ

これはスレッド追加用のダイアログを表示するだけです。

// スレッド追加用ダイアログの表示
function openDialog() {
  this.querySelector('ons-dialog').show();
}

スレッド追加ボタンを押した時のイベント

これは入力された内容を取得して、NCMBにスレッドを登録します。コードのコメントを参照してください。

注意点としては、ACL(アクセス権限)として、誰でも読み込み可能としています。そしてadminグループに所属するユーザ、またはユーザ自身は編集、削除権限を付与しています。ファイル file がある場合は、それをNCMBのファイルストアにアップロードしています。これは ncmb.File.upload の1行だけで完了します。このファイルについてもACLで制御されています。データを登録した後はダイアログを非表示にして、スレッド詳細画面 thread.html に遷移しています。

// スレッドを追加する処理
async function addThread() {
  // 変数の準備
  const title = this.querySelector('#title').value;
  const body  = this.querySelector('#body').value;
  const file = this.querySelector('#image').files[0];
  const user = ncmb.User.getCurrentUser();
  // 権限の設定
  // 全体に読み込み権限
  // adminまたは自分に編集・削除権限
  const acl = new ncmb.Acl();
  acl
    .setPublicReadAccess(true)
    .setRoleWriteAccess('admin', true)
    .setUserWriteAccess(user, true);
  // 画像があればアップロード
  if (file) {
    await ncmb.File.upload(file.name, file, acl);
  }
  // スレッドクラスを作成
  const Thread = ncmb.DataStore('Thread');
  const thread = new Thread;
  // 値を設定して保存
  await thread
    .set('title', title)
    .set('body', body)
    .set('image', file.name)
    .set('acl', acl)
    .save();
  // ダイアログを閉じる
  this.querySelector('ons-dialog').hide();
  // スレッド画面へ移動
  document.querySelector('#nav').pushPage('thread.html', { data: { thread }});
}

スレッドをタップした際のイベント

スレッドをタップした際には、スレッドを追加した時と同様にスレッド詳細画面 thread.html に遷移しています。

// スレッドをタップした際のイベント
this.querySelectorAll('ons-list-item').forEach(d => {
  d.onclick = () => {
    document.querySelector('#nav').pushPage('thread.html', { data: {
      thread: this.threads.filter(t => t.objectId === d.dataset.objectId)[0]
    }});
  }
});

削除アイコンをタップした際のイベント

削除アイコンをタップした際にはスレッドの削除処理を実行しています。この時、クラウド側でACLをチェックしていますので、不正ユーザが削除を実行しようと思っても権限がなければ削除できません。

// スレッドを削除する処理
async function deleteThread(dom) {
  // 削除前の確認
  const res = await ons.notification.confirm('スレッドは一度削除すると元には戻せません。削除してよろしいですか?');
  if (res === 0) return; // キャンセルの場合
  // 削除対象のデータを設定
  const Thread = ncmb.DataStore('Thread');
  const thread = new Thread;
  thread.objectId = dom.dataset.objectId;
  // 削除実行
  await thread.delete();
  // 表示を更新
  showThread.bind(this)();
}

スレッド詳細画面について

ここまででスレッド一覧画面が完成です。続いてスレッド詳細画面の実装を解説します。

画面初期化時のイベント

画面初期化時には以下のイベントを設定しています。

  • コメントダイアログ表示ボタンを押した時
  • コメントするボタンを押した時
  • 画像ダイアログの閉じるボタンを押した時
// 画面初期化時の処理
ons.getScriptPage().onInit = function() {
  // コメントダイアログ表示ボタンを押した時の処理
  this.querySelector('#comment-button').onclick = openModal.bind(this);
  // コメントするボタンを押した時の処理
  this.querySelector('#add').onclick = addComment.bind(this);
  // 画像ダイアログを閉じる際の処理
  this.querySelector('#close').onclick = closeDialog.bind(this);
}

画面表示時のイベント

画面表示時には、前の画面から受け取ったスレッド情報を表示します。また、画像があれば追加表示したり、既存のコメントを一覧表示します。

// スレッド画面表示時の処理
ons.getScriptPage().onShow = function() {
  // スレッドの内容を画面に表示
  const { thread } = this.data;
  for (const key in thread) {
    const value = thread[key];
    const dom = this.querySelector(.thread-${key});
    if (dom) {
      dom.innerHTML = value;
    }
  }
  // 画像があれば表示
  if (thread.get('image')) {
    loadImage.bind(this)(thread.get('image'), .caption)
  }
  // コメントを表示
  showComments.bind(this)();
}

コメント一覧の表示について

showComments の実装についてです。まずコメントの一覧を読み込みます。

// 表示対象のスレッド
const thread = this.data.thread;
// スレッドをキーとしてコメントを取得
const ary = await getComments(thread);

getCommentsはNCMBのCommentクラスからデータを取得します。

// NCMBからコメントを取得する処理
async function getComments(thread) {
  const Commnet = ncmb.DataStore('Comment');
  return await Commnet
    .equalTo('thread', {
      __type: 'Pointer',
      className: 'Thread',
      objectId: thread.objectId
    })
    .order('createDate', false)
    .limit(100)
    .fetchAll();
}

そして取得したコメントを ons-list の中に出力します。

// 表示対象のDOM
const dom = this.querySelector('#comments');
// 表示用HTMLを蓄積しておく
const html = [];
ary.forEach(comment => {
  // HTMLの組み立て
  html.push(`
    <ons-list-item>
      <div class="left">
        ${addImage.bind(this)(comment)}
      </div>
      <div class="center">
        <span class="list-item__title">${comment.get('body')}</span>
        <span class="list-item__subtitle">
          ${ago(comment.get('createDate'))}
          ${deletable(comment)}
        </span>
      </div>
    </ons-list-item>
  `);
});
// HTMLを流し込み
dom.innerHTML = html.join('');

ユーザに削除権限があるかどうか判定してHTMLを返す deletable はすでに紹介した通り js/app.js に定義してあります。HTMLを描画したら、イベントリスナーを追加します。

addEvent.bind(this)();

今回は以下のイベントを追加しています。

  • 画像をNCMBから読み込んで表示する
  • 削除アイコンをタップしたらコメントを削除する
// コメントのイベント設定
function addEvent() {
  // コメントの画像に対するイベント
  this.querySelectorAll('.square-image').forEach(d => {
    // プレイスホルダーから実際の画像に差し替え
    loadImage.bind(this)(d.dataset.name, #${d.getAttribute('id')});
    // クリック時に画像の拡大表示を行うイベントを設定
    d.onclick = () => {
      showImage.bind(this)(d)
    }
  });
  // 削除アイコンをクリックした際のイベント
  this.querySelectorAll('.delete').forEach(d => {
    d.onclick = () => {
      deleteComment.bind(this)(d);
    }
  });
}

画像を読み込む関数 loadImage はすでに紹介済みです。 js/app.js に定義してあります。ここまでで画面の初期表示が完了します。

イベント処理

スレッド詳細におけるイベント処理は次の通りです。

  • コメントダイアログ表示ボタンを押した時
  • コメントするボタンを押した時
  • 画像ダイアログを閉じる時
  • 削除アイコンをクリックした時

コメントダイアログ表示ボタンを押した時

この時にはコメント入力用のダイアログを表示するだけです。

// コメント用のダイアログを表示
function openModal() {
  this.querySelector('ons-dialog.comment').show();
}

コメントするボタンを押した時

この時にはNCMBにコメントを追加します。実装内容はスレッドの時とほとんど変わりません。該当するスレッドを登録して、関連づけるのを忘れないでください。

// コメントを追加する処理
async function addComment() {
  // 変数の準備
  const body = this.querySelector('#body').value;
  const file = this.querySelector('#image').files[0];
  const { thread } = this.data;
  const user = ncmb.User.getCurrentUser();

  // 権限設定
  // 全体に読み込み権限付与
  // adminまたは自分に削除権限付与
  const acl = new ncmb.Acl();
  acl
    .setPublicReadAccess(true)
    .setRoleWriteAccess('admin', true)
    .setUserWriteAccess(user, true);
  // ファイルが指定されている場合はアップロード
  if (file) {
    await ncmb.File.upload(file.name, file, acl);
  }
  // コメントクラスの準備
  const Comment = ncmb.DataStore('Comment');
  const comment = new Comment;
  // 値を設定
  await comment
    .set('body', body)
    .set('thread', thread)
    .set('image', file ? file.name : null)
    .set('acl', acl)
    .save();
  // フォームをリセット
  this.querySelector('#comment-form').reset();
  // ダイアログを非表示
  this.querySelector('ons-dialog.comment').hide();
  // 表示を更新
  showComments.bind(this)();
}

画像ダイアログを閉じる時

この時には画像ダイアログを非表示にするだけです。

// 閉じるボタンを押した時の処理
function closeDialog() {
  this.querySelector('ons-dialog.image').hide();
}

削除アイコンをクリックした時

削除アイコンをタップした際には該当データをCommentクラスから削除します。こちらもスレッドと同様にACL管理されていますので、不正ユーザによるデータ操作は防げますので安心してください。

// コメントを削除する処理
async function deleteComment(dom) {
  // 削除前の確認
  const res = await ons.notification.confirm('コメントは一度削除すると元には戻せません。削除してよろしいですか?');
  if (res === 0) return; // キャンセルの場合
  // 削除対象のデータを設定
  const Comment = ncmb.DataStore('Comment');
  const comment = new Comment;
  comment.objectId = dom.dataset.objectId;
  // 削除実行
  await comment.delete();
  // 表示を更新
  showComments.bind(this)();
}

ここまででスレッド詳細画面が完成となります。

まとめ

これで掲示板アプリが完成です。今回は次のような画面を持ったアプリを開発しました。

  • スレッド一覧画面
  • スレッド詳細画面

また、ニフクラ mobile backendの次の機能を利用しました。

  • 会員管理
    • 匿名認証
    • ロール(グループ)管理
  • データストア
    • Threadクラス
      • データ登録
      • データ削除
      • データ検索
    • Commentクラス
      • データ登録
      • データ削除
      • データ検索
  • ファイルストア
    • 画像登録
    • 画像取得

今回のデータの保存と取得、認証といった機能はどのようなアプリでも使える機能だと思います。今後のアプリ開発に応用してください。