[TECHNICAL] Reusable web components and infrastructure

This technical documentation only applies to our theme Impact and Prestige v7 (and higher). While we plan to further extend our other themes to share the same technical foundation, if you are using a different theme, please refer to the file "custom.js" inside the Assets folder to access the technical documentation for your theme.

Our themes are using web components and JavaScript ES modules. This architecture allows you to re-use all the theme infrastructure in your code without having to touch the original code.

This documentation will go through various elements (not all) that may be useful.

Utilities

The theme export several utilities that can be used in your own code. Those are all exported as part of the theme.js asset file.

Retrieving the cart content

You can retrieve the most recent cart content (in JSON) by importing the fetch cart. It will return a promise that is resolved when the cart is up-to-date. Because the theme implements a smart caching, you can call this several times without worrying about multiple Ajax calls being done:

<script type="module">
  import {fetchCart} from "{{ 'theme.js' | asset_url }}";

  (async () => {
    const cartContent = await fetchCart(); // JSON cart content
  })();
</script>
Retrieving product content

You can retrieve the JSON content about a product by its handle. It is based on the Shopify [product Ajax API] ( https://shopify.dev/api/ajax/reference/product#get-locale-products-product-handle-js) excepts that it implements a smart caching mechanism, so only one Ajax call will be sent for a given handle. It also returns a promise that resolve when the product has been loaded.

<script type="module">
  import {ProductLoader} from "{{ 'theme.js' | asset_url }}";

  (async () => {
    const product = await ProductLoader.load('product-handle'); // JSON product content
  })();
</script>
Formatting a price

The formatMoney function can be used to format a price (in cents). The first parameter is the price to format (in cent), while the second parameter is a format (supported values: amount, amount_no_decimals, amount_with_space_separator, amount_with_comma_separator, amount_with_apostrophe_separator, amount_no_decimals_with_comma_separator, amount_no_decimals_with_space_separator and amount_no_decimals_with_apostrophe_separator).

<script type="module">
  import {formatMoney} from "{{ 'theme.js' | asset_url }}";

  console.log(formatMoney(4000, 'amount')); // Example result: "$40.00"
</script>
Image utilities

When working with product media, it is often useful to be able to create new image tag based on a given media object. The theme gives you the necessary tools to do that with the createMediaImg which returns a new "img" element that can be added to the DOM. The first parameter is a valid media JavaScript object, the second an array of requested widths, and the last one an extra set of extra attributes to be added on the image tag. The theme takes care of all the hard work to generating all the correct URL.

<script type="module">
  import {createMediaImg} from "{{ 'theme.js' | asset_url }}";

  const imageTag = createMediaImg(mediaObject, [60, 120], {"class": "thumbnail", "sizes": "60px"});
</script>

The theme also exposes a convenient imageLoaded that returns a promise that is resolved when the image is fully loaded:

<code><script type="module">
  import {imageLoaded} from "{{ 'theme.js' | asset_url }}";

  (async () => {
    await imageLoaded(imgElement);
    // Here, the imgElement image is fully loaded
  })();
</script>
throttle and debounce

When listening to heavy events such as on scroll, it can be useful to throttle the callback so that it is not called too much time. You can use the theme built-in throttle or debounce methods to do that in an efficient way:

<script type="module">
  import {throttle, debounce} from "{{ 'theme.js' | asset_url }}";

  window.addEventListener('scroll', throttle(() => {
    // Throttled function
  }));

  inputElement.addEventListener('keydown', debounce(() => {
   // This function will be debounced to be called once every 150ms
  }), 150);
</script>

Web components

The theme offers a lot of built-in web components that you can use in your customizations to create advanced customizations with minimal JavaScript code efforts. Not all components are documented here, but those should be the most important ones.

Drawers

Drawers are off-screen elements that allow to show extra information. The theme uses a shadow-DOM approach that allows to quickly create your own drawers. To open a drawer, you must create a "button" with an aria-controls referring to the actual drawer.

Basic usage

Here is the basic code:

{%- assign drawer_id = 'your-drawer-id' -%}

<button aria-controls="{{ drawer_id }}" aria-expanded="false">Open</button>

<x-drawer id="{{ drawer_id }}" class="drawer">
  <h2 slot="header">My drawer</h2>

  <p>Content</p>
</x-drawer>
The drawer ID must be unique across the whole page.

As you can see, we are simply creating a button, and a custom elements. This component uses the shadow DOM to create itself (the template is defined in the assets/shadow-dom-templates.liquid file), so you do not have to add all the boilerplate code such as the close button. Everything is done automatically.

The drawer has two main slots:

  • title: the title slot (it can be any element such as a simple h2 or a complex div) will be automatically positioned inside the header of the drawer.
  • default: everything that has not a named slot is automatically placed into the drawer's body.

Events

Every dialogs (such as a drawer) emits the following events that you can listen to:

  • dialog:after-show: this event is triggered once the drawer has completely opened (after the animation)
  • dialog:after-hide: this event is triggered once the drawer has completely closed (after the animation)

Methods

Once you have retrieved a specific drawer instance, you can the show and hide methods. Those returns a promise being resolved once the drawer has completely opened or closed, respectively:

const drawer = document.querySelector('.your-drawer');
drawer.show(); // Open the drawer and return a promise
drawer.hide(); // Close the drawer and return a promise

Advanced usage

Sometimes, you may need to create your own custom drawer while still reusing the appearance and accessibility benefits of the default drawer.

Because our themes export all components, can create your own customized drawers and register them as your own custom elements:

<script type="module">
  import {Drawer} from "{{ 'theme.js' | asset_url }}"; // Import the theme web component

  // Extend it
  class CustomDrawer extends Drawer {

  }

  // Create the custom elements
  window.customElements.define('custom-drawer', CustomDrawer);
</script>
Popovers Similarly to drawer, popover allow to create contextual information flying on top of content, and are usually a less obtrusive approach compared to drawer.
{%- assign popover_id = 'your-popover-id' -%}

<button aria-controls="{{ drawer_id }}" aria-expanded="false">Open</button>

<x-popover id="{{ popover_id }}" anchor-vertical="start" anchor-horizontal="center" class="popover">
  <h2 slot="title">My popover</h2>

  <p>Content</p>
</x-popover>

The anchor-vertical and anchor-horizontal supports the values "start", "center" or "end" and allow to control how the drawer is being opened relative to its closest positioned parent.

Product form

By default, on product page, quick buy... the theme adds the product in Ajax without leaving the page. You can actually very easily make your custom product form being the same by adding the is="product-form" attribute to any product form:

{%- form 'product', product, is: 'product-form' -%}
  Your code

  <button type="submit">Buy</button>
{%- endfor -%}

This form will automatically be "augmented" thanks to the is attribute. Once a customer click on the button, it will follow the theme behavior (such as adding the product in Ajax).