アプリなしで、閲覧履歴セクションを自作し、card-product (product-card) を使いたい。
これを実現するのが、普通の方法では難しかった。
なぜなら、クッキーなり、ブラウザのlocalStorageに保存した情報をliquidコードで解釈するところが最大の障壁になるため、商品カード(snippet/card-product.liquid)に閲覧履歴情報内のproduct情報を渡してrenderで呼ぶという処理ができない。
だから多くの閲覧履歴セクションを自作の方法では、この問題を回避するために、card-productをrenderすることを諦め、商品カードをjsで擬似的にhtmlレンダリングする手法を取っている記事がほとんどだし、筆者もそうしていた。
ただ、この方法では、他の仕様変更で、商品カード自体の表示を拡張するときに、jsでレンダリングするhtmlコードにも改良をしないといけなくなる。
かつ、この手法はShopifyのテーマに見られる便利な「商品カード(card-prodcut.liquid)」の概念に反して、ソースコードの見通しも悪くなる。
かつ、筆者はhtmlレンダリングだけでは実現できない機能にもぶつかった。
Judge.meのレビューレーティングの表示が、judge.meのアプリ経由でないと呼び出しできないためだ。
なので、なんとかして他の商品カードを表示しているセクション同様に、商品カードにproduct情報を渡して表示する方法でないと無理、となった。
これを解決するために、以下の記事を参考にし、ShopifyのSection Rendering APIというものを利用する形になった。
参考になった記事
https://qiita.com/Shopify_ojisan/items/f66b321dfdec4b43ad23
素晴らしい気づきを得られる本記事の著者さまには頭が上がりません。。!ありがとうございます。
上記の記事をさらに以下の点で改良させていただきました。
- Cookieにでなくブラウザの「localStorage」に閲覧履歴情報を保存する仕様に変更
- セクション上で閲覧履歴のデスクトップでの商品表示列数を変更できるようにスキーマをセット
1. section/recently-product-card.liquidを作成
<div class="js-recentlySectionId" data-recently-section-id="{{ section.id }}">
{% if template contains 'product' %}
{% render 'card-product',
card_product: product,
show_rating: true,
show_quick_add: false
%}
{% endif %}
</div>
{% schema %}
{
"name": "最近見た商品(埋め込み用セクション)",
"class": "section"
}
{% endschema %}
2. section/recently-prouct-slider.liquidを作成
{% comment -%}
------------------------------------
最近チェックした商品
------------------------------------
{%- endcomment -%}
{%- comment -%} コントロールjs {%- endcomment -%}
<script>
window.addEventListener('load', function(){
const recentlyViewController = (function(){
// ローカルストレージからデータを取得する
let recentlyItems = localStorage.getItem('shopify_recently_viewed');
recentlyItems = recentlyItems ? JSON.parse(recentlyItems) : [];
// 閲覧履歴がある時 ローカルストレージにデータがある時
if(recentlyItems.length > 0) {
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.querySelector('.recently_product_slider_section').style.display = 'none';
}
})();
}, false);
</script>
{{ 'component-card.css' | asset_url | stylesheet_tag }}
{{ 'component-price.css' | asset_url | stylesheet_tag }}
{{ 'component-slider.css' | asset_url | stylesheet_tag }}
{{ 'template-collection.css' | asset_url | stylesheet_tag }}
{%- style -%}
.section-{{ section.id }}-padding {
padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
}
@media screen and (min-width: 750px) {
.section-{{ section.id }}-padding {
padding-top: {{ section.settings.padding_top }}px;
padding-bottom: {{ section.settings.padding_bottom }}px;
}
}
{%- endstyle -%}
<div class="page-width section-{{ section.id }}-padding">
<ul style="padding-inline-start :0 !important;" id="js-recentlySliderBody" class="collection-slider__list product-recently-viewed__list tw-gap-[2rem] tw-grid product-grid contains-card contains-card--product{% if settings.card_style == 'standard' %} contains-card--standard{% endif %} tw-grid-cols-{{ section.settings.columns_desktop }}{% if section.settings.collection == blank %} {% if show_mobile_slider == false %}max-md:tw-grid-cols-2{% endif %}{% else %} grid--{{ section.settings.columns_mobile }}-col-tablet-down{% endif %}"
role="list"
aria-label="{{ 'general.slider.name' | t }}"
>
</ul>
{%- if show_mobile_slider or show_desktop_slider -%}
<div class="slider-buttons no-js-hidden">
<button
type="button"
class="slider-button slider-button--prev"
name="previous"
aria-label="{{ 'general.slider.previous_slide' | t }}"
aria-controls="Slider-{{ section.id }}"
>
{% render 'icon-caret' %}
</button>
<div class="slider-counter caption">
<span class="slider-counter--current">1</span>
<span aria-hidden="true"> / </span>
<span class="visually-hidden">{{ 'general.slider.of' | t }}</span>
<span class="slider-counter--total">{{ products_to_display }}</span>
</div>
<button
type="button"
class="slider-button slider-button--next"
name="next"
aria-label="{{ 'general.slider.next_slide' | t }}"
aria-controls="Slider-{{ section.id }}"
>
{% render 'icon-caret' %}
</button>
</div>
{%- endif -%}
</div>
{% schema %}
{
"name": "最近閲覧した商品",
"tag": "section",
"class": "recently_product_slider_section",
"disabled_on": {
"groups": ["header", "footer"]
},
"settings": [
{
"type": "range",
"id": "products_to_show",
"min": 2,
"max": 25,
"step": 1,
"default": 4,
"label": "t:sections.featured-collection.settings.products_to_show.label"
},
{
"type": "range",
"id": "columns_desktop",
"min": 1,
"max": 5,
"step": 1,
"default": 4,
"label": "t:sections.featured-collection.settings.columns_desktop.label"
},
{
"type": "range",
"id": "padding_top",
"min": 0,
"max": 100,
"step": 4,
"unit": "px",
"label": "t:sections.all.padding.padding_top",
"default": 36
},
{
"type": "range",
"id": "padding_bottom",
"min": 0,
"max": 100,
"step": 4,
"unit": "px",
"label": "t:sections.all.padding.padding_bottom",
"default": 36
},
{
"type": "color_scheme",
"id": "color_scheme",
"label": "t:sections.all.colors.label",
"info": "t:sections.all.colors.has_cards_info",
"default": "scheme-1"
],
"presets": [
{
"name": "最近閲覧した商品"
}
]
}
{% endschema %}
3. assets/jquery.products.jsを作成
/**
* 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 localStorageHandler = {
name: 'shopify_recently_viewed',
write: function(recentlyViewed) {
localStorage.setItem(this.name, JSON.stringify(recentlyViewed));
},
read: function() {
var recentlyViewed = localStorage.getItem(this.name);
return recentlyViewed ? JSON.parse(recentlyViewed) : [];
},
destroy: function() {
localStorage.removeItem(this.name);
},
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) { console.log(error); }
}
};
var moveAlong = function() {
if (productHandleQueue.length && shown < config.howManyToShow) {
fetch(`/products/${productHandleQueue[0]}?sections=${config.recentlySectionId}`)
.then((r) => r.json())
.then(
(d) => {
wrapper.append(d[`${config.recentlySectionId}`]);
productHandleQueue.shift();
shown++;
moveAlong();
},
(e) => {
console.log(e);
localStorageHandler.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 from localStorage.
productHandleQueue = localStorageHandler.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() {
localStorageHandler.destroy();
},
recordRecentlyViewed: function(params) {
//window.alert('recordRecentlyViewed function has been called.');
var params = params || {};
// Update defaults.
jQuery.extend(config, params);
// Read from localStorage.
var recentlyViewed = localStorageHandler.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];
console.log('Product handle:', productHandle);
// In what position is that product in memory.
var position = jQuery.inArray(productHandle, recentlyViewed);
console.log('Product position in list:', position);
// If not in memory.
if (position === -1) {
// Add product at the start of the list.
recentlyViewed.unshift(productHandle);
console.log('Product added to recently viewed list:', recentlyViewed);
// 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);
console.log('Product reordered in recently viewed list:', recentlyViewed);
}
// Update localStorage.
localStorageHandler.write(recentlyViewed);
console.log('LocalStorage updated with:', recentlyViewed);
}
}
};
})();
4. theme.liquid内の</head>の手前に以下を設置
{%- comment -%} 最近チェックした商品 セッティング{%- endcomment -%}
<!-- jqueryを他の箇所で読み込みしていないものと仮定しています -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js" defer="defer"></script>
<script src="{{ "jquery.products.js" | asset_url }}" defer="defer"></script>
<script>
window.addEventListener('load', function() {
Shopify.recentlySectionId = document.querySelector('.js-recentlySectionId').dataset.recentlySectionId;
});
</script>
{%- comment -%} 最近チェックした商品 セッティング{%- endcomment -%}
5. theme.liquid内の</body>の手前に以下を設置
{%- comment -%} 最近チェックした商品 読み込み用インスタンス {%- endcomment -%}
<div style="display: none;">
{% section 'recently-product-card' %}
</div>
{%- comment -%} 最近チェックした商品 読み込み用インスタンス {%- endcomment -%}
6. main-product.liquidの冒頭に以下を設置
{%- comment -%} 最近チェックした商品 書き込み {%- endcomment -%}
<script>
window.addEventListener('load', function() {
Shopify.Products.recordRecentlyViewed();
});
</script>
{%- comment -%} 最近チェックした商品 書き込み {%- endcomment -%}
めちゃくちゃニッチすぎる備忘録でした!