🦁

Firebase とVanillaJavaScript で手書きノートアプリ作成(その1)

2020/05/06に公開

Firebase と VanillaJavaScript で手書きノートアプリを作成しました。
今回はその1ということで、完成までの経緯や基本機能部分について画面とコードの説明をまとめました。

その2(認証関連 firebaseAuth のコードの説明等)はこちら

1. 完成までの経緯

最終形のコードが完成するまでに、以下のステップを踏んでいます。

Step1   LocalStorage バージョンを作成

Udemy や Youbube の動画チュートリアルと、ネットの記事等を参考に、ベースとなる、手書きメモアプリを作成しました。

Github : https://github.com/IoT-Arduino/CanvasNoteApp-Localstorage
アプリ URL : https://canvasnoteapp-localstorage.netlify.app/ (Closeしました)

Step2   Firebase Firesore  バージョンを作成

上記 Step 1で作成したものを改良。LocalStorage に保存していたデータを FireStore に保存するように改良しました。

Step3   FirebaseAuth  認証機能の追加 (FireStore   Rules の設定)

Step2 の機能のままで、ユーザーが複数人いた場合、他人が自分のデータを読み書きできるし、自分も他人のデータがみれてしまうので、FirebaseAuth  認証機能を追加した。
(LocalStorage 版では、データは自分の端末に保存されるので問題にならなかった。)

Github : https://github.com/IoT-Arduino/CanvasNoteApp-Firebase
アプリ URL :https://canvasnoteapp.web.app/ (Closeしました)

2. 機能概要

手書きノート機能がついた、シンプルなノートアプリです。
検索、ソート機能があります。認証機能付きです。(以下 14 秒動画)

機能説明

2.1. サインアップ及びログイン認証機能

ログイン

2.2. ノート一覧表示

初期画面

2.3. 複合機能(検索+ソート)

検索機能

2.4. 手書きノート機能

手書きノート機能

3. コードの説明

3.1. ファイル構成

HTML
   index.html
   edit.html

JavaScript
   index.js
   notes-function.js
   edit.js
   edit-canvas.js
   firebase-init.js

3.2. ドキュメント ID をキーにした、edit ページへの遷移

index ページ側の処理

renderNoteDOM 関数(ノートアイテムのひとつづつの要素を作る関数)

各ノートの div 要素の data-id 属性に引数として受け取った noteId を設定する。
この data-id 属性があることで、削除時のドキュメント ID を指定することができる。
(このノート ID の値は Firebase のドキュメント ID)
次に、a タグの href 属性に、edit ページへのリンクを設定する。

./src/notes-function.js
// render Each note item
const renderNoteDOM = (note, noteId) => {
  const noteEl = document.createElement("div");
  const textEl = document.createElement("a"); // 中略

  noteEl.setAttribute("data-id", noteId);
  noteEl.classList.add("list-item"); // 中略

  textEl.setAttribute("href", `edit.html#${noteId}`);
  noteEl.appendChild(textEl);
  document.querySelector("#notes").appendChild(noteEl);
};

edit ページ側の処理

先ほどのページで a タグに指定した、url 情報をもとに編集ページに遷移します。
この時に、設定された url 情報である、edit.html#${noteId}の以降の部分は以下の関数で値を改めて取り出すことができる。

./src/edit.js
const noteId = location.hash.substring(1);

この noteId をつかって、firebase から該当のドキュメントデータを指定して取得することができる。

title=/src/edit.js
db.collection("notes")
  .doc(noteId)
  .get()
  .then((snapshot) => {
    titleElement.value = snapshot.data().title;
    bodyElement.value = snapshot.data().body;
    canvasData = snapshot.data().canvas;
    dateElement.textContent = generateLastEdited(snapshot.data().updatedAt);
  })
  .then(() => {
    if (canvasData.length > 0) {
      draw(canvasData[0]["png"]);
    }
  });

<a id="markdown-33-オブジェクト形式の違い" name="33-オブジェクト形式の違い"></a>

3.3. オブジェクト形式の違い

LocalStorage 版と Firebase 版とでは、それぞれのドキュメントオブジェクトの形式が若干異なります。
LocalStorage 版では、id は、すべてのデータ項目と同じ階層で扱われているが、Firebase 版では、id と他のデータ項目の階層を変えているために、関連するプログラムを変更する必要がある。
(例)renderNoteDOM()への引数は、note オブジェクトと併せて id を渡す必要があります。

./src/notes-function.js
notes.forEach((item) => {
  renderNoteDOM(item.note, item.id);
});



LocalStorage 版のオブジェクトの形式

LocalStorage版のオブジェクト



Firebase 版のオブジェクトの形式

Firebase版のオブジェクト

notes 配列作成時に、スプレッド構文を用いることで、id と他の note オブジェクトの階層をそろえることも可能です。今後は状況に応じて使い分けたいともいます。

./src/index.js "現在のコード"
snapshot.forEach((doc) => {
  const id = doc.id;
  const note = doc.data();
  notes.push({
    id,
    note,
  });
});
"スプレッド構文を使用"
snapshot.forEach((doc) => {
  const id = doc.id;
  const note = doc.data();
  notes.push({ ...note, id });
});

3.4. 複合機能(検索+ソート)

firebase で取得できるドキュメントオブジェクトから、JavaScript で処理しやすい配列形式にする為に、async/await 構文で、いったん snapshot オブジェクトを取得し、それを forEach 文で回して、uid と noteobject を notes 配列に push するようにしました。

title=/src/index.js

// sort select listener
document.querySelector("#filterBy").addEventListener("change", async (e) => {

  // 中略

  const snapshot = await db
    .collection("notes")
    .where("createdBy", "==", user.uid)
    .get()
  snapshot.forEach((doc) => {
    const id = doc.id
    const note = doc.data()
    notes.push({
      id,
      note,
    })
  })

上記で取得した notes 配列にフィルタ処理をかけ、フィルタ処理後の配列に対して、ソート処理を行うようにしました。

./src/index.js
const filteredNotes = notes.filter((item) => {
  return item.note.title
    .toLowerCase()
    .includes(filters.searchText.toLowerCase());
});
let sortBy = filters.sortBy;
const sortedNotes = sortNotes(filteredNotes, sortBy);

renderNotes(sortedNotes, sortBy);

このやり方は、データが大量に増えたときに問題になりそうですがが、firebase の Like 検索+複合キーの検索の実装の難易度が高そうなのと、今回のアプリではデータ件数はまだ少ない前提なので、この方法を採用しました。

ソート機能のコード、プルダウンの選択により、更新日順、作成日順、アルファベット順の 3 種類の並べ替えが可能です。(以下はソート関数のコード)

./src/notes-function.js
// sort notes
const sortNotes = (notes, sortBy) => {
  if (sortBy === "updatedAt") {
    return notes.sort((a, b) => {
      if (a.note.updatedAt > b.note.updatedAt) {
        return -1;
      } else if (a.note.updatedAt < b.note.updatedAt) {
        return 1;
      } else {
        return 0;
      }
    });
  } else if (sortBy === "createdAt") {
    return notes.sort((a, b) => {
      if (a.note.createdAt > b.note.createdAt) {
        return -1;
      } else if (a.note.createdAt < b.note.createdAt) {
        return 1;
      } else {
        return 0;
      }
    });
  } else if (sortBy === "title") {
    return notes.sort((a, b) => {
      if (a.note.title.toLowerCase() < b.note.title.toLowerCase()) {
        return -1; // a comes first
      } else if (a.note.title.toLowerCase() > b.note.title.toLowerCase()) {
        return 1; // b comes first
      } else {
        return 0;
      }
    });
  } else {
    return notes;
  }
};

3.5. canvas 機能

描画のコードは、これまで作成したノートアプリに組み込みます。
参考にさせていただいた描画のコードは1ペンストロークを1つのデータとした、配列要素となっていました。
したがって、createNote()関数の中で、note オブジェクトの1データ項目として canvas(配列  型)を用意し、そこに描画データを入れるようにしました。

描画に関する関数は、すべて edit-canvas.js に記述し、それらを、edit.js から呼び出すようにしました。

注意点としては、PC 用のマウスイベントと、スマホ用のタッチイベントで取得できるデータや型が違う点です、スマホの場合は、マルチタッチに対応するため、e.changedTouches で取得できる値が配列形式で複数の値た取得できるようになっていますので、0 番目の値を取得するようにします。
このように記述をしないと、PC では手書きメモがかけるのに、スマホの場合は、タッチイベントを認識せず、手書きメモが真っ白なままになります。

また、タッチイベントは、mouse イベントとちがって、e.offsetX/Y の値を直接とることができないので、getBoundingClientRect()メソッドをつかって、個別に計算するようにしました。

./src/edit-canvas.js
if (e.changedTouches) {
  e = e.changedTouches[0];
  Xpoint = e.clientX - event.target.getBoundingClientRect().left - 2;
  Ypoint = e.clientY - event.target.getBoundingClientRect().top - 2;
} else {
  Xpoint = e.offsetX - 2;
  Ypoint = e.offsetY - 2;
}

その2(認証関連 firebaseAuth のコードの説明等)へ続く

Discussion