はじめに
コロナが落ち着いて来月からサークル企画も実施していこうと思っているので、普段活用しているエクセルシートをもっと扱いやすくできるアプリを作ってみました。
※CSVやExcelの仕様に関しては以下で調べました。
現状
✅パターン①
サークルスクエアで出欠管理。コメントでやりたい曲を書いてもらう。⇒自分がエクセルシートを編集していく形で運用。
👇これをスマホで見ながら当日進行。

良い点
※これがあれば最低限、進行&企画管理(当日演奏できそうか事前に把握)ができる。ミニマムマストなアイテム。
・曲ごとに埋まっていないパートを一目で確認できる
課題点
・参加者の参加表明の手間(サークルスクエアへログイン&コメント記入の必要性※フォーマットもなし)
・幹事がエクセルに追記する必要がある(※スマホから見にくい&編集しにくい)
→参加表明から反映までのタイムラグや入力ミスの可能性(※当日あれ?やらないの?みたいな食い違いが発生する)
・参加者人数がわかりにくい
→会計時の計算&確認が手間。
・参加者で見ていない方も多い(※見にくいのに起因)
→進行役が一人で責任を担う負担
✅パターン②
グーグルフォームで出欠管理

以下に自動反映。出欠や曲リクエストの変更があれば参加者は幹事側に連絡して、幹事が変更する。

良い点
・サークルスクエアというライングループとは別のプラットフォームを介さなくて済む
・ログイン不要
・参加者の入力しやすさ
課題点
・変更修正を参加者ができないので、幹事に負担がかかる
・曲ごとにパッと見で埋まっていないパートを確認することができない
仕様
上記を踏まえて良いところ取りしていきます。
✅機能
・グーグルフォーム的な入力画面
→参加者の入力が容易
・入力すると自動でシートに反映
→幹事側の負担減
・参加者数とパート毎の埋まり具合をパッと見で確認できる
→参加者・幹事両方にメリット
・幹事側の編集が不要
→幹事側の負担減
・入力画面と確認画面をタグなりで切替
✅実現するための仕様
・firebaseでログイン機能/ログインせずとも登録可能
→ユーザーは自分の登録の編集が可能。ログインしない場合は登録後、編集不可(これは目をつぶる)。
→幹事権限を設ける(全てのデータを編集可能)ことで万が一の登録間違いを修正可能。
制作①表示画面の作成
とりあえずグーグルフォームを参考に入力画面を作成。
👇スマホから見た画面。ほぼグーグルフォームのパクリ。笑

シートは以下の感じに。

※管理シート側のコード
section#sheet p 参加者人数 table(border="1") tr td.title 課題曲 td Vo td Gt<br><span style="font-size:10px;">(Lead)<span> td Gt<br><span style="font-size:10px;">(Side)<span> td Ba td Dr each song,key in song_list tr td #{song} td ふわふわ<br>ひつじ<br>ぱげぱげ td ふわふわ<br>ひつじ<br>ぱげぱげ td ふわふわ<br>ひつじ<br>ぱげぱげ td ふわふわ<br>ひつじ<br>ぱげぱげ td ふわふわ<br>ひつじ<br>ぱげぱげ
tdタグの中に、firebaseから取り出した名前を表示していく感じにします。
シートは確認用のみにしました。
理由は、そもそもスマホから入力するのが手間なこと、加えて編集できるようにするとカンマの入れ方やら、いろいろ考慮しなければいけないことが増えるためです。
編集・更新・削除はフォームで処理するようにします。
制作②firebaseとの連携
続いてfirebaseのCRUD実装をしていきます。
✅追加機能
checkboxのidを取得して配列にぶっこむ。
song_checkbox_all.addEventListener('change', (event) => { let eventElem = event.target; // チェックが入って、かつ配列になかった場合 if (eventElem.checked === true && checkbox_array.indexOf(eventElem.id) === -1) { console.log("push") checkbox_array.push(eventElem.id); } // チェックが外れた場合 if (eventElem.checked === false ) { let delKey = checkbox_array.indexOf(eventElem.id); checkbox_array.splice(delKey,1); } });
名前と上記のidタグを保存する。
let addFC = () => { db.ref(`/users/${firebase.auth().currentUser.displayName}/`).set({ name:firebase.auth().currentUser.displayName, id:checkbox_array }); }
確認

配列ぶっこむと自動で配列として保存されるんですね!!
そしてこれだと実質ログインしていないと追加できない&ログインしている場合、名前がログインユーザー名に依存してしまうので修正。
ログインしている場合としていない場合で2つ処理を用意。
let addFC = () => { console.log("addFC") if(firebase.auth().currentUser != null) { db.ref(`/users/${firebase.auth().currentUser.displayName}/`).set({ name:user_name.value, id:checkbox_array }); } else { db.ref(`/users/${user_name.value}/`).set({ name:user_name.value, id:checkbox_array }); } }
こんな感じで保存される。

✅参加者人数の出力
let databaseInitFC = () => { all_DB = db.ref(`/users`); // DB取得※usersを取得。配列キーの数を参考に参加者人数算出。 all_DB.on('child_added', (data) => { console.log("全てのDBを取得") firebase_db_latest = data.val(); all_data.push(firebase_db_latest); people_number.textContent = `参加者人数:${all_data.length}人` }); if (firebase.auth().currentUser != null){ ~~~~
上記のようにログイン有無に関わらず全体のデータを取得。
ログインしている場合は追加で、自分のデータを取得するようにした。
✅更新時のリロードの配慮
今回、データ追加後のリロードは自分のデータを更新したときのみリロードされるよう処理。
検証時の画像。(シークレットウィンドウ起動しないと検証できない。)

といってもコード自体はシンプルで、child_changedイベントを自身のURLに絞ればいいだけ!
connect_DB = db.ref(`/users/${firebase.auth().currentUser.displayName}`); connect_DB.on('child_changed', function(data) { location.reload(); });
✅ログインユーザーの更新・削除処理
child_addedはABC順でキーの値(配列)を取得していることがわかりました。
なので順番を意識しないとデータが上手く取得処理できません。
たとえば以下のようにユーザーネームを編集して登録していた場合の取得。
nameが1回目はundefinedで取得できていないことがわかります。


ですので、databaseの値を取得→その関数内で認証関連の関数を発火させるように対処しました。
今回、更新・削除できるのはログインユーザーのみ。
次にチェックしたものを表示できるようにしていきます。
途中で配列から文字で保存するようにしましたが、mapメソッドとか使えたらよさそうなので配列で保存するよう変更。
id:checkbox_array // id:checkbox_array.toString()

配列データは以下で取得できる。
> login_user_data[0] ["vo0", "gtLead1", "gtSide2"]
これを元に該当の要素を取得。
if (login_user_data[0] != null) { login_user_data[0].map((id) => { let v_id = document.getElementById(id); v_id.checked = true; }); };
取得できた

ただ再度送信してエラー。nameもundefinedになってしまった。。。
改めてチェックしたところしかcheckbox_arrayにpushされないため。以下追記。
checkbox_array.push(v_id);
するとこうなってしまう。
checkbox_array [input#gtLead0, input#gtLead0]
child_addedが回数分実行されるのを配慮しないといけない。
※非同期で処理できないのかな。。。
ログインユーザーデータの取得はonceとvalueイベントでまとめて取得。
// DB取得※ユーザー毎。nameとidで2回処理される connect_DB.once('value', (data) => { firebase_db_user = data.val();
そうするとオブジェクトとしてデータをまとめて取得できる。
※ここらへんでfirebaseから取得したデータをわざわざ配列にpushしてややこしくしていたことに気付く。
firebase_db_all = data.val(); // db_all.push(firebase_db_all); // これいらな過ぎる!!
更新は以上。
削除は簡単。以下を追記でOK。
let removeFC = () => { db.ref(`/users/${firebase.auth().currentUser.displayName}`).remove(); }
✅シートへの反映
ここが今回の肝ですね!
まずはセルごとにIDを付けていきます。
pug
td #{song} td(id=`sheet_vo${key}`) td(id=`sheet_gtLead${key}`) td(id=`sheet_gtSide${key}`) td(id=`sheet_ba${key}`) td(id=`sheet_dr${key}`)
データの取得。正直以下で取得はしんどい。。。
firebase_db_all.ポポ.id ["vo0", "gtLead1", "gtSide2"]
でもユーザーごとに表示を変更するわけではないから別途取得したほうが良さそう。
最終的にこんな感じ。
let firebase_db_sheet let mkSheetFC = () => { console.log("mksheet!!"); console.log(firebase_db_all); db.ref(`/users`).on('child_added', (data) => { firebase_db_sheet = data.val(); console.log(firebase_db_sheet.id); // masaru popoの順 // DBにデータがあった場合 if (firebase_db_sheet != null) { firebase_db_sheet.id.map((id) => { let sheet_elem = document.getElementById(`sheet_${id}`); console.log(sheet_elem) // ifまだ名前がシートにない場合elseある場合 if (sheet_elem.textContent != ""){ console.log("名前がある"); // sheet_elem.textContent = ""; sheet_elem.innerHTML = `${sheet_elem.innerHTML}<br>${firebase_db_sheet.name}`; } else { console.log("名前がないので追記"); sheet_elem.textContent = firebase_db_sheet.name; } }); }; }); }
textContentだと改行されないのでinnerHTMLを活用(よくない??)。

あとはログインしていないユーザーのデータを簡単に編集できればOK。
✅バリデーション
追加処理の前にバリデーションチェックを行います。
beforeValidate関数で2つのバリデートを実行。
両方がtrueの場合、追加処理が実行されるようにしました。
※一部抜粋
let beforeValidate = () => { beforeValidateName(); beforeValidateCheck(); if (beforeValidateName() === true && beforeValidateCheck() === true){ return true; }; } // 名前のバリデーション let beforeValidateName = () => { if(user_name.value != ""){ return true; } else { console.log("validate name fail") validate_message_name.className = "display-block validate-message" }

✅DBの設計変更
DB設計をroot(幹事),login,user(未ログイン)に分けて管理していこうと思います。

追加処理。
以下の感じで3つのパターンで条件分岐させて登録。
※一部抜粋
if(firebase.auth().currentUser.displayName = "まさる"){ console.log("root") db.ref(`/users/root/${firebase.auth().currentUser.displayName}/`).set({ name:"まさる", id:checkbox_array }); } else if (firebase.auth().currentUser != null) {
合計人数の計算。
この部分。
// DB取得※usersを取得。配列キーの数を参考に参加者人数算出。 db.ref(`/users`).once('value', (data) => { firebase_db_all = data.val(); console.log("全てのDBを取得,ユーザー数分実行") console.log(firebase_db_all) people_number.textContent = `参加者人数:${Object.keys(firebase_db_all).length}人` mkSheetFC(); });
こうする。
let number = Object.keys(firebase_db_all.not_login).length + Object.keys(firebase_db_all.login).length + Object.keys(firebase_db_all.root).length; people_number.textContent = `参加者人数:${number}人`
→0の場合を想定していなかった。。。
let not_login_number = firebase_db_all.not_login === undefined ? 0 : Object.keys(firebase_db_all.not_login).length
上記の感じで3つ三項演算子を用意して算出。
シートへの反映
かなり長めの条件分岐になってしまったので割愛。
3パターンそれぞれ対応。
✅名前が一意じゃない(被りのケース)
ルートユーザー/ログインユーザー保存先のURLはuidで一意になるように変更。
Googleアカウントの名前が同じ(まさる)だとルート権限を持ててしまうため。
※追記※
→ルートユーザーである自身だけにしました。
他の方もuidで保存してしまうと後のルートユーザーによる編集のURLをuidで取得しなくてはならなくなり、実装が複雑になると感じたためです。
✅ルートユーザーはは全てのデータを編集できる
ルートユーザーの場合は名前の部分をセレクトタグにしてきます。
以下の形でセレクトタグを生成。
let rootFC = () => { console.log("root"); root_edit.className = "display-inline"; firebase_db_root.map((value) => { console.log(value.name); let option = el('option'); option.value = value.name; option.textContent = value.name; option.id = value.name; root_edit.appendChild(option); }) }
チェックのリセット方法がわからない。。。
参考:【Javascript】querySelector、querySelectorAll(CSSセレクタで要素取得)
let all_checkbox_ID = document.querySelectorAll("input[type='checkbox']");
取得はNodeList(参考)で配列とは別らしい。
Array.fromで配列に変換してからmapメソッドを使ってリセット→チェックを実装。
Array.from(all_checkbox).map((nodelist) => { nodelist.checked = false; }) // チェックを付ける edit_user_obj.id.map((id) => { let v_id = document.getElementById(id); v_id.checked = true; // input要素にcheck付ける }); checkbox_array = edit_user_obj.id; // 登録時に使う配列にデータを初期値として0ベースで更新
こんな感じ。

あとは別途編集用のURLを参照してupdateすればよいだけ。
ログインしていないユーザーの処理は以下でできた。
let rootEditFC = () => { console.log("rootEditFC!!") db.ref(`/users/not_login/${edit_user_obj.name}/`).update({ id:edit_user_obj.id, name:edit_user_obj.name }); }; let rootRemoveFC = () => { console.log("rootRemoveFC!!") db.ref(`/users/not_login/${edit_user_obj.name}/`).remove(); }
あとはログイン有無を事前に判断して条件分岐させたい。
上記を参考にこんな感じで実装※抜粋
let checkNotLoginUser = Object.keys(firebase_db_all.not_login) //ここまでで名前 ["test3", "testt2"] .filter(function(value) { console.log(value); // test3とtestt2をそれぞれ1回ずつ出力 return firebase_db_all.not_login[value].name == edit_user_obj.name; // 該当した場合配列を返す // firebase_db_all.not_login.test3.nameを出力している });
if (checkNotLoginUser.length === 1){ db.ref(`/users/not_login/${edit_user_obj.name}/`).update({ id:edit_user_obj.id, name:edit_user_obj.name }); } else if (checkLoginUser.length === 1) { db.ref(`/users/login/${edit_user_obj.name}/`).update({ id:edit_user_obj.id, name:edit_user_obj.name }); }
これでうまく動きました◎
✅見学ボタン

※初参加ボタンも後日追加。
p 見学者 p#sheet_visiter.visiter-text p 初参加 p#sheet_first_time.visiter-text
上記の形で表示してあげる場所のidタグを付けただけで、テーブル生成の関数が一緒に処理してくれる。
検証時の注意
・ルートユーザー、ログインユーザー、未ログインユーザーそれぞれで検証する。
・すべてのデータがある状態、ない状態も試す。
未実装
・ルール周りの設定
→後程修正
・ルートユーザーでセレクトタグ選択後、自身の編集ができない。
→リロードで対処。
・リロードなしでのリアルタイム更新
→ステイ
・幹事用に会計用の合計金額を計算
→ステイ
・ログイン済みユーザーの削除
→自身で編集できるので今回は未配慮
躓いた点
・firebase.auth().currentUserが取得できない
原因不明。firebase serveで立ち上げたら動いた。
ひょっとするとGulp側(BrowserSync)で検証するのもよくないのかも??
Firebase側でDB更新した際にリロードされないのもfirebase serveで解決された。
firebaseを使う際はfirebaseのサーバーで検証すること!!
おわりに
途中で上げることができなくなった作業中のリポジトリ※修正したい。。。
→修正できたのでリポジトリ
プロパティは変数展開できないので、配列よりオブジェクトの方が扱いづらいなと思った。
firebaseのおかげでだいぶJSの理解が進んできている。
DB周りの処理も非同期でJSコードで記述できるのが要因かな??
※追記※
下記に追加項目を記載したけど、拘り始めると本当にキリがない。。。
そして、どうやってバリデーションでエラーが出ないようにすべきか。
フローチャート的なのを作って制作中に見ながら実装できると良いなと思いました。
・uidのjsファイルへのベタ書きリスク
→firebase.auth()に依存するので問題ないと思われる。
・ルート(幹事)権限の付与
→uidを登録するのが手間なので、URLで対処するのもありかもしれない。不特定多数の方に操作されないように最低限、ログインを必要にすれば少しハードルを上げることもできるし。
後日追加した修正
※追記です※
✅(ルートユーザー以外対象)ログイン後、チェックが反映されない
firebase_db_userがnullになっているぽい。
取得時のURLを修正で解決
// let connect_DB = db.ref(`/users/login/${firebase.auth().currentUser.uid}`); let connect_DB = db.ref(`/users/login/${firebase.auth().currentUser.displayName}`);
ただこれ、このままだとユーザー名を編集した場合も、編集後の名前のURLを取得できないので編集できなくなることに気付く。。。
これが原因で、1つのログインユーザーで複数の名前を登録できてしまう問題も。。。
ログインユーザーの登録は1つにしたいので、一意にすべく以下に戻します。
let connect_DB = db.ref(`/users/login/${firebase.auth().currentUser.uid}`);
更に現状の登録処理を編集。
// db.ref(`/users/login/${user_name.value}/`).set({ db.ref(`/users/login/${firebase.auth().currentUser.uid}/`).set({ name:user_name.value, id:checkbox_array });
これで登録と再表示OK


ルートユーザー用の編集権限のURLも編集。
// db.ref(`/users/login/${edit_user_obj.name}/`).update({ id:edit_user_obj.id, name:edit_user_obj.name });
ルートユーザー側から確認。表示はされるも更新できない。

単純にログインユーザーの処理を記載し忘れていました。。。
// 編集したいユーザーがログインユーザーの場合 if(firebase_db_all.login !== undefined){ checkLoginUser = Object.keys(firebase_db_all.login) // uid取得 .filter(function(value) { // valueはuid return firebase_db_all.login[value].name == edit_user_obj.name; }); ~~~~抜粋です } else if (checkLoginUser !== undefined) { console.log(checkLoginUser[0]) if (checkLoginUser.length === 1) { db.ref(`/users/login/${checkLoginUser[0]}/`).update({ id:edit_user_obj.id, name:edit_user_obj.name }); } }
こんな感じで処理◎
削除は未ログインユーザーにしか原則起きない現象かなと思ったので編集せずにステイ。
✅幹事編集権で同名のユーザーを識別できない
→nameのバリデーションチェックを実装して解決へ。
// 名前かぶりのメッセージ firebase_db_root.map((obj) => { all_name.push(obj.name); }) console.log(all_name.indexOf(user_name.value)); if (all_name.indexOf(user_name.value) !== -1){ console.log("名前かぶり") validate_overlap_name.className = "display-block validate-message" } else { nameEmptyCheck(); }
これだけだと一度登録した後の更新処理時に確実にバリデートがかかるので、追加と更新処理をきちんとわけます。
button#editBtn(onclick="editFC()") 更新
ログイン&登録後の場合は更新ボタンが表示されるように編集
editFC関数はバリデーションをかけないことにしました。
→editFCはbeforeValidateCheckだけ実装。チェックが0にならないようにだけ配慮!
→幹事の編集権限でも同様の処理を施しました。
✅会計機能
ルートユーザーには会計が楽になるよう参加者から合計金額を表示するように実装していきます。
// 会計金額の表示 let accountingFC = () => { let ttlMoney = number * 1500; accounting_text.innerHTML = `今日の合計金額:${ttlMoney}円<br>※一律1500円で計算。初回の方考慮にできていません。` }
といっても1500円かけただけです。
初回の場合1000円にしたかったけど、改めて実装するのが手間だったので省略。
🙄DB設計時に考慮していればもっと簡単に実装できましたね。。。
✅ルートユーザー権限追加
これはuidが該当のidだった場合、追加できるようにしていきます。
現状
if(firebase.auth().currentUser.uid === "じぶんのuid"){
でがっつり1つに絞っています。
どうやらuidはアプリごとに一意のIDが発行されるっぽいので、指定のuidをルートユーザーとして配列に追加して管理できるようにします。
let root_uid = ["幹事1","幹事2"] // 実際はuid ~~~~ if(root_uid.indexOf(firebase.auth().currentUser.uid) !== -1 ){
追加・更新・取得の部分で編集してOK。
✅rootユーザーのoptionタグが生成されない
該当の部分はrootFC()で生成される。
firebase_db_root.map((value) => { console.log(value.name); let option = el('option'); option.value = value.name; option.textContent = value.name; option.id = value.name; root_edit.appendChild(option); })
firebase_db_root変数に依存している。
この変数はmkSheetFC()でシート作成時に一緒に生成される。
どうやらchild_addedの実行完了の順序がrootのpushが完了される前にoptionタグの処理が完了することが原因ぽい。
mkSheetFC()はdatabaseInitFC()ですべてのデータ取得を終えた後に実行。
rootFC();はmksheetFC()と同タイミングで実行されるuserDataFC()でルートユーザーだった場合に実行される関数。
諸々の絶妙なタイミングでたまたまそうなっているぽい。
ただ他の幹事メンバーが幹事メンバー同士で編集することを考慮する必要もないと思うので今回はそのままに。
ついでに、他に調べたところfirebase_db_root変数に依存している関数は名前かぶりのバリデーションなので全てのユーザーデータは確かにプッシュしたい。
となるとこの変数に依存したくない。
// let firebase_db_root = []; let firebase_mk_select = []; let firebase_name_all = [];
こんな役割を分けて定義、命名もわかりやすく。かつ上記の非同期の処理順による誤作動が起きないようにした。(思惑通りに動くようには定めた)
更に、セレクトタグを選択した際はこんがらがらないように、名前の編集はできないようにinputをreadonlyに変更。
user_name.readOnly = true; user_name.value = "※名前の編集はできません"
🙄readOnlyがJSからだと大文字にしないと適用できなくて地味にハマりました。。。

※それとこれ、なぜかスマホからだとvalue値の更新が確認できず困惑。
備忘録
✅ポートフォリオ用に複製する
①.firebasercを削除してfirebase initでプロジェクトを再指定
②initファイル(API記載のファイル)を書き換えればOK。
※localhostサーバーのポート番号が同じだとまれにJSファイルがブラウザ側で更新されないケースがあるので注意。
✅テストログイン機能を実装
※デモ実装したのは幹事ログイン機能のみです。通常のログイン機能は変わらずグーグルログインです。
前回調べた通り、uidはテストログインごとに付与される。
さりげリロードしてもブラウザに状態が保持されるのは初めて気づいた。
最初は以下のように実装。

けどなんか負けた気がする。。。笑
なので、ルートユーザー用の処理であるrootFC();を非同期処理のあとになるようにsetTimeout()を設定して対処。
init.jsのfirebaseConnected関数内に追記
// 接続後の処理まとめ let firebaseConnected = () => { databaseInitFC(); setTimeout(()=> { if (firebase.auth().currentUser != null && firebase.auth().currentUser.displayName === null){ console.log("rootFC") rootFC(); test_login_btn.className = "display-none"; user_name.value = "幹事(テストユーザー)" root_uid.push(firebase.auth().currentUser.uid); } },2000); };
🙄displayNameがテストログインだとnullになる仕様微妙すぎる。。。
また、ログイン後、テストログイン押しても、ユーザーは上書きされないようなのでログインとテストログインは同時に表示されないようにした。
auth.jsのloggedInFC関数内に追記。
// 通常ログインの場合テストログインのボタンを非表示 if (firebase.auth().currentUser != null && firebase.auth().currentUser.displayName != null){ test_login_btn.className = "display-none"; }
※PCからだとログインできない現象がまれに起きた・・・あとチェックもリロードすると付かない現象が起きてる。。。ここあたりは一旦ステイします。。。
まとめ
pugの追記部分
button#testLoginBtn.display-inline(onclick="testRootLoginFC()") テストログイン(幹事)
✅git pushできない
参考:GitHubへのpushが「fetch first」と表示されてrejectedとなったときの対処
> git push origin master To https://github.com/chobi1125/studio-sheet.git ! [rejected] master -> master (fetch first) > git push origin prototype error: src refspec prototype does not match any
フェッチして差分をマージ。
git fetch git merge origin/master
どうやら誤ってfirebaseのAPIが記載されているファイルをGitHub上で削除したことが原因ぽい。マージしたら消えていた。
なので、予備で取っておいたファイルを追加してあげてpushして解決。
✅URLごとに入手したデータはPUSHする配列を気を付ける。
下記は同じ配列にPushしていたことによるエラー。
参加者人数が4人になっていることに気付く。
→出し方をusersの部分のキーの数にしないといけない。
基になっている配列を出力するとこんな感じ。

これは扱うデータごとにURL指定して何回かDB接続しないといけないかもしれない。。。
コメントを残す