Home Manual Reference Source Repository

@eastsideco/escshopify

A modular support library for building Shopify themes with ES6 and webpack.

While the library can be used directly from any ES6 code (or traditional non-compiled scripts via a script tag), this library is ultimately intended to be used alongside two other tools:

  • The SALVO theme development framework, which provides an environment for structuring themes and components. SALVO usually takes care of initializing many aspects of this library and providing a convinient interface for the theme developer.
  • MATTv2, the second generation theme deployment framework.

The library is still a WIP and may change radically in structure and/or function.

Overview

Entities

Entities represent common environment models that all Shopify themes need to work with. For example: the session cart; shop settings; product information.

These entities may be read-only (i.e. shop current format info), or provide an interface for modifying the entity (i.e. cart items).

Entities can be accessed from the object under EscShopifyJS.Entities.

Entities/Cart

The Cart module provides an interface to the state of the visitor's cart and the cart AJAX API.

For member and method documentation, see the API reference. For event documentation and examples, see below.

Example

The following example shows a simple JS component which keeps track of the number of items in the visitor's cart.

ES6:

import {entities, utils} from '@eastsideco/escshopify';

const cart = new entities.Cart;

export default class CartCounter {
    constructor(element) {
        this._element = element;

        cart.on('update', () => this.render());
        utils.onLoad(() => this.render());
    }

    async render() {
        await cart.ready();

        var count = cart.items.reduce((total, i) => total + i.quantity, 0);
        this._element.innerText = count;
    }
}

Getting Started

Initializing the cart state

Before using the cart module, you should initialize it with the current state of the cart, for example from a binding created in Liquid.

This is done for you automatically if you use SALVO.

ES6:

import {entities, utils} from '@eastsideco/escshopify';

const cart = new entities.Cart;

cartData = { token: '...', items: [ ... ], ... };
cart.initialize(cartData);

Reading the cart

You can access the items in the cart using .items. It's highly recommended to check or wait for the cart entity to be 'ready' before doing this. Async/await makes this quite trivial.

ES6:

import {entities, utils} from '@eastsideco/escshopify';

const cart = new entities.Cart;

// Using async/await
async function printCartItemNames() {
    await cart.ready();
    for (let cart.items as item) {
        console.log(item.title + ' x' + item.quantity);
    }
}

// Using promises
function printCartItemNames() {
    cart.ready().then(() => {
        for (let cart.items as item) {
            console.log(item.title + ' x' + item.quantity);
        }
    });
}

// Checking cart state syncronously
function printCartItemNames() {
    if (cart.isReady) {
        for (let cart.items as item) {
            console.log(item.title + ' x' + item.quantity);
        }
    }
}

Setting cart items and attributes

You can modify the cart items using #addItem, #updateItem, etc. You can modify cart attributes using #setAttribute or #setAttributes.

ES6:

cart.addItem(variantId, 1);
await cart.addItem(variantId, 1);
cart.updateItemById(variantId, 3);
// Setting a line item attribute on each item in the cart:
for (let item, lineNumber of cart.items) {
    cart.updateItem(lineNumber, item.quantity, {
        promotion: 'Added by promotion'
    });
}

Important warning about async

Warning: Most setter methods are asynchronous. Await the promise fulfillment, or be aware that the cart state may not reflect your changes.

Examples:

ES6:

// starting cart attributes: { test: 1 }
cart.setAttribute('test', 2);
console.log(cart.getAttribute('test')); // may still 1?
// starting cart attributes: { test: 1 }
await cart.setAttribute('test', 2);
console.log(cart.getAttribute('test')); // definitely 2
// starting cart attributes: { test: 1 }
cart.setAttribute('test', 2).then(() => {
    console.log(cart.getAttribute('test'); // definitely 2
});

You can use Promise#all to wait for multiple changes to complete:

// Set attributes on all line items and wait until complete
var promises = [];
for (let item, lineNumber of cart.items) {
    promises.push(cart.updateItem(lineNumber, item.quantity, { attribute: 'test' }));
}
console.log(cart.items); // Attributes may or may not be updated?
await Promise.all(promises);
console.log(cart.items); // Attributes are definitely updated

Events

Listen to the following events to track the lifecycle of this entity.

update

The cart was modified.

Object[]    items       - The new state of items in the cart.
String      operation   - 'add', 'item-updated', 'remove', 'init', or 'attribute-updated'
Object|Null item        - Item that was added/updated/removed.

Updating an item to 0 qty is considered a remove.

cart.on('update', (e) => {
    this.cartTotalMinusGiftcards = _.reduce(e.items, (total, item) => {
        return item.gift_card ? total : total + item.line_price;
    }, 0);
});

add

An item has been added.

Object[]    items       - The new state of items in the cart.
Object      item        - Item that was added.

Updating an existing item with higher qty does not fire this event (see item-updated).

cart.on('add', (e) => {
// "Thanks for adding Example Product!"
window.alert('Thanks for adding ' + e.item.product_title + '!');
});

item-updated

An item has been modified.

Object[]    items       - The new state of items in the cart.
Object      item        - Item that was updated (after update).

Adding a new line item does not fire this event (see add).

remove

An item has been removed.

Object[]    items       - The new state of items in the cart.
Object      item        - Item that was removed.

clear

The cart is now empty.

Object[]    oldItems    - Items that were previously in the cart.

This will typically fire when the last item is removed from the cart.

Entities/Shop

The Shop entity represents global shop settings and details (i.e. contact email address, policies, URL, etc.)

This entity is read-only, and it properties match that of the Shopify shop liquid object. There are some extra methods and properties added for convinience.

Examples

ES6:

import {entities, utils} from '@eastsideco/escshopify';

const shop = new entities.Shop;

// Accessing shop properties:
var primaryDomain = shop.domain;
var myshopifyDomain = shop.permanent_domain;

var storeName = shop.name;

var defaultCurrency = shop.currency;

var averageCollectionSize = shop.products_count / shop.collections_count;


// Generating absolute shop URLs:
var absoluteUrl = shop.makeAbsoluteUrl('/collections/all'); // https://shop.com/collections/all
var permanentUrl = shop.makePermanentUrl('/collections/all'); // https://shop.myshopify.com/collections/all

Getting Started

Initializing the shop state

Before using the shop module, you should initialize it with the global shop state, for eaxmple from a binding created in Liquid.

This is done for you automatically if you use SALVO.

ES6:

import {entities, utils} from '@eastsideco/escshopify';

const shop = new entities.Shop;

shopData = { name: '...', domain: '...', ... };
shop.initialize(shopData);

Events

Listen to the following events to track the lifecycle of this entity.

init

The shop entity was initialized.

Object  shop        - The current shop entity.

Plugins

Plugins are larger modules which encapsulate a more complex piece of functionality (i.e. currency conversion and formatting). Plugins are self-contained and entirely optional, but they provide an easy way to implement common complex funtionality.

Plugins can be accessed via the object under EscShopifyJs.Plugins.

Plugins/EasyCurrency

The EasyCurrency module provides easy-to-use and highly customizable frontend currency conversion.

Getting started

Simple example

By default, EC will convert elements with [data-money], optionally making use of the [data-money-currency] property.

For example, this element:
<span data-money="1234" data-money-currency="USD">$12.34</span>
May be converted to:
<span>£10.00</span>
Note that amounts are specified in integer units (i.e. cents).

Note that the default parser removes the initialization properties from the element after initialization, but this may not always be the case.
Warning: Don't use the initialization properties to read/write conversion state after initialization (as you would have in the classic EC) - there are now other mechanisms for supporting this.

ES6:

import {plugins, utils} from '@eastsideco/escshopify';

const easyCurrency = new plugins.EasyCurrency;

// Initialize EC once the page has loaded.
utils.onLoad(() => {
    easyCurrency.useGeoserviceResolver();
    easyCurrency.initialize({
        defaultCurrency: 'GBP'
    });
});

ES5:

<script src="escshopify.web.js"></script>
<script>
(function() {
    // Initialize EC once the page has loaded.
    escshopify.utils.onLoad(function() {
        var easyCurrency = new escshopify.plugins.EasyCurrency();

        easyCurrency.useGeoserviceResolver();
        easyCurrency.initialize({
            defaultCurrency: 'GBP'
        });
    });
})();
</script>

Config options

Here is an example with all of the config options and default values:
See the reference for Config#constructor for more info.

ES6:

import {plugins, utils} from '@eastsideco/escshopify';

const easyCurrency = new plugins.EasyCurrency;

// Initialize EC once the page has loaded.
utils.onLoad(() => {
    easyCurrency.useGeoserviceResolver();
    easyCurrency.initialize({
        // Currency to use before geolocation is first resolved.
        defaultCurrency: 'GBP',
        // List of currencies the user can use, or the string 'any'.
        allowedCurrencies: 'any', // ['USD', 'EUR'],
        // Whether to set currency based on geolocation.
        useGeoForCurrency: true,
        // Selectors to parse as MoneySpans
        moneySpanSelectors: [
            '[data-money]'
        ],
        // How to extract amount + currency from the selected elements
        moneySpanParser: function(el, easyCurrency) {
            var amount = el.dataset.money;
            var currency = el.dataset.moneyCurrency || easyCurrency.getState().currency;
            delete el.dataset.money;
            delete el.dataset.moneyCurrency;
            amount = Number(amount);
            return new Money(amount, currency);
        },
    });
});

Events

EasyCurrency extends evee and provides a variety of events for to listening to changes. See the 'Emit' sections of the class reference for a list of events. ES6:

import {plugins, utils} from '@eastsideco/escshopify';

const easyCurrency = new plugins.EasyCurrency;

// Initialize EC once the page has loaded.
utils.onLoad(() => {
    easyCurrency.useGeoserviceResolver();
    easyCurrency.initialize({
        defaultCurrency: 'GBP',
    });

    easyCurrency.on('currencyChanged', (e) => {
        alert('Currency is now: ' + e.data);
    });
});

Extending EasyCurrency

Formatters

EasyCurrency uses a Formatter to format currencies.

To override currency formatting, make an object extending Formatter (or with #format), and then call EasyCurrency#setFormatter.

ES6:

// CustomFormatter.js
import {plugins} from '@eastsideco/escshopify';

export default class CustomFormatter extends plugins.EasyCurrency.formatters.Formatter {
    format(amount, currency) {
        return (amount / 100).toFixed(2) + ' ' + currency; // i.e. "12.34 USD"
    }
}

// main.js
import {plugins, utils} from '@eastsideco/escshopify';
import CustomFormatter from './CustomFormatter';

const easyCurrency = new plugins.EasyCurrency;

// Initialize EC once the page has loaded.
utils.onLoad(() => {
    easyCurrency.useGeoserviceResolver();
    easyCurrency.setFormatter(new CustomFormatter);
    easyCurrency.initialize({
        defaultCurrency: 'GBP',
    });
});

Currency Resolvers

EasyCurrency uses a CurrencyResolver to determine which currencies are available and the conversion rate between them.

To override convertion rates or available currencies, make an object extending CurrencyResolver (or with #getConversionRate and #listCurrencyCodes), and call EasyCurrency#setCurrencyResolver.

ES6:

// CustomResolver.js
import {plugins} from '@eastsideco/escshopify';

export default class CustomResolver extends plugins.EasyCurrency.resolvers.CurrencyResolver {
    async listCurrencyCodes() {
        return ['USD', 'GBP', 'EUR'];
    }

    async getConversionRate(from, to) {
        // Just an example, please don't write rates like this.
        var rates = {
            USD: { USD: 1, GBP: 0.8, EUR: 0.9 },
            GBP: { USD: 1/0.8, GBP: 1, EUR: (1/0.8)*0.9 },
            EUR: { USD: 1/0.9, GBP (1/0.9)*0.8, EUR: 1}
        };

        return rates[from][to];
    }
}

// main.js
import {plugins, utils} from '@eastsideco/escshopify';
import CustomResolver from './CustomResolver';

const easyCurrency = new plugins.EasyCurrency;

// Initialize EC once the page has loaded.
utils.onLoad(() => {
    easyCurrency.useGeoserviceResolver();
    easyCurrency.setCurrencyResolver(new CustomResolver);
    easyCurrency.initialize({
        defaultCurrency: 'GBP',
    });
});

Plugins/Geoservice

The Geoservice module provides an easy-to-use interface for visitor geolocation and currency information.

Getting started

Get visitor geolocation info

ES6:

import {plugins} from '@eastsideco/escshopify';

const geoService = new plugins.GeoService;

async function alertVisitorCountry() {
    var geoInfo = await geoService.lookupGeo();
    alert(geoInfo.country.iso_code);
}

Get currency exchange rates

Note: It's highly recommended to use the EasyCurrency plugin instead of re-implementing currency conversion yourself - it had more features and covers many easy-to-miss edge cases.

ES6:

import {plugins} from '@eastsideco/escshopify';

const geoService = new plugins.GeoService;

class VisitorCurrencyConverter {
    constructor(baseCurrency) {
        this._baseCurrency = basyCurrency;
        this._visitorCurrency = 'USD';
        this._rates = null

        this._getRates();
        this._getVisitorCurrency();
    }

    async _getVisitorCurrency() {
        var geoInfo = await geoService.lookupGeo();
        this._visitorCurrency = geoInfo.currency;
    }

    async _getRates() {
        var res = await geoService.getCurrencyInfo();
        this._rates = res.rates;
    }

    convert(amount) {
        var baseToUsd = this._baseCurrency == 'USD' ? 1 : this._rates[this._baseCurrency];
        var usdToVisitor = this._visitorCurency == 'USD' ? 1 : this._rates[this._visitorCurrency;

        var rate = usdToVisitor / baseToUsd;

        return amount * rate;
    }    
}

Utilities

The library provides various utility functions to reduce boilerplate and make common tasks easier.

Most of these utilties can be found in the general utils module, but some groups of functionality have been put into their own standalone modules (i.e. logging).

Utilities/Log

The Log util module provides a very basic logging framework for level and module-based logging. The log utility support custom loggers (i.e. to send logs via AJAX or to the DOM), and level-based muting (i.e. ignoring debug-level messages).

Using SALVO

If you're using SALVO, a logger has already been configured for you - simple use salvo.log:

ES6:

import salvo from 'salvo';

salvo.log.send(salvo.log.WARN, 'Example', 'This is an example message.');

Log levels and tags

The log interface accepts three arguments: log level, tag, and message.

The log level is a description of the purpose and importance of the log message: DEBUG, INFO, WARN, ERROR, or FATAL. These constants are defined on the log instance, i.e. log.DEBUG, log.WARN, etc.

The log tag is a description of where the log message comes from. Typically, it should be the name of a component or class. Log tags help find track down log messages, or filter messages in the debugger. A common pattern is to have a TAG constant in your classes like so:

ES6:

import log from 'log';

const TAG = 'ExampleComponent';

class ExampleComponent {
    constructor() {
        log.send(log.DEBUG, TAG, 'Created a new thing!');
    }
}

Getting started

Create a logging module

Create a module in your application which bootstraps logging like so:

ES6:

// log.js
import {Log} from '@eastsideco/esc-shopify';
import ConsoleLogger from '@eastsideco/esc-shopify/src/utils/loggers/ConsoleLogger.js';

const log = new Log;
const defaultLogger = new ConsoleLogger();

log.addLogger(logger);

export default log;

You can then use the log like so:

import log from 'log';

log.send(log.WARN, 'Example', 'This is an example message');
log.sendObject(log.DEBUG, 'Example', 'This is an example object', {
    test: 123
});

Setting a log level

You can mute logging below a given level by calling Log#setLogLevel:

if (!config.debug) {
    log.setLogLevel(log.WARN);
}

Writing new loggers

A single log instance can write to multiple loggers. By default, the library provides a ConsoleLogger, which simply passes the log arguments on to window.console, but you can extend this if you need to display logs elsewhere for some reason.

ES6:

// DOMLogger.js
import Logger from '@eastsideco/escshopify/src/utils/loggers/Logger.js';

export default class DOMLogger extends Logger {
    constructor(element) {
        this._element = element;
    }

    send(level, tag, text) {
        var child = document.createElement('p');
        child.classList.add('level-'+level);
        child.innerText = tag + ': ' + text;
        this._element.appendChild(child);
    }

    sendObject(level, tag, text, object) {
        this.send(level, tag, text + ' -- ' + JSON.stringify(object);
    }
}

// log.js
import {Log} from '@eastsideco/shopify';
import ConsoleLogger from '@eastsideco/shopify/src/utils/loggers/ConsoleLogger.js';
import DOMLogger from './DomLogger';

const log = new Log;
const defaultLogger = new ConsoleLogger;
const customLogger = new DOMLogger;

log.addLogger(defaultLogger);
log.addLogger(customLogger);

export default log;