Shopify商品閲覧履歴の実装方法２

以前紹介した、<a href="https://qiita.com/Shopify_ojisan/items/83021d9b1a382c9a8c16">Shopify商品閲覧履歴の実装方法</a>なのですが…少し難点がありまして、他の商品カードと同じスタイルで表示しようとすると、JSで処置を追加してあげないといけなくなります。
どうゆうことかというと、「販売開始から特定の期間だけ新着とか出したい。」みたいなことがあると、その表示をするための処理を別で組まないといけなくなります。
また、その処理に変更があった場合、商品一覧などに使っている処理と、最近チェックした商品の処理と２ヶ所変更して、２ヶ所確認して…。
めっちゃ面倒だぁーー！
ということで、商品一覧で使うようなカードをそのまま使う形のコードを紹介しようと思います。
仕組み自体は、色々なところで応用が効くかと思うので、いろんな箇所で利用されてみてください。

## recently-product-card.liquidセクションファイルの作成
```
<div class="js-recentlySectionId" data-recently-section-id="{{ section.id }}"  style="display: none;">
  {% if template contains 'product' %}
    {% render 'product-card', product: product %}
  {% endif %}
</div>
{% schema %}
{
  "name": "最近チェックした商品テンプレート",
  "class": "カードの外側にあたるクラス名を指定"
}
{% endschema %}
```
まずは、recently-product-card.liquidファイルをセクションフォルダーに作成します。この時、スキーマのクラス名にカードの一つ外側にあたるクラス名を指定します。js-recentlySectionIdの<div>タグに商品カードの基準となるクラス名を指定します。
renderで読み込んでいるファイル名はテーマに合わせて変更してください。

## theme.liquidファイル内で、recently-product-card.liquidを読み込む
```
{% section 'recently-product-card' %}
<script type='text/javascript'>Shopify.recentlySectionId = document.querySelector('.js-recentlySectionId').dataset.recentlySectionId;</script>
```
上記のコードをtheme.liquid内のなるべく下の方に読み込みます。
早い段階で読み込む必要がないので、なるべく下で大丈夫です。


## assetsフォルダー内にjquery.products.jsを作成します
```
/**
 * Cookie plugin
 *
 * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

jQuery.cookie=function(b,j,m){if(typeof j!="undefined"){m=m||{};if(j===null){j="";m.expires=-1}var e="";if(m.expires&&(typeof m.expires=="number"||m.expires.toUTCString)){var f;if(typeof m.expires=="number"){f=new Date();f.setTime(f.getTime()+(m.expires*24*60*60*1000))}else{f=m.expires}e="; expires="+f.toUTCString()}var l=m.path?"; path="+(m.path):"";var g=m.domain?"; domain="+(m.domain):"";var a=m.secure?"; secure":"";document.cookie=[b,"=",encodeURIComponent(j),e,l,g,a].join("")}else{var d=null;if(document.cookie&&document.cookie!=""){var k=document.cookie.split(";");for(var h=0;h<k.length;h++){var c=jQuery.trim(k[h]);if(c.substring(0,b.length+1)==(b+"=")){d=decodeURIComponent(c.substring(b.length+1));break}}}return d}};

/**
 * Module to show Recently Viewed Products
 *
 * Copyright (c) 2014 Caroline Schnapp (11heavens.com)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

Shopify.Products = (function() {

  var config = {
    howManyToShow: 3,
    howManyToStoreInMemory: 10,
    wrapperId: 'recently-viewed-products',
    templateId: 'recently-viewed-product-template',
    onComplete: null,
    recentlySectionId: null
  };

  var productHandleQueue = [];
  var wrapper = null;
  var template = null;
  var shown = 0;

  var cookie = {
    configuration: {
      expires: 90,
      path: '/',
      domain: window.location.hostname
    },
    name: 'shopify_recently_viewed',
    write: function(recentlyViewed) {
      jQuery.cookie(this.name, recentlyViewed.join(' '), this.configuration);
    },
    read: function() {
      var recentlyViewed = [];
      var cookieValue = jQuery.cookie(this.name);
      if (cookieValue !== null) {
        recentlyViewed = cookieValue.split(' ');
      }
      return recentlyViewed;
    },
    destroy: function() {
      jQuery.cookie(this.name, null, this.configuration);
    },
    remove: function(productHandle) {
      var recentlyViewed = this.read();
      var position = jQuery.inArray(productHandle, recentlyViewed);
      if (position !== -1) {
        recentlyViewed.splice(position, 1);
        this.write(recentlyViewed);
      }
    }
  };

  var finalize = function() {
    wrapper.show();
    // If we have a callback.
    if (config.onComplete) {
      try { config.onComplete() } catch (error) { }
    }
  };

  var moveAlong = function() {
    if (productHandleQueue.length && shown < config.howManyToShow) {
      fetch(`/products/${productHandleQueue[0]}?sections=${config.recentlySectionId}`)
        .then((r) => r.json())
        .then(
          (d) => {
            // console.log(d);
            // console.log(d[`${config.recentlySectionId}`]);
            wrapper.append(d[`${config.recentlySectionId}`]);
            productHandleQueue.shift();
            shown++;
            moveAlong();
          },
          (e) => {
            console.log(e);
            cookie.remove(productHandleQueue[0]);
            productHandleQueue.shift();
            moveAlong();
          }
      );
    }
    else {
      finalize();
    }
  };

  return {

    resizeImage: function(src, size) {
      if (size == null) {
        return src;
      }

      if (size == 'master') {
        return src.replace(/http(s)?:/, "");
      }

      var match  = src.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?/i);

      if (match != null) {
        var prefix = src.split(match[0]);
        var suffix = match[0];

        return (prefix[0] + "_" + size + suffix).replace(/http(s)?:/, "")
      } else {
        return null;
      }
    },

    showRecentlyViewed: function(params) {
      var params = params || {};

      // Update defaults.
      jQuery.extend(config, params);

      // Read cookie.
      productHandleQueue = cookie.read();

      // Template and element where to insert.
      wrapper = jQuery('#' + config.wrapperId);

      // How many products to show.
      config.howManyToShow = Math.min(productHandleQueue.length, config.howManyToShow);

      // If we have any to show.
      if (config.howManyToShow && wrapper.length) {
        // Getting each product with an Ajax call and rendering it on the page.
        moveAlong();
      }
    },

    getConfig: function() {
      return config;
    },

    clearList: function() {
      cookie.destroy();
    },

    recordRecentlyViewed: function(params) {

      var params = params || {};

      // Update defaults.
      jQuery.extend(config, params);

      // Read cookie.
      var recentlyViewed = cookie.read();

      // If we are on a product page.
      if (window.location.pathname.indexOf('/products/') !== -1) {

        // What is the product handle on this page.
        var productHandle = window.location.pathname.match(/\/products\/([a-z0-9\-]+)/)[1];
        // In what position is that product in memory.
        var position = jQuery.inArray(productHandle, recentlyViewed);
        // If not in memory.
        if (position === -1) {
          // Add product at the start of the list.
          recentlyViewed.unshift(productHandle);
          // Only keep what we need.
          recentlyViewed = recentlyViewed.splice(0, config.howManyToStoreInMemory);
        }
        else {
          // Remove the product and place it at start of list.
          recentlyViewed.splice(position, 1);
          recentlyViewed.unshift(productHandle);
        }

        // Update cookie.
        cookie.write(recentlyViewed);

      }
    }
  };
})();
```
上記の内容をassetsフォルダ内に作成し、以下のコードをtheme.liquidのhead内の{{ content_for_header }}より下に追加する
※既に別でjQueryを読み込んでいる場合は、削除してください。
```
{%- comment -%} 最近チェックした商品 読み込み {%- endcomment -%}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
{{ "jquery.products.js" | asset_url | script_tag }}
```

## main-product.liquidに以下のコードを書き込む
```
{%- comment -%} 最近チェックした商品 書き込み {%- endcomment -%}
<script type="text/javascript">
Shopify.Products.recordRecentlyViewed();
</script>
```
商品ページが読み込まれた時に、商品データをクッキーに入力するコードになります。

## sectionsフォルダにrecently-prouct-slider.liquidを作成するし以下のコードを書き込む
```
{% comment -%}
------------------------------------
最近チェックした商品
------------------------------------
{%- endcomment -%}

<!-- product-recently-viewed -->
<div class="product-recently-viewed collection-slider">
  <div class="container">
    <h2 class="product-recently-viewed__title section__title">最近チェックした商品</h2>

    <ul id="js-recentlySliderBody" class="js-recentlySlider collection-slider__list product-recently-viewed__list">

      {% unless customer %}
        {%- comment -%} 未ログイン時 {%- endcomment -%}
        <p id="wishlist-empty-text"
          style="padding: 60px 0; text-align: center;min-height: 320px; display: flex; align-items: center;">
          まだ最近チェックした商品がありません
        </p>
      {% endunless %}

    </ul>
  </div>
</div>

{%- comment -%} ログイン時のみ発動 {%- endcomment -%}
{% if customer %}
  {%- comment -%} コントロールjs {%- endcomment -%}
  <script>
    window.addEventListener( 'load', function(){
      const recentlyViewController = (function(){

        // クッキーからデータを取得する
        let recentlyItems = jQuery.cookie('shopify_recently_viewed');

        // 閲覧履歴がある時 クッキーにデータがある時
        if(recentlyItems != null ) {
          Shopify.Products.showRecentlyViewed({
            howManyToShow: 8,// 表示する商品数を変更できます。
            wrapperId: 'js-recentlySliderBody',
            recentlySectionId: Shopify.recentlySectionId,
            onComplete: function() {
              // データが読み込まれた後発火させたい時

              // 非表示にしているアイテムを表示させる
              let lists = Array.from(document.querySelectorAll('li.recently-slider__item'));
              lists.forEach((item) => {
                item.style.display = "block";
              });
            }
          });
        } else {
          // 閲覧履歴がない時の処理 クッキーにデータがない時
          document.getElementById('js-recentlySliderBody').insertAdjacentHTML("afterbegin",`<p id="wishlist-empty-text" style="padding: 60px 0; text-align: center;min-height: 320px; display: flex; align-items: center;">まだ最近チェックした商品がありません</p>`);
        }

      })();
    }, false);
  </script>
{% endif %}

{% schema %}
{
  "name": "Product Recently Viewed",
  "settings": [
  ],
  "presets": [
    {
      "name": "Product Recently Viewed"
    }
  ]
}
{% endschema %}
```

## 注意点
商品詳細ページで読み込まれている商品カードは表示させないようにstyle="display:none;"を指定してあります。
なので、最近チェックした商品内の商品カードは、表示されるようにスタイルを指定してやる必要があります！

## 仕組み
どういう仕組みかというと、<a href="https://shopify.dev/api/section-rendering">セクションのレンダリング機能</a>を使用して商品カードを呼び出しています。
本家のjquery.products.jsの商品データを取得している処理を変更し、商品ページに読み込まれている商品に付随した商品カードデータを呼び出して、閲覧履歴内で表示しています。（fetch(`/products/${productHandleQueue[0]}?sections=${config.recentlySectionId}`)の部分）
　
なので、最初に作成したrecently-product-card.liquid内で呼び出している{% render 'product-card', product: product %}をテーマ内で統一してあげることで、スタイルや特定の条件で表示するラベルのコントロールなどを一元管理することができるようになります。

## まとめ
面倒くさがりが産んだ、仕組みですが、今回学んだjquery.product.jsの仕組みとセクションのレンダリングを使用することで、アプリを使わずに機能を拡張することができそうです。
例えば、商品一覧の商品カードをクリックして、商品詳細のモーダルが開くとか…。商品詳細でカートに追加ボタンを押すことでサイドからカートの中身が出てくるなど…。jquery.product.jsは可変してお気に入り機能にしている人がいるとかいないとか…。
また、一段とLiquidが面白くなりました！
機会があれば、その内容も記事にしてみようと思います。
　
あとは、お決まりの…
ARCHETYPでは、Shopifyのテーマ開発やアプリ開発をやりたいエンジニアを募集しております！
気になった方はお気軽にエントリーをお待ちしております！
https://www.archetyp.jp/recruit/
noteでブログもやってるので良かったらチェックしてください！
https://note.com/archetyp