Skip to main content

Dispatch events

You dispatch standard events so apps can respond to what buyers do on your storefront, without reading your markup or watching your network calls. An app listening for shopify:cart:lines-update gets the same payload from your theme as it does from any other.

These are ordinary DOM events. The standard events library gives you a class for each one, so you construct an event with its payload and dispatch it on the element where the interaction happened. It bubbles to document, and a listener can attach anywhere above it.

You add these calls next to the code that already handles the interaction. The cart events are the exception. When an app calls updateCart, the action emits those itself, whether or not your storefront has configured it.

So don't dispatch a cart event after an updateCart, or listeners see the same change twice. Your own calls cover the changes your theme makes on its own.


The standard events library is hosted on the Shopify CDN. How you load it depends on whether your theme uses JavaScript modules.

If your theme uses JavaScript modules, as Horizon does, then register the library in an import map in theme.liquid:

<script type="importmap">
{ "imports": { "@shopify/standard-events": "https://cdn.shopify.com/storefront/standard-events.js" } }
</script>

If your theme uses plain <script> tags without modules, such as some Dawn-derived themes, then assign the library to a global instead:

<script type="module">
import * as SE from 'https://cdn.shopify.com/storefront/standard-events.js';
window.StandardEvents = SE;
</script>

Type definitions are available at https://cdn.shopify.com/storefront/standard-events.d.ts.


Anchor to Dispatching an eventDispatching an event

Every event is a class. You construct it with new ClassName(payload) and dispatch it with element.dispatchEvent():

import { PageViewEvent } from '@shopify/standard-events';

document.addEventListener('DOMContentLoaded', () => {
document.dispatchEvent(new PageViewEvent({
page: {
template: 'product',
title: document.title,
url: window.location.href,
},
}));
});

shopify:page:view belongs inside a DOMContentLoaded listener, so app scripts have time to attach their listeners first.

Every other event dispatches from the most specific element that contains the interaction:

  • Product events dispatch on the product card or product section.
  • Cart events dispatch on the cart drawer or cart page section.
  • shopify:page:view dispatches on document.

Because events bubble, a listener on document receives them wherever they come from, while dispatching on a specific element lets other listeners scope to part of the page. Each event's reference page names its target.


Anchor to Dispatching view events from LiquidDispatching view events from Liquid

shopify:product:view, shopify:collection:view, and shopify:cart:view fire when a buyer sees something, so dispatching them means tracking element visibility.

The library ships a custom element that does the tracking for you. You register it once in your theme's JavaScript, under whatever tag name you want:

import { createViewEventElement } from '@shopify/standard-events';

customElements.define('s-view-event', createViewEventElement());

Then you wrap the content in that element. The standard_event_data filter builds the payload from a product, collection, or cart object:

<s-view-event
view-event-trigger="intersect"
view-event-payload='{{ product | standard_event_data: "view", context: "collection" | escape }}'
>
<!-- product card content -->
</s-view-event>

The filter's optional context argument records where the view happened. Which values it takes depends on the object, so check the filter reference before you pass one.

The view-event-trigger attribute controls when the element dispatches. It defaults to connect, which fires as soon as the element is added to the page. intersect fires when the element scrolls into view, dialog fires when an ancestor <dialog> opens, and manual waits for a dispatchViewEvent() call.


Anchor to Dispatching an operation that can failDispatching an operation that can fail

A cart update takes time and might not succeed, so its event carries a promise. You dispatch as the operation starts and settle the promise after you know how it went, which is what lets a listener show a loading state in between.

createPromise() on the event class returns that promise and its resolvers. The example below is the whole pattern end to end: dispatch, run your own cart code, then resolve or reject, and dispatch shopify:cart:error if the request failed.

import { CartLinesUpdateEvent, CartErrorEvent } from '@shopify/standard-events';

const deferred = CartLinesUpdateEvent.createPromise();

element.dispatchEvent(new CartLinesUpdateEvent({
action: 'add',
context: 'product',
lines: [{ merchandiseId: variantId, quantity: 1 }],
promise: deferred.promise,
}));

try {
const ajaxCart = await addToCart(variantId);

deferred.resolve({
cart: CartLinesUpdateEvent.createCartFromAjaxResponse(ajaxCart),
});
} catch (error) {
element.dispatchEvent(new CartErrorEvent({
error: error.message,
code: 'SERVICE_UNAVAILABLE',
}));

deferred.reject(error);
}

An unresolved promise leaves every listener waiting forever, which shows up as a spinner that never stops. Every path through your code needs to end in a resolve or a reject.

addToCart() is your theme's own cart request. If it uses the AJAX cart API, then createCartFromAjaxResponse() converts a /cart.js response into the cart shape the payload expects. If it calls the Storefront API directly, then build the cart from the GraphQL response instead.

Anchor to Which failures get an error eventWhich failures get an error event

When a request fails, you reject the promise and dispatch shopify:cart:error, as the catch block above does. The rejection reaches whoever called the operation, and the event reaches every other listener on the page.

When the cart declines a change, you resolve the promise instead. An unavailable variant or a discount code that doesn't apply comes back with userErrors describing the problem, and you don't dispatch an error event.

The code field takes a Storefront API cart error code, such as INVALID for a malformed input or MAXIMUM_EXCEEDED for a quantity above the item's maximum. See CartErrorCode for the full list.


Anchor to Validating payloads in developmentValidating payloads in development

It's easy to make a mistake and dispatch a payload with a field missing. The bug won't show up until an app tries to read that event.

shopify theme dev checks payloads for you. It serves a development build of the runtime that validates each one against the schema for its event, then prints what's wrong in the console:

[Shopify Standard Events] Invalid payload for "shopify:cart:lines-update":
✖ Invalid input: expected string, received undefined
→ at lines[0].merchandiseId

Every message carries the [Shopify Standard Events] prefix, so you can filter the console down to them. The production runtime doesn't include these checks.

If you misspell an event name, the runtime has no schema to check it against. It throws an error instead of logging one.

If your event carries a promise, the payload gets checked twice. The fields are checked as you dispatch, and the resolved value is checked after the promise settles. That second message can appear well after the dispatch that caused it, so console order won't match the order you dispatched in.

You can also watch events as they fire and call actions by hand with the --standard-events-inspector flag, which adds a panel to your storefront while you develop.

Note

These checks come from shopify theme dev. shopify app dev serves the production runtime, so an app or extension you run that way gets neither the checks nor the inspector.



Was this page helpful?