クリックで展開するアコーディオン式のコンテンツを実装する【jQuery】

公開日:

最終更新日:

クリックで展開するアコーディオン式のコンテンツを実装する【jQuery】

WEBサイトのコンテンツを作成していくうちによくあることだと思うのですが
「付属している情報で載せておきたいけど、縦長になりすぎて文末にたどり着くまでに読み疲れて閉じられるのではないか・・・」といった問題です。

私自身はどちらかというと、必要な情報を折りたたんで盛り込んでいくよりも
読んでいくうえで付属的な情報を折りたたんで表示する方が実用的と思う派です。

何れにしても折りたたみできるコンテンツを実装する機会は多いと思うので、jQueryを使ってサクッと実装したいと思います。

なので、今回は当ブログで実装している「展開ボタンを設置して折りたたみコンテンツを実装できるJS」について書こうと思います。

脱jQueryと言われている時代に逆行しているかもしれませんが・・・。

まずはjQueryを設置する

この部分に関してはもはや恒例行事なので多くを説明することは必要ないと思います。

headで読み込んでくださいと書かれている記事が多いですが、実行されるのはドキュメントを読み込んだ後といった状態になり無駄が多いので
私自身はbody閉じタグ前に設置する方が多いです。(この辺りは他のドキュメントを参考にして進める方がいいと思います。)

CDNで読み込む方が読み込みも早いという噂があるのですが、私はローカルに落として設置する方がエラーの心配もなくいいのかなと思っています。
以前まではjQuery1系を使用していたのですが、IEのサポートもあまり必要ないと思うのでjQuery3系としています。

その後「script.js」という名称でjsファイルを作成しておいて、同じようにscriptタグで設置します。
もちろん「path」となっている部分は任意で書き換えてください。

  <script src="path/jquery-3.3.1.min.js"></script>
  <script src="path/script.js"></script>
</body>
</html>

実装する構想を練る

私はいつもjQueryでアニメーションやその他のインタラクションを実装する場合
コードを書く前に「どんな動き」をして「どんな結果」が必要なのかを全体像として想像してから取り掛かるようにしています。

私自身がよく陥ることなのですが、コードを書いているうちに目的がわからなくなって実装に時間がかかったり、HTMLに無駄に記述を増やしてしまったりということがよくあります。(私だけでしょうか・・・)

まずはHTMLの構造を見てどんな要素が必要か考える

アコーディオンの実装といえばボタンがあって、押したら開くというのが想像できると思います。

当ブログの場合だとサンプルコードを載せるとどうしても記事が縦に長くなるので、最初のうちは邪魔にならないように折りたたんでしまうようにしています。

下のようなHTML構造の部分はある程度の高さに達するとアコーディオン式になります。

<pre><code>
  // サンプルコードが続く
<code><pre>

ボタンの記述はいちいち設置がめんどくさいので、どうせならjQueryで自動化してしまおうと思い、HTMLには記述していません。

パフォーマンスなどを考慮するのであればめんどくさくてもHTMLに記述する方がいいと思います。

jsファイルにコードを記述する

長くなりましたが、ここから実装コードの話です。
実際に自分で書いたコードは以下です。

// コードを折りたたむ
var preCount = [];
var tagStr = 'singlePageSection pre';
var btnActionClass = 'js-codePreview';
var btnStrO = 'コードを展開する';
var btnStrC = 'コードを閉じる';
var btnTag = '<a class="' + btnActionClass + ' btn btn-outline-secondary mb-3" href="#">' + btnStrO + '</a>';
var codeHeight = 120;
// コード記述(pre)前にボタン設置
function codeBtnBefore(codeStr){
	var singlePreTag = $('.' + codeStr);
	var dfd = new $.Deferred();
	var singlePreTagCount = (singlePreTag.length - 1);
	if(singlePreTagCount > -1){
		singlePreTag.each(function(i){
			// console.log($(this).outerHeight());
			if( $(this).outerHeight() > codeHeight ){
				$(this).css({'height' : codeHeight + 'px'}).before( btnTag );
				preCount.push(i);
			}
			if( i === singlePreTagCount ){
				dfd.resolve();
			}
		});
	}else{
		dfd.reject();
	}
	// console.log(preCount);
	return dfd.promise();
}
// ボタンクリック時に展開(もしくは閉じる)
function codePreviewClick(fNum, codeStr){
	var btnSwitch = !false;
	var codePreview = $('.' + btnActionClass);
	$.each(preCount, function(key, value){
		codePreview.eq(key).on('click.codePreview', function(){
			if( btnSwitch ){
				// false
				btnSwitch = !btnSwitch;
				if( $(this).text() === btnStrO ){
					$(this).text(btnStrC);
					var precodePadding = parseInt($('.' + codeStr).eq(value).css('padding-top'));
					var precodeHeight = Math.ceil(
						$('.' + codeStr).eq(value).find('code').outerHeight() + (precodePadding * 2)
					);
					// console.log(precodeHeight);
					$('.' + codeStr).eq(value).css({'height' : precodeHeight});
				}else{
					$(this).text(btnStrO);
					$('.' + codeStr).eq(value).css({'height' : codeHeight + 'px'});
				}
				setTimeout(function(){
					// true
					btnSwitch = !btnSwitch;
				}, 200);
			}
			return false;
		});
	});
}
// 実行
codeBtnBefore(tagStr).then(
	function(){
		codePreviewClick(preCount, tagStr);
	}
);

人によってはパフォーマンスが気になったり、コードの書き方や変数の名称など様々な意見があると思いますが、もっと勉強して改善しますということで。

今回はボタンもjQueryで実装するので、まずはトリガーとなるボタンをpreタグの前にインサート→インサート処理が終わったら各ボタンにclickイベントを付与するといった流れです。

ざっくりと説明

最近のJavaScriptの書き方でいくとletで書く方が一般的だと思いますが、まだ知識が追いついていないのでvarで書いています。今まで書いていたJSの見直しもしなければ・・・。

ひとまずfunctionごとに少し説明をしようと思います。
複雑なように見えますが、実際には大したことはしていないのでまだ読める範囲かと思います。

codeBtnBeforeについて

対象となるpreタグの分だけeachで繰り返し、ボタンを配置していきます。
完全に折りたたむような仕様ではないので、まず対象のpreタグの高さをcodeHeightと比較しています。

高さが設定値(codeHeight)を超えている場合、preタグの高さを設定値にしてからbeforeで直前の要素としてbtnTagで設定しているHTMLをインサートします。

each開始前にsinglePreTagCountで条件分岐している理由としては、preタグが1要素以上ある場合のみ実行させて、ない場合はDeferredで即rejectをしています。
この判断基準がベストプラクティスなのかと言われれば違うかもしれませんが、常にeachを通るよりかはエラーのハンドリングがしやすいかと思います。

また、eachで要素分HTMLのインサートを繰り返し、要素のindexをpreCountの配列に格納していきます。
繰り返しが終わったあとはresolveを返すようにするため、eachで定義したindexのiとsinglePreTagCountが同じかどうかを条件分岐で判断しています。

最終的にreturn dfd.promise()とすることでインサート処理が完了してから次の処理へ進むようになっています。

codePreviewClickについて

次にクリックした時の動作についての関数です。
preCountを基準に$.eachで繰り返し処理をしていきます。

この際に$.eachの引数として配列のkeyとvalueを指定します。
こうすることで、preタグのindexとボタンのindexがずれないようにしています。

あとは該当するボタンにクリックイベントとしてon(‘click.codePreview’)といった感じで名前空間付きで記述していきます。

通常は名前空間を利用しなくても特に問題はないですが.off()でイベントを解除する場合は名前空間を利用しておくと判別がきいてとても便利です。

クリックした時に一時的にbtnSwitchにfalseを入れておくことで、クリック連打によるイベントの複数回重複を防いでいます。
その後テキストの状態を判断してコードを展開するか閉じるかを条件分岐として入れています。

最後にbtnSwitchをtrueに戻さなければいけないのでsetTimeoutで200ミリ秒後にbtnSwitchをtrueにするようにしています。
おそらくこの辺りはtransitionendなどでアニメーションの終了を感知させるようなイベントを付与しておくべきかもしれません。

各functionの実行

最後にfunctionを実行します。
codeBtnBeforeを実行したあとに.then()でつなぐことでcodeBtnBeforeの処理が完了後にcodePreviewClickが処理されていくといった流れです。

Deferredで処理完了を感知するのはajaxで処理をするときも同じなので、上手く使えばコードの見通しも良くなりメンテナンス性も向上します。

まとめ

アコーディオンの実装はCSSで実装できたりするのですが、不要なタグの挿入やHTMLの見通しが悪くなる実装の場合もあるので、私はどちらかというとjQueryで手っ取り早く済ませています。

どちらで実装するかは条件によりけりなのでなんとも言えないですが、クライアントがHTMLを直接編集するような案件であれば、jQueryの方が影響範囲がわかりやすいのでいいかもしれません。

ある程度コードの組み方さえ理解できれば、イベントの付与や値の取得などは調べればたくさん出てくるので、より良いコードを書くための参考程度にでもなればと思います。

関連記事