この記事はラクス 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
にアクセスしてみます。以下のような画面が表示されるはずです。
コンポーネントを作る
それではカレンダーを実装していきます。
Svelteはコンポーネント指向のフレームワークですので、表示する各要素をコンポーネントに分けて作成していきます。
今回は、以下のように分けることにしました。
- カレンダー(全体)
- 週
- 日
Dayコンポーネント
まず最も小さい単位の"日"コンポーネントから作っていきましょう。
コンポーネントは、srcディレクトリの下に拡張子.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
のように月も表示するように分岐しています。
- 通常のHTMLの中に、変数や式を埋め込んだり、条件分岐やループを書けます。上記では、日付が1日の場合だけ
Weekコンポーネント
次に"週"のコンポーネントを作っていきます。上で作ったDayコンポーネントを7日分並べるようにすればできそうです。初日の日付だけ外から受け取るようにしましょう。
<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`のコード
<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コンポーネントを呼び出すようにすれば、画面に今月のカレンダーが表示されるようになります!
なお、ここまでで実装したコード全体は以下のURLから確認できます。
https://github.com/takaram/svelte-calendar-sample/tree/f51892c54bf4848d2041847219cb46e79022f0d3
ページに動きをつける
このカレンダーを表示するだけであれば、PHPでHTMLを出力するのとさほど変わらないでしょう。
ここから、前月・次月に移動できるようにしていきましょう。
まず、CalendarコンポーネントのHTMLで利用している変数today
とsundays
を動的に変更できるよう、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}"><</span>
<h1>{currentDay.getFullYear()}年{currentDay.getMonth() + 1}月</h1>
+ <span class="month-control" role="button" on:click="{goToNextMonth}">></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
所感
あまり他のフレームワークに詳しくないため比較はできませんが、比較的少ないコード量で実装できたのではないでしょうか。
表示される値の更新に多少クセがあるような気もしますが、慣れれば記述量が少なくて問題なさそうです。
ちなみに、開発中に編集したファイルを保存すると、変更が自動的に反映されるのがとても楽でした。ブラウザの更新ボタンすら押す必要がないので、行った変更を即座に確認することができます。
今度はもう少し複雑なアプリも作成してみたいですね。