【JavaScript】複数のアコーディオンパネルを作成する方法

大阪でRailsを中心に学習している薬剤師エンジニア（初学者）こと、ヨマ([@yoma_2003](https://twitter.com/yoma_2003))です！  
JavaScriptで複数のアコーディオンパネルを作成する方法をまとめます。

※おことわり※  
断定口調でまとめますが、初学者であるため間違い等あればご指摘頂けると嬉しいです。

# はじめに
**今回作成したい目標物は、以下の様な複数のアコーディオンパネル（クリックで開閉する要素）です。**

![ezgif-3-4f45f72301.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2711042/18e39525-0c2d-7ac9-dfe9-f5abcac57aaa.gif)


### 特徴

- クリックするとクリックしたBoxが開く
- 別のBoxをクリックすると開いていたBoxが閉じ、クリックしたBoxが開く（常に開くのは１つだけ）
- Box以外をクリックすると開いていたBoxが閉じる

# 結論

```html:HTML
<div class="main_contents">
    <div class="box">
      <h3>Box1</h3>
      <p>クリックで内容表示</p>
      <div class="box_content">
        内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。
      </div>
    </div>
    <div class="box">
      <h3>Box2</h3>
      <p>クリックで内容表示</p>
      <div class="box_content">
        内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。
      </div>
    </div>
    <div class="box">
      <h3>Box3</h3>
      <p>クリックで内容表示</p>
      <div class="box_content">
        内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。
      </div>
    </div>
    <div class="box">
      <h3>Box4</h3>
      <p>クリックで内容表示</p>
      <div class="box_content">
        内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。内容を記入します。
      </div>
    </div>
  </div>
```
```CSS:CSS
.main_contents {
  margin: 50px auto;
  width: 800px;
  display: flex;
  justify-content: space-between;
}

.box {
  height: 100px;
  width: 180px;
  margin: 0 10px;
  padding: 5px;
  text-align: center;
  overflow: hidden;
  border: 1px solid skyblue;
  border-radius: 3px;
  transition: .2s;
  cursor: pointer;
}

.box:hover {
  box-shadow: 0px 0px 7px #aaa;
}

.box_content {
  margin-top: 20px;
}

.open {   /* Boxが開いた場合に付与するクラス */
  height: 260px;
}
```
```JavaScript:JavaScript
const accordionBox = () => {
  isOpen = false;
  window.addEventListener("click", function(e) {
    if (isOpen == true && (e.target.closest('.box') == null)) {   // いずれかのBoxが開いた状態で、Box以外をクリック
      const OpenedBox = document.querySelector(".open");
      OpenedBox.classList.remove("open");
      isOpen = false;
    }
  });
  
  const boxes = document.querySelectorAll(".box");
  boxes.forEach(function(box) {
    box.addEventListener("click", function(e) {
      if (this.classList.contains("open") == true) {   // 開いているBox自身をクリック
        return;
      } else if (isOpen == true) {   // 開いているBox以外のBoxをクリック
        const OpenedBox = document.querySelector(".open");
        OpenedBox.classList.remove("open");
        this.classList.add("open");
      } else {   // いずれのBoxも開いていない状態で、Boxをクリック
        this.classList.add("open");
      }
      isOpen = true;
    })
  });  
};

window.addEventListener("load", accordionBox);
```

# 解説

今回の目標物の動作のポイントは、**クリックしたときの条件分岐**です。

クリックしたときの状態は大きく分けて２つその上で定義する必要がある条件は以下の４つです。

### 条件分岐
- Box以外をクリック
  - いずれかのBoxが開いている状態
- Boxをクリック
  - 開いているBox自身をクリック
  - 開いているBox以外のBoxをクリック
  - いずれのBoxも開いていない状態

これらの条件は以下のように定義できます。
### いずれかのBoxが開いている状態でBox以外をクリック
```JS
isOpen = false;　　 // Boxの状態を定義（デフォルトはfalse）
window.addEventListener("click", function(e) {
    if (isOpen == true && (e.target.closest('.box') == null)) {処理}
});
```

まず`isOpen == true`とすることで「いずれかのBoxが開いている状態」を定義します。

その上で、`window`のクリックイベントを`e`で取得し、`e.target`とすることでクリックイベントが発生した要素を取得します。

このクリックされた要素に対し、`.closest('.box')`とすることで、この要素とその親に、`'.box'`というノードがあるか探索します。

`== null`とすることで、クリックした要素にboxクラスがない場合、つまり「**Box要素以外をクリックした状態**」を定義することができます。

### 開いているBox自身をクリック
```JS
const boxes = document.querySelectorAll(".box");   // 全てのBoxを取得
boxes.forEach(function(box) {   //それぞれのBoxに対して処理
    box.addEventListener("click", function(e) {
      if (this.classList.contains("open") == true) {処理}
      ・・・
```

まず全ての`Box`を取得し`forEach`を使うことでそれぞれの`Box`に対して処理を定義します。

`this`（クリックされたBox要素）に対して`.classList.contains("open")`とすることで、この要素がopenクラスを含むかを判断します。

`== true`とすることで「**開いているBox自身をクリックした状態**」を定義することができます。

### 開いているBox以外のBoxをクリック
```JS
・・・
else if (isOpen == true) {処理}
・・・
```

`isOpen == true`なのでいずれかのBoxが開いている状態を定義でき、かつ先ほどの「開いているBox自身をクリックした状態」以外に当たるので、「**開いているBox以外のBoxをクリックした状態**」を定義できます。

### いずれのBoxも開いていない状態で、Boxをクリック
```JS
・・・
else　｛処理｝
・・・
```

先ほどの`isOpen == true`という条件以外にあたるので、「**いずれのBoxも開いていない状態で、Boxをクリックした状態**」が定義できます。

### openクラスの付与とisOpenの状態変更でパネルの開閉を管理
```JS
Element.classList.add("open")
```
条件分岐した中で`.classList`メソッドを使用してopenクラスをつけ外ししてBox要素を開閉、`isOpen`という変数でBox開閉状態を変更し管理します。

# おわりに
要素の開閉自体はもう少し簡単に定義できると思いますが、これらの条件分岐を定義することで、Box開閉だけでなく更に処理を追加することもできます。

# 参考

https://developer.mozilla.org/ja/docs/Web/API/Event/target

https://developer.mozilla.org/ja/docs/Web/API/Element/closest