# 1hで半分わかった気になる[Alpine.js](https://alpinejs.dev/)
概要
> 今ホットなwebフレームワークのひとつである Alpine.js を使って、カレンダーツールを作ってみましょう!
> Alpine.js は非常に小さく、特別なインストール操作などをせずに開発できるフレームワークです。
> 実装されている15個の属性、6個のプロパティ、2個の関数のうち、半分を1hで一緒にコーディングしながら学びます。
## Alpine.js とは?
- シンプルで軽量でパワフルなJavaScriptフレームワーク
- 15のアトリビュート(属性)
- HTMLタグのあとに続いて書くもの
- 例) imgタグの src 属性
- `<img src="example.png"/>`
- 6のプロパティ(値)
- Alpine.jsで定義済みの便利な値を利用できる
- 2のメソッド(ほぼ関数)
- JavaScriptから利用できる処理
→合わせて23個覚えればほぼマスター!
## 【前編】 使ってみよう Alpine.js
1. [codepen](https://codepen.io/pen/) を開く
2. CDNインポートでAlpine.jsを利用できるようにする
- ↓をHTMLのエディタの右上の歯車から設定画面を開いて`Stuff for head`に追加する
- `<script src="//unpkg.com/alpinejs" defer></script>`
### HTMLを書いて試してみる
#### ボタンでテキストの表示を制御できるようにしてみる
```htmlembedded=
<div x-data="{ flag: false }">
<button @click="flag = !flag">toggle</button>
<span x-show="flag" x-transition>hello, Alpine.js !</span>
</div>
```
##### 使った機能
- **x-data**
- この属性が指定された要素以下で、Alpine.jsでの制御が可能になる
- 属性に渡すオブジェクトの中身 (↑の例では`{ flag: false }`) に自身と子孫要素からアクセスできる
- **x-on:\<event\> (@\<event\>)**
- 要素のeventに対してイベントハンドラを設定する
- ↑の例では`@click="flag = !flag"` のことで、`x-on:click="flag = !flag"` としても同義
- **x-show**
- 要素の表示・非表示を制御する
- 渡す値がtruthyなら表示、 falsyなら非表示
- **x-transition**
- 要素が表示されるときにトランジション(エフェクト)がかかる
##### おまけ
- codepenを保存してリロードしてみよう
- 一瞬だけ最初見えてほしくない `hello, Alpine.js !` が見えてしまうかも
- ↑はAlpine.jsによる初期化より前にブラウザで描画されてしまうせい
- これを解決するには、 div要素に `x-cloak` を追加する
- **x-cloak**
- Alpine.jsによる初期化が終わるまで要素を隠す
### 値を表示してみる
#### ボタンを押した回数をカウントして表示する
```htmlembedded=
<div x-cloak x-data="{ count: 0 }">
<button x-on:click="count++">click me!</button>
<span x-text="count"></span>
</div>
```
##### 使った機能
- **x-text**
- 渡された値を属性が指定された要素のinnerTextに設定する
#### 現在時刻を表示する
```htmlembedded=
<div x-cloak x-data x-init="now = new Date()">
<span x-text="now.toLocaleString()"></span>
</div>
```
##### 使った機能
- **x-init**
- Alpine.jsが要素を初期化するときに渡された処理を実行する
### 要素の表示をもっと制御する
#### ボタンで要素の表示を切り替える
```htmlembedded=
<div x-data="{ flag: false }">
<button @click="flag = !flag">toggle</button>
<template x-if="flag">
<span>hello, Alpine.js !<span>
</template>
</div>
```
##### 使った機能
- **x-if**
- templateタグにのみ使用できる
- x-showと表示上は似て見えるが、内部的に以下の差がある
- x-showはCSSで `display: none` している
- x-ifはtemplateタグの中身そのものをhtmlに追加したり削除したりして制御している
- x-ifには `x-transition` が使えない
#### 要素を繰り返し作成する
```htmlembedded=
<div x-data>
<template x-for="i in 5">
<div>
<span x-text="i"></span>
</div>
</template>
<template x-for="i in ['あ', 'い', 'う', 'え', 'お']">
<div>
<span x-text="i"></span>
</div>
</template>
</div>
```
##### 使った機能
- **x-for**
- templateタグにのみ使用できる
- `x-for="i in 5"` のようにすると`i`は`1~5`になり、`x-for=""['あ', 'い', 'う', 'え', 'お']"` のようにすると配列の要素を一つずつ`i`として要素を作成する
## 【後編】 カレンダー作るぞ :muscle:
### 下準備
- このあとの作業を楽にしたいので、CSSに↓を追加します
```css=
* {
box-sizing: border-box;
margin: 0;
border: none;
padding: 0;
background-color: transparent;
}
html, body {
height: 100%;
width: 100%;
}
```
- 色を選ぶタイミングがいくつかあるので↓のページを開いておくといいです
- https://developer.mozilla.org/ja/docs/Web/CSS/named-color
### 見た目を作る
`x-for`を利用して7日×6週分の欄を用意する
→ 日曜始まりカレンダーのとき、1日が土曜になると30,31が6週目になるので
html
```htmlembedded=
<main x-cloak x-data="calenderData()">
<div class="month">
<span x-text="getMonthText()"></span>
</div>
<div class="calender">
<template x-for="i in 7*6">
<div class=cell>
<span class="day"
x-text="getDateText(i)"
></span>
</div>
</template>
</div>
</main>
```
css
```css=
* {
box-sizing: border-box;
margin: 0;
border: none;
padding: 0;
background-color: transparent;
}
html, body, main {
height: 100%;
width: 100%;
}
.month {
height: 40px;
width: 100%;
font-size: 32px;
fort-weight: bold;
display: grid;
place-content: center;
}
.calender {
height: calc(100% - 40px);
width: 100%;
padding: 8px;
display: grid;
background-color: ghostwhite;
grid-template-rows: repeat(6, 1fr);
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.calender .cell {
border-radius: 8px;
padding: 8px;
background-color: white;
}
.calender .cell:nth-child(7n+1) {
background-color: aliceblue;
color: midnightblue;
}
.calender .cell:nth-child(7n+2) {
background-color: lavenderblush;
color: darkred;
}
```
js
```javascript=
const daysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const calenderData = () => ({
currentMonth: null,
init() {
this.currentMonth = new Date();
this.currentMonth.setDate(1);
},
getDateText(i) {
const date = i - this.currentMonth.getDay()
if (date < 1) {
return '-'
}
else if (date > daysOfMonth[this.currentMonth.getMonth()]) {
return '-'
}
else {
return date
}
},
getMonthText() {
// 0~11 で帰ってくるので +1 する
return (this.currentMonth.getMonth() + 1).toString() + '月'
},
});
```
↑のコードを書いたあと↓

### クリックした日付がわかるようにする
html
```htmlembedded=7
<div class=cell
@click="onClickCell($el)">
```
css
```css=49
.calender .cell.clicked {
box-shadow: 0px 0px 10px gray;
}
```
js
```javascript=26
onClickCell(el) {
if (this.clickedCell) {
this.clickedCell.classList.remove('clicked')
if(this.clickedCell.isEqualNode(el)) {
this.clickedCell = null
return
}
}
this.clickedCell = el
this.clickedCell.classList.add('clicked')
},
```
##### 使った機能
- **$el**
- 現在のDOMへの参照
- ↑の例では関数に渡して `document.querySelector~~~...` のような面倒な処理をしなくてもいいようにしています
### 予定を入れられるようにする
html
```htmlembedded=7
<div class=cell
@click="onClickCell($el, getDateText(i))">
<span class="day"
x-text="getDateText(i)"
></span>
<br>
<span class="schedule-detail"
x-text="$store.schedules[getDateText(i)]"
></span>
</div>
```
```htmlembedded=20
<div class="popup"
x-cloak
x-show="$store.popupData && $store.popupData.date"
x-data="popup">
<span class="title"
x-text="$store.popupData.date + '日の予定'"></span>
<button class="close"
@click="close()">close</button>
<button class="save"
@click="save($store.schedules)">save</button>
<textarea class="schedule"
x-model="$store.schedules[$store.popupData.date]"
type="text">
</textarea>
</div>
```
css
```css=53
.popup {
position: absolute;
top: 0;
left: 0;
width: 80%;
height: 80%;
margin: 10%;
border-radius: 16px;
padding: 16px;
box-shadow: 0px 0px 10px tomato;
background-color: white;
display: grid;
grid-template: 'title save-button close-button' 24px
'schedule schedule schedule' 1fr
/ 1fr 80px 80px;
gap: 8px;
}
.popup span.title {
grid-area: title;
}
.popup button.close {
grid-area: close-button;
width: 100%;
height: 100%;
border-radius: 24px;
background-color: lightgray;
font-size: 20px;
}
.popup button.save {
grid-area: save-button;
width: 100%;
height: 100%;
border-radius: 24px;
background-color: lightgray;
font-size: 20px;
}
.popup textarea.schedule {
grid-area: schedule;
border: 2px solid lightgray;
border-radius: 16px;
padding: 16px;
}
```
js
```javascript=
const daysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const calenderData = () => ({
currentMonth: null,
clickedCell: null,
openPopuo: false,
init() {
this.currentMonth = new Date();
this.currentMonth.setDate(1);
Alpine.store('schedules', JSON.parse(window.localStorage.getItem('schedules') ?? '{}'))
},
getDateText(i) {
const date = i - this.currentMonth.getDay()
if (date < 1) {
return '-'
}
else if (date > daysOfMonth[this.currentMonth.getMonth()]) {
return '-'
}
else {
return date
}
},
getMonthText() {
// 0~11 で帰ってくるので +1 する
return (this.currentMonth.getMonth() + 1).toString() + '月'
},
onClickCell(el, date) {
if (date !== '-') {
Alpine.store('popupData', { 'date': date })
Alpine.store('schedule', { date: '' })
}
if (this.clickedCell) {
this.clickedCell.classList.remove('clicked')
if(this.clickedCell.isEqualNode(el)) {
this.clickedCell = null
Alpine.store('popupData', { 'date': null })
return
}
}
this.clickedCell = el
this.clickedCell.classList.add('clicked')
}
});
const popup = () => ({
close() {
Alpine.store('popupData', { 'date': null })
},
save(schedules) {
Alpine.store('schedule', schedules)
window.localStorage.setItem('schedules', JSON.stringify(schedules))
Alpine.store('popupData', { 'date': null })
}
});
```
##### 使った機能
- **x-model**
- input要素やtextarea要素のような入力を受け付ける要素に指定できるアトリビュート
- 入力された値を属性に渡された変数にバインド(同期)する
- **$store**
- テンプレート(HTML)から`Alpine.store()`で保持している値にアクセスするためのプロパティ
- **Alpine.store()**
- テンプレートから`$store`でアクセスできるようにしたい値を流す関数
- alpineの機能じゃないけど[*localStorage*](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage)
- 値をブラウザに保存しておける標準API
## まとめ
出来あがったものは[こんな感じ](https://codepen.io/yamaji-toshiyuki/pen/XWPbNQy)になっていると思います。
Alpine.jsの今日使った機能の一覧は↓
- Attributes
- `x-data`
- `x-on:<event> (@)`
- `x-show`
- `x-transition`
- `x-cloak`
- `x-text`
- `x-init`
- `x-if`
- `x-for`
- `x-model`
- Properties
- `$el`
- `$store`
- Methods
- `Alpine.store()`
の13個でした〜!
[Alpine.jsの公式トップ](https://alpinejs.dev)に書いてある数は 15+6+2 = 23 なので、これで半分わかった気になれましたね!
Alpine.jsは軽量でCDNインポートなどで手軽に利用できるため、様々な場面で活用してもらえると思います!
勉強会の内容を生かして、まずは今回作ったカレンダーツールを改善してみてはいかがでしょうか? :smile: