モーダルの中で Slick を動かす場合の諸注意(setPotion指定、 Uncaught TypeError: Cannot read property ‘add’ of null エラー回避のために slick-initialized クラスの確認、 unslick 指定)

モーダルのような、初期状態で 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 が表示されない現象が発生します。

画像が多かったり重かったりすると出やすくなるかもしれません。

ページ再読み込みすれば大丈夫と言えば大丈夫ですが……今回のシチュエーションではこの頻度がわりと大きく、無視できない状態でした。そのため、さらに対策を講じる必要がありました。

そこで、以下のような処理を行うことにしました。

  • モーダルを開くボタンクリック時
    1. click イベント発火
    2. setTimeout で 0.3s 後に Slick 実行 (const slickInit = setTimeout())
      • .not('.slick-initialized') 付き
  • モーダルを閉じる時
    1. hidden.bs.modal イベント発火(Bootstrap 独自イベント)
    2. jqueryObject.slick('unslick');unslick 発動、 Slick を解除
    3. clearTimeout(slickInit); によって見初期化であろうとも上述「モーダルを開くボタンクリック時 2.」の動作を解除
    4. 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」のケース

ここまでやって、ようやく納得のいく挙動になりました。思ったよりも紆余曲折ありました……。

参考

Slick

lazyLoad

setPotion

Uncaught TypeError: Cannot read property ‘add’ of null

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。