Shopify Ajax Cart Drawer for Shopify [No APP or external library]

Shopify Ajax Cart Drawer for Shopify [No APP or external library]

Compatibility: Dawn 5.0 and earlier version ONLY

Many of our customers have been inquiring about an Ajax Cart Drawer for Shopify. Creating a sidebar is straightforward, but developing a cart that updates in real-time without needing to refresh the page is more complex. This is where the Ajax Cart Drawer comes into play. This feature allows you to add a cart drawer using only pure JavaScript code with Shopify Ajax API and Shopify's Section Rendering API, without the need for any apps or external libraries.

Please note that this functionality is already included in the latest versions of Shopify 2.0 FREE themes, although the design and functionality might differ

Check DEMO store here 💻. Quickview button not included. Password: made4uo

What is an Ajax Cart Drawer?

A traditional shopping cart on a Shopify store typically requires users to navigate to a separate cart page to view their added items. An Ajax cart drawer, on the other hand, is a more streamlined approach. Here's a breakdown:

  • Ajax: This refers to a web development technique that allows for asynchronous data updates without reloading the entire page.
  • Cart Drawer: This is a sidebar element that slides out or expands to reveal the contents of the shopping cart.

So, an Ajax cart drawer combines these aspects to create a cart that updates dynamically within a drawer on the same page. Users can add or remove items, view cart totals, and potentially proceed to checkout without ever leaving the current page they're browsing.

Why Use an Ajax Cart Drawer?

There are several advantages to incorporating an Ajax cart drawer on your Shopify store:

  • Improved User Experience: By allowing users to manage their cart without page reloads, the Ajax cart drawer creates a smoother and more efficient shopping experience.
  • Increased Sales: Faster access to the cart can potentially lead to higher conversion rates, as users are less likely to abandon their carts during the checkout process.
  • Reduced Bounce Rates: By keeping users engaged on the same page, an Ajax cart drawer can potentially reduce bounce rates (the percentage of visitors who leave your site after viewing only one page).
  • Enhanced Mobile Usability: On mobile devices, where screen space is limited, an Ajax cart drawer can provide a more user-friendly way to access and manage the cart compared to navigating to a separate cart page.

Additional Considerations:

While an Ajax cart drawer offers numerous benefits, here are some additional factors to keep in mind for successful implementation:

  • Performance:

    • Page Loading Speed: Ensure the Ajax functionality doesn't negatively impact your page loading speed. Large or complex code snippets could slow down your website, hindering user experience.
    • Server Load: Consider the potential increase in server load with frequent Ajax updates. Ensure your hosting plan can handle the additional traffic without performance issues.
  • Design and User Experience:

    • Visual Appeal: Design the cart drawer to seamlessly integrate with your store's overall aesthetic.
    • Clarity and Usability: Make the cart drawer content clear and easy to understand. Users should be able to quickly see product information, edit quantities, and proceed to checkout without confusion.
    • Mobile Optimization: Ensure the cart drawer displays and functions flawlessly on mobile devices, considering the smaller screen size and potential touch interactions.
  • Functionality:

    • Cross-browser Compatibility: Test the Ajax cart drawer functionality across different web browsers to ensure it works as intended for all users.
    • Error Handling: Implement proper error handling mechanisms to address any potential issues with product data retrieval or cart updates.
    • Wishlist Integration: Consider integrating the cart drawer with a wishlist functionality, allowing users to save items for later without committing to checkout immediately.

 What you are buying:

  • Change the background of the drawer
  • Add a cart note
  • Have an option to open the cart whenever the customer goes to the cart url
  • Cart updates without refreshing the browser
  • You may also like feature is added

What makes our code better:

  • We do not use external libraries, with that being said, our code will have no to minimal effect to your website's speed performance
  • We do not leave or add codes use to advertise for our website
  • Our code is mobile friendly

Any issues related to the code will be fix with no additional cost, excluding code customization requests. Simply contact us with "Chat with us." We are just a button away. 

[]

Steps on how to add Shopify Ajax Cart Drawer to Your Shopify Store

Step #1. Open your code editor

From you Admin page, go to Online store, then Themes. Choose the theme you want to edit, then click the three dots, then Edit code

Step #2. Create a new section file

In Section folder, create a new section, name it "shopify-ajax-cart-drawer", then paste the code below. 

{{ 'component-cart.css' | asset_url | stylesheet_tag }}
{{ 'component-cart-items.css' | asset_url | stylesheet_tag }}
{{ 'component-totals.css' | asset_url | stylesheet_tag }}
{{ 'component-price.css' | asset_url | stylesheet_tag }}
{{ 'component-discounts.css' | asset_url | stylesheet_tag }}
{{ 'component-loading-overlay.css' | asset_url | stylesheet_tag }}

<link rel="stylesheet" href="{{ 'component-card.css' | asset_url }}" media="print" onload="this.media='all'">
<link rel="stylesheet" href="{{ 'section-product-recommendations.css' | asset_url }}" media="print" onload="this.media='all'">

<noscript>{{ 'component-card.css' | asset_url | stylesheet_tag }}</noscript>
<noscript>{{ 'section-product-recommendations.css' | asset_url | stylesheet_tag }}</noscript>

<script src="{{ 'cart.js' | asset_url }}" defer="defer"></script>
<div class="cart-overlay">&nbsp</div>
<cart-items class="close-drawer{% if cart == empty %} is-empty{% endif %}" data-open-cart-with-url= {{section.settings.open_with_url}}>
{% include "icon-close" %}
<div class="title-wrapper-with-link">
<h2 class="title title--primary">{{ 'sections.cart.title' | t }}</h2>
<a href="{{ routes.all_products_collection_url }}" class="underlined-link">{{ 'general.continue_shopping' | t }}</a>
</div>

<div class="cart__warnings">
<h2 class="cart__empty-text">{{ 'sections.cart.empty' | t }}</h2>
<a href="{{ routes.all_products_collection_url }}" class="button">
{{ 'general.continue_shopping' | t }}
</a>

{%- if shop.customer_accounts_enabled and customer == nil -%}
<h2 class="cart__login-title">{{ 'sections.cart.login.title' | t }}</h2>
<p class="cart__login-paragraph">
{{ 'sections.cart.login.paragraph_html' | t: link: routes.account_login_url }}
</p>
{%- endif -%}
</div>

<form action="{{ routes.cart_url }}" class="cart__contents critical-hidden" method="post" id="cart">
<div class="cart__items" id="cart-drawer" data-id="{{ section.id }}">
<div class="js-contents">
{%- if cart != empty -%}
<table class="cart-items">
<tbody>

{%- for item in cart.items -%}
<tr class="cart-item" id="CartItem-{{ item.index | plus: 1 }}">
<td class="cart-item__details">
<div class="cart-item__media">
{% if item.image %}
{% comment %} Leave empty space due to a:empty CSS display: none rule {% endcomment %}
<a href="{{ item.url }}" class="cart-item__link" aria-hidden="true" tabindex="-1"> </a>
<div class="cart-item__image-container gradient global-media-settings">
<img src="{{ item.image | image_url: width: 300 }}"
class="cart-item__image"
alt="{{ item.image.alt | escape }}"
loading="lazy"
width="150"
height="{{ 150 | divided_by: item.image.aspect_ratio | ceil }}"
>
</div>
{% endif %}
</div>
{%- if section.settings.show_vendor -%}
<p class="caption-with-letter-spacing">{{ item.product.vendor }}</p>
{%- endif -%}
</td>

<td class="cart-item__totals right medium-hide large-up-hide">
<div class="loading-overlay hidden">
<div class="loading-overlay__spinner">
<svg aria-hidden="true" focusable="false" role="presentation" class="spinner" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="path" fill="none" stroke-width="6" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</div>
<div class="cart-item__price-wrapper">
{%- if item.original_line_price != item.final_line_price -%}
<dl class="cart-item__discounted-prices">
<dt class="visually-hidden">
{{ 'products.product.price.regular_price' | t }}
</dt>
<dd>
<s class="cart-item__old-price price price--end">
{{ item.original_line_price | money }}
</s>
</dd>
<dt class="visually-hidden">
{{ 'products.product.price.sale_price' | t }}
</dt>
<dd class="price price--end">
{{ item.final_line_price | money }}
</dd>
</dl>
{%- else -%}
<span class="price price--end">
{{ item.original_line_price | money }}
</span>
{%- endif -%}

{%- if item.variant.available and item.unit_price_measurement -%}
<div class="unit-price caption">
<span class="visually-hidden">{{ 'products.product.price.unit_price' | t }}</span>
{{ item.variant.unit_price | money }}
<span aria-hidden="true">/</span>
<span class="visually-hidden">&nbsp;{{ 'accessibility.unit_price_separator' | t }}&nbsp;</span>
{%- if item.variant.unit_price_measurement.reference_value != 1 -%}
{{- item.variant.unit_price_measurement.reference_value -}}
{%- endif -%}
{{ item.variant.unit_price_measurement.reference_unit }}
</div>
{%- endif -%}
</div>
</td>

<td class="cart-item__totals right small-hide">
<div class="cart-item__quantity">
<div class="cart-item__quantity-wrapper">
<label class="visually-hidden" for="Quantity-{{ item.index | plus: 1 }}">
{{ 'products.product.quantity.label' | t }}
</label>
<quantity-input class="quantity">
<button class="quantity__button no-js-hidden" name="minus" type="button">
<span class="visually-hidden">{{ 'products.product.quantity.decrease' | t: product: item.product.title | escape }}</span>
{% render 'icon-minus' %}
</button>
<input class="quantity__input"
type="number"
name="updates[]"
value="{{ item.quantity }}"
min="0"
aria-label="{{ 'products.product.quantity.input_label' | t: product: item.product.title | escape }}"
id="Quantity-{{ item.index | plus: 1 }}"
data-index="{{ item.index | plus: 1 }}">
<button class="quantity__button no-js-hidden" name="plus" type="button">
<span class="visually-hidden">{{ 'products.product.quantity.increase' | t: product: item.product.title | escape }}</span>
{% render 'icon-plus' %}
</button>
</quantity-input>

<cart-remove-button id="Remove-{{ item.index | plus: 1 }}" data-index="{{ item.index | plus: 1 }}">
<a href="{{ item.url_to_remove }}" class="button button--tertiary" aria-label="{{ 'sections.cart.remove_title' | t: title: item.title }}">
{% render 'icon-remove' %}
</a>
</cart-remove-button>
</div>
<div class="cart-item__error" id="Line-item-error-{{ item.index | plus: 1 }}" role="alert">
<small class="cart-item__error-text"></small>
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-error" viewBox="0 0 13 13">
<circle cx="6.5" cy="6.50049" r="5.5" stroke="white" stroke-width="2"/>
<circle cx="6.5" cy="6.5" r="5.5" fill="#EB001B" stroke="#EB001B" stroke-width="0.7"/>
<path d="M5.87413 3.52832L5.97439 7.57216H7.02713L7.12739 3.52832H5.87413ZM6.50076 9.66091C6.88091 9.66091 7.18169 9.37267 7.18169 9.00504C7.18169 8.63742 6.88091 8.34917 6.50076 8.34917C6.12061 8.34917 5.81982 8.63742 5.81982 9.00504C5.81982 9.37267 6.12061 9.66091 6.50076 9.66091Z" fill="white"/>
<path d="M5.87413 3.17832H5.51535L5.52424 3.537L5.6245 7.58083L5.63296 7.92216H5.97439H7.02713H7.36856L7.37702 7.58083L7.47728 3.537L7.48617 3.17832H7.12739H5.87413ZM6.50076 10.0109C7.06121 10.0109 7.5317 9.57872 7.5317 9.00504C7.5317 8.43137 7.06121 7.99918 6.50076 7.99918C5.94031 7.99918 5.46982 8.43137 5.46982 9.00504C5.46982 9.57872 5.94031 10.0109 6.50076 10.0109Z" fill="white" stroke="#EB001B" stroke-width="0.7"/>
</svg>
</div>
</div>

<div class="loading-overlay hidden">
<div class="loading-overlay__spinner">
<svg aria-hidden="true" focusable="false" role="presentation" class="spinner" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="path" fill="none" stroke-width="6" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</div>

<div class="cart-item__price-wrapper">
{%- if item.original_line_price != item.final_line_price -%}
<dl class="cart-item__discounted-prices">
<dt class="visually-hidden">
{{ 'products.product.price.regular_price' | t }}
</dt>
<dd>
<s class="cart-item__old-price price price--end">
{{ item.original_line_price | money }}
</s>
</dd>
<dt class="visually-hidden">
{{ 'products.product.price.sale_price' | t }}
</dt>
<dd class="price price--end">
{{ item.final_line_price | money }}
</dd>
</dl>
{%- else -%}
<span class="price price--end">
{{ item.original_line_price | money }}
</span>
{%- endif -%}

{%- if item.variant.available and item.unit_price_measurement -%}
<div class="unit-price caption">
<span class="visually-hidden">{{ 'products.product.price.unit_price' | t }}</span>
{{ item.variant.unit_price | money }}
<span aria-hidden="true">/</span>
<span class="visually-hidden">&nbsp;{{ 'accessibility.unit_price_separator' | t }}&nbsp;</span>
{%- if item.variant.unit_price_measurement.reference_value != 1 -%}
{{- item.variant.unit_price_measurement.reference_value -}}
{%- endif -%}
{{ item.variant.unit_price_measurement.reference_unit }}
</div>
{%- endif -%}
</div>
</td>
</tr>

<tr>
<td colspan="2" style="padding-top: 1rem">
<a href="{{ item.url }}" class="cart-item__name h4 break">{{ item.product.title | escape }}</a>

<div class="product-price">
{%- if item.original_price != item.final_price -%}
<div class="cart-item__discounted-prices">
<span class="visually-hidden">
{{ 'products.product.price.regular_price' | t }}
</span>
<s class="cart-item__old-price product-option">
{{- item.original_price | money -}}
</s>
<span class="visually-hidden">
{{ 'products.product.price.sale_price' | t }}
</span>
<strong class="cart-item__final-price product-option">
{{ item.final_price | money }}
</strong>
</div>
{%- else -%}
<div class="product-option">
{{ item.original_price | money }}
</div>
{%- endif -%}
</div>

{%- if item.product.has_only_default_variant == false or item.properties.size != 0 or item.selling_plan_allocation != nil -%}
{%- if item.product.has_only_default_variant == false -%}
{%- for option in item.options_with_values -%}
<div class="product-option">
<dt>{{ option.name }}: </dt>
<dd>{{ option.value }}</dd>
</div>
{%- endfor -%}
{%- endif -%}

{%- for property in item.properties -%}
{%- assign property_first_char = property.first | slice: 0 -%}
{%- if property.last != blank and property_first_char != '_' -%}
<div class="product-option">
<dt>{{ property.first }}: </dt>
<dd>
{%- if property.last contains '/uploads/' -%}
<a href="{{ property.last }}" class="link" target="_blank">
{{ property.last | split: '/' | last }}
</a>
{%- else -%}
{{ property.last }}
{%- endif -%}
</dd>
</div>
{%- endif -%}
{%- endfor -%}

<p class="product-option">{{ item.selling_plan_allocation.selling_plan.name }}</p>
{%- endif -%}

<ul class="discounts list-unstyled" role="list" aria-label="{{ 'customer.order.discount' | t }}">
{%- for discount in item.discounts -%}
<li class="discounts__discount">
{%- render 'icon-discount' -%}
{{ discount.title }}
</li>
{%- endfor -%}
</ul>
</td>
</tr>
{%- endfor -%}
</tbody>
</table>
{%- endif -%}

<div class="cart__footer">
{%- if section.settings.show_cart_note -%}
<cart-note class="cart__note field">
<label for="Cart-note">{{ 'sections.cart.note' | t }}</label>
<textarea class="text-area field__input" name="note" form="cart" id="Cart-note" placeholder="{{ 'sections.cart.note' | t }}">{{ cart.note }}</textarea>
</cart-note>
{%- endif -%}

<div class="cart__blocks">
<div class="js-contents" {{ block.shopify_attributes }}>
<div class="totals">
<h3 class="totals__subtotal">{{ 'sections.cart.subtotal' | t }}</h3>
<p class="totals__subtotal-value">{{ cart.total_price | money_with_currency }}</p>
</div>

<div>
{%- if cart.cart_level_discount_applications.size > 0 -%}
<ul class="discounts list-unstyled" role="list" aria-label="{{ 'customer.order.discount' | t }}">
{%- for discount in cart.cart_level_discount_applications -%}
<li class="discounts__discount discounts__discount--position">
{%- render 'icon-discount' -%}
{{ discount.title }}
(-{{ discount.total_allocated_amount | money }})
</li>
{%- endfor -%}
</ul>
{%- endif -%}
</div>

<small class="tax-note caption-large rte">
{%- if cart.taxes_included and shop.shipping_policy.body != blank -%}
{{ 'sections.cart.taxes_included_and_shipping_policy_html' | t: link: shop.shipping_policy.url }}
{%- elsif cart.taxes_included -%}
{{ 'sections.cart.taxes_included_but_shipping_at_checkout' | t }}
{%- elsif shop.shipping_policy.body != blank -%}
{{ 'sections.cart.taxes_and_shipping_policy_at_checkout_html' | t: link: shop.shipping_policy.url }}
{%- else -%}
{{ 'sections.cart.taxes_and_shipping_at_checkout' | t }}
{%- endif -%}
</small>
</div>

<div class="cart__ctas" {{ block.shopify_attributes }}>
<noscript>
<button type="submit" class="cart__update-button button button--secondary" form="cart">
{{ 'sections.cart.update' | t }}
</button>
</noscript>

<button type="submit" id="checkout" class="cart__checkout-button button" name="checkout"{% if cart == empty %} disabled{% endif %} form="cart">
{{ 'sections.cart.checkout' | t }}
</button>
</div>

{%- if additional_checkout_buttons -%}
<div class="cart__dynamic-checkout-buttons additional-checkout-buttons">
{{ content_for_additional_checkout_buttons }}
</div>
{%- endif -%}
<div id="cart-errors"></div>
</div>
</div>
</div>
</div>
<p class="visually-hidden" id="cart-live-region-text" aria-live="polite" role="status"></p>
<p class="visually-hidden" id="shopping-cart-line-item-status" aria-live="polite" aria-hidden="true" role="status">{{ 'accessibility.loading' | t }}</p>
</form>

<cart-recommendations class="product-recommendations" data-products="{{ section.settings.number_of_products }}">
{% if recommendations.performed and recommendations.products_count > 0 %}
<h2 class="product-recommendations__heading">{{ section.settings.heading | escape }}</h2>
<slider-component>
<ul class="cart-slider" id="Slider-{{ section.id }}" role="list">
{% for recommendation in recommendations.products %}
<li id="Slide-{{ section.id }}">
{% if section.settings.old_version %}
{% render 'product-card',
product_card_product: recommendation,
media_size: section.settings.image_ratio,
show_secondary_image: section.settings.show_secondary_image,
add_image_padding: section.settings.add_image_padding,
show_vendor: section.settings.show_vendor,
show_image_outline: section.settings.show_image_outline,
show_rating: section.settings.show_rating
%}
{% else %}
{% render "card-product",
card_product: recommendation,
media_aspect_ratio: section.settings.image_ratio,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating
%}
{% endif %}

</li>
{% endfor %}
</ul>
{% endif %}
{% if recommendations.products_count > 2 %}
<div class="slider-buttons no-js-hidden">
<button type="button" class="slider-button slider-button--prev" name="previous" aria-label="{{ 'general.slider.previous_slide' | t }}" aria-controls="Slider-{{ section.id }}">{% render 'icon-caret' %}</button>
<div class="slider-counter caption">
<span class="slider-counter--current">1</span>
<span aria-hidden="true"> / </span>
<span class="visually-hidden">{{ 'general.slider.of' | t }}</span>
<span class="slider-counter--total">{{ recommendations.products_count }}</span>
</div>
<button type="button" class="slider-button slider-button--next" name="next" aria-label="{{ 'general.slider.next_slide' | t }}" aria-controls="Slider-{{ section.id }}">{% render 'icon-caret' %}</button>
</div>
{% endif %}
</slider-component>
</cart-recommendations>
</cart-items>

{% javascript %}
class CartNote extends HTMLElement {
constructor() {
super();

this.addEventListener('change', debounce((event) => {
const body = JSON.stringify({ note: event.target.value });
fetch(`${routes.cart_update_url}`, {...fetchConfig(), ...{ body }});
}, 300))
}
}

customElements.define('cart-note', CartNote);
{% endjavascript %}
<script>
document.addEventListener('DOMContentLoaded', function() {
function isIE() {
const ua = window.navigator.userAgent;
const msie = ua.indexOf('MSIE ');
const trident = ua.indexOf('Trident/');

return (msie > 0 || trident > 0);
}

if (!isIE()) return;
const cartSubmitInput = document.createElement('input');
cartSubmitInput.setAttribute('name', 'checkout');
cartSubmitInput.setAttribute('type', 'hidden');
document.querySelector('#cart').appendChild(cartSubmitInput);
document.querySelector('#checkout').addEventListener('click', function(event) {
document.querySelector('#cart').submit();
});
});
</script>

<style>
.cart-overlay {
position: fixed;
display: none;
background: transparent;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 5;
}

.show-overlay {
display: block !important;
}

cart-items {
position: fixed;
height: 100%;
width: 100%;
max-width: 40rem;
background: {{section.settings.background}};
z-index: 10;
right: 0;
padding: 3rem;
overflow-y: scroll;
transition: right .7s cubic-bezier(.94,.28,1,-0.08);
}

cart-items .icon-close {
position: absolute;
height: 2rem;
width: 2rem;
right: 3rem;
}

cart-items .icon-close:hover {
stroke: rgba(var(--color-foreground),.75);
stroke-width: 1px;
}

.cart__contents {
margin: auto;
width: 100%;
}
.cart-item__price-wrapper .price--end {
text-align: left;
}

cart-items .cart-item {
display: flex;
}

cart-items .cart-item__media {
width: 13rem;
}

.cart-item cart-remove-button{
margin: 0 !important;
height: unset !important;
}

cart-remove-button svg.icon-remove {
height: 2.5rem;
width: 2.5rem;
}

cart-items .totals {
display: flex;
align-items: center;
justify-content: space-between;
}

cart-items .quantity {
width: 8rem;
min-height: 2rem;
height: 3.5rem;
}


quantity-input .quantity__button {
width: 2rem;
}

.empty-cart {
display: none !important;
}

.close-drawer {
right: -60rem;
}

cart-items .cart__footer {
flex-direction: column;
margin-top: 2rem;
}

cart-items .title-wrapper-with-link {
margin-bottom: 0;
}

cart-items .cart__footer>*+* {
margin-top: 4rem;
margin-left: 0;
}

cart-items .cart-items {
table-layout: fixed;
}

.cart__footer > * {
width: 100%
}

.cart-items .cart-item__totals {
text-align: left;
width: 100%;
}

.cart-item .cart-item__quantity, .cart-items .cart-items__heading--wide {
padding-left: 0 !important;
}

.is-empty cart-recommendations {
display: none !important;
}

cart-recommendations slider-component {
width: 100%;
}

.cart-slider {
position: relative;
display: flex;
width: 100%;
flex-wrap: inherit;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scroll-padding-left: 1.5rem;
-webkit-overflow-scrolling: touch;
margin-bottom: 1rem;
padding-inline-start: 0;
list-style: none;
}

.cart-slider li {
min-width: 50%
}
.cart-slider li:not(:first-child) {
margin-left: 1rem
}

.cart-slider {
scrollbar-color: rgb(var(--color-foreground)) rgba(var(--color-foreground), 0.04);
-ms-overflow-style: none;
scrollbar-width: none;
}

.cart-slider::-webkit-scrollbar {
height: 0.4rem;
width: 0.4rem;
display: none;
}

cart-items .cart__warnings {
position: relative;
margin: auto;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

</style>

{% schema %}
{
"name": "Cart drawer",
"settings": [
{
"type": "checkbox",
"id": "show_vendor",
"default": false,
"label": "t:sections.main-cart-items.settings.show_vendor.label"
},
{
"type": "checkbox",
"id": "show_cart_note",
"default": false,
"label": "t:sections.main-cart-footer.settings.show_cart_note.label"
},
{
"type": "checkbox",
"id": "open_with_url",
"default": false,
"label": "Open drawer with cart url",
"info": "Cart drawer opens whenever the url is ends with /cart"
},
{
"type": "color",
"id": "background",
"default": "#F27272",
"label": "Drawer's background color"
},
{
"type": "header",
"content": "Product Recommendation"
},
{
"type": "paragraph",
"content": "t:sections.product-recommendations.settings.paragraph__1.content"
},
{
"type": "text",
"id": "heading",
"default": "You may also like",
"label": "t:sections.product-recommendations.settings.heading.label"
},
{
"type": "range",
"id": "number_of_products",
"min": 2,
"max": 10,
"step": 1,
"default": 4,
"label": "Maximum products to show"
},
{
"type": "header",
"content": "t:sections.product-recommendations.settings.header__2.content"
},
{
"type": "checkbox",
"id": "old_version",
"default": false,
"label": "Check if you have Dawn 2.5 version and below"
},
{
"type": "select",
"id": "image_ratio",
"options": [
{
"value": "adapt",
"label": "t:sections.product-recommendations.settings.image_ratio.options__1.label"
},
{
"value": "portrait",
"label": "t:sections.product-recommendations.settings.image_ratio.options__2.label"
},
{
"value": "square",
"label": "t:sections.product-recommendations.settings.image_ratio.options__3.label"
}
],
"default": "adapt",
"label": "t:sections.product-recommendations.settings.image_ratio.label"
},
{
"type": "checkbox",
"id": "show_rating",
"default": false,
"label": "t:sections.product-recommendations.settings.show_rating.label",
"info": "t:sections.product-recommendations.settings.show_rating.info"
}
]
}
{% endschema %}
      

Step #3. Find the header.liquid file

In the same folder, Sections, find the header.liquid. Find the word "cart_url".  Replace the highlighted code (Refer to the image below) with the code below.

header.liquid file
<cart-toggle class="header__icon header__icon--cart link focus-inset" id="cart-icon-bubble">
{%- liquid
if cart == empty
render 'icon-cart-empty'
else
render 'icon-cart'
endif
-%}
<span class="visually-hidden">{{ 'templates.cart.cart' | t }}</span>
{%- if cart != empty -%}
<div class="cart-count-bubble">
{%- if cart.item_count < 100 -%}
<span aria-hidden="true">{{ cart.item_count }}</span>
{%- endif -%}
<span class="visually-hidden">{{ 'sections.header.cart_count' | t: count: cart.item_count }}</span>
</div>
{%- endif -%}
</cart-toggle>

 

Step #4. See the cart-notification

In the same area, you can see the "cart-notification." Please refer to the image below. 

Shopify ajax cart notification code image

Step #5. Open product-form.js

Go to Asset folder, and open the product-form.js, then replace the code with the code below. 

if (!customElements.get('product-form')) {
customElements.define('product-form', class ProductForm extends HTMLElement {
constructor() {
super();

this.form = this.querySelector('form');
this.form.querySelector('[name=id]').disabled = false;
this.form.addEventListener('submit', this.onSubmitHandler.bind(this));
this.cartItems = document.querySelector('cart-items');
this.submitButton = this.querySelector('[type="submit"]');
}

onSubmitHandler(evt) {
evt.preventDefault();
if (this.submitButton.getAttribute('aria-disabled') === 'true') return;

this.handleErrorMessage();

this.submitButton.setAttribute('aria-disabled', true);
this.submitButton.classList.add('loading');
this.querySelector('.loading-overlay__spinner').classList.remove('hidden');

const config = fetchConfig('javascript');
config.headers['X-Requested-With'] = 'XMLHttpRequest';
delete config.headers['Content-Type'];

const formData = new FormData(this.form);
formData.append('sections', this.cartItems.getSectionsToRender().map((section) => section.id));
formData.append('sections_url', window.location.pathname);
config.body = formData;

fetch(`${routes.cart_add_url}`, config)
.then((response) => response.json())
.then((response) => {
if (response.status) {
this.handleErrorMessage(response.description);

const soldOutMessage = this.submitButton.querySelector('.sold-out-message');
if (!soldOutMessage) return;
this.submitButton.setAttribute('aria-disabled', true);
this.submitButton.querySelector('span').classList.add('hidden');
soldOutMessage.classList.remove('hidden');
this.error = true;
return;
}

this.error = false;
const quickAddModal = this.closest('quick-add-modal');
if (quickAddModal) {
document.body.addEventListener('modalClosed', () => {
setTimeout(() => { this.cartItems.renderContents(response) });
}, { once: true });
quickAddModal.hide(true);
} else {
this.cartItems.renderContents(response);
}
this.cartItems.viewDrawer();
})

.catch((e) => {
console.error(e);
})
.finally(() => {
this.submitButton.classList.remove('loading');
if (!this.error) this.submitButton.removeAttribute('aria-disabled');
this.querySelector('.loading-overlay__spinner').classList.add('hidden');
});
}

handleErrorMessage(errorMessage = false) {
this.errorMessageWrapper = this.errorMessageWrapper || this.querySelector('.product-form__error-message-wrapper');
if (!this.errorMessageWrapper) return;
this.errorMessage = this.errorMessage || this.errorMessageWrapper.querySelector('.product-form__error-message');

this.errorMessageWrapper.toggleAttribute('hidden', !errorMessage);

if (errorMessage) {
this.errorMessage.textContent = errorMessage;
}
}
});
}

 

Step #6. Find cart.js 

Still at the same folder, Asset, find the cart.js and replace the code with the code below. 

class CartRemoveButton extends HTMLElement {
constructor() {
super();
this.addEventListener('click', (event) => {
event.preventDefault();
this.closest('cart-items').updateQuantity(this.dataset.index, 0);
});
}
}

customElements.define('cart-remove-button', CartRemoveButton);

class CartItems extends HTMLElement {
constructor() {
super();
this.closeBtn = this.querySelector('.icon-close');
this.openBtn = document.querySelector('cart-toggle');
this.overlay = document.querySelector('.cart-overlay');

this.closeBtn.addEventListener('click', this.viewDrawer.bind(this));
this.openBtn.addEventListener('click', this.viewDrawer.bind(this));
this.overlay.addEventListener('click', this.overlayListener.bind(this))


this.lineItemStatusElement = document.getElementById('shopping-cart-line-item-status');
this.currentItemCount = Array.from(this.querySelectorAll('[name="updates[]"]'))
.reduce((total, quantityInput) => total + parseInt(quantityInput.value), 0);

this.debouncedOnChange = debounce((event) => {
this.onChange(event);
}, 300);

this.addEventListener('change', this.debouncedOnChange.bind(this));
this.cartFooter = document.getElementById('main-cart-footer');
this.parsedId;

this.productRecommendation = this.querySelector('cart-recommendations');
this.openWithUrl = this.dataset.openCartWithUrl;

if(this.openWithUrl == 'true') {
if(location.href.includes('/cart')) {
this.viewDrawer();
}
}
}

overlayListener() {
if(this.overlay.classList.contains('show-overlay')) {
this.viewDrawer();
}
}

getProductRecommendation(parsedId) {
this.parsedId = parsedId;
const numberOfProducts = this.productRecommendation.dataset.products;
const url = `/recommendations/products?section_id=cart-drawer&product_id=${this.parsedId}&limit=${numberOfProducts}`;

fetch(url)
.then(response => response.text())
.then(text => {
const html = document.createElement('div');
html.innerHTML = text;
const recommendations = html.querySelector('cart-recommendations');

if (recommendations && recommendations.innerHTML.trim().length) {
this.productRecommendation.innerHTML = recommendations.innerHTML;
}

if (html.querySelector('.grid__item')) {
this.productRecommendation.classList.add('product-recommendations--loaded');
}
})
.catch(e => {
console.error(e);
});
}

viewDrawer() {
this.classList.toggle('close-drawer');
this.overlay.classList.toggle('show-overlay');
}

onChange(event) {
this.updateQuantity(event.target.dataset.index, event.target.value, document.activeElement.getAttribute('name'));
}

getSectionsToRender() {
return [
{
id: 'cart-drawer',
section: document.getElementById('cart-drawer').dataset.id,
selector: '.js-contents',
},
{
id: 'cart-icon-bubble',
section: 'cart-icon-bubble',
selector: '.shopify-section'
},
{
id: 'cart-live-region-text',
section: 'cart-live-region-text',
selector: '.shopify-section'
}
];
}

renderContents(parsedState) {
this.parsedId = parsedState.product_id;
this.getProductRecommendation(parsedState.product_id)

this.classList.toggle('is-empty', parsedState.item_count === 0);
if (this.cartFooter) this.cartFooter.classList.toggle('is-empty', parsedState.item_count === 0);

this.getSectionsToRender().forEach((section => {
const elementToReplace =
document.getElementById(section.id).querySelector(section.selector) || document.getElementById(section.id);

elementToReplace.innerHTML =
this.getSectionInnerHTML(parsedState.sections[section.section], section.selector);
}));

this.updateLiveRegions(1, parsedState.item_count);
const lineItem = document.getElementById(`CartItem-1`);
if (lineItem && lineItem.querySelector(`[name="${name}"]`)) lineItem.querySelector(`[name="${name}"]`).focus();
this.disableLoading();
}

updateQuantity(line, quantity, name) {
this.enableLoading(line);

const body = JSON.stringify({
line,
quantity,
sections: this.getSectionsToRender().map((section) => section.section),
sections_url: window.location.pathname
});

fetch(`${routes.cart_change_url}`, {...fetchConfig(), ...{ body }})
.then((response) => {
return response.text();
})
.then((state) => {
const parsedState = JSON.parse(state);
this.parsedId = parsedState.product_id;
this.classList.toggle('is-empty', parsedState.item_count === 0);

if (this.cartFooter) this.cartFooter.classList.toggle('is-empty', parsedState.item_count === 0);

this.getProductRecommendation(parsedState.items[0].product_id);

this.getSectionsToRender().forEach((section => {
const elementToReplace =
document.getElementById(section.id).querySelector(section.selector) || document.getElementById(section.id);

elementToReplace.innerHTML =
this.getSectionInnerHTML(parsedState.sections[section.section], section.selector);
}));

this.updateLiveRegions(line, parsedState.item_count);
const lineItem = document.getElementById(`CartItem-${line}`);
if (lineItem && lineItem.querySelector(`[name="${name}"]`)) lineItem.querySelector(`[name="${name}"]`).focus();
this.disableLoading();
}).catch(() => {
this.querySelectorAll('.loading-overlay').forEach((overlay) => overlay.classList.add('hidden'));
document.getElementById('cart-errors').textContent = window.cartStrings.error;
this.disableLoading();
});
}

updateLiveRegions(line, itemCount) {
if (this.currentItemCount === itemCount) {
document.getElementById(`Line-item-error-${line}`)
.querySelector('.cart-item__error-text')
.innerHTML = window.cartStrings.quantityError.replace(
'[quantity]',
document.getElementById(`Quantity-${line}`).value
);
}

this.currentItemCount = itemCount;
this.lineItemStatusElement.setAttribute('aria-hidden', true);

const cartStatus = document.getElementById('cart-live-region-text');
cartStatus.setAttribute('aria-hidden', false);

setTimeout(() => {
cartStatus.setAttribute('aria-hidden', true);
}, 1000);
}

getSectionInnerHTML(html, selector) {
return new DOMParser()
.parseFromString(html, 'text/html')
.querySelector(selector).innerHTML;
}

enableLoading(line) {
document.getElementById('cart-drawer').classList.add('cart__items--disabled');
this.querySelectorAll(`#CartItem-${line} .loading-overlay`).forEach((overlay) => overlay.classList.remove('hidden'));
document.activeElement.blur();
this.lineItemStatusElement.setAttribute('aria-hidden', false);
}

disableLoading() {
document.getElementById('cart-drawer').classList.remove('cart__items--disabled');
}
}

customElements.define('cart-items', CartItems);

Step #7. Find the annoncement-bar

Go to the layout folder, and open the theme.liquid. Find the "announcement-bar", then paste the code below before it. 

   {% section 'shopify-ajax-cart-drawer' %}

 

Step #8. 

Almost done, all we have to do is to clean up. We need to delete this following section files to remove the errors. Open the Section folder, and find the "main-cart-items.liquid", open the file and click "delete file." Same with the "main-cart-footer.liquid." 

Step #9. Save the changes

Once you've added the code, save your changes to the theme code by clicking the SAVE button on the right hand upper corner.

Step #10. Customize theme

Go to the theme editor by clicking three dots on your left hand upper corner, then Customize theme. Make sure to SAVE once you are done customizing.

Conclusion 

An Ajax cart drawer can be a valuable addition to your Shopify store, enhancing user experience, potentially increasing sales, and improving mobile usability. However, consider the technical expertise required for code-based implementation and explore alternative options like Shopify apps if coding is not your forte.

2 comments

Hi @Sanjay,

Line item properties will need to be added to the code. Regarding the selling plan, the code is derive from Dawn cart, if the original code of Dawn supports it then yes.

Made4Uo

IS this code support extra line items property and selling plan option ?

sanjay

Leave a comment