Skip to content

Commit

Permalink
[Fix] QOL Cart race conditions (Shopify#3395)
Browse files Browse the repository at this point in the history
  • Loading branch information
sofiamatulis committed May 6, 2024
1 parent 55d2d5c commit 85b75b0
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 169 deletions.
41 changes: 41 additions & 0 deletions assets/bulk-add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class BulkAdd extends HTMLElement {
constructor() {
super();
this.queue = []
this.requestStarted = false;
this.ids = []
}

startQueue(id, quantity) {
this.queue.push({id, quantity})
const interval = setInterval(() => {
if (this.queue.length > 0) {
if (!this.requestStarted) {
this.sendRequest(this.queue)
}
} else {
clearInterval(interval)
}
}, 250)
}


sendRequest(queue) {
this.requestStarted = true;
const items = {}
const ids = []
queue.forEach((queueItem) => {
items[parseInt(queueItem.id)] = queueItem.quantity;
ids.push(queueItem.id)
});
this.queue = this.queue.filter(queueElement => !queue.includes(queueElement));
const quickOrderList = this.closest('quick-order-list');
quickOrderList.updateMultipleQty(items, ids)
}
}

if (!customElements.get('bulk-add')) {
customElements.define('bulk-add', BulkAdd)
};


4 changes: 4 additions & 0 deletions assets/quick-order-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ quick-order-list-remove-button .icon-remove {
left: 2rem;
top: 1.2rem;
}

.variant-remove-total--empty .loading__spinner {
top: -1rem;
}
}

quick-order-list-remove-button:hover .icon-remove {
Expand Down
218 changes: 57 additions & 161 deletions assets/quick-order-list.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
if (!customElements.get('quick-order-list-remove-button')) {
customElements.define(
'quick-order-list-remove-button',
class QuickOrderListRemoveButton extends HTMLElement {
class QuickOrderListRemoveButton extends BulkAdd {
constructor() {
super();
this.addEventListener('click', (event) => {
event.preventDefault();
const quickOrderList = this.closest('quick-order-list');
quickOrderList.updateQuantity(this.dataset.index, 0);
this.startQueue(this.dataset.index, 0);
});
}
}
Expand Down Expand Up @@ -69,15 +68,10 @@ if (!customElements.get('quick-order-list-remove-all-button')) {
if (!customElements.get('quick-order-list')) {
customElements.define(
'quick-order-list',
class QuickOrderList extends HTMLElement {
class QuickOrderList extends BulkAdd {
constructor() {
super();
this.cart = document.querySelector('cart-drawer');
this.actions = {
add: 'ADD',
update: 'UPDATE',
};

this.quickOrderListId = `quick-order-list-${this.dataset.productId}`;
this.defineInputsAndQuickOrderTable();

Expand Down Expand Up @@ -145,14 +139,13 @@ if (!customElements.get('quick-order-list')) {
const inputValue = parseInt(event.target.value);
const cartQuantity = parseInt(event.target.dataset.cartQuantity);
const index = event.target.dataset.index;
const name = document.activeElement.getAttribute('name');

const quantity = inputValue - cartQuantity;
this.cleanErrorMessageOnType(event);
if (inputValue == 0) {
this.updateQuantity(index, inputValue, name, this.actions.update);
this.startQueue(index, inputValue);
} else {
this.validateQuantity(event, name, index, inputValue, cartQuantity, quantity);
this.validateQuantity(event, index, inputValue, cartQuantity, quantity);
}
}

Expand All @@ -163,7 +156,7 @@ if (!customElements.get('quick-order-list')) {
});
}

validateQuantity(event, name, index, inputValue, cartQuantity, quantity) {
validateQuantity(event, index, inputValue, cartQuantity, quantity) {
if (inputValue < event.target.dataset.min) {
this.setValidity(
event,
Expand All @@ -178,9 +171,9 @@ if (!customElements.get('quick-order-list')) {
event.target.setCustomValidity('');
event.target.reportValidity();
if (cartQuantity > 0) {
this.updateQuantity(index, inputValue, name, this.actions.update);
this.startQueue(index, inputValue);
} else {
this.updateQuantity(index, quantity, name, this.actions.add);
this.startQueue(index, quantity);
}
}
}
Expand Down Expand Up @@ -262,57 +255,52 @@ if (!customElements.get('quick-order-list')) {
this.querySelectorAll('quantity-input').forEach((qty) => {
const debouncedOnChange = debounce((event) => {
this.onChange(event);
}, ON_CHANGE_DEBOUNCE_TIMER);
}, 100);
qty.addEventListener('change', debouncedOnChange.bind(this));
});
}

addDebounce(id) {
const element = this.querySelector(`#Variant-${id} quantity-input`);
const debouncedOnChange = debounce((event) => {
this.onChange(event);
}, ON_CHANGE_DEBOUNCE_TIMER);
element.addEventListener('change', debouncedOnChange.bind(this));
}

renderSections(parsedState, id) {
this.getSectionsToRender().forEach((section) => {
const sectionElement = document.getElementById(section.id);
if (
sectionElement &&
sectionElement.parentElement &&
sectionElement.parentElement.classList.contains('drawer')
) {
parsedState.items.length > 0
? sectionElement.parentElement.classList.remove('is-empty')
: sectionElement.parentElement.classList.add('is-empty');
setTimeout(() => {
document.querySelector('#CartDrawer-Overlay').addEventListener('click', this.cart.close.bind(this.cart));
renderSections(parsedState, ids) {
if (ids) {
this.ids.push(ids)
}
const intersection = this.queue.filter(element => ids.includes(element.id));
if (intersection.length === 0) {
this.getSectionsToRender().forEach((section) => {
const sectionElement = document.getElementById(section.id);
if (
sectionElement &&
sectionElement.parentElement &&
sectionElement.parentElement.classList.contains('drawer')
) {
parsedState.items.length > 0
? sectionElement.parentElement.classList.remove('is-empty')
: sectionElement.parentElement.classList.add('is-empty');
setTimeout(() => {
document.querySelector('#CartDrawer-Overlay').addEventListener('click', this.cart.close.bind(this.cart));
});
}
const elementToReplace =
sectionElement && sectionElement.querySelector(section.selector)
? sectionElement.querySelector(section.selector)
: sectionElement;
if (elementToReplace) {
if (section.selector === `#${this.quickOrderListId} .js-contents` && this.ids.length > 0) {
this.ids.flat().forEach((i) => {
elementToReplace.querySelector(`#Variant-${i}`).innerHTML =
this.getSectionInnerHTML(parsedState.sections[section.section], `#Variant-${i}`);
});
} else {
elementToReplace.innerHTML = this.getSectionInnerHTML(
parsedState.sections[section.section],
section.selector
);
}
}
});
}
const elementToReplace =
sectionElement && sectionElement.querySelector(section.selector)
? sectionElement.querySelector(section.selector)
: sectionElement;
if (elementToReplace) {
if (section.selector === `#${this.quickOrderListId} .js-contents` && id !== undefined) {
elementToReplace.querySelector(`#Variant-${id}`).innerHTML = this.getSectionInnerHTML(
parsedState.sections[section.section],
`#Variant-${id}`
);
} else {
elementToReplace.innerHTML = this.getSectionInnerHTML(
parsedState.sections[section.section],
section.selector
);
}
}
});
this.defineInputsAndQuickOrderTable();
if (id) {
this.addDebounce(id);
} else {
this.defineInputsAndQuickOrderTable();
this.addMultipleDebounce();
this.ids = []
}
}

Expand Down Expand Up @@ -406,8 +394,8 @@ if (!customElements.get('quick-order-list')) {
}
}

updateMultipleQty(items) {
this.querySelector('.variant-remove-total .loading__spinner').classList.remove('hidden');
updateMultipleQty(items, ids) {
this.querySelector('.variant-remove-total .loading__spinner')?.classList.remove('hidden');

const body = JSON.stringify({
updates: items,
Expand All @@ -424,13 +412,13 @@ if (!customElements.get('quick-order-list')) {
})
.then((state) => {
const parsedState = JSON.parse(state);
this.renderSections(parsedState);
})
.catch(() => {
this.renderSections(parsedState, ids);
}).catch(() => {
this.setErrorMessage(window.cartStrings.error);
})
.finally(() => {
this.querySelector('.variant-remove-total .loading__spinner').classList.add('hidden');
this.querySelector('.variant-remove-total .loading__spinner')?.classList.add('hidden');
this.requestStarted = false;
});
}

Expand All @@ -442,98 +430,6 @@ if (!customElements.get('quick-order-list')) {
}
}

updateQuantity(id, quantity, name, action) {
this.toggleLoading(id, true);
this.cleanErrors();

let routeUrl = routes.cart_change_url;
let body = JSON.stringify({
quantity,
id,
sections: this.getSectionsToRender().map((section) => section.section),
sections_url: this.getSectionsUrl(),
});
let fetchConfigType;
if (action === this.actions.add) {
fetchConfigType = 'javascript';
routeUrl = routes.cart_add_url;
body = JSON.stringify({
items: [
{
quantity: parseInt(quantity),
id: parseInt(id),
},
],
sections: this.getSectionsToRender().map((section) => section.section),
sections_url: this.getSectionsUrl(),
});
}

this.updateMessage();
this.setErrorMessage();

fetch(`${routeUrl}`, { ...fetchConfig(fetchConfigType), ...{ body } })
.then((response) => {
return response.text();
})
.then((state) => {
const parsedState = JSON.parse(state);
const quantityElement = document.getElementById(`Quantity-${id}`);
const items = document.querySelectorAll('.variant-item');

if (parsedState.description || parsedState.errors) {
const variantItem = document.querySelector(
`[id^="Variant-${id}"] .variant-item__totals.small-hide .loading__spinner`
);
variantItem.classList.add('loading__spinner--error');
this.resetQuantityInput(id, quantityElement);
if (parsedState.errors) {
this.updateLiveRegions(id, parsedState.errors);
} else {
this.updateLiveRegions(id, parsedState.description);
}
return;
}

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

this.renderSections(parsedState, id);

let hasError = false;

const currentItem = parsedState.items.find((item) => item.variant_id === parseInt(id));
const updatedValue = currentItem ? currentItem.quantity : undefined;
if (updatedValue && updatedValue !== quantity) {
this.updateError(updatedValue, id);
hasError = true;
}

publish(PUB_SUB_EVENTS.cartUpdate, { source: this.quickOrderListId, cartData: parsedState });

if (hasError) {
this.updateMessage();
} else if (action === this.actions.add) {
this.updateMessage(parseInt(quantity));
} else if (action === this.actions.update) {
this.updateMessage(parseInt(quantity - quantityElement.dataset.cartQuantity));
} else {
this.updateMessage(-parseInt(quantityElement.dataset.cartQuantity));
}
})
.catch((error) => {
this.querySelectorAll('.loading__spinner').forEach((overlay) => overlay.classList.add('hidden'));
this.resetQuantityInput(id);
console.error(error);
this.setErrorMessage(window.cartStrings.error);
})
.finally(() => {
this.toggleLoading(id);
if (this.lastKey && this.lastElement === id) {
this.querySelector(`#Variant-${id} input`).select();
}
});
}

resetQuantityInput(id, quantityElement) {
const input = quantityElement ?? document.getElementById(`Quantity-${id}`);
input.value = input.getAttribute('value');
Expand Down Expand Up @@ -592,9 +488,9 @@ if (!customElements.get('quick-order-list')) {
this.updateLiveRegions(id, message);
}

cleanErrors() {
this.querySelectorAll('.desktop-row-error').forEach((error) => error.classList.add('hidden'));
this.querySelectorAll(`.variant-item__error-text`).forEach((error) => (error.innerHTML = ''));
cleanErrors(id) {
// this.querySelectorAll('.desktop-row-error').forEach((error) => error.classList.add('hidden'));
// this.querySelectorAll(`.variant-item__error-text`).forEach((error) => error.innerHTML = '');
}

updateLiveRegions(id, message) {
Expand Down
1 change: 1 addition & 0 deletions layout/theme.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

{% render 'meta-tags' %}

<script src="{{ 'bulk-add.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'constants.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'pubsub.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'global.js' | asset_url }}" defer="defer"></script>
Expand Down
2 changes: 1 addition & 1 deletion sections/featured-collection.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

{%- if section.settings.quick_add == 'bulk' -%}
<script src="{{ 'quick-add-bulk.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'quick-order-list.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'quantity-popover.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'price-per-item.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'quick-order-list.js' | asset_url }}" defer="defer"></script>
{%- endif -%}

{%- style -%}
Expand Down
2 changes: 1 addition & 1 deletion sections/main-collection-product-grid.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

{%- if section.settings.quick_add == 'bulk' -%}
<script src="{{ 'quick-add-bulk.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'quick-order-list.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'quantity-popover.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'price-per-item.js' | asset_url }}" defer="defer"></script>
<script src="{{ 'quick-order-list.js' | asset_url }}" defer="defer"></script>
{%- endif -%}

{%- style -%}
Expand Down
Loading

0 comments on commit 85b75b0

Please sign in to comment.