採点されません。匿名です。 間違えることは学習の一部です。読むだけでもかまいません。

datepicker 写経 — 日付入力と送信ペイロード

テクノロジ系 / L18 datepicker — 日付入力と送信ペイロード (vue-datepicker)

datepicker 写経 — 日付入力と送信ペイロード

> forms/ch01 のフォームに「期限 (dueDate)」 を足す。 ただの input ではなくライブラリ製のカレンダーを使い、

> 選んだ日付を壊さずにサーバーへ送る ところまでをやる。

0. 導入: なぜカレンダーは「送信で壊れる」のか

<input type="date"> で済むならそれでいい。 だが要件 (範囲選択 / 和暦 / 時刻つき / locale) が増えると

ライブラリ製の datepicker を使うことになる。 ここで初学者が必ず踏むのが タイムゾーンによる日付ズレ だ。

このトピックは「直し方を覚える」 前に 一度壊す。 壊れる様を見ないと、 なぜ対策が要るのか腹落ちしないからだ。

1. セットアップ

cd frontend/vue
# 既に node_modules にある前提。 無ければ:
# bun add @vuepic/vue-datepicker

DueDateField.vue を作る。

<script setup>
import { ref } from 'vue'
import VueDatePicker from '@vuepic/vue-datepicker'   // ← default import (波括弧なし)
import '@vuepic/vue-datepicker/dist/main.css'        // ← CSS の読み込みを忘れない

const dueDate = ref(null)
</script>

<template>
  <div>
    <VueDatePicker v-model="dueDate" locale="ja" />
    <p>選択値: {{ dueDate }}</p>
    <p>型: {{ typeof dueDate }}</p>
  </div>
</template>

> ⚠️ このサンプルは v11 系 (package.json: ^11.0.2)。 v11 は default export なので 波括弧なし が正しい。

> ここで import { VueDatePicker } (波括弧あり) と書くと undefined になり描画されない / ビルドが落ちる。

> ただし v12 以降は named export に変わり、 波括弧ありが正しくなる。 版で逆転する点に注意 — 詳細は README の「import の波括弧問題」 参照。

ブラウザで日付を選び、 選択値 に注目する。 Wed Jun 24 2026 00:00:00 GMT+0900 のような Date オブジェクト が表示され、

object になる。 これが落とし穴の入口

2. 壊す: 素朴に送ると前日にズレる

「送信」 を模した処理を書く。 多くの初学者は Date を JSON にするとき toISOString() を使う。

function buildPayloadBroken(date) {
  return { due: date.toISOString().slice(0, 10) }   // ← これがバグる
}

2026-06-24 の 0:00 (JST) を選んで実行すると:

{ due: "2026-06-23" }   // ← 6/24 を選んだのに 6/23 になる!

なぜか

toISOString()UTC に変換する。 日本 (UTC+9) の 6/24 00:00 は、 UTC では 6/23 15:00

日付部分だけ切り出すと 前日 になる。 深夜〜早朝に選択したユーザーで必ず再現する、 タチの悪いバグだ。

入力 (JST)toISOString() の結果切り出した日付
6/24 00:002026-06-23T15:00:00.000Z6/23 (NG)
6/24 15:302026-06-24T06:30:00.000Z6/24 (たまたま合う)

> 昼に選ぶと正しく見えるので、 開発中は気づかず、 本番で深夜利用者からバグ報告が来る。 最悪のパターン

3. 直す (本命): model-type で v-model を文字列に固定

一番良いのは 入口で Date を文字列にしてしまう こと。 model-type prop を渡すと、

v-model に入る値そのものが指定フォーマットの文字列になる。

<template>
  <VueDatePicker
    v-model="dueDate"
    model-type="yyyy-MM-dd"
    locale="ja"
  />
  <p>選択値: {{ dueDate }}</p>   <!-- "2026-06-24" (文字列) -->
</template>

これで dueDate.value は最初から "2026-06-24" (文字列)。

function buildPayload(dueDate) {
  return { due: dueDate }   // そのまま送れる。 変換不要 = ズレようがない
}
  • Date がアプリ内を流れなくなる → 送信時に変換し忘れる事故が構造的に消える。
  • 日時も送るなら model-type="yyyy-MM-dd'T'HH:mm:ss"
  • まずこれを試す。 大半のケースはこれで終わり。

4. 直す (保険): 既存コードが Date 前提なら送信前に変換

既存実装が Date を前提に組まれていて model-type を入れにくい場合は、 送信処理だけ 直す。

鉄則は toISOString() を使わない

// ローカル日付を YYYY-MM-DD に組む (タイムゾーン安全)
function toLocalYMD(d) {
  if (!d) return null                          // ← null ガードも兼ねる (5節)
  const y = d.getFullYear()
  const m = String(d.getMonth() + 1).padStart(2, '0')   // getMonth は 0 始まり → +1
  const day = String(d.getDate()).padStart(2, '0')
  return `${y}-${m}-${day}`
}

function buildPayload(date) {
  return { due: toLocalYMD(date) }   // 6/24 → "2026-06-24" 正しい
}

getFullYear / getMonth / getDateローカルタイムゾーンの値 を返すので、 UTC 変換が挟まらず、 ズレない。

> Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Tokyo' }).format(d) でも YYYY-MM-DD が得られる。

> タイムゾーンを明示したいときはこちら。

5. 未選択の null をガードする

datepicker の初期値・クリア時は null。 これを必須フィールドにそのまま送るとサーバーに弾かれる (422)。

const dueDate = ref(null)

const canSubmit = computed(() => dueDate.value !== null)   // UI 側: ボタン無効化
</script>

<template>
  <VueDatePicker v-model="dueDate" model-type="yyyy-MM-dd" locale="ja" />
  <button :disabled="!canSubmit" @click="onSubmit">登録</button>
</template>
function onSubmit() {
  if (dueDate.value === null) return            // ロジック側: 二重防御 (ch01 と同じ作法)
  // fetch(...)
}

ch01 の disabled (UI) + 判定 (ロジック) の二重防御」 がここでもそのまま効く。

6. range 選択は配列になる (送信設計が変わる)

範囲選択を有効にすると、 v-model は 2 要素の配列 [開始, 終了] になる。 送信ペイロードの形が変わる。

<VueDatePicker v-model="period" :range="true" model-type="yyyy-MM-dd" locale="ja" />
const period = ref(null)   // 選択後: ["2026-06-24", "2026-06-30"]

function buildPayload(period) {
  if (!period) return { from: null, to: null }
  return { from: period[0], to: period[1] }   // model-type 指定済みなら各要素も文字列
}

> model-type を付けていないと period[0] は Date のまま。 配列でも 3〜4 節と同じ変換が必要になる。

まとめ

症状効く対策
日付が 1 日ズレるmodel-type で文字列固定 (本命) / 送信時に toLocalYMDtoISOString 禁止3, 4
422 で送信失敗・型エラーmodel-type で文字列固定 / range は配列前提でペイロード設計3, 6
値が空・null で弾かれる送信前に null ガード + 必須なら未選択時にバリデーション5
import で undefined / ビルド落ち版で波括弧が逆転 (v11=なし / v12+=あり)。 namespace を実機確認README

迷ったら model-type="yyyy-MM-dd" を 1 行足す。 これだけで日付ズレと型不一致の大半が同時に片付く。

設計の勘所

外部ライブラリの v-model が何を返すか (Date? 文字列? 配列?) を最初に typeof で確かめる」。

返り値の型を把握しないまま送信処理を書くと、 今回のような「昼は通るが深夜で壊れる」 不可視のバグを埋め込む。

ライブラリは便利だが、 境界 (= データが入る/出る所) は自分で型を確認する

考えてみよう

既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。

datepicker — 演習

Lv1: v-model の中身を観察する

DueDateField.vue を作り、 datepicker で選んだ値の 型と中身 を画面に出す。

import VueDatePicker from '@vuepic/vue-datepicker'   // 波括弧なし
import '@vuepic/vue-datepicker/dist/main.css'
const dueDate = ref(null)
<VueDatePicker v-model="dueDate" locale="ja" />
<p>値: {{ dueDate }}</p>
<p>型: {{ typeof dueDate }}</p>

検証

  • 日付を選ぶと Wed Jun 24 2026 ... のような表示になる
  • object (= Date オブジェクト) と出る

提出物

「v-model に入っているのは何型か」 を 1 語で答えよ。

Lv2: 壊れるのを再現する

toISOString().slice(0, 10) で日付を切り出す関数を書き、 前日にズレる のを自分で再現する。

function brokenYMD(date) {
  return date.toISOString().slice(0, 10)   // TODO: これがなぜ壊れるか確認
}

検証

選ぶ日付 (時刻 0:00)brokenYMD の出力
6/242026-06-23 ← 1 日前にズレる

> ズレない場合は時刻が昼になっている。 0:00 で選ぶ か、 new Date('2026-06-24T00:00:00+09:00') で試す。

提出物

「なぜ 1 日ズレるのか」 を toISOStringUTC の語を使って 1 文で説明せよ。

Lv3: model-type で文字列固定して直す

model-type="yyyy-MM-dd" を渡し、 v-model が 文字列 になることを確認する。

<!-- TODO: model-type を追加 -->
<VueDatePicker v-model="dueDate" model-type="yyyy-MM-dd" locale="ja" />

検証

操作dueDate の値typeof dueDate
6/24 を選択"2026-06-24"string

toISOString を一切使わず、 dueDate をそのまま送れる状態になっていること。

Lv4: toLocalYMD で保険の変換を書く

model-type付けない バージョンに戻し、 送信前に安全変換する関数を実装する。

function toLocalYMD(d) {
  // TODO 1: d が null なら null を返す
  // TODO 2: getFullYear / getMonth(+1) / getDate を padStart で 2 桁に
  // TODO 3: `${y}-${m}-${day}` を返す
}

検証

入力期待
new Date('2026-06-24T00:00:00+09:00')"2026-06-24" (ズレない)
nullnull
new Date('2026-01-05T00:00:00+09:00')"2026-01-05" (0 埋め確認)

提出物

toISOString ではなく getFullYear 等を使う理由」 を 1 文で書け。

Lv5: null ガードと二重防御

未選択 (null) のまま送信できないようにする。

const canSubmit = computed(() => dueDate.value !== null)
function onSubmit() {
  // TODO: null なら return (ロジック側の防御)
  console.log('送信', dueDate.value)
}
<button :disabled="!canSubmit" @click="onSubmit">登録</button>

検証

  • 初期表示: ボタンが無効
  • 日付選択後: ボタンが有効、 押すと送信ログが出る
  • (DevTools で) canSubmit を回避して onSubmit を呼んでも、 null では送信処理に進まない

チャレンジ

  • :range="true" を付け、 v-model が [開始, 終了]配列 になることを確認。 { from, to } 形式のペイロードを組む
  • 期限が「今日以降」 でなければ送信不可、 を computed で追加 (= ch01 のバリデーション作法を日付に適用)

ヒント

  • import の波括弧は 版で逆転: v11 (このサンプル) は波括弧なし、 v12+ は波括弧あり。 namespace を実機確認するのが確実。 vue の import { ref } とも混同しない
  • CSS (dist/main.css) を import しないとカレンダーが崩れる
  • getMonth()0 始まり (1 月 = 0)。 +1 を忘れると 1 ヶ月ズレる
  • 「昼に選ぶと正しく見える」 のが罠。 必ず 0:00 か深夜で検証する