find()を使ってタブ機能を開発してみよう

UI開発者 大石

この記事はミツエーリンクスアドベントカレンダー2019 - Qiitaの15日目の記事です。

突然ですが、みなさんはタブ機能を開発したことがありますか?
いろいろなWebサイトやアプリで使われており、JavaScript初学者にも作りやすい機能です。
今回はこの機能の開発を通じて、個人的に好きなfind()を紹介していきます。

find()とは

まずはみんなの味方、MDNを読んでみましょう。

find()は簡単に言うと、配列の中から、指定した条件を満たす最初の要素を返すメソッドです。
「配列の中から」とあるように、Arrayオブジェクトでしか使えません。
例えば、querySelectorAll()で取得したNodeListには使えません。もし使う場合はArray.from()を使ってArrayオブジェクトに変換してから使いましょう。
そして、指定した条件を満たす最初の要素を返すので、複数を返してほしいときにはうまくいきません。複数の要素を取得したいときはfilter()を使いましょう。

使用できない場合の説明を最初にしてしまいましたが、具体的には以下のような機能を作る場面で使用することができます。

  • タブ機能
  • カルーセル機能
  • ナビゲーションのカレント表示機能

この3つの機能のように、複数の中から1つだけを選び、表示を切り替えるロジックが必要な場面では、とても便利です。
基本的な使い方は以下のようになります。

const item = arr.find((現在の要素, 現在のインデックス, 配列自体) => {
    // ここに条件文を書く
    return element;
});

find()は条件を満たす最初の要素を返すので、返し先の格納先変数をしっかり用意し、最後のreturn文も忘れないようにしましょう。もし要素が見つからなければundefinedが返されます。
引数にはforEach()などと同様、現在の要素、現在のインデックス、配列自体が渡されます。
配列の中から指定した条件を満たす要素が返されたら、格納された変数(上記の例だとitem)に対し、その後の処理をしていきます。

タブ機能を作ってみよう

ここまで簡単にfind()の説明をさせていただきました。
ここからは、具体的にタブ機能を作っていきましょう。まずはHTMLのコードを掲載します。

<ul role="tablist" class>
    <li class="tab selected" role="tab" aria-selected="true" aria-controls="TabPanel1" tabindex="0">Tab1</li>
    <li class="tab" role="tab" aria-selected="false" aria-controls="TabPanel2" tabindex="0">Tab2</li>
    <li class="tab" role="tab" aria-selected="false" aria-controls="TabPanel3" tabindex="0">Tab3</li>
    <li class="tab" role="tab" aria-selected="false" aria-controls="TabPanel4" tabindex="0">Tab4</li>
</ul>
<div class="panel-wrapper">
    <div class="tabpanel" role="tabpanel" id="TabPanel1">TabPanel1</div>
    <div class="tabpanel hidden" role="tabpanel" id="TabPanel2">TabPanel2</div>
    <div class="tabpanel hidden" role="tabpanel" id="TabPanel3">TabPanel3</div>
    <div class="tabpanel hidden" role="tabpanel" id="TabPanel4">TabPanel4</div>
</div>

簡単なコードですが、.selectedでタブボタンの選択状態を制御、.hiddenでタブパネルの表示状態を制御しています。

続いてJavaScriptのコードを掲載します。
今回はタブボタンを押下した際にタブボタンの選択状態を制御する関数タブパネルの表示状態を制御する関数を作っていきますので、それぞれに分けて解説していきます。

タブボタンの選択状態を制御する関数

const changeTab = (selected) => {
    const current = tab.find((element) => {
        // タブボタンの中から.selectedがついているものを探す
        return element.classList.contains('selected');
    });
    current.classList.remove('selected');
    selected.classList.add('selected');
};

押下したタブボタンは発火した要素を引数(上記selected)で渡すことで判断できるのですが、もともと.selectedがついている選ばれていたタブボタンは発火しないので、.selectedがついているのはどのタブボタンなのかを探さなければなりません。
その「選ばれていたタブボタン(.selectedのついているタブボタン)を探す」という処理をfind()を使って行い、見つかったタブボタンの.selectedを外しています。
あとは引数で渡されている「押下したタブボタン」に.selectedをつければ完成です。

ここで気をつけなければならないのが、処理の順番です。
押下したタブボタンに先に.selectedをつけてしまうと、.selectedを持つタブボタンが2つ存在してしまいます。
find()は配列の中から、指定した条件を満たす最初の要素を返すメソッドなので、先に見つかった.selectedを持つタブボタンが返され、意図した挙動にならなくなってしまいます。
探したい条件にあてはまる要素が2つ以上にならないように、処理の順番をコントロールしましょう。

続いてタブパネルの表示状態を制御する関数を見ていきます。

タブパネルの表示状態を制御する関数

const changeTabPanel = (num) => {
    const currentPanel = tabPanel.find((element) => {
        // 選ばれていたタブパネルを探す
        return !element.classList.contains('hidden');
    });
    currentPanel.classList.add('hidden');

    const selectedPanel = tabPanel.find((element, index) => {
        // 選ばれたタブパネルを探す
        return index === num;
    });
    selectedPanel.classList.remove('hidden');
};

changeTabPanel関数では選ばれていたタブパネル選ばれたタブパネルの2つを探しています。

まず、選ばれていたタブパネルを探すには「.hiddenのついていないタブパネルを探して現在表示されているタブパネルを取得し、.hiddenをつける」処理を行います。 returnのあとに!をつけて「~でない」ものを探す、という否定での探し方もできれば、if文を使って&&||で複数条件も指定できます。

最後に、選ばれたタブパネルを探すには「押下されたタブボタンのインデックス(引数num)とタブパネルのインデックスが同じものを探す」処理を行います。
ただし、今回の方法では「タブボタンとタブパネルのインデックス番号が同じ」という条件が必須です。
タブボタンの並び順とタブパネルの並び順を統一できない場合は、今回は実装していないですがdata属性やclass属性で任意の同じ値を付与しておき、探す際の条件にする、などといった探し方もできます。

まとめ

いかがでしたでしょうか。
今回タブ機能の開発を通じてfind()を紹介させていただきましたが、JavaScriptにはほかにもたくさんのメソッドが存在し、メソッドを覚えていくだけで書き方の幅がとても広がります。
「そんなことしなくてもforEach()ですべての要素の属性を見て、if文で条件分岐すればできるじゃん」と思うかたもいるかもしれませんが、可読性という面ではfind()(探す)のほうが「あ、なにかを探しているのか」とパッと見で理解がしやすい、という利点があります。
「いつ、だれが見ても理解がしやすいコードを書く」ということは、開発者にとって原点にして最大の課題でもあります。

最後に、find()を使う際の最大の注意点ですが、find()はIE11にデフォルトで対応していません。(Can I Use
ポリフィルは存在しておりますので、必要に応じて活用しましょう。

拙い文章ですが、このBlogを見た方の少しでもお役に立てれば幸いです。