JavaScript/jQuery初心者が、アコーディオンメニューの開閉に伴って回転する矢印アイコンをなんとか実装したときの記録

## はじめに

JavaScript/jQuery初心者が、コンテンツの開閉状態と連動する矢印アニメーションを、jQueryで実装するまでにやったことの記録です。

## 3行で

- `this`でクリックした要素にだけ処理を行う
- アニメーション用のCSSを書く
- `toggleClass`でアニメーション用クラスの付け替えをする

## 環境 + 前提

Rails 5.2.4.3
Ruby 2.6.5
Bootstrap 4.5.0
jquery-rails 4.4.0
sass-rails 5.1.0

このポートフォリオに実装
[YWT Quest](https://ywtquest.herokuapp.com/)

Bootstrapのカードを使用。
カードコンポーネントによるアコーディオンを実装していて、`card-header`をクリックすると開閉する。

## やりたいこと

1. このようなカードが並んでいて、開閉可能なものには矢印がついている。
![スクリーンショット 2020-06-30 0.12.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451253/4448d091-d937-1b4c-a418-a9c2e143d20a.png)
1. カードのヘッダー部分をクリックすることで開閉。
1. カードが開いた時に矢印も回転（アニメーション）する。
1. カードが閉じる時に矢印が回転（アニメーション）し元の状態に戻す

## 実装に必要そうなこと

- `card-header`をJavaScriptで指定し、クリックされた時にクラス名を追加する。
- 追加したクラスに、アイコンの表示を変えるCSSを書く。
- もう1度クリックされた時には上で追加されたクラスを削除する。

## 実際にやったこと

### 1. viewファイルに直書きではなく、JavaScriptを読み込んで使う

まず、`assets/javascripts/logs/index.js`を読み込む設定をする。

[rails　任意のviewのみで、特定のjsを読み込む方法 - Qiita](https://qiita.com/Hijiri-K/items/1a3f7137cf3186580447)
`logs/index.js`は`views/logs/index.slim.html`に対してだけ読み込ませたいので、上記を参考にviewファイルの下部に以下を記述。

```erb:views/logs/index.slim.html
<%= javascript_include_tag 'logs/index.js' %>
```

### 2. jQueryでクリックイベントを追加する

`card-header`部分をクリックすることで開閉可能で、カードが開いた時に矢印も回転するのであれば、**`card-header`をクリックした時に、矢印が回転するような処理** を書かないといけない。

`card-header`はココ

![スクリーンショット 2020-06-29 22.32.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451253/db0f78f7-97f2-d37e-8fd3-144d058f5ffd.png)

`card-header`をクリックした、とJavaScript側に分かってもらえてるかどうかをまずチェック。

```javascript:logs/index.js
$(function(){
  $('.card-header').click(function(){
    alert("click event")
  });
});
```

`card-header`をクリックしてアラートが出ればOK

![click-event.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451253/51d5f1ff-c917-b27e-a235-9f8b5f0b8553.gif)

どうやらうまくいってるみたい。

### 3. カードの開閉状態で条件分岐させる

次に、クリックしたカードが開くかどうかをチェック。
現在はcontentカラム（詳細文）が空でなければ開くし、`card-header`の文字の右側に矢印が表示される。

つまり矢印の有無でカードが開くかどうかをチェックできる、と考えた。
クリックした`card-header`に`fa-chevron-down`というクラスが存在すれば開く、しなければ開かない。

jQueryに`find`という子孫要素を取得できるメソッドがあったので使ってみる。

[【jQuery入門】find()で子要素を取得する手法まとめ！ \| 侍エンジニア塾ブログ（Samurai Blog） - プログラミング入門者向けサイト](https://www.sejuku.net/blog/37474)
[find(expr) - jQuery 日本語リファレンス](http://semooh.jp/jquery/api/traversing/find/expr/)

以下のようなコードを書いた。まずは条件分岐が成功するかどうかをチェックする。

```javascript:logs/index.js
$(function(){
  $('.card-header').click(function(){
    var icon = $(this).find('.fa-chevron-down');
    // 本文がなく、アイコンがないcard-headerをクリックした時はundefinedが返されることを利用した
    if(icon[0] !== undefined) {
      console.log("true");
    } else {
      console.log("false");
    }
  });
});
```

最初は`icon[0] == "<i class=\"fas fa-chevron-down\"></i>"`と、`icon[0]`に入っている情報そのものと照らし合わせたり
`icon[0].toString();`として文字列に変換してから比較したり
`indexOf`を使って前方一致（`<i class`だけ等）で検索をかけてみたりしたが、**全て失敗。**

どうしよう？と`console.log`を見ていると、開閉できないカードを押したとき`undefined`が返ってきているのを発見。

「じゃあ、`undefined`が返ってきていないときにアニメーション用の処理をすればいいのでは……？」

と考えて、`if`の中身を`if(icon[0] !== undefined)`と書き換えることで、開くカードの`card-header`をクリックしたときには`true`が、開かないカードの`card-header`をクリックしたときには`false`が返ってくるようになった。

### 4. 矢印をアニメーションさせる処理を書く

[rotate() - CSS: カスケーディングスタイルシート \| MDN](https://developer.mozilla.org/ja/docs/Web/CSS/transform-function/rotate)

矢印を回転させるには`rotate()`プロパティでアイコンを回転させれば良いと考えた。
しかし、肝心のアニメーションの実装は……？

[jQueryでCSSのtransform: rotate()を使った回転アニメーションする際のメモ ‹ jQuery ‹ JavaScript ‹ emwaiblog](https://blog.emwai.jp/javascript/transform-rotate/)

この記事では、回転用のクラス名を追加することで、回転を実装している。
また、`transition`を使うことで`transform`をアニメーション化している。

`transition`のオプションやプロパティについてはここを参考にした。

[【CSS3】Transition（変化）関連のまとめ](https://qiita.com/7968/items/812d6a21fc4dd9ae9c75)

まず、開閉に伴ってクラスを付けたり外したりする処理をjQueryで書く。

```javascript:assets/javascripts/logs/index.js
$(function(){
  $('.card-header').click(function(){
    var icon = $(this).find('.fa-chevron-down');
    // 本文がなく、アイコンがないcard-headerをクリックした時はundefinedが返されることを利用した
    if(icon[0] !== undefined) {
      $('.fa-chevron-down').toggleClass("spined-icon")
    }
    // else以下を削除
  });
});
```

`toggleClass`で、`fa-chevron-down`クラスを持つ要素に`spined-icon`クラスがついてなければ追加、ついてたら削除する。

これも検証ツールで確認する。

![class-toggle.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451253/4586e8e7-0c35-a7e9-7a39-085fe0b7b4bf.gif)

`<i class="fas fa-chevron-down>"`の横に`spined-icon`が追加されているのが確認できた。

CSSで`spined-icon`に矢印を回転させる処理を書く。

```css:assets/stylesheets/logs/index.scss
.spined-icon {
  transform: rotate(90deg);
  transition-duration: 0.5s;
}
```

`transform: rotate(90deg);`で要素を90度回転、それが始まってから終わるまでの時間を`transition-duration: 0.5s`で設定している。

「0.5秒かけて要素を90度回転させてください」ということ。実際の動きを見てみる。

![ohno.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451253/b82c9fd0-88cd-6e73-a6fa-509ac8d03015.gif)


閉じるときのアニメーションがない。それはともかく……

**クリックしてないカードの矢印も動いちゃってる……**

## 何がいけなかったのか？

コードをもう一度見てみる。

```javascript:assets/javascripts/logs/index.js
$(function(){
  // 'card-header'クラスを持つ要素をクリックしたとき、
  $('.card-header').click(function(){
    // 'card-header'クラスの子孫要素の中から'fa-chevron-down'クラスを持つ要素を探し、変数iconに格納する
    var icon = $(this).find('.fa-chevron-down');
    if(icon[0] !== undefined) {
      // 'fa-chevron-down'クラスを持つ要素に'.spined-icon'クラスを追加(既にある場合は削除)
      $('.fa-chevron-down').toggleClass("spined-icon")
    }
  });
});
```

このコード`$('.fa-chevron-down').toggleClass("spined-icon")`では、`fa-chevron-down`クラスを持つ全ての`i`要素に`spined-icon`クラスを付与することになってしまい、全ての矢印が回転してしまう。

「クリックした要素のみ」「クリックした要素 jQuery」等で検索していると、この記事を見つけた。
[jQueryの$(this)の使い方(どこを指してるのか？)](https://www.flatflag.nir87.com/this-1371#i-4)

ここに、`this`を使ってクリックした要素にのみ処理を行っている例と、`this`ではなく要素（記事中では`p`）を指定してしまった時の例があり、後者では自分の失敗例と同じで全ての`p`要素に処理が行われてしまっている。

これを参考に、**クリックした`card-header`の`i`にのみクラスを追加/削除する処理を行うコードを考える。**
また、クリックした`card-header`の`i`に処理を行うため、再び`find`を使った。

[jQueryで子要素を取得するいくつかの方法〜children,find,contents](http://www.flatflag.nir87.com/children-996#find)

## 完成形

![icon-animation-kansei.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451253/197c87ae-d471-619f-4857-d58caad00b17.gif)

```javascript:logs/index.js
$(function(){
  $('.card-header').click(function(){
    var icon = $(this).find('.fa-chevron-down');
    // 詳細文がなく、アイコンがないcard-headerをクリックした時はundefinedが返されることを利用した
    if(icon[0] !== undefined) { 
      // thisをつけてクリックした要素にだけ効かせる。そうしないと全部の矢印にtoggleClassが効いてアニメーションしてしまう
      // findで.fa-chevron-downのついた要素を見つけ、そこにクラスを追加している
      // spined-iconというクラスでアニメーションをつけている
      $(this).find('.fa-chevron-down').toggleClass("spined-icon")
    }
  });
});
```

また、`spined-icon`が`toggleClass`によって削除されても、`fa-cheveron-down`に`transition-duration`を設定することでアニメーションがうまくいった。

```sass:logs/index.scss
// ywtのカードヘッダーに、開閉可能な場合つける矢印
@mixin ywt-header-angle($anglecolor: $second-color) {
  i.fa-chevron-down {
    color: darken($anglecolor, 30%);
    // アイコンのクラスに、transitionをつけることで、閉じる際にもアニメーションを有効化している
    transition-duration: 0.5s;
  }
}

// 矢印のアニメーション用
.spined-icon {
  transform: rotate(90deg);
  transition-duration: 0.5s;
}
```

## おわりに

JavaScriptやjQueryへの苦手意識はまだまだ消えないが、`console.log`で処理や戻り値を追いながら、1つずつ進めていくことで、なんとか実装。
心残りとしては、カードの開閉が可能かどうかをチェックするための`if(icon[0] !== undefined)`について。
`if(icon[0] == "<i class=\"fas fa-chevron-down\"></i>")`じゃなぜダメだったのか？
`indexOf`が使えなかったのはなぜか？
`icon`の中の情報はどのように使うのが正解だったのか？

ここらへんに対してしっかり調べて解決することが出来なかったこと。
学習が進み、分かり次第追記予定。