Hit the books!!

プログラミング学習記録

カレンダー作ってVue.jsの復習

これはフィヨルドブートキャンプ Part 1 Advent Calendar 202110日目の記事です。 part2もあります。

昨日はいっしーさんの「達人プログラマー輪読会をはじめた話 - Leap of faITh」という記事でした。輪読会したい、めっちゃしたい。

はじめに

しばらくブログを更新していない間、フィヨルドブートキャンプでの勉強もついに「Webサービスを作って公開する」ところまできました。

f:id:ud_ike:20211115081848p:plain

去年のアドカレに2021年の目標は卒業&就職とか書いたけど何も達成できてないので、もうそういうの書くのはやーめた。

今日はライブラリなどを使わずにカレンダーを表示させることによって、苦手なJavaScriptとVue.jsの復習をしたいと思います。なるべく丁寧に書くようにします。

やりたいこと

Vue.jsを使ってマンスリーカレンダーを表示させる(日付のみ)

実装

準備

Vue.jsを使えるようにする方法はいくつかあるけど、今回はCDNでやるぞ。

  • index.html
  • js/calendar.js
  • css/main.css

というファイルを作る。

cssファイルは作って読み込んだものの今回デザインは何も入れてないです。

Visual Studio Codeでhtmlファイルを開いて「!+ Enter(Tab)」で雛形が出てくることを知った。すごい。

f:id:ud_ike:20211208100054g:plain

この雛形を使って、まずは文字列を表示させる。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <link rel="stylesheet" href='css/main.css'> <!-- cssファイルの読み込み -->
</head>
<body>
  <div id="app"> <!-- ルートのテンプレートをbodyタグの中に作成 -->
    <p>{{ message }}</p>
  </div>
  <script src="https://unpkg.com/vue@3.1.5"></script> <!-- Vue.jsの読み込み -->
  <script src="js/calendar.js"></script> <!-- jsファイルの読み込み -->
</body>
</html>

なんかhtmlのコード表示ってカラフルだなぁ。

jsファイルの内容はこちら。Vueインスタンスを作成してマウントする。マウントすることによって<div id="app">内でVue.jsが使えるようになる。

const app = Vue.createApp({
  data() {
    message: 'Hello Vue.js!!'
  }
})
app.mount('#app')

index.htmlを開くと、

f:id:ud_ike:20211208110428p:plain

表示された〜

参考

今の年月を取得する

まずはカレンダーのタイトルを表示させるために、現在の年と月を取得する。

htmlファイルのdivタグの中身がこんな感じになるように西暦と月を取得したい。

  <div id="app">
    <p>{{ currentYear }}年{{ currentMonth }}月</p>
  </div>

JavaScriptの日付の扱いはけっこう混乱する。

new Date()が現在の日時で、getFullYear()は4桁の西暦を表す。 月がややこしくてgetMonth()だと実際より1つ少ない数になってしまうので1を加える必要がある。

const app = Vue.createApp({
  data() {
    return {
      currentYear: this.getCurrentYear(),
      currentMonth: this.getCurrentMonth(),
    }
  },
  methods: {
    getCurrentYear() {
      return new Date().getFullYear()
    },
    getCurrentMonth() {
      return new Date().getMonth() + 1
    },
  }
})
app.mount('#app')

ブラウザをみると、できた〜

f:id:ud_ike:20211208142108p:plain

参考

今月のカレンダーを表示させる

次は今月のカレンダーを表示する。曜日で縦に揃える必要があるのと、毎月の日数は変動するので、

  • 1日の曜日
  • 末日の日付

が必要そう。 1日の曜日はgetFirstWday、月の末日はgetLastDateに書いた。こういうの考えてるとDayとDateがごっちゃになるの私だけかな...

  methods: {
    getFirstWday() {
      const firstDay = new Date(this.currentYear, this.currentMonth - 1, 1)
      return firstDay.getDay()
    },
    getLastDate() {
      const lastDay = new Date(this.currentYear, this.currentMonth, 0)
      return lastDay.getDate()
    },

    (省略)

    getMonthlyCalendar() {
      console.log(this.getFirstWday())
      console.log(this.getLastDate())
    }

曜日は0~6で0が日曜日。2021/12/1は水曜日なので3が返ればOK。

コンソールで出力すると、

f:id:ud_ike:20211208214455p:plain

よさそう。

次は1~31を週で折り返して表示させる。

月曜日(曜日=1)スタートにしたいので、それ以外の曜日からはじまる場合はスペースを入れる必要がある。 日曜日(曜日=0)の場合は空白6つ、土曜日(曜日=6)の場合は空白が5つ、金曜日(曜日=5)は4つ...なので、曜日と空白の組み合わせをパターンわけすると

  • 0の場合は6つ
  • 1の場合は不要
  • 2~6の場合は(曜日-1)つ
    getMonthlyCalendar() {
      let weeklyCalendar = []
      if (this.getFirstWday() >= 2) {
        for (let blank = 1; blank < this.getFirstWday(); blank++) {
          weeklyCalendar.push('')
        }
      } else if (this.getFirstWday() === 0) {
        weeklyCalendar.push('', '', '', '', '', '')
      }
      for (let date = 1; date < this.getLastDate() + 1; date++) {
        weeklyCalendar.push(date)
        if (weeklyCalendar.length % 7 === 0 || date === this.getLastDate()) {
          this.monthlyCalendar.push(weeklyCalendar)
          weeklyCalendar = []
        }
      }
    }

こんな感じでどうでしょう。

monthlyCalendarという名前の2次元配列を作って、その中に1週間分の日付をいれたweeklyCalendarという配列をpushした。これがいいやり方なのかは不明...

ブラウザにmonthlyCalendarを表示させると、改行されてないけどできた〜

f:id:ud_ike:20211208225343p:plain

見た目を整える。

  <div id="app">
    <p>{{ currentYear }}年{{ currentMonth }}月</p>
    <table>
      <thead>
        <tr>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td v-for='date in monthlyCalendar[0]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr>
          <td v-for='date in monthlyCalendar[1]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr>
          <td v-for='date in monthlyCalendar[2]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr>
          <td v-for='date in monthlyCalendar[3]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr v-if='monthlyCalendar[4]'>
          <td v-for='date in monthlyCalendar[4]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr v-if='monthlyCalendar[5]'>
          <td v-for='date in monthlyCalendar[5]' :key='date.id'>{{ date }}</td>
        </tr>
        </tbody>
    </table>
  </div>

毎月のカレンダーは5行だったり6行だったりするのでv-ifを使った。もっと美しい書き方があれば教えてください。

f:id:ud_ike:20211208231335p:plain

前の月、次の月に移動する

さらに前の月や未来の月のカレンダーも表示できるようにしたい。 そのためにはcurrentMonth、currentYearとは別に表示用の年月が必要そうだ。

const app = Vue.createApp({
  data() {
    return {
      currentYear: this.getCurrentYear(),
      currentMonth: this.getCurrentMonth(),
      calendarYear: this.getCurrentYear(), // 追加
      calendarMonth: this.getCurrentMonth(), // 追加
      monthlyCalendar: [],
    }
  },

calendarYearとcalendarMonthを追加。 前の月ボタンを押すとひと月さかのぼって〜という処理をmethodsに書く。

    previousMonth() {
      if (this.calendarMonth === 1) {
        this.calendarMonth = 12
        this.calendarYear--
      } else {
        this.calendarMonth--
      }
      this.monthlyCalendar = []
      this.getMonthlyCalendar()
    },
    nextMonth() {
      if (this.calendarMonth === 12) {
        this.calendarMonth = 1
        this.calendarYear++
      } else {
        this.calendarMonth++
      }
      this.monthlyCalendar = []
      this.getMonthlyCalendar()
    },

f:id:ud_ike:20211209082535g:plain

これだと中身が変わらない。

今月の1日の曜日と末日の日付を取得していたところを、currentからcalendarYear、calendarMonthに変更する。

  methods: {
    getFirstWday() {
      const firstDay = new Date(this.calendarYear, this.calendarMonth - 1, 1) //修正
      return firstDay.getDay()
    },
    getLastDate() {
      const lastDay = new Date(this.calendarYear, this.calendarMonth, 0) //修正
      return lastDay.getDate()
    },

f:id:ud_ike:20211209083155g:plain

おっ、できた!

ライフサイクルと算出プロパティ

Vue.jsやってるとcreatedとかmountedとかcomputedとか出てきてややこしい。腹立たしいけどかっこよく使い分けたいので、とりあえずmethodsに書いていたものを見直して修正した。

最終版はこうした↓

# js/calendar.js

const app = Vue.createApp({
  data() {
    return {
      currentYear: this.getCurrentYear(),
      currentMonth: this.getCurrentMonth(),
      calendarYear: this.getCurrentYear(),
      calendarMonth: this.getCurrentMonth(),
      monthlyCalendar: []
    }
  },
  mounted() {
    this.getMonthlyCalendar()
  },
  computed: {
    firstWday() {
      const firstDay = new Date(this.calendarYear, this.calendarMonth - 1, 1)
      return firstDay.getDay()
    },
    lastDate() {
      const lastDay = new Date(this.calendarYear, this.calendarMonth, 0)
      return lastDay.getDate()
    }
  },
  methods: {
    getCurrentYear() {
      return new Date().getFullYear()
    },
    getCurrentMonth() {
      return new Date().getMonth() + 1
    },
    getMonthlyCalendar() {
      let weeklyCalendar = []
      if (this.firstWday >= 2) {
        for (let blank = 1; blank < this.firstWday; blank++) {
          weeklyCalendar.push('')
        }
      } else if (this.firstWday === 0) {
        weeklyCalendar.push('', '', '', '', '', '')
      }
      for (let date = 1; date < this.lastDate + 1; date++) {
        weeklyCalendar.push(date)
        if (weeklyCalendar.length % 7 === 0 || date === this.lastDate) {
          this.monthlyCalendar.push(weeklyCalendar)
          weeklyCalendar = []
        }
      }
    },
    previousMonth() {
      if (this.calendarMonth === 1) {
        this.calendarMonth = 12
        this.calendarYear--
      } else {
        this.calendarMonth--
      }
      this.monthlyCalendar = []
      this.getMonthlyCalendar()
    },
    nextMonth() {
      if (this.calendarMonth === 12) {
        this.calendarMonth = 1
        this.calendarYear++
      } else {
        this.calendarMonth++
      }
      this.monthlyCalendar = []
      this.getMonthlyCalendar()
    }
  }
})
app.mount('#app')

1日の曜日と末日のは計算しているのでcomputedが適切なんじゃないかと。クリックとかマウスオンなどのイベントはmethodsのままで。

<computedとmethodsの違い>

  • computedは()が不要、methodsは必要
  • computedはキャッシュされる、methodsはされない
  • computedはgetterとsetterが使える、methodsはgetterのみ

あとcreatedはDOMがまだ作られていない状態で、mountedはDOM作成後というのが大きい違い。

他にもたくさんあるはずだけど最初はこんなもんでいいのでは〜。

ついでにhtmlファイルの最終版↓

# index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Calendar</title>
  <link rel="stylesheet" href='css/main.css'>
</head>
<body>
  <div id="app">
    <div class='column' @click='previousMonth'>前の月</div>
    <div class='column'>{{ calendarYear }}年{{ calendarMonth }}月</div>
    <div class='column' @click='nextMonth'>次の月</div>
    <table>
      <thead>
        <tr>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td v-for='date in monthlyCalendar[0]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr>
          <td v-for='date in monthlyCalendar[1]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr>
          <td v-for='date in monthlyCalendar[2]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr>
          <td v-for='date in monthlyCalendar[3]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr v-if='monthlyCalendar[4]'>
          <td v-for='date in monthlyCalendar[4]' :key='date.id'>{{ date }}</td>
        </tr>
        <tr v-if='monthlyCalendar[5]'>
          <td v-for='date in monthlyCalendar[5]' :key='date.id'>{{ date }}</td>
        </tr>
        </tbody>
    </table>
  </div>
  <script src="https://unpkg.com/vue@3.1.5"></script>
  <script src="js/calendar.js"></script>
</body>
</html>

cssファイルは空なのでデザインはイケてません。

参考

おしまい

Vue.jsをRailsで動かすとなるとさらに難しく感じるので2022年はもうちょっと理解したいです。また目標立ててしまった。

追記:修正

もっと美しいというか正しい書き方をフィヨルドブートキャンプの卒業生に教えてもらったので修正しました🌵

v-forのkeyに指定しているものにちゃんと値を設定していませんでした!

参考:スタイルガイド | Vue.js

それでも動くけど、ちゃんとしよう。

monthlyCalendarの中身をこのように変えたい。

例:水曜日はじまりの場合↓

monthlyCalendar = [
  { id: 1, value: [{ key: 0 }, { key: 1 }, { key: 2, date: 1 }, { key: 3, date: 2 }, ...] },
  { id: 2, value: [{ key: 0, date: 6 }, ...]},
  :
  :
]

idとkeyをv-forのkeyに指定する。

dateは日付、keyは適当に0, 1, ...として、idは第何週目を表すようにした。

修正前は、

①月曜日スタートではない場合、空白を入れる

②1, 2, 3, ...と日付を入れて、月曜日で折り返す

という流れだったのを、

①まずvalueを作る(valueは修正前のweeklyCalendar)

②空白を入れていたところに、0, 1, ...とkeyのみを入れる(dateはナシ)

③日付をkeyをセットにしてvalueに入れる、を繰り返す

に変更した。

# js/calendar.js

const app = Vue.createApp({
  data() {
    return {
      currentYear: this.getCurrentYear(),
      currentMonth: this.getCurrentMonth(),
      calendarYear: this.getCurrentYear(),
      calendarMonth: this.getCurrentMonth(),
      monthlyCalendar: []
    }
  },
  mounted() {
    this.getMonthlyCalendar()
  },
  computed: {
    firstWday() {
      const firstDay = new Date(this.calendarYear, this.calendarMonth - 1, 1)
      return firstDay.getDay()
    },
    lastDate() {
      const lastDay = new Date(this.calendarYear, this.calendarMonth, 0)
      return lastDay.getDate()
    }
  },
  methods: {
    getCurrentYear() {
      return new Date().getFullYear()
    },
    getCurrentMonth() {
      return new Date().getMonth() + 1
    },
    getMonthlyCalendar() {
      let value = []
      let key = 0
      let id = 1
      if (this.firstWday >= 2) {
        for (let blank = 1; blank < this.firstWday; blank++) {
          value.push({ key: key })
          key++
        }
      } else if (this.firstWday === 0) {
        for (key = 0; key < 6; key++) {
          value.push({ key: key })
        }
      }
      for (let date = 1; date < this.lastDate + 1; date++) {
        value.push({ key: key, date: date })
        key++
        if (value.length % 7 === 0 || date === this.lastDate) {
          this.monthlyCalendar.push({ id: id, value: value })
          value = []
          id++
          key = 0
        }
      }
    },
    previousMonth() {
      if (this.calendarMonth === 1) {
        this.calendarMonth = 12
        this.calendarYear--
      } else {
        this.calendarMonth--
      }
      this.monthlyCalendar = []
      this.getMonthlyCalendar()
    },
    nextMonth() {
      if (this.calendarMonth === 12) {
        this.calendarMonth = 1
        this.calendarYear++
      } else {
        this.calendarMonth++
      }
      this.monthlyCalendar = []
      this.getMonthlyCalendar()
    }
  }
})
app.mount('#app')

こうすると、htmlがスッキリ!!<tbody>の中身にご注目ください!

# index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Calendar</title>
  <link rel="stylesheet" href='css/main.css'>
</head>
<body>
  <div id="app">
    <div class='column' @click='previousMonth'>前の月</div>
    <div class='column'>{{ calendarYear }}年{{ calendarMonth }}月</div>
    <div class='column' @click='nextMonth'>次の月</div>
    <table>
      <thead>
        <tr>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr v-for='week in monthlyCalendar' :key='week.id'>
          <td v-for='date in week.value' :key='date.key'>{{ date.date }}</td>
        </tr>
      </tbody>
    </table>
  </div>
  <script src="https://unpkg.com/vue@3.1.5"></script>
  <script src="js/calendar.js"></script>
</body>
</html>

書くとアッサリだけど時間かかった...でも最近停滞してるので、なんか久しぶりに学んだ感じがする〜。