LoginSignup
4

More than 3 years have passed since last update.

Svelteでカレンダーを作ってみる

Posted at

この記事はラクス Advent Calendar 2020 23日目の投稿です。

今回は、フロントエンド開発で最近注目されているSvelteを試してみた結果を記事にまとめます。

Svelteって?

Svelte(スヴェルトと読むらしい)って何?と思う人も多いかもしれません。
が、すでにQiitaにもいくつか記事があるので、そちらに譲ります。

Svelteでカレンダーを作ってみる

こういったチュートリアルの定番といえばTODOアプリですが、すでに作っている方がいたので今回はカレンダーアプリを作ることにします。
ひとまず、今月1か月の日付を表示して、ボタンで前月・次月を行き来できるようにしてみます。

準備

公式サイトを参考に、Svelteで開発を始める準備をします。
https://svelte.dev/blog/svelte-and-typescript#Try_it_now

Node.jsとnpmはインストール済みだったので、サイトの記述どおりに下記のコマンドを実行します。
SvelteはJavaScriptでもTypeScriptでもコーディングできますが、今回はTypeScriptで開発することにしましょう。

$ npx degit sveltejs/template svelte-calendar
$ cd svelte-calendar
$ node scripts/setupTypeScript.js
$ npm install

エディタも公式サイトに従い、公式の拡張機能をインストールしたVS Codeを使います。

ここまで来たらnpm run devを実行し、http://localhost:5000にアクセスしてみます。以下のような画面が表示されるはずです。
image.png

コンポーネントを作る

それではカレンダーを実装していきます。
Svelteはコンポーネント指向のフレームワークですので、表示する各要素をコンポーネントに分けて作成していきます。
今回は、以下のように分けることにしました。

  • カレンダー(全体)

Dayコンポーネント

まず最も小さい単位の"日"コンポーネントから作っていきましょう。
コンポーネントは、srcディレクトリの下に拡張子.svelteのファイルとして作成します。
実際に実装したDay.svelteはこのような形になりました。

Day.svelte
<script lang="ts">
  export let date: Date;
</script>

<style>
  div {
    flex: 1;
    border: 1px solid #ccc;
    border-top-width: 0;
  }
  div:nth-child(n + 2) {
    border-left-width: 0;
  }
</style>

<div>
  {#if date.getDate() === 1}
    {date.getMonth() + 1}/{date.getDate()}
  {:else}
    {date.getDate()}
  {/if}
</div>

Svelteファイルの中身は、script + CSS + テンプレート になっています。

  • <script>タグ
    • 変数をexportすることで、コンポーネントの外からデータを受け取ります。
    • 今回はTypeScriptを使っているので、lang="ts"を付けます。
  • <style>タグ
    • このコンポーネントで利用するCSSを書きます。ここではセレクタにdivを使っていますが、これはコンポーネント外のdivタグには影響しません(描画時に自動でclass属性を追加してくれます)。
  • テンプレート
    • 通常のHTMLの中に、変数や式を埋め込んだり、条件分岐やループを書けます。上記では、日付が1日の場合だけ12/1のように月も表示するように分岐しています。

Weekコンポーネント

次に"週"のコンポーネントを作っていきます。上で作ったDayコンポーネントを7日分並べるようにすればできそうです。初日の日付だけ外から受け取るようにしましょう。

Week.svelte
<script lang="ts">
  import Day from "./Day.svelte";

  export let startDate: Date;

  // 1週間のDateオブジェクトの配列
  const week = Array.from(Array(7).keys(), (i) => {
    const date = new Date(startDate);
    date.setDate(startDate.getDate() + i);
    return date;
  });
</script>

<style>
  div {
    display: flex;
    flex: 1;
  }
</style>

<div class="week">
  {#each week as date}
    <Day {date} />
  {/each}
</div>

基本はDayコンポーネントと変わりませんが、他のコンポーネントを利用している点が先程と違います。

コンポーネントから他コンポーネントを利用するには、importで対象のコンポーネントを読み込みます。
そして、HTML部分にコンポーネント名のタグ<Day />を書けば、そのコンポーネントを呼び出すことができます。
ただし、Dayコンポーネントは、外部からdateという変数を受け取る必要があります。変数は、コンポーネントのタグの属性として変数名={データ}とすることで渡せます。上の例で言うと<Day date={date} />なのですが、今回は変数名が一致しているので、=の前を省略できます。

Calendarコンポーネント

次にWeekコンポーネントを組み合わせてCalendarコンポーネントを作ります。
と言っても、表示する日付の範囲をDateオブジェクトをゴリゴリ操作して決めている以外はWeekコンポーネントと同じような書き方をしているので、折りたたんだ中にコードを載せるだけにしておきます。


Calendar.svelteのコード
Calendar.svelte
<script lang="ts">
  import Week from "./Week.svelte";

  const today = new Date();

  // 今月1日
  const firstDayOfMonth = new Date(today);
  firstDayOfMonth.setDate(1);

  // 1日が属する週の日曜日
  const firstDayOfFirstWeek = new Date(firstDayOfMonth);
  firstDayOfFirstWeek.setDate(1 - firstDayOfMonth.getDay());

  // 表示するすべての日曜日
  const sundays = Array.from(Array(6).keys(), (i) => {
    const sunday = new Date(firstDayOfFirstWeek);
    sunday.setDate(sunday.getDate() + 7 * i);
    return sunday;
  }).filter((date, i) => i === 0 || date.getMonth() === today.getMonth());
</script>

<style>
  h1 {
    margin: 0;
  }
  .calendar {
    display: flex;
    flex-direction: column;
    flex: 1;
    padding-bottom: 18px;
  }
  .week-header {
    display: flex;
  }
  .dow {
    flex: 1;
    border: 1px solid #cccccc;
  }
  .dow:nth-child(n + 2) {
    border-left-width: 0;
  }
  .days {
    display: flex;
    flex-direction: column;
    flex: 1;
  }
</style>

<div>
  <h1>{today.getFullYear()}年{today.getMonth() + 1}月</h1>
</div>
<div class="calendar">
  <div class="week-header">
    {#each ['日', '月', '火', '水', '木', '金', '土'] as dow}
      <div class="dow">{dow}</div>
    {/each}
  </div>
  <div class="days">
    {#each sundays as sunday}
      <Week startDate={sunday} />
    {/each}
  </div>
</div>


あとはApp.svelteからCalendarコンポーネントを呼び出すようにすれば、画面に今月のカレンダーが表示されるようになります!
image.png

なお、ここまでで実装したコード全体は以下のURLから確認できます。
https://github.com/takaram/svelte-calendar-sample/tree/f51892c54bf4848d2041847219cb46e79022f0d3

ページに動きをつける

このカレンダーを表示するだけであれば、PHPでHTMLを出力するのとさほど変わらないでしょう。
ここから、前月・次月に移動できるようにしていきましょう。

まず、CalendarコンポーネントのHTMLで利用している変数todaysundaysを動的に変更できるよう、letでの宣言に変更します(ついでにtodayの変数名をcurrentDayに変更しました)。

-  const today = new Date();
+  let currentDay: Date;
+  let sundays: Date[];

そして、これらの変数へ代入する部分のコードを関数化します。

const setCurrentDay = (currentDay_: Date) => {
  currentDay = currentDay_;

  // 今月1日
  const firstDayOfMonth = new Date(currentDay);
  firstDayOfMonth.setDate(1);

  // 1日が属する週の日曜日
  const firstDayOfFirstWeek = new Date(firstDayOfMonth);
  firstDayOfFirstWeek.setDate(1 - firstDayOfMonth.getDay());

  // 表示するすべての日曜日
  sundays = Array.from(Array(6).keys(), (i) => {
    const sunday = new Date(firstDayOfFirstWeek);
    sunday.setDate(sunday.getDate() + 7 * i);
    return sunday;
  }).filter(
    (date, i) => i === 0 || date.getMonth() === currentDay.getMonth()
  );
};

この関数を使って、表示を前月・次月に切り替える関数を作ることができます。

const goToPrevMonth = () => {
  currentDay.setMonth(currentDay.getMonth() - 1);
  setCurrentDay(currentDay);
};
const goToNextMonth = () => {
  currentDay.setMonth(currentDay.getMonth() + 1);
  setCurrentDay(currentDay);
};

currentDayに破壊的にsetMonth()しているので、setCurrentDayの1行目のcurrentDay = currentDay_;は不要なのでは?と思うかもしれませんが、これをコメントアウトすると上手く動きません。
これはSvelteが代入をトリガーに画面の再描画を行うためです1

あとは、月を移動するボタンを付けます。要素にクリックイベントを設定するには、on:click={イベントハンドラ}とします。この例は関数名ですが、直接関数リテラルをon:click={() => ...}のように書くこともできます。

+ <span class="month-control" role="button" on:click="{goToPrevMonth}">&lt;</span>
  <h1>{currentDay.getFullYear()}年{currentDay.getMonth() + 1}月</h1>
+ <span class="month-control" role="button" on:click="{goToNextMonth}">&gt;</span>

ここまで来れば完成……と思いきや、実はこれではYYYY年M月のタイトル部分しか変わりません(私はここでハマりました)。
Week.svelteも以下のように変更する必要があります。

-  const week = Array.from(Array(7).keys(), (i) => {
+  $: week = Array.from(Array(7).keys(), (i) => {
     const date = new Date(startDate);
     date.setDate(startDate.getDate() + i);
     return date;
   });

代入文の頭に$:をつけると、右辺で使われている値が変更された際に再代入・再描画が行われます2

これでようやく表示する月を変更できるようになりました。
完成したコードは以下のリポジトリで確認できます。
https://github.com/takaram/svelte-calendar-sample/tree/09f7dbe9bb588b661a5947aa83e8f9f8ce4e0ddf

所感

あまり他のフレームワークに詳しくないため比較はできませんが、比較的少ないコード量で実装できたのではないでしょうか。
表示される値の更新に多少クセがあるような気もしますが、慣れれば記述量が少なくて問題なさそうです。

ちなみに、開発中に編集したファイルを保存すると、変更が自動的に反映されるのがとても楽でした。ブラウザの更新ボタンすら押す必要がないので、行った変更を即座に確認することができます。

今度はもう少し複雑なアプリも作成してみたいですね。


  1. そのため、配列に要素を追加するような場合はlist.push(item)ではダメで、list = [...list, item]のようにします。 

  2. $:は文法的には、多重ループを抜けるときなどに使うラベル文です。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4