Skip to content

Instantly share code, notes, and snippets.

@petercossey
Created June 27, 2023 02:09
Show Gist options
  • Save petercossey/ef62f50c9be0eb24091469e1ee37e1cf to your computer and use it in GitHub Desktop.
Save petercossey/ef62f50c9be0eb24091469e1ee37e1cf to your computer and use it in GitHub Desktop.
Render catalog price for products in a pick-list option modifier using client-side Storefront GraphQL API
{{#partial "page_bottom"}}
<script>
// Get product IDs for GraphQL query on product prices.
const pickListProductIds = []
{{#each product.options}}
{{#if partial '===' 'product-list'}}
pickListProductIds.push({{pluck values 'data'}})
{{/if}}
{{/each}}
// Filter out duplicate product IDs
const uniquePickListProductIds = [... new Set(pickListProductIds)]
let fetchedPickList = null;
// See https://developer.bigcommerce.com/api-docs/storefront/graphql/graphql-api-overview#querying-within-a-bigcommerce-storefront
fetch('/graphql', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer {{ settings.storefront_api.token }}'
},
body: JSON.stringify({
query: `query PickListPrices {
site {
products (entityIds: [${uniquePickListProductIds}]) {
edges {
node {
entityId
prices(includeTax: true) {
price {
value
}
}
}
}
}
}
}`
})
})
.then(res => res.json())
.then(data => {
fetchedPickList = data;
console.log('Pick list products fetched:', fetchedPickList);
// Use Array.prototype.reduce() to create a key/value object made of of productId: price
pickListPriceMap = fetchedPickList.data.site.products.edges.reduce((obj, item) => {
obj[item.node.entityId] = item.node.prices.price.value;
return obj;
}, {});
// Update the pick-list label elements with prices from GraphQL
const pickListNodes = document.querySelectorAll('[data-product-option-type="product-list-item"]');
pickListNodes.forEach(node => {
const priceElement = document.createElement('span');
priceElement.style.marginLeft = '0.5rem';
// update price
const productId = node.getAttribute('data-product-id');
priceElement.textContent = (pickListPriceMap[productId] > 0) ? '$' + parseFloat(pickListPriceMap[productId]).toFixed(2) : '$0.00';
node.appendChild(priceElement);
});
})
.catch(err => console.error(err));
</script>
{{/partial}}
<div class="form-field" data-product-attribute="product-list" role="radiogroup" aria-labelledby="product-list-label">
<label class="form-label form-label--alternate form-label--inlineSmall" id="product-list-label">
{{display_name}}:
<span data-option-value></span>
{{> components/common/requireness-msg}}
</label>
{{#if values.0.image}}
<ul class="productOptions-list">
{{#unless required}}
<li class="productOptions-list-item">
<input class="form-radio"
type="radio"
name="attribute[{{id}}]"
value=""
id="attribute_productlist_{{id}}_none"
checked="{{#if defaultValue '==' ''}}checked{{/if}}">
<label class="form-label" for="attribute_productlist_{{id}}_none">{{lang 'products.none'}}</label>
</li>
{{/unless}}
{{#each values}}
<li class="productOptions-list-item" data-product-attribute-value="{{id}}">
{{#if image}}
<figure class="productOptions-list-item-figure">
{{> components/common/responsive-img
image=image
class="productOptions-list-item-image"
lazyload='lazyload+lqip'
}}
</figure>
{{/if}}
<div class="productOptions-list-item-content">
<input
class="form-radio"
type="radio"
name="attribute[{{../id}}]"
value="{{id}}"
id="attribute_productlist_{{../id}}_{{id}}"
{{#if selected}}
checked
data-default
{{/if}}
{{#if ../required}}required{{/if}}>
<label data-product-option-type="product-list-item" data-product-id="{{data}}" data-product-attribute-value="{{id}}" class="form-label" for="attribute_productlist_{{../id}}_{{id}}">{{label}}</label>
</div>
</li>
{{/each}}
</ul>
{{else}}
{{#unless required}}
<input class="form-radio"
type="radio"
name="attribute[{{id}}]"
value="0"
id="attribute_productlist_0_{{id}}"
{{#if defaultValue '==' 0}}checked{{/if}}>
<label class="form-label" for="attribute_productlist_0_{{id}}">{{lang 'products.none'}}</label>
{{/unless}}
{{#each values}}
<input
class="form-radio"
type="radio"
name="attribute[{{../id}}]"
value="{{id}}"
id="attribute_productlist_{{../id}}_{{id}}"
{{#if selected}}checked{{/if}}
{{#if ../required}}required{{/if}}>
<label data-product-option-type="product-list-item" data-product-id="{{data}}" data-product-attribute-value="{{id}}" class="form-label" for="attribute_productlist_{{../id}}_{{id}}">{{label}}</label>
{{/each}}
{{/if}}
</div>
<div class="productView"
{{#if settings.data_tag_enabled}}
data-event-type="product"
data-entity-id="{{product.id}}"
data-name="{{product.title}}"
data-product-category="
{{#each product.category}}
{{#if @last}}{{this}}{{else}}{{this}}, {{/if}}
{{/each}}"
data-product-brand="{{product.brand.name}}"
data-product-price="
{{#or customer (unless theme_settings.restrict_to_login)}}
{{#if product.price.with_tax}}
{{product.price.with_tax.value}}
{{else}}
{{product.price.without_tax.value}}
{{/if}}
{{else}}
{{lang 'common.login_for_pricing'}}
{{/or}}"
data-product-variant="single-product-option"{{/if}}>
{{#each product.reviews.messages}}
{{#if error}}
{{> components/common/alert/alert-error error}}
{{/if}}
{{#if success}}
{{> components/common/alert/alert-success success}}
{{/if}}
{{/each}}
<section class="productView-images" data-image-gallery>
{{!--
Note that these image sizes are coupled to image sizes used in /assets/js/theme/common/product-details.js
for variant/rule image replacement
--}}
{{#if product.images.length '>' 1 }}
{{> components/carousel-content-announcement}}
{{/if}}
<figure class="productView-image"
data-image-gallery-main
{{#if product.main_image}}
data-zoom-image="{{getImageSrcset product.main_image (cdn theme_settings.default_image_product) 1x=theme_settings.zoom_size }}"
{{/if}}
>
<div class="productView-img-container">
{{!-- Remove the surrounding a-element if there is no main image. --}}
{{#if product.main_image}}
<a href="{{getImageSrcset product.main_image (cdn theme_settings.default_image_product) 1x=theme_settings.zoom_size}}"
target="_blank">
{{/if}}
{{> components/common/responsive-img
image=product.main_image
class="productView-image--default"
fallback_size=theme_settings.product_size
lazyload=theme_settings.lazyload_mode
default_image=theme_settings.default_image_product
otherAttributes="data-main-image"
}}
{{!-- Remove the surrounding a-element if there is no main image. --}}
{{#if product.main_image}}
</a>
{{/if}}
</div>
</figure>
<ul class="productView-thumbnails"{{#gt product.images.length 5}} data-slick='{
"infinite": false,
"mobileFirst": true,
"dots": false,
"accessibility": false,
"slidesToShow": 5,
"slidesToScroll": 5
}'{{/gt}}>
{{#each product.images}}
<li class="productView-thumbnail">
<a
class="productView-thumbnail-link"
href="{{getImageSrcset this (cdn ../theme_settings.default_image_product) 1x=../theme_settings.zoom_size}}"
data-image-gallery-item
data-image-gallery-new-image-url="{{getImageSrcset this (cdn ../theme_settings.default_image_product) 1x=../theme_settings.product_size}}"
data-image-gallery-new-image-srcset="{{getImageSrcset this use_default_sizes=true}}"
data-image-gallery-zoom-image-url="{{getImageSrcset this (cdn ../theme_settings.default_image_product) 1x=../theme_settings.zoom_size}}"
>
{{> components/common/responsive-img
image=this
fallback_size=../theme_settings.productview_thumb_size
lazyload=../theme_settings.lazyload_mode
}}
</a>
</li>
{{/each}}
</ul>
</section>
<section class="productView-details product-data">
<div class="productView-product">
<h1 class="productView-title">{{product.title}}</h1>
{{#if product.brand}}
<h2 class="productView-brand">
<a href="{{product.brand.url}}"><span>{{product.brand.name}}</span></a>
</h2>
{{/if}}
{{#if product.call_for_price}}
<p class="productView-price">
<span>{{product.call_for_price}}</span>
</p>
{{/if}}
<div class="productView-price">
{{#or customer (if theme_settings.restrict_to_login '!==' true)}}
{{> components/products/price price=product.price}}
{{else}}
{{> components/common/login-for-pricing}}
{{/or}}
</div>
{{{region name="product_below_price"}}}
<div class="productView-rating">
{{#if settings.show_product_rating}}
{{> components/products/ratings rating=product.rating}}
{{#if product.num_reviews '>' 0}}
<a href="{{product.url}}{{#if is_ajax}}#product-reviews{{/if}}" id="productReview_link">
{{lang 'products.reviews.link_to_review' total=product.num_reviews}}
</a>
{{else}}
<span>{{lang 'products.reviews.link_to_review' total=product.num_reviews}}</span>
{{/if}}
{{/if}}
{{#if settings.show_product_reviews}}
<a href="{{product.url}}{{#if is_ajax}}#write_review{{/if}}"
class="productView-reviewLink productView-reviewLink--new"
{{#unless is_ajax}}data-reveal-id="modal-review-form"{{/unless}}
role="button"
>
{{lang 'products.reviews.new'}}
</a>
{{#unless is_ajax}}
{{> components/products/modals/writeReview}}
{{/unless}}
{{/if}}
</div>
{{product.detail_messages}}
<dl class="productView-info">
<dt class="productView-info-name sku-label"{{#unless product.sku}} style="display: none;"{{/unless}}>{{lang 'products.sku'}}</dt>
<dd class="productView-info-value" data-product-sku>{{product.sku}}</dd>
<dt class="productView-info-name upc-label"{{#unless product.upc}} style="display: none;"{{/unless}}>{{lang 'products.upc'}}</dt>
<dd class="productView-info-value" data-product-upc>{{product.upc}}</dd>
{{#if product.condition}}
<dt class="productView-info-name">{{lang 'products.condition'}}</dt>
<dd class="productView-info-value">{{product.condition}}</dd>
{{/if}}
{{#if product.availability}}
<dt class="productView-info-name">{{lang 'products.availability'}}</dt>
<dd class="productView-info-value">{{product.availability}}</dd>
{{/if}}
{{#all product.weight theme_settings.show_product_weight}}
<dt class="productView-info-name">{{lang 'products.weight'}}</dt>
<dd class="productView-info-value" data-product-weight>{{product.weight}}</dd>
{{/all}}
{{#all product.width product.height product.depth theme_settings.show_product_dimensions}}
<dt class="productView-info-name">{{lang 'products.width'}}</dt>
<dd class="productView-info-value" data-product-width>
{{product.width}}
{{#if settings.measurements.length '==' 'Centimeters'}}
({{lang 'products.measurement.metric'}})
{{else}}
({{lang 'products.measurement.imperial'}})
{{/if}}
</dd>
<dt class="productView-info-name">{{lang 'products.height'}}</dt>
<dd class="productView-info-value" data-product-height>
{{product.height}}
{{#if settings.measurements.length '==' 'Centimeters'}}
({{lang 'products.measurement.metric'}})
{{else}}
({{lang 'products.measurement.imperial'}})
{{/if}}
</dd>
<dt class="productView-info-name">{{lang 'products.depth'}}</dt>
<dd class="productView-info-value" data-product-depth>
{{product.depth}}
{{#if settings.measurements.length '==' 'Centimeters'}}
({{lang 'products.measurement.metric'}})
{{else}}
({{lang 'products.measurement.imperial'}})
{{/if}}
</dd>
{{/all}}
{{#if product.min_purchase_quantity}}
<dt class="productView-info-name">{{lang 'products.min_purchase_quantity'}}</dt>
<dd class="productView-info-value">{{lang 'products.purchase_units' quantity=product.min_purchase_quantity}}</dd>
{{/if}}
{{#if product.max_purchase_quantity}}
<dt class="productView-info-name">{{lang 'products.max_purchase_quantity'}}</dt>
<dd class="productView-info-value">{{lang 'products.purchase_units' quantity=product.max_purchase_quantity}}</dd>
{{/if}}
{{#if product.gift_wrapping_available}}
<dt class="productView-info-name">{{lang 'products.gift_wrapping'}}</dt>
<dd class="productView-info-value">{{lang 'products.gift_wrapping_available'}}</dd>
{{/if}}
{{#if product.shipping}}
{{#if product.shipping.calculated}}
<dt class="productView-info-name">{{lang 'products.shipping'}}</dt>
<dd class="productView-info-value">{{lang 'products.shipping_calculated'}}</dd>
{{else}}
{{#if product.shipping.price.value '===' 0}}
<dt class="productView-info-name">{{lang 'products.shipping'}}</dt>
<dd class="productView-info-value">{{lang 'products.shipping_free'}}</dd>
{{else}}
<dt class="productView-info-name">{{lang 'products.shipping'}}</dt>
<dd class="productView-info-value">{{lang 'products.shipping_fixed' amount=product.shipping.price.formatted}}</dd>
{{/if}}
{{/if}}
{{/if}}
{{#if settings.bulk_discount_enabled}}
<div class="productView-info-bulkPricing">
{{> components/products/bulk-discount-rates bulk_discount_rates=product.bulk_discount_rates}}
</div>
{{/if}}
{{#filter product.custom_fields 'Product spec sheet' property='name'}}
<div style="margin: 1rem 0; padding: 0.5rem; border: 1px dashed grey;">Download: <a href="{{value}}">Product spec sheet PDF</a></div>
{{/filter}}
{{#if theme_settings.show_custom_fields_tabs '!==' true}}
{{#each product.custom_fields}}
{{!-- do not display the 'Product spec sheet' custom field here --}}
{{#isnt name 'Product spec sheet'}}
<dt class="productView-info-name">{{name}}:</dt>
<dd class="productView-info-value">{{{ sanitize value}}}</dd>
{{/isnt}}
{{/each}}
{{/if}}
</dl>
</div>
</section>
<section class="productView-details product-options">
<div class="productView-options">
{{#if product.release_date }}
<p>{{product.release_date}}</p>
{{/if}}
<form class="form" method="post" action="{{product.cart_url}}" enctype="multipart/form-data"
data-cart-item-add>
<input type="hidden" name="action" value="add">
<input type="hidden" name="product_id" value="{{product.id}}"/>
<div data-product-option-change style="display:none;">
{{inject 'showSwatchNames' theme_settings.show_product_swatch_names}}
{{assignVar 'hasPickList' 0}}
{{#each product.options}}
{{{dynamicComponent 'components/products/options'}}}
{{#if partial '===' 'product-list'}}
{{assignVar 'hasPickList' 1}}
{{/if}}
{{/each}}
</div>
<div class="form-field form-field--stock{{#unless product.stock_level}} u-hiddenVisually{{/unless}}">
<label class="form-label form-label--alternate">
{{lang 'products.current_stock'}}
<span data-product-stock>{{product.stock_level}}</span>
</label>
</div>
{{> components/products/add-to-cart}}
{{#if product.out_of_stock}}
{{#if product.out_of_stock_message}}
{{> components/common/alert/alert-error product.out_of_stock_message}}
{{else}}
{{> components/common/alert/alert-error (lang 'products.sold_out')}}
{{/if}}
{{/if}}
</form>
{{#if settings.show_wishlist}}
{{> components/common/wishlist-dropdown}}
{{/if}}
</div>
{{> components/common/share url=product.url}}
</section>
<article class="productView-description">
{{#if theme_settings.show_product_details_tabs}}
{{> components/products/description-tabs}}
{{else}}
{{> components/products/description}}
{{/if}}
{{> components/custom/product-files}}
</article>
</div>
<div id="previewModal" class="modal modal--large" data-reveal>
{{> components/common/modal/modal-close-btn }}
<div class="modal-content"></div>
<div class="loadingOverlay"></div>
</div>
{{#if (getVar 'hasPickList')}}
{{> components/custom/pick-list-prices }}
{{/if}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment