Created
June 27, 2023 02:09
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{{#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}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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