shopify-cart
Version:
Shopify AJAX cart API wrapper.
489 lines (432 loc) • 14.4 kB
JavaScript
import Q from '@butsandcats/ajax-queue'
export default class Cart {
constructor (config = {}) {
// Check each of the parameters exist and assign them to their default type if not.
const { cart = {}, selectors = {}, options = {} } = config
// Define the default selectors and options for the class
const defaultSelectors = {
decreaseQuantity: '[data-minus-one]',
increaseQuantity: '[data-plus-one]',
addItem: '[data-add-to-cart]',
quickAdd: '[data-quick-add]',
quickAddQuantity: '[data-quick-add-qty]',
quickAddProperties: '[data-quick-add-properties]',
removeItem: '[data-remove-item]'
}
// This object will be used to define default callbacks, money formatting and other options
const defaultOptions = {
getUrl: '/cart.json',
timeOut: 1000
}
// Create a new queue
this.queue = new Q({
method: 'POST',
completedAllRequestsEvent: 'cart:requestsCompleted',
completedRequestEvent: 'cart:requestCompleted'
})
this.defaultBeforeSend = (type, item) => {
const response = {
type: type,
item: item
}
const sendEvent = new CustomEvent('cart:sending', {
detail: {
response: response
}
})
document.dispatchEvent(sendEvent)
return response
}
this.defaultSuccess = (response) => {
this.getCart()
}
this.defaultError = (resp) => {
const response = JSON.parse(resp)
const errorEvent = new CustomEvent('cart:error', {
detail: {
response: response
}
})
this.preparingItemsArray = this.createItemArray()
document.dispatchEvent(errorEvent)
return response
}
// Merge and add the settings into the prototype
this.cart = cart
// Keep track of items in an id centric structure
this.items = this.createItems(cart)
this.selectors = Object.assign(defaultSelectors, selectors)
this.options = Object.assign(defaultOptions, options)
// Debounce requests
this.requestTimeout = null
this.preparingItemsArray = null
// Build the event listeners from the selectors
this.buildEventListeners(this.selectors)
return this
}
buildEventListeners (selectors) {
this.allSelectors = {
addItem: {
selector: selectors.addItem,
listen: 'click',
callback: () => this.addToCart
},
removeItemSelectors: {
selector: selectors.removeItem,
listen: 'click',
callback: () => this.removeFromCart
},
decreaseQuantitySelectors: {
selector: selectors.decreaseQuantity,
listen: 'click',
callback: () => this.decreaseQuantity
},
increaseQuantitySelectors: {
selector: selectors.increaseQuantity,
listen: 'click',
callback: () => this.increaseQuantity
},
quickAddSelectors: {
selector: selectors.quickAdd,
listen: 'click',
callback: () => this.quickAdd
}
}
function on (eventName, selector, fn) {
document.addEventListener(eventName, function (event) {
var possibleTargets = document.querySelectorAll(selector)
var target = event.target
for (var i = 0, l = possibleTargets.length; i < l; i++) {
var el = target
var p = possibleTargets[i]
while (el && el !== document) {
if (el === p) {
event.preventDefault()
event.elem = p
return fn.call(p, event)
}
el = el.parentNode
}
}
})
}
for (const key of Object.keys(this.allSelectors)) {
const { listen, selector, callback } = this.allSelectors[key]
on(listen, selector, callback)
}
// return this to and the ability to chain functions
return this
}
addToCart (event) {
const idInput = document.querySelector('[name=id]')
const quantityInput = document.querySelector('[name=quantity]')
const propertyInputs = document.querySelectorAll('[name*=properties]')
const id = Number(idInput.value || idInput.options[idInput.selectedIndex].value)
const quantity = Number(quantityInput.value)
const properties = {}
// Create an object of properties from the properties input fields
for (let input = 0; input < propertyInputs.length; input += 1) {
const element = propertyInputs[input]
if (element.type === 'radio' && !element.checked) {
continue
}
const key = element.getAttribute('name').split('[')[1].split(']')[0]
const value = element.value
properties[key] = value
}
const data = {
id: id,
quantity: quantity,
properties: properties
}
this.addItem(data)
}
quickAdd (event) {
const idAttribute = this.getDataAttribute(this.selectors.quickAdd)
const qtyAttribute = this.getDataAttribute(this.selectors.quickAddQuantity)
const propertiesAttribute = this.getDataAttribute(this.selectors.quickAddProperties)
const id = Number(event.elem.getAttribute(idAttribute))
const quantity = Number(event.elem.getAttribute(qtyAttribute)) || 1
const properties = JSON.parse(event.elem.getAttribute(propertiesAttribute))
const data = {
id: id,
quantity: quantity,
properties: properties
}
const config = {
beforeRequestType: 'quickAdd'
}
this.addItem(data, config)
}
removeFromCart (event) {
const attribute = this.getDataAttribute(this.selectors.removeItem)
const line = Number(event.elem.getAttribute(attribute))
this.removeItemByLine(line)
}
decreaseQuantity (event) {
const attribute = this.getDataAttribute(this.selectors.decreaseQuantity)
const line = Number(event.elem.getAttribute(attribute))
const index = line - 1
const quantity = this.cart.items[index].quantity - 1
const config = {
beforeRequestType: 'decreaseQuantity'
}
this.updateItemByLine(line, quantity, config)
}
increaseQuantity (event) {
const attribute = this.getDataAttribute(this.selectors.increaseQuantity)
const line = Number(event.elem.getAttribute(attribute))
const index = line - 1
const quantity = this.cart.items[index].quantity + 1
const config = {
beforeRequestType: 'increaseQuantity'
}
this.updateItemByLine(line, quantity, config)
}
getCart (options = {}) {
options.success = options.success || function (response) {
const cart = JSON.parse(response)
this.cart = cart
const updateEvent = new CustomEvent('cart:updated', {
detail: {
response: cart
}
})
document.dispatchEvent(updateEvent)
return this.cart
}
options.error = options.error || this.defaultError
const request = {
url: this.options.getUrl,
method: 'GET',
success: options.success,
error: options.error
}
// Add the request to the ajax request queue
this.queue.add(request)
}
addItem (item = {}, options = {}) {
if (item.id === undefined) {
return false
}
item.quantity = item.quantity || 1
item.properties = item.properties || {}
// Define callback functions for after the ajax request has been completed
options.beforeRequestType = options.beforeRequestType || 'addItem'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || function (lineItem) {
const item = JSON.parse(lineItem)
this.getCart({
success: (response) => {
const cart = JSON.parse(response)
const addedEvent = new CustomEvent('cart:itemAdded', {
detail: {
response: {
item: item,
cart: cart
}
}
})
this.preparingItemsArray = this.createItemArray()
document.dispatchEvent(addedEvent)
}
})
}
options.error = options.error || this.defaultError
// Build the ajax request
const request = {
url: '/cart/add.js',
data: item,
success: options.success,
error: options.error
}
// Send a beforeSend event and dd the request to the ajax request queue
options.beforeSend(options.beforeRequestType, item.id)
this.queue.add(request)
return this
}
removeItemById (id, options = {}) {
const data = {
updates: {}
}
data.updates[id] = 0
options.beforeRequestType = options.beforeRequestType || 'removeItemById'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const request = {
url: '/cart/update.js',
data: data,
error: options.error,
success: options.success
}
options.beforeSend(options.beforeRequestType, id)
this.queue.add(request)
return this
}
removeItemByLine (line, options = {}) {
options.beforeRequestType = options.beforeRequestType || 'removeItemByLine'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const request = {
url: '/cart/change.js',
data: {
line: line,
quantity: 0
},
success: options.success,
error: options.error
}
options.beforeSend(options.beforeRequestType, line)
this.queue.add(request)
return this
}
updateItemById (id, quantity, options = {}) {
const data = {
updates: {}
}
data.updates[id] = quantity
options.beforeRequestType = options.beforeRequestType || 'updateItemById'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const request = {
url: '/cart/update.js',
data: data,
error: options.error,
success: options.success
}
options.beforeSend(options.beforeRequestType, id)
this.queue.add(request)
return this
}
updateItemByLine (line, quantity = 0, config) {
clearTimeout(this.requestTimeout)
this.preparingItemsArray = this.preparingItemsArray || this.createItemArray()
this.preparingItemsArray[line - 1] = quantity
const sendRequest = () => {
const options = config || {}
options.beforeRequestType = options.beforeRequestType || 'updateItemByLine'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const updates = this.preparingItemsArray
const request = {
url: '/cart/update.js',
data: {
updates: updates
},
success: options.success,
error: options.error
}
options.beforeSend(options.beforeRequestType, line)
this.queue.add(request)
const newArray = []
for (let i = 0; i < this.preparingItemsArray.length; i += 1) {
if (this.preparingItemsArray[i] > 0) {
newArray.push(this.preparingItemsArray[i])
}
}
this.preparingItemsArray = newArray
}
this.requestTimeout = setTimeout(sendRequest, this.options.timeOut)
return this
}
changeItemByLine (line, quantity, options = {}) {
options.beforeRequestType = options.beforeRequestType || 'changeItemByLine'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const data = {
line: line,
quantity: quantity
}
if (options.properties !== undefined) {
data.properties = options.properties
}
const request = {
url: '/cart/change.js',
data: data,
success: options.success,
error: options.error
}
options.beforeSend(options.beforeRequestType, data)
this.queue.add(request)
return this
}
getShippingRates (options = {}) {
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const request = {
url: '/cart/shipping_rates.json?' + options.data,
method: 'GET',
success: options.success,
error: options.error
}
this.queue.add(request)
return this
}
setAttributes (options = {}) {
options.beforeRequestType = options.beforeRequestType || 'setAttributes'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const attributes = options.attributes || {}
const data = {
attributes: attributes
}
const request = {
url: '/cart/update.js',
success: options.success,
error: options.error,
data: data
}
if (attributes !== {}) {
options.beforeSend(options.beforeRequestType, data)
this.queue.add(request)
}
return this
}
setNote (options = {}) {
options.beforeRequestType = options.beforeRequestType || 'setNote'
options.beforeSend = options.beforeSend || this.defaultBeforeSend
options.success = options.success || this.defaultSuccess
options.error = options.error || this.defaultError
const note = options.note || null
const data = {
note: note
}
const request = {
url: '/cart/update.js',
success: options.success,
error: options.error,
data: data
}
options.beforeSend(options.beforeRequestType, data)
this.queue.add(request)
return this
}
getDataAttribute (selector) {
return selector.replace('[', '').replace(']', '')
}
createItems (cart) {
const items = {}
for (let i = 0; i < cart.items.length; i++) {
items[cart.items[i].id] = {
line: i + 1,
quantity: cart.items[i].quantity,
key: cart.items[i].key
}
}
return items
}
createItemArray (cartObj) {
const array = []
const cart = cartObj || this.cart
for (let i = 0; i < cart.items.length; i++) {
array.push(cart.items[i].quantity)
}
return array
}
}