Reagentによるアコーディオンメニューの実装


こんにちは、@na4daです。

今回のテーマはアコーディオンメニューの実装です。


アコーディオンメニューの実装方法を調べると、CSSとjQueryを使うやり方がたくさん出てくるのですが、CSSだけでも作れるという記事をいくつか見かけました。そこで、ClojuresScriptとcssを組み合わせて、アコーディオンメニューを実装をしてみようと思います。

## 用意

- ClojureScript (Reagent)
- Sass (CSS)

今回は、タイトルの通り[Reagent](https://github.com/reagent-project/reagent)で実装を行います。また、スタイルの追加は、SCSSファイルと[sass4clj](https://github.com/Deraen/sass4clj)を用いてCSSファイルの生成を行います。

作成したコードは[こちら](https://github.com/na4da/accordion-menu)に置いてあります。これ以降は、こちらのコードを取り上げながら説明していきます。内容は、九九の各段をアコーディオンメニューで簡単に実装したものになります。

![accordion-menu.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323578/3a59db98-4291-dd79-a6dd-d6646c5d55cc.gif)

## ClojureScript側

アコーディオンメニューの核となる部分は以下のようになっています。

```clojure
(defn accordion-list [num]
  (let [default-idx 3
        content-list* (partial content-list num)]
    [:div {:id num :key num}
     [:input.acc-toggle {:type "checkbox"
                         :id (str "toggle_" num)
                         :default-checked (= num default-idx)}]
     [:label.acc-label {:for (str "toggle_" num)}
      [:span (str num "の段")]]
     [:div.acc-content
      [:ul
       (map content-list* (range 1 10))]]]))
```

アコーディオンメニューを構成するためには、大きく分けて3つの要素が必要となります。

### `input`

アコーディオンの開閉の切り替えを担当します。

```clojure
[:input.acc-toggle {:type "checkbox"
                    :id (str "toggle_" num)
                    :default-checked (= num default-idx)}]
```

`type`属性には`"checkbox"`または`"radio"`のいずれかを与えます。`checked`になった要素のアコーディオンが展開されるように実装します。

また、`"checkbox"`と`"radio"`それぞれでアコーディオンの挙動が異なります。大まかな違いとしては

- `"checkbox"`: 複数のアコーディオンを展開できる
- `"radio"`: アコーディオンは常に一つだけ展開される

となります。

`"checkbox"`の場合は、任意のアコーディオンを展開したまま、別のアコーディオンを開くことができます。`"radio"`の場合は、別のアコーディオンを展開する際、すでに開いてるアコーディオンは閉じられます。そのため、アコーディオンは常に一つしか展開されません。

`:id`には、ユニークな名前を与えます。こちらは後で登場する`for`と紐付けされます。

`:default-checked`は、条件を満たす要素をあらかじめ`checked`の状態にすることができます。例えば、ページを開いた際にあらかじめ特定のアコーディオンを展開しておきたい場合などに便利です。


### `label`

アコーディオンが閉じた状態の表示部分を担当します。

```clojure
[:label.acc-label {:for (str "toggle_" num)}
    [:span (str num "の段")]]
```

`label`要素の`for`属性には、上記の`input`の`id`属性と同じクラス名を与えるようにします。`for`属性は、任意のクラス名を持つ要素と関連づけることができます。これを利用して、`label`で囲まれた要素をクリックすることで`input`の`checked/unchecked`を切り替えられるようにします。

`label`要素の内側には、アコーディオンが閉じた状態で表示する要素を与えます。今回は`span`要素で1行分の内容しか記述していませんが、複数行の内容も入れられます。

### `:div.acc-content`

アコーディオンを展開した際に表示される内容を担当します。

```clojure
[:div.acc-content
 [:ul
  (map #(content-list num %) (range 1 10))]]
```

こちらの`div`以下には、任意の内容を与えることができます。今回はリストを表示させていますが、`table`要素で表を与えたりすることなども可能です。

## CSS側 (SCSS)

SCSSは以下のようになっています。

```scss
$border: #e6e6e6;
$selected: #C1DEFF;
$hovering: #A5C2FF;
$label-background: #89A7E8;
$content-background: #FDF7E8;

.accbox {
  max-width: 450pt;
  div:first-child {
    .acc-label {
      border-top: 1px solid $border;
    }
  }
}

.acc-toggle {
  display: none;
  &:checked {
    ~ .acc-label {
      background-color: $selected;
      &:hover {
        background: $hovering;
      }
    }
    ~ .acc-content {
      height: auto;
      padding: 10px;
    }
  }
}

.acc-label {
  display: block;
  height: 30px;
  background-color: $label-background;
  border-bottom: 1px solid $border;
  cursor: pointer;
  transition: all 0.1s;
  &:hover {
    background: $hovering;
  }
  span {
    padding: 5px 10px 0px 10px;
    display: inline-block;
    text-align: left;
    width: 100%;
  }
}

.acc-content {
  height: 0;
  overflow-y: hidden;
  transition: all 0.2s;
  margin: 0pt;
  background-color: $content-background;
  ul {
    list-style-type: none;
    margin: 0pt;
  }
}
```

アコーディオンメニューに最低限必要なスタイルは、以下の３つになります。

### `.acc-content`

```scss
.acc-content {
  height: 0;
  overflow-y: hidden;
  transition: all 0.2s;
  margin: 0pt;
  background-color: $content-background;
  ul {
    list-style-type: none;
    margin: 0pt;
  }
}
```

ここでは、アコーディオンを展開した時の内容は、**あらかじめ見えないようにしておく**ことがポイントです。そのため、`height`、`overflow-y`に非表示向けのスタイルを与えています。

また、`transition`プロパティは、アコーディオンの開閉時に内容の`表示/非表示`が切り替わる時間を与えています。小さい値ほど、素早く切り替わります。

### `.acc-label`

```scss
.acc-label {
  display: block;
  height: 30px;
  background-color: $label-background;
  border-bottom: 1px solid $border;
  cursor: pointer;
  transition: all 0.1s;
  &:hover {
    background: $hovering;
  }
  span {
    padding: 5px 10px 0px 10px;
    display: inline-block;
    text-align: left;
    width: 100%;
  }
}
```

この部分に関しては、内容に応じて適宜スタイルを与えます。例えば、`cursor`プロパティに`pointer`を与えたり、マウスオーバーした際のスタイルを与える等があります。

### `.acc-toggle`

```scss
.acc-toggle {
  display: none;
  &:checked {
    ~ .acc-label {
      background-color: $selected;
      &:hover {
        background: $hovering;
      }
    }
    ~ .acc-content {
      height: auto;
      padding: 10px;
    }
  }
}
```

まず、`input`要素は表示させないようにするため、`display: none;`を与えます。

その下では、`.acc-toggle`クラスに対して擬似クラス`:checked`を与え、その中では間接セレクタ`~`を用いてスタイルを与えます。この組み合わせによって、`input`要素が`checked`になった際に`.acc-toggle`クラスと同じ階層に位置する`.acc-label`クラスや`.acc-content`クラスにスタイルを与えることができます。

この2つのクラスのうち、最低限必要なのは`.acc-content`クラスで、**デフォルトで非表示だった中身を表示させる**スタイルを与えています。


## ざっくりとしたまとめ

- 基本的な仕組みは`input`の`checked/unchecked`
- `label`の`for`と`input`の`id`を対応づける
- アコーディオンの開閉はCSSで与える
    - デフォルトは非表示にする
    - `input`が`checked`の場合に中身を表示させる

---

今回はReagentとCSSでアコーディオンメニューの実装を行いました。

ReagentのHiccup-likeな記法と、SCSSの入れ子構造が相まって、全体的にとてもコンパクトに記述できた気がします。今回の例では、スタイルの付加は極力少なめにしていますが、さらに調整を加えることでよりオシャレなアコーディオンメニューが作れると思います。


少しでも参考になりましたら、幸いです。


## 参考

- [CSSだけでアコーディオンを実装](https://qiita.com/takanorip/items/217f9c29715825de0fdc)