-
This week, I’ve been trying to make Shopify BuyButton.js play nicely with Turbolinks.
Integration with Turbolinks is often tricky because it retains the state of
window
anddocument
across page changes and any other JavaScript objects will stay in memory. In the case of BuyButton.js, this means any UI state and bound event listeners will persist as a user clicks around your site. This is further complicated by Turbolinks’ caching which means any transformations may be applied multiple times.If you don’t take Turbolinks into consideration, you might see broken behaviour such as clicks on “Add to cart” not working, the entire UI failing to appear or multiple buy buttons appearing every time you use the browser’s back and forward buttons.
While I’m still working on this, I’ve had some success with the following strategy:
-
Create a Shop client once to be re-used across all pages.
const client = ShopifyBuy.buildClient({ domain: "my-shop.myshopify.com", storefrontAccessToken: "your-storefront-access-token", });
-
Initialize the library once to be re-used across all pages.
const ui = ShopifyBuy.UI.init(client);
This will append a
<style>
tag to the<head>
of yourdocument
and add various event listeners todocument
. As the<head>
of your page will be merged when a user navigates between pages this will retain the styles and it is best practice to only bind event listeners once ondocument
. -
Onturbolinks:load
, reset the inner state of the library usingdestroyComponent
as you can no longer guarantee the various DOM elements the UI will have injected into your<body>
are still present.ui.destroyComponent("product"); ui.destroyComponent("cart"); ui.destroyComponent("collection"); ui.destroyComponent("productSet"); ui.destroyComponent("modal"); ui.destroyComponent("toggle");
Update: In writing this, I discovered
destroyComponent
doesn’t work if you don’t pass the ID of a component’s model. It usesArray.prototype.splice()
to mutate an array while looping over it. This means the position of each component in the array is changing as the loop advances so some components will not be destroyed, e.g.> const foo = [1, 2, 3, 4]; > foo.forEach((e, index) => foo.splice(index, 1)); > foo [ 2, 4 ]
We can fix this by destroying and resetting the components ourselves:
ui.components.product.forEach((component) => component.destroy()); ui.components.cart.forEach((component) => component.destroy()); ui.components.collection.forEach((component) => component.destroy()); ui.components.productSet.forEach((component) => component.destroy()); ui.components.modal.forEach((component) => component.destroy()); ui.components.toggle.forEach((component) => component.destroy()); ui.components.product = []; ui.components.cart = []; ui.components.collection = []; ui.components.productSet = []; ui.components.modal = []; ui.components.toggle = [];
Or, more concisely:
Object.keys(ui.components).forEach((type) => { ui.components[type].forEach((component) => component.destroy()); ui.components[type] = []; });
We can do this when the
turbolinks:before-cache
event fires as recommended in the documentation:You can use this event to reset forms, collapse expanded UI elements, or tear down any third-party widgets so the page is ready to be displayed again.
This will ensure any DOM elements injected by BuyButton.js won’t be cached so you don’t end up with duplicate “Add to cart” buttons that don’t do anything when clicked.
document.addEventListener("turbolinks:before-cache", () => { Object.keys(ui.components).forEach((type) => { ui.components[type].forEach((component) => component.destroy()); ui.components[type] = []; }); });
-
Following that, create your components as normal.
ui.createComponent("product", { handle: "some-product-handle", node: element, });
To make it easy for Turbolinks to create the appropriate components based on the
<body>
contents for everyturbolinks:load
, I embedded the necessary information in data attributes.document.querySelectorAll("[data-shopify-product-handle]").forEach((element) => { const { dataset: { shopifyProductHandle } } = element; const temporaryWrapper = document.createElement("div"); element.appendChild(temporaryWrapper); ui.createComponent("product", { handle: shopifyProductHandle, node: temporaryWrapper, }); });
<div data-shopify-product-handle="some-product-handle"></div>
I create a new
<div>
for the component to use as callingdestroy
will remove the node from the DOM.
I still need to iron out some issues with Turbolinks caching but I’m optimistic.Update: with the update to useturbolinks:before-cache
above, all my caching issues are now resolved. -
-
On Tuesday, Apple announced the M1 system on a chip. While I’m not in a hurry to replace my beloved refurbished 2015 15-inch MacBook Pro, the pandemic and my musculoskeletal woes have made my working setup decidedly stationary. I’m seriously considering the Mac mini for the first time.
-
I’ve also been working on an integration with Stripe and have been amazed how good their developer tools are. From the ability to test webhooks with their CLI, their go-live checklist and extensive support for testing, it has been surprisingly pleasant to work with.
If you need to test handling their webhooks but are verifying their signatures, I found the following useful in my controller specs (assuming you’re storing your signing secret in
Rails.configuration.stripe_webhook_signing_secret
):def sign_stripe_webhook(payload) timestamp = Time.zone.now signature = Stripe::Webhook::Signature.compute_signature( timestamp, payload.to_json, Rails.configuration.stripe_webhook_signing_secret ) request.headers['Stripe-Signature'] = Stripe::Webhook::Signature.generate_header(timestamp, signature) end
Then I can use it like so:
it 'returns a 200 OK with a valid Stripe event' do event = { 'id' => 'evt_1', 'object' => 'event', 'type' => 'checkout.session.completed', 'data' => {} } sign_stripe_webhook(event) post :create, params: event, as: :json expect(response).to have_http_status(:ok) end
-
Following last week’s adventures with
brew bundle
and Scott’s tip about Homebrew Bundle being integrated with mas, I committed myBrewfile
to my dotfiles. -
The days of spring will surely bring the birds and bees cavorting.
But since I am a gentleman, I’d much rather be jorting.
Weeknotes 55
By Paul Mucur,
on