Skip to content

Instantly share code, notes, and snippets.

@marioloncarek
Created September 26, 2018 14:06
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save marioloncarek/93c8aaf2c9e2f29054b70e1c69105ae2 to your computer and use it in GitHub Desktop.
Save marioloncarek/93c8aaf2c9e2f29054b70e1c69105ae2 to your computer and use it in GitHub Desktop.
complete shopify ajax cart solution with drawer and modal, adding and removing products - ugly AF
const defaults = {
cartModal: '.js-ajax-cart-modal', // classname
cartModalContent: '.js-ajax-cart-modal-content', // classname
cartModalClose: '.js-ajax-cart-modal-close', // classname
cartDrawer: '.js-ajax-cart-drawer', // classname
cartDrawerContent: '.js-ajax-cart-drawer-content', // classname
cartDrawerClose: '.js-ajax-cart-drawer-close', // classname
cartDrawerTrigger: '.js-ajax-cart-drawer-trigger', // classname
cartOverlay: '.js-ajax-cart-overlay', // classname
cartCounter: '.js-ajax-cart-counter', // classname
addToCart: '.js-ajax-add-to-cart', // classname
removeFromCart: '.js-ajax-remove-from-cart', //classname
removeFromCartNoDot: 'js-ajax-remove-from-cart', //classname,
checkoutButton: '.js-ajax-checkout-button',
};
const cartModal = document.querySelector(defaults.cartModal);
const cartModalContent = document.querySelector(defaults.cartModalContent);
const cartModalClose = document.querySelector(defaults.cartModalClose);
const cartDrawer = document.querySelector(defaults.cartDrawer);
const cartDrawerContent = document.querySelector(defaults.cartDrawerContent);
const cartDrawerClose = document.querySelector(defaults.cartDrawerClose);
const cartDrawerTrigger = document.querySelector(defaults.cartDrawerTrigger);
const cartOverlay = document.querySelector(defaults.cartOverlay);
const cartCounter = document.querySelector(defaults.cartCounter);
const addToCart = document.querySelectorAll(defaults.addToCart);
let removeFromCart = document.querySelectorAll(defaults.removeFromCart);
const checkoutButton = document.querySelector(defaults.checkoutButton);
const htmlSelector = document.documentElement;
for (let i = 0; i < addToCart.length; i++) {
addToCart[i].addEventListener('click', function(event) {
event.preventDefault();
const formID = this.parentNode.getAttribute('id');
console.log(formID);
addProductToCart(formID);
});
}
function addProductToCart(formID) {
$.ajax({
type: 'POST',
url: '/cart/add.js',
dataType: 'json',
data: $('#' + formID)
.serialize(),
success: addToCartOk,
error: addToCartFail,
});
}
function fetchCart() {
$.ajax({
type: 'GET',
url: '/cart.js',
dataType: 'json',
success: function(cart) {
onCartUpdate(cart);
if (cart.item_count === 0) {
cartDrawerContent.innerHTML = 'Cart is empty';
checkoutButton.classList.add('is-hidden');
} else {
renderCart(cart);
checkoutButton.classList.remove('is-hidden');
}
},
});
}
function changeItem(line, callback) {
const quantity = 0;
$.ajax({
type: 'POST',
url: '/cart/change.js',
data: 'quantity=' + quantity + '&line=' + line,
dataType: 'json',
success: function(cart) {
if ((typeof callback) === 'function') {
callback(cart);
} else {
onCartUpdate(cart);
fetchCart();
removeProductFromCart();
}
},
});
}
function onCartUpdate(cart) {
console.log('items in the cart?', cart.item_count);
}
function addToCartOk(product) {
cartModalContent.innerHTML = product.title + ' was added to the cart!';
cartCounter.innerHTML = Number(cartCounter.innerHTML) + 1;
openAddModal();
openCartOverlay();
fetchCart();
}
function removeProductFromCart() {
cartCounter.innerHTML = Number(cartCounter.innerHTML) - 1;
}
function addToCartFail() {
cartModalContent.innerHTML = 'The product you are trying to add is out of stock.';
openAddModal();
openCartOverlay();
}
function renderCart(cart) {
console.log(cart);
clearCartDrawer();
cart.items.forEach(function(item, index) {
//console.log(item.title);
//console.log(item.image);
//console.log(item.line_price);
//console.log(item.quantity);
const productTitle = '<div class="ajax-cart-item__title">' + item.title + '</div>';
const productImage = '<img class="ajax-cart-item__image" src="' + item.image + '" >';
const productPrice = '<div class="ajax-cart-item__price">' + item.line_price + '</div>';
const productQuantity = '<div class="ajax-cart-item__quantity">' + item.quantity + '</div>';
const productRemove = '<div class="ajax-cart-item__remove ' + defaults.removeFromCartNoDot + '"></div>';
const concatProductInfo = '<div class="ajax-cart-item__single" data-line="' + Number(index + 1) + '">' + productTitle + productImage + productPrice + productQuantity + productRemove + '</div>';
cartDrawerContent.innerHTML = cartDrawerContent.innerHTML + concatProductInfo;
});
// document.querySelectorAll('.js-ajax-remove-from-cart')
// .forEach((element) => {
// element.addEventListener('click', function() {
// const lineID = this.parentNode.getAttribute('data-line');
// console.log('aa');
// });
// });
removeFromCart = document.querySelectorAll(defaults.removeFromCart);
for (let i = 0; i < removeFromCart.length; i++) {
removeFromCart[i].addEventListener('click', function() {
const line = this.parentNode.getAttribute('data-line');
console.log(line);
changeItem(line);
});
}
}
function openCartDrawer() {
cartDrawer.classList.add('is-open');
}
function closeCartDrawer() {
cartDrawer.classList.remove('is-open');
}
function clearCartDrawer() {
cartDrawerContent.innerHTML = '';
}
function openAddModal() {
cartModal.classList.add('is-open');
}
function closeAddModal() {
cartModal.classList.remove('is-open');
}
function openCartOverlay() {
cartOverlay.classList.add('is-open');
htmlSelector.classList.add('is-locked');
}
function closeCartOverlay() {
cartOverlay.classList.remove('is-open');
htmlSelector.classList.remove('is-locked');
}
cartModalClose.addEventListener('click', function() {
closeAddModal();
closeCartOverlay();
});
cartDrawerClose.addEventListener('click', function() {
closeCartDrawer();
closeCartOverlay();
});
// cart is empty stanje
cartOverlay.addEventListener('click', function() {
closeAddModal();
closeCartDrawer();
closeCartOverlay();
});
cartDrawerTrigger.addEventListener('click', function(event) {
event.preventDefault();
//fetchCart();
openCartDrawer();
openCartOverlay();
});
document.addEventListener('DOMContentLoaded', function() {
fetchCart();
});
<!--ajax cart modal-->
<div class="ajax-cart__modal js-ajax-cart-modal">
<div class="ajax-cart-modal">
<!--ajax cart modal close-->
<div class="ajax-cart-modal__close js-ajax-cart-modal-close">
{% include 'icon-close' %}
</div>
<!--end ajax cart modal close-->
<!--ajax cart modal content-->
<div class="ajax-cart-modal__content js-ajax-cart-modal-content"></div>
<!--end ajax cart modal content-->
</div>
</div>
<!--end ajax cart modal-->
<!--ajax cart drawer-->
<div class="ajax-cart__drawer js-ajax-cart-drawer">
<div class="ajax-cart-drawer">
<!--ajax cart drawer close-->
<div class="ajax-cart-drawer__close js-ajax-cart-drawer-close">
{% include 'icon-close' %}
</div>
<!--end ajax cart drawer close-->
<!--ajax cart drawer content-->
<div class="ajax-cart-drawer__content js-ajax-cart-drawer-content"></div>
<!--end ajax cart drawer content-->
<!--ajax cart drawer buttons-->
<div class="ajax-cart-drawer__buttons">
<a href="/cart/" class="button button--black button--full-width js-button">
<span>Go to cart</span>
</a>
<a href="/checkout/" class="button button--black button--full-width js-button js-ajax-checkout-button">
<span>Proceed to checkout</span>
</a>
</div>
<!--end ajax cart drawer buttons-->
</div>
</div>
<!--end ajax cart drawer-->
<!--ajax cart overlay-->
<div class="ajax-cart__overlay js-ajax-cart-overlay"></div>
<!--end ajax cart overlay-->
.ajax-cart {
&__modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 40;
max-width: 575px;
background: getColor('white', 'default');
border: 1px solid #e9e9e9;
padding: 50px 65px;
opacity: 0;
visibility: hidden;
will-change: opacity, visibility;
&.is-open {
opacity: 1;
visibility: visible;
}
}
&__overlay {
position: fixed;
z-index: 30;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: getColor('black-40', 'variations');
opacity: 0;
visibility: hidden;
will-change: opacity, visibility;
&.is-open {
opacity: 1;
visibility: visible;
}
}
&__drawer {
transition: getTransition();
position: fixed;
z-index: 40;
right: -400px;
top: 0;
width: 400px;
height: 100%;
background: #f6f6f6;
will-change: transform;
border-left: 1px solid #e9e9e9;
&.is-open {
transform: translateX(-100%);
}
}
}
.ajax-cart-modal {
position: relative;
&__close {
position: absolute;
right: 10px;
top: 10px;
}
&__content {
padding: 20px;
}
}
.ajax-cart-drawer {
position: relative;
height: 100%;
&__close {
position: absolute;
right: 10px;
top: 5px;
}
&__content {
padding: 25px 25px 190px;
height: 100%;
overflow: hidden;
overflow-y: scroll;
}
&__buttons {
position: absolute;
z-index: 10;
left: 0;
bottom: 0;
width: 100%;
height: 190px;
background: #f6f6f6;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: flex-end;
.button {
&:last-child {
margin-top: auto;
}
}
}
}
.ajax-cart-item {
&__single {
position: relative;
margin-bottom: 20px;
border-bottom: 2px solid red;
}
&__title {
}
&__image {
width: 100px;
}
&__price {
}
&__quantity {
}
&__remove {
@include center(vertical);
right: 5px;
width: 15px;
height: 15px;
background: url('data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiBpY29uLWNsb3NlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj4KICAgIDxwYXRoIGQ9Ik0xNS44OSAxNC42OTZsLTQuNzM0LTQuNzM0IDQuNzE3LTQuNzE3Yy40LS40LjM3LTEuMDg1LS4wMy0xLjQ4NXMtMS4wODUtLjQzLTEuNDg1LS4wM0w5LjY0MSA4LjQ0NyA0Ljk3IDMuNzc2Yy0uNC0uNC0xLjA4NS0uMzctMS40ODUuMDNzLS40MyAxLjA4NS0uMDMgMS40ODVsNC42NzEgNC42NzEtNC42ODggNC42ODhjLS40LjQtLjM3IDEuMDg1LjAzIDEuNDg1czEuMDg1LjQzIDEuNDg1LjAzbDQuNjg4LTQuNjg3IDQuNzM0IDQuNzM0Yy40LjQgMS4wODUuMzcgMS40ODUtLjAzcy40My0xLjA4NS4wMy0xLjQ4NXoiLz4KPC9zdmc+Cg==') center no-repeat;
background-size: cover;
cursor: pointer;
}
}
<!--cart-->
<div class="header__cart">
<a class="js-ajax-cart-drawer-trigger" href="/cart">
{% include 'icon-cart' %}
<span class="js-ajax-cart-counter">{{ cart.item_count }}</span>
</a>
</div>
<!--end cart-->
<!--product form-->
<form action="/cart/add" method="post" enctype="multipart/form-data" id="add-to-cart-{{ product.handle }}">
<!--product variants-->
{% unless product.has_only_default_variant %}
{% for option in product.options_with_values %}
<div class="single-product__option-wrapper js">
{% assign option-name = option.name | downcase %}
<div class="js-single-product-option-{{ option-name }} single-product-option-{{ option-name }}"
id="SingleOptionSelector-{{ forloop.index0 }}">
{% for value in option.values %}
{% assign product-handle = product.handle %}
{% assign is_color = false %}
{% assign stripped-value = value | split: ' ' | last | handle %}
{% if option-name contains 'color' or option-name contains 'colour' %}
{% assign is_color = true %}
{% endif %}
{% if is_color %}
<input type="radio" name="{{ option-name }}-{{ product-handle }}"
class="single-option-selector single-product-option-{{ option-name }}__input js-radio-button"
data-single-option-selector
data-index="option{{ option.position }}"
value="{{ value | escape }}"
data-color="{{ value | handleize }}"
{% if option.selected_value == value %}checked="checked"{% endif %}
id="variant_{{ option-name }}-{{ product-handle }}-{{ forloop.index0 }}"/>
<label for="variant_{{ option-name }}-{{ product-handle }}-{{ forloop.index0 }}"
class="single-product-option-{{ option-name }}__label {% if stripped-value contains 'white' %}single-product-option-{{ option-name }}__label--white{% endif %}"
style="background-color: {{ stripped-value }};">
{% include 'icon-check' %}
</label>
{% else %}
<input type="radio" name="{{ option-name }}-{{ product-handle }}"
class="single-option-selector single-product-option-{{ option-name }}__input"
data-single-option-selector
data-index="option{{ option.position }}"
value="{{ value | escape }}"
{% if option.selected_value == value %}checked="checked"{% endif %}
id="variant_{{ option-name }}-{{ product-handle }}-{{ forloop.index0 }}"/>
<label for="variant_{{ option-name }}-{{ product-handle }}-{{ forloop.index0 }}"
class="single-product-option-{{ option-name }}__label">{{ value }}</label>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
{% endunless %}
<select name="id" class="no-js" data-product-select>
{% for variant in product.variants %}
<option
{% if variant == current_variant %}selected="selected"{% endif %}
{% unless variant.available %}disabled="disabled"{% endunless %}
value="{{ variant.id }}">
{{ variant.title }}
</option>
{% endfor %}
</select>
<!--end product variants-->
<!--product add to cart-->
<button
class="single-product__add-to-cart u-b6 js-ajax-add-to-cart"
type="submit"
name="add"
data-add-to-cart
{% unless current_variant.available %}disabled="disabled"{% endunless %}>
<span data-add-to-cart-text>
{% if current_variant.available %}
{{ 'products.product.add_to_cart' | t }}
{% else %}
{{ 'products.product.sold_out' | t }}
{% endif %}
</span>
</button>
<!--end product add to cart-->
</form>
<!--end product form-->
@McDracostar
Copy link

Hi @marioloncarek I have been trying for a bit but I can't seem to get the const to work correctly as I am unsure to what I have to reference.

Once again sorry for bothering you about this.

const ajaxloop = document.querySelectorAll(".go-cart__trigger");

for (const div of ajaxloop) {
div.addEventListener('click', () => {
console.log("for .. of worked");
});
}

@marioloncarek
Copy link
Author

hey @McDracostar actually i can see that goCart already uses querySelectorAll for cartTrigger, so actually you dont have to write any code for gocart as it will adopt. but now you need to find out why its not registering the second cart trigger. Wild guess, maybe you initialize goCart before the theme script dedicated to make copy of navigation. Firstly clone your navigation, then init goCart.

this.cartTrigger.forEach((item) => {
            item.addEventListener('click', () => {
                if (this.isDrawerMode) {
                    this.openCartDrawer();
                } else {
                    this.openMiniCart();
                }
                this.openCartOverlay();
            });
        });

@McDracostar
Copy link

Hi @marioloncarek, thanks fain for helping. And I think I might be a little confused as you mention for me to

initialize goCart before the theme script dedicated to make copy of navigation.

However I am unsure to which section you mean as you then say to clone the navigation and then initiate the goCart.

So do you mean in the core theme.js code itself, I add the code that you gave me, in the following segment?


this.cartTrigger.forEach((item) => {
item.addEventListener('click', () => {
if (this.isDrawerMode) {
this.openCartDrawer();
} else {
this.openMiniCart();
}
this.openCartOverlay();
});
});

function init() {
cacheSelectors();
config.isActive = false;

// Clone search/cart icons into fixed nav
if (cache.$siteNavSearchCart.contents().length) {
cache.$siteNavSearchCart
.contents()
.clone()
.appendTo($(selectors.stickyNavSearchCart));
}


Otherwise i have attached an image to see if I understand correctly~

Apologies once again if I am being a bother I genuinely am trying to learn it as best as I can along with your guidance, as I love how this cart works.

code-insertion-point

@marioloncarek
Copy link
Author

no no dont doo anything about goCart inside your core js file. Just make sure you firstly call your core .js file and then call gocart @McDracostar

@min23a
Copy link

min23a commented Feb 1, 2021

@marioloncarek can you help me with the install process for california theme i am not using any module?

@marioloncarek
Copy link
Author

@min23a
Copy link

min23a commented Feb 2, 2021

@marioloncarek But i am not coding in vs or any ide so its in the the shopify admin panel..... So how can i import?

@marioloncarek
Copy link
Author

@min23a not possible like that

@McDracostar
Copy link

@marioloncarek, I think I missed a bit of code within this javascript segment that might explain the initialization issue with the go-cart, currently the search function still works fine as part of the core theme.

It seems that there is a duplicate function within the core .js file seen here for the search function in the stickynav. Would you know how I could duplicate and modify it to work the same for go-cart.js?

function initHeaderSearch() {
// This function runs after the header search element
// is duplicated into the sticky nav, meaning there are
// two search fields to be aware of at this point.
var $searchForm = $('.site-header__search');

$searchForm.each(function(i, el) {
  var $form = $(el);
  var $input = $form.find('.' + config.searchInputClass);
  var $submit = $form.find('.' + config.searchSubmitClass);

  $input.add($submit).on('focus blur', function() {
    $form.toggleClass('active-form');
  });

  $submit.on('mousedown', function() {
    if ($form.hasClass('active-form')) {
      $form.submit();
    }
  });
});

}

I have linked the whole stickynav segment from the core .js file for you, I believe its on line 177

https://we.tl/t-weKtNZInjz

@marioloncarek
Copy link
Author

@McDracostar link is not working and it would be very hard for me to fix anything from github without an context

@McDracostar
Copy link

@McDracostar link is not working and it would be very hard for me to fix anything from github without an context

Hi @marioloncarek , apologies, what I mean is the code I found, seems to duplicate the search bar from the standard navigation bar, to the sticky so it can remain functional.
(image attached)

Untitled-1

In saying that do you know if (and how) the code seen in my previous comment can be duplicated and modified, to do the same for the go-cart?

Also sorry somehow it linked your own name this link should work now https://we.tl/t-weKtNZInjz

@marioloncarek
Copy link
Author

as i said, goCart already searches for cart classes, your goCart script tag is probably before the core js script tag, it should be the other way around @McDracostar

@McDracostar
Copy link

McDracostar commented Feb 3, 2021

as i said, goCart already searches for cart classes, your goCart script tag is probably before the core js script tag, it should be the other way around @McDracostar

Hi @marioloncarek , if you meant where in the theme file it is implemented it is placed afterwards near the bottom of the theme.liquid file
If you mean the following:

{{ 'gocart.js' | asset_url | script_tag | async }}
<script>
var goCart = new GoCart();
</script>

Then yes this is placed just before the < /body > closing tag as goCart requires. And the Theme.js script is up the top within the < head >tags

@marioloncarek
Copy link
Author

@McDracostar and the cloned cart is not opening, right?

@McDracostar
Copy link

McDracostar commented Feb 4, 2021

@marioloncarek , yeah~ that is correct, the cloned cart won't open, and I think its because the core theme.js isn't duplicating the cart correctly.

But I think the snippet of code I showed you earlier might explain why the search function still works fine, because it is duplicating the functions from my understanding.

@marioloncarek
Copy link
Author

@McDracostar check if gocart class gets cloned

@marioloncarek
Copy link
Author

also try to remove async/defer from both tags and see if maybe this is causing the problem

@McDracostar
Copy link

@McDracostar check if gocart class gets cloned

@marioloncarek , can confirm the class is cloned yes. and I removed the defer from the theme.js tag, and the async from the gocart.js tag.

  1. First tested the removal of defer from the theme.js and the stickynav stopped working.
  2. Along with that, I then the removed async from gocart so both no longer have them, the go-cart continued to work, but the stickynav still wouldn't show.
  3. So I restored the codes back to how they were after that, trying a combination of one without the other and even changing from defer to async and visa vie.
    goCart

@marioloncarek
Copy link
Author

sorry bro all i can do from this position

@McDracostar
Copy link

sorry bro all i can do from this position

All good man! Thankyou anyway, I appreciate your assistance.

@hamzachughtai
Copy link

@marioloncarek Hope you are doing well mate I have integrated go cart in my theme but it is not triggering with Add to Cart button using debut theme
https://b5c4mxvk6b7ol054-53338505408.shopifypreview.com

@hamzachughtai
Copy link

hamzachughtai commented Aug 23, 2021

https://b5c4mxvk6b7ol054-53338505408.shopifypreview.com/products/test

Here on the product page it is not triggering. integration done on debut theme. And also the cart is not updating in real time have to refresh again.

@sajalghoshui
Copy link

sajalghoshui commented Sep 21, 2021

@marioloncarek code is very nice and working fine, Just need one assistance that price is skipping decimal value

If price is 62.99 it is showing 6299

@marioloncarek
Copy link
Author

marioloncarek commented Sep 21, 2021

@sajalghoshui please try this - this is the production version https://github.com/bornfight/goCart.js

@sajalghoshui
Copy link

@sajalghoshui please try this - this is the production version https://github.com/bornfight/goCart.js

Thanks for your reply :)

@Anit2000
Copy link

@Deniz-Isso

But you have {% form 'product', product, id: product_form_id %} ? that is what you are looking for, you just copied it to me :))

after changing it's id as u mentioned it shows some liquid error how can i fix that

@sanjay-makwana-avidbrio
Copy link

@marioloncarek does this solution working with a prestige theme?

@marioloncarek
Copy link
Author

@Sanjayavidbrio i have no idea what a prestige theme is, sorry :/

i suggest you try and let us all know :) https://github.com/bornfight/goCart.js

@thanhkma1996
Copy link

Hi @marioloncarek
after i installed it, accessing the collection page and product both shows error 404 product not found i tried to uninstall but my product is still not showing please help me answer this question thanks
image

@marioloncarek
Copy link
Author

my plugin would not cause this kind of problem :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment