はじめに
Day 11では、swagger-typescript-apiを用いたAPIクライアント自動生成の導入について書きました。
今回はtimer画面の本実装について書きます。テーブル描画の実装過程でハマったポイントと、非同期通信の問題を記録します。
画面の構成
今回のツールは機能ごとにメインエリアを切り替える仕様です。timer画面は以下のエリアで構成されています。
| エリア | 役割 |
|---|---|
| タイマーエリア | カウントアップ表示 |
| ボタンエリア | start・stopボタン |
| ボタンログエリア | startボタン押下時のバックエンド戻り値表示 |
| CSV表示エリア | 当日分の記録をテーブル形式で表示 |
| 統計エリア | NN機能による統計データ表示(未実装) |
タイマーエリア
タイマーはフロントエンド側でカウントアップを描画しています。startボタン押下時にsetIntervalで1秒ごとにカウントを進め、stopボタン押下時にclearIntervalでカウントを停止します。
実際の時間計測はバックエンドのstart_checker・end_checkerが行っています。フロントのカウンターは表示専用であり、計測結果はバックエンドがCSVに保存します。再始動時はカウントを0に戻してから再開する設計にしています。
this.start_timer.addEventListener("click", () => {
if(isTracking === true){
alert('It has already started.')
return
}
clearInterval(this.timer_click)
count = 0
this._click_button()
isTracking = !isTracking
this.timer_click = setInterval(() => {
count++;
const h = Math.floor(count / 3600)
const m = Math.floor((count % 3600) / 60)
const s = count % 60
this.timer.textContent = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0')}, 1000)
})
// stop
this.end_timer.addEventListener("click", () => {
if(isTracking === false){
alert('Not started')
return
}
api.timer.timerEndTimerStopPost()
isTracking = !isTracking
clearInterval(this.timer_click)
this._csv_log()
})
ボタンログエリア
ボタンログエリアには、startボタン押下時のバックエンド戻り値を表示しています。
CSV表示エリアはstopを押下してはじめて更新されるため、startが正しく押下されているかを即座に確認できません。この問題を解消するため、startボタン押下時にstart_checkerの戻り値をそのまま表示する領域を設けました。バックエンドへの保存は行わず、表示専用の役割です。
private async _click_button(){
const start_data = (await api.timer.timerStartTimerStartGet()).data
this.button_log.append(start_data + '\n')
}
CSV表示エリアの実装
CSV表示エリアには当日分の記録をテーブル形式で表示しています。当日分のフィルタリングはバックエンドのimport_to_csvが行っており(Day 10参照)、フロントは受け取ったデータをそのまま描画します。
表示するのはstart-time・end-time・totalの3カラムのみです。当日分のみを表示する仕様のため日付カラムは除外しており、インデックス指定(row[1]・row[3]・row[4])でカラムを取り出しています。
private async _csv_log(){
const csv_data = (await api.timer.todayTimerTimerTodayGet()).data
this.csv_body.innerHTML = ''
csv_data.forEach((row: String) => {
const tr = document.createElement('tr')
const data1 = row[1]
const data3 = row[3]
const data4 = row[4]
const td1 = document.createElement('td')
td1.classList.add('td1')
const td3 = document.createElement('td')
td3.classList.add('td3')
const td4 = document.createElement('td')
td4.classList.add('td4')
td1.textContent = data1 +' / '
td3.textContent = data3 +' / '
td4.textContent = data4
tr.appendChild(td1)
tr.appendChild(td3)
tr.appendChild(td4)
this.csv_body.appendChild(tr)
});
}
this.csv_body.innerHTML = ''でtbodyを初期化してから描画しています。初期化を行わないとstopのたびに行が追記され重複が発生するため、この処理が必要でした。
ハマったポイント
forEachの戻り値
実装当初、APIレスポンスを処理する際にforEachの戻り値を変数に代入しようとしていましたが、forEachはundefinedを返すため意図した動作になりませんでした。配列の各要素に対してDOM操作を行う場合は戻り値を使わず、ループ内で直接操作する方針に切り替えました。
統計エリア
統計エリアは現時点では領域の確保のみを行っています。NN機能によるタスク進捗の統計グラフを表示する予定ですが、NN機能の実装完了後に対応します。
おわりに
今日はtimer画面の本実装について書きました。
テーブル描画はシンプルな処理に見えて、tbody初期化・非同期処理の順序・forEachの扱いと詰まる箇所が重なりました。awaitの問題は引き続き修正予定です。
Day 13ではgoals画面の仮実装について書く予定です。timer画面との構造の違いと、レイアウト設計の判断を記録します。
この記事は連載「クラウドに依存しないマイルストーン管理ツール開発記」のDay 12です。