モーダルのような、初期状態で display: none; がかかっているような要素の中で Slick を動かす場合にハマった事項のメモです。
やりたかったこと
- lazyLoad
- モーダルの中で Slick を動かす
- 複数のモーダルがある(それぞれに Slick あり)
シチュエーションとしてはこんなところ。
サンプル
以下に一つずつ解説してきますが、動作確認のためのサンプルを置いておきます。
リポジトリも。
1. lazyLoad
まずは lazyLoad
ですが、これについては slick で用意されている lazyLoad
プロパティが使えます。
$('slick').slick({
lazyLoad: 'ondemand'
});
プロパティはスライドする度に画像を読み込む ondemand
とページ読み込み後に読み込む progressive
の2つ。
また、 lazyLoad
で画像が読み込まれたときに発火するイベント lazyLoaded
とエラー時に発火する lazyLoadError
があるので、ケースによってはこれらも利用できそう。
$('slick').on('lazyLoaded', function (event, slick, image, imageSource) {
// lazy load succeed
console.log(`${event.type}: ${imageSource} / ${image[0].alt} の読み込みが完了しました。`);
// 「lazyLoaded: http://placehold.jp/667x375.jpg / Slick 1-4 の読み込みが完了しました。」のようなメッセージを出力
});
$('slick').on('lazyLoadError', function (event, slick, image, imageSource) {
// lazy load failed
console.log(`${event.type}: ${imageSource} の読み込みが失敗しました。`);
});
$('slick').slick({
lazyLoad: 'ondemand'
});
ちなみに、 lazyLoad
は JS 側だけではなく、 HTML 側にも既述が必要です。
<ul class="c-slick" id="lazyLoad">
<li><img data-lazy="http://placehold.jp/667x375.jpg" src="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/ajax-loader.gif" alt="Slick 1-1"></li>
<li><img data-lazy="http://placehold.jp/667x375.jpg" src="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/ajax-loader.gif" alt="Slick 1-2"></li>
<li><img data-lazy="http://placehold.jp/667x375.jpg" src="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/ajax-loader.gif" alt="Slick 1-3"></li>
<li><img data-lazy="http://placehold.jp/667x375.jpg" src="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/ajax-loader.gif" alt="Slick 1-4"></li>
</ul>
img
タグの src
属性は画像が読み込まれるまでのダミー画像の指定、本番のスライドショーで表示させたい画像は独自の data-lazy
属性で指定、となります。
ここまでがサンプルの「Slick 1」のケース。
2. モーダルの中で Slick を動かす
2.1. 普通に Slick
さてここからが本題。
例として Bootstrap 4 のモーダルコンポーネントを利用しますが、モーダルのような初期状態で display: none;
となっている要素の中に Slick をセットすると動かないようです。
原因としては、親要素が display: none;
なので、 Slick の要素が width: 0px;
で潰れてしまうようです(ついでに height: 1px;
でもあった)。
※サンプルの「Slick 2」の「モーダルを開く (setPotion 指定なし)」のケース
2.2. setPotion
ここで Slick 公式やいくつかの記事を見ると「 setPotion
を指定すると良い」という記述が散見されます。
これは setPotion
を指定したタイミングで Slick 要素のサイズを再計算してくれるようです。
今回は「モーダルが開いた時」なので、ボタンの click
イベントにバインドすると良さそうです。これならば複数モーダル + Slick でもクリックされたモーダルのみ Slick が動くようになるので処理も軽減できます。
注意点としては、 click
イベント直後だとまだモーダルがフェードのエフェクトで表示されている途中なのでせっかく再計算しても潰れてしまう可能性があること。
そのため、 setTimeout
で処理する時間を指定しましょう。
$('#setPotionModalButton').on('click', function () {
// 0.3s 後に setPosition 付きで Slick 実行
const slickInit = setTimeout(() => {
const $setPotion = $('#setPotion');
// Slick
$setPotion.slick({
lazyLoad: 'ondemand'
});
$setPotion.slick('setPosition');
}, 300);
});
※サンプルの「Slick 2」の「モーダルを開く (setPotion 指定あり)」のケース
これでモーダルの中でも開くようになりました。よしよし……ではなかった。
3. Uncaught TypeError: Cannot read property ‘add’ of null エラーの回避 → .not(‘.slick-initialized’)
先程のモーダル、1回開くだけならば良いのですが、2回目以降モーダルを開こうとすると
Uncaught TypeError: Cannot read property ‘add’ of null
というエラーが発せられます。既に Slick が動いているところに、 click
イベントでもう一度 Slick をバインドしようとしたので怒られてしまったわけです。
回避策としては、 .not('.slick-initialized')
を追加すること。
Slick は初期化を完了すると該当要素に slick-initialized
を付与するため、 .not('.slick-initialized')
で「まだ Slick の初期化が済んでいない要素」と指定することでエラーを回避できる、ということですね。
$('#unInitializedModalButton').on('click', function () {
// 0.3s 後に setPosition 付きで Slick 実行
const slickInit = setTimeout(() => {
const $unInitialized = $('#unInitialized');
// Slick
$unInitialized.not('.slick-initialized').slick({ // .not('.slick-initialized') を追加
lazyLoad: 'ondemand'
});
$unInitialized.slick('setPosition');
}, 300);
});
※サンプルの「Slick 3」のケース
console.log()
等で拾うと2回目以降 $unInitialized.not('.slick-initialized')
は undefined
になっていますが、エラーは回避できています。これで良し……ではなかった。
4. unslick
稀ですが、高速でモーダルを開いたり閉じたりして(あるいは何らかの理由で Slick の初期化が遅れて)しまうと、それ以降モーダルを開いても正常に Slick が表示されない現象が発生します。
画像が多かったり重かったりすると出やすくなるかもしれません。
ページ再読み込みすれば大丈夫と言えば大丈夫ですが……今回のシチュエーションではこの頻度がわりと大きく、無視できない状態でした。そのため、さらに対策を講じる必要がありました。
そこで、以下のような処理を行うことにしました。
- モーダルを開くボタンクリック時
click
イベント発火setTimeout
で 0.3s 後に Slick 実行 (const slickInit = setTimeout()
).not('.slick-initialized')
付き
- モーダルを閉じる時
hidden.bs.modal
イベント発火(Bootstrap 独自イベント)jqueryObject.slick('unslick');
でunslick
発動、 Slick を解除clearTimeout(slickInit);
によって見初期化であろうとも上述「モーダルを開くボタンクリック時 2.」の動作を解除modalButtonjqueryObject.off('hidden.bs.modal');
でhidden.bs.modal
イベントもバインド解除
// setPosition ありで モーダル内 Slick を実行させる
const $unSlickModalButton = $('#unSlickModalButton');
const unSlickModalID = $unSlickModalButton.attr('data-target');
const $unSlickModal = $(unSlickModalID);
const slickInitializedClass = 'slick-initialized';
$unSlickModalButton.on('click', function () {
const $unSlick = $('#unSlick');
// 0.3s 後に setPosition 付きで Slick 実行
const slickInit = setTimeout(() => {
// Slick
$unSlick.not('.slick-initialized').slick({
lazyLoad: 'ondemand'
});
$unSlick.slick('setPosition');
}, 300);
$unSlickModal.on('hidden.bs.modal', function () {
// Bootstrap のモーダルを閉じるイベントが発火したら
if ($unSlick.hasClass(slickInitializedClass)) {
// Slick 対象要素が slick-initialized クラスを持っていたら
// unslick で Slick をアンバインド
$unSlick.slick('unslick');
}
// setTimeout 発動前にモーダルを閉じると setTimeout が生き残っているので、すぐまたモーダルを開いたりすると Slick のバインドが2回走ったりしておかしくなるので clearTimeout する
clearTimeout(slickInit);
// モーダルを閉じる度に hidden.bs.modal イベントがバインドされて重複処理してしまうので閉じられたらアンバインドする
$unSlickModal.off('hidden.bs.modal');
});
});
※サンプルの「Slick 4」のケース
ここまでやって、ようやく納得のいく挙動になりました。思ったよりも紆余曲折ありました……。