@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.
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/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;