tag:crieit.net,2005:https://crieit.net/tags/%E3%83%A2%E3%83%BC%E3%83%80%E3%83%AB/feed 「モーダル」の記事 - Crieit Crieitでタグ「モーダル」に投稿された最近の記事 2020-12-15T00:06:21+09:00 https://crieit.net/tags/%E3%83%A2%E3%83%BC%E3%83%80%E3%83%AB/feed tag:crieit.net,2005:PublicArticle/16364 2020-12-15T00:06:21+09:00 2020-12-15T00:06:21+09:00 https://crieit.net/posts/note-of-slick-in-modal-20201215 モーダルの中で Slick を動かす場合の諸注意(setPotion指定、 Uncaught TypeError: Cannot read property 'add' of null エラー回避のために slick-initialized クラスの確認、 unslick 指定) <p>モーダルのような、初期状態で display: none; がかかっているような要素の中で Slick を動かす場合にハマった事項のメモです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://kenwheeler.github.io/slick/">slick - the last carousel you'll ever need</a></li> </ul> <h2 id="やりたかったこと"><a href="#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">やりたかったこと</a></h2> <ul> <li>lazyLoad</li> <li>モーダルの中で Slick を動かす</li> <li>複数のモーダルがある(それぞれに Slick あり)</li> </ul> <p>シチュエーションとしてはこんなところ。</p> <h2 id="サンプル"><a href="#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB">サンプル</a></h2> <p>以下に一つずつ解説してきますが、動作確認のためのサンプルを置いておきます。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="http://scrunchy56.starfree.jp/test_slick/">Slick Test</a></li> </ul> <p>リポジトリも。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="http://scrunchy56.starfree.jp/test_slick/">Slick Test</a></li> </ul> <h2 id="1. lazyLoad"><a href="#1.+lazyLoad">1. lazyLoad</a></h2> <p>まずは <code>lazyLoad</code> ですが、これについては <a target="_blank" rel="nofollow noopener" href="https://kenwheeler.github.io/slick/">slick</a> で用意されている <code>lazyLoad</code> プロパティが使えます。</p> <pre><code class="javascript">$('slick').slick({ lazyLoad: 'ondemand' }); </code></pre> <p>プロパティはスライドする度に画像を読み込む <code>ondemand</code> とページ読み込み後に読み込む <code>progressive</code> の2つ。</p> <p>また、 <code>lazyLoad</code> で画像が読み込まれたときに発火するイベント <code>lazyLoaded</code> とエラー時に発火する <code>lazyLoadError</code> があるので、ケースによってはこれらも利用できそう。</p> <pre><code class="javascript">$('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' }); </code></pre> <p>ちなみに、 <code>lazyLoad</code> は JS 側だけではなく、 <strong>HTML 側にも既述が必要</strong>です。</p> <pre><code class="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> </code></pre> <p><code>img</code> タグの <code>src</code> 属性は画像が読み込まれるまでのダミー画像の指定、本番のスライドショーで表示させたい画像は<strong>独自の <code>data-lazy</code> 属性で指定</strong>、となります。</p> <p>ここまでがサンプルの「Slick 1」のケース。</p> <h2 id="2. モーダルの中で Slick を動かす"><a href="#2.+%E3%83%A2%E3%83%BC%E3%83%80%E3%83%AB%E3%81%AE%E4%B8%AD%E3%81%A7+Slick+%E3%82%92%E5%8B%95%E3%81%8B%E3%81%99">2. モーダルの中で Slick を動かす</a></h2> <h3 id="2.1. 普通に Slick"><a href="#2.1.+%E6%99%AE%E9%80%9A%E3%81%AB+Slick">2.1. 普通に Slick</a></h3> <p>さてここからが本題。</p> <p>例として Bootstrap 4 のモーダルコンポーネントを利用しますが、モーダルのような初期状態で <code>display: none;</code> となっている要素の中に Slick をセットすると動かないようです。</p> <p>原因としては、親要素が <code>display: none;</code> なので、 Slick の要素が <code>width: 0px;</code> で潰れてしまうようです(ついでに <code>height: 1px;</code> でもあった)。</p> <p>※サンプルの「Slick 2」の「モーダルを開く (setPotion 指定なし)」のケース</p> <h3 id="2.2. setPotion"><a href="#2.2.+setPotion">2.2. setPotion</a></h3> <p>ここで Slick 公式やいくつかの記事を見ると「 <code>setPotion</code> を指定すると良い」という記述が散見されます。</p> <p>これは <code>setPotion</code> を指定したタイミングで Slick 要素のサイズを再計算してくれるようです。</p> <p>今回は「モーダルが開いた時」なので、ボタンの <code>click</code> イベントにバインドすると良さそうです。これならば複数モーダル + Slick でもクリックされたモーダルのみ Slick が動くようになるので処理も軽減できます。</p> <p>注意点としては、 <code>click</code> イベント直後だとまだモーダルがフェードのエフェクトで表示されている途中なのでせっかく再計算しても潰れてしまう可能性があること。</p> <p>そのため、 <code>setTimeout</code> で処理する時間を指定しましょう。</p> <pre><code class="javascript">$('#setPotionModalButton').on('click', function () { // 0.3s 後に setPosition 付きで Slick 実行 const slickInit = setTimeout(() => { const $setPotion = $('#setPotion'); // Slick $setPotion.slick({ lazyLoad: 'ondemand' }); $setPotion.slick('setPosition'); }, 300); }); </code></pre> <p>※サンプルの「Slick 2」の「モーダルを開く (setPotion 指定あり)」のケース</p> <p>これでモーダルの中でも開くようになりました。よしよし……<strong>ではなかった</strong>。</p> <h2 id="3. Uncaught TypeError: Cannot read property 'add' of null エラーの回避 → .not('.slick-initialized')"><a href="#3.+Uncaught+TypeError%3A+Cannot+read+property+%27add%27+of+null+%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AE%E5%9B%9E%E9%81%BF+%E2%86%92+.not%28%27.slick-initialized%27%29">3. Uncaught TypeError: Cannot read property 'add' of null エラーの回避 → .not('.slick-initialized')</a></h2> <p>先程のモーダル、1回開くだけならば良いのですが、2回目以降モーダルを開こうとすると</p> <blockquote> <p>Uncaught TypeError: Cannot read property 'add' of null</p> </blockquote> <p>というエラーが発せられます。既に Slick が動いているところに、 <code>click</code> イベントでもう一度 Slick をバインドしようとしたので怒られてしまったわけです。</p> <p>回避策としては、 <code>.not('.slick-initialized')</code> を追加すること。</p> <p>Slick は初期化を完了すると該当要素に <code>slick-initialized</code> を付与するため、 <code>.not('.slick-initialized')</code> で「まだ Slick の初期化が済んでいない要素」と指定することでエラーを回避できる、ということですね。</p> <pre><code class="javascript">$('#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); }); </code></pre> <p>※サンプルの「Slick 3」のケース</p> <p><code>console.log()</code> 等で拾うと2回目以降 <code>$unInitialized.not('.slick-initialized')</code> は <code>undefined</code> になっていますが、エラーは回避できています。これで良し……<strong>ではなかった</strong>。</p> <h2 id="4. unslick"><a href="#4.+unslick">4. unslick</a></h2> <p>稀ですが、高速でモーダルを開いたり閉じたりして(あるいは何らかの理由で Slick の初期化が遅れて)しまうと、それ以降モーダルを開いても正常に Slick が表示されない現象が発生します。</p> <p>画像が多かったり重かったりすると出やすくなるかもしれません。</p> <p>ページ再読み込みすれば大丈夫と言えば大丈夫ですが……今回のシチュエーションではこの頻度がわりと大きく、無視できない状態でした。そのため、さらに対策を講じる必要がありました。</p> <p>そこで、以下のような処理を行うことにしました。</p> <ul> <li>モーダルを開くボタンクリック時 <ol> <li><code>click</code> イベント発火</li> <li><code>setTimeout</code> で 0.3s 後に Slick 実行 (<code>const slickInit = setTimeout()</code>) <ul> <li><code>.not('.slick-initialized')</code> 付き</li> </ul></li> </ol></li> <li>モーダルを閉じる時 <ol> <li><code>hidden.bs.modal</code> イベント発火(Bootstrap 独自イベント)</li> <li><code>jqueryObject.slick('unslick');</code> で <code>unslick</code> 発動、 Slick を解除</li> <li><code>clearTimeout(slickInit);</code> によって見初期化であろうとも上述「モーダルを開くボタンクリック時 2.」の動作を解除</li> <li><code>modalButtonjqueryObject.off('hidden.bs.modal');</code> で<code>hidden.bs.modal</code> イベントもバインド解除</li> </ol></li> </ul> <pre><code class="javascript">// 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'); }); }); </code></pre> <p>※サンプルの「Slick 4」のケース</p> <p>ここまでやって、ようやく納得のいく挙動になりました。思ったよりも紆余曲折ありました……。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="Slick"><a href="#Slick">Slick</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://kenwheeler.github.io/slick/">slick - the last carousel you'll ever need</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://tr.you84815.space/slick/">slickドキュメント翻訳 | slick - にほんご。</a></li> </ul> <h3 id="lazyLoad"><a href="#lazyLoad">lazyLoad</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://takblog.site/web/?p=63">「slick.js」の使い方応用編 | takblog</a></li> </ul> <h3 id="setPotion"><a href="#setPotion">setPotion</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/mimoe/items/c4f4754815525b08041e">slick.jsをモーダルなど、最初非表示のもので使うとき - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.a-in-hello.world/tab_slick.html">タブ切り替え時にslickスライダーが崩れる問題を3行で解決 | "A" In Hello,World!</a></li> </ul> <h3 id="Uncaught TypeError: Cannot read property 'add' of null"><a href="#Uncaught+TypeError%3A+Cannot+read+property+%27add%27+of+null">Uncaught TypeError: Cannot read property 'add' of null</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.proglad.tokyo/2017/08/slickjs-uncaught-typeerror-cannot-read.html">slick.js で Uncaught TypeError: Cannot read property 'add' of null のエラーがでる | Proglad</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.d-wood.com/blog/2019/02/19_11196.html">スマートフォンでのみ Slick スライダーを表示させる | deadwood</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ituki_b/items/cdca1b4914428f8e095c">slickでレスポンシブ覚書 - Qiita</a></li> </ul> arm-band