Skip to content

Commit 993a38c

Browse files
- add, update and remove orderlines rather then rerender everything
- add reusable locazation function
1 parent fd0377d commit 993a38c

File tree

4 files changed

+194
-152
lines changed

4 files changed

+194
-152
lines changed

src/Umbraco.Commerce.Cart/Client/src/apis/ucc.api.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ export class UccApi {
6464
const observer = new MutationObserver((mutations) => {
6565
mutations.forEach((mutation) => {
6666
if (mutation.type === 'attributes' && mutation.attributeName === 'lang') {
67-
this._context.config.set({ ...this._context.config.get()!, lang: this._host.ownerDocument.documentElement.lang });
67+
if (Object.keys(this._context.config.get()!.locales!).includes(this._host.ownerDocument.documentElement.lang)) {
68+
this._context.config.set({
69+
...this._context.config.get()!,
70+
lang: this._host.ownerDocument.documentElement.lang
71+
});
72+
} else {
73+
this.setLang('en');
74+
}
6875
}
6976
});
7077
});

src/Umbraco.Commerce.Cart/Client/src/components/ucc-cart-modal.element.ts

Lines changed: 131 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { UCC_CART_CONTEXT } from "../contexts/ucc.context.ts";
22
import { CartConfig, CartItem } from "../types.ts";
33
import { UccModalElement } from "./ucc-modal.element.ts";
4-
import { debounce, delegate, getUniqueSelector } from "../utils.ts";
4+
import { debounce, delegate, difference } from "../utils.ts";
55
import { UccCartRepository } from "../repositories/cart.respository.ts";
66
import { UccEvent } from "../events/ucc.event.ts";
7+
import { localize } from "../localization.ts";
78

89
export class UccCartModalElement extends UccModalElement
910
{
@@ -29,20 +30,34 @@ export class UccCartModalElement extends UccModalElement
2930
this._context.config.subscribe((config: CartConfig) => {
3031
if (config) {
3132

32-
this.setTitle(config.locales![config.lang].cart_title);
33-
this.setCloseButtonLabel(config.locales![config.lang].close_cart);
33+
// Localize UI
34+
35+
this.setTitle(localize('cart_title'));
36+
this.setCloseButtonLabel(localize('close_cart'));
3437

35-
this._host.querySelector<HTMLElement>('.ucc-cart-total--subtotal .ucc-cart-total-label')!.textContent = config.locales![config.lang].subtotal;
36-
this._host.querySelector<HTMLElement>('.ucc-cart-total--taxes .ucc-cart-total-label')!.textContent = config.locales![config.lang].taxes;
37-
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.textContent = config.locales![config.lang].shipping_and_discounts_message;
38-
this._host.querySelector<HTMLElement>('.ucc-cart-total--total .ucc-cart-total-label')!.textContent = config.locales![config.lang].total;
39-
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.textContent = config.locales![config.lang].checkout;
38+
this._host.querySelector<HTMLElement>('.ucc-cart-empty__message')!.textContent = localize('cart_empty');
39+
this._host.querySelector<HTMLElement>('.ucc-cart-total--subtotal .ucc-cart-total-label')!.textContent = localize('subtotal');
40+
this._host.querySelector<HTMLElement>('.ucc-cart-total--taxes .ucc-cart-total-label')!.textContent = localize('taxes');
41+
this._host.querySelector<HTMLElement>('.ucc-cart-total--total .ucc-cart-total-label')!.textContent = localize('total');
42+
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.textContent = localize('shipping_and_discounts_message');
43+
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.textContent = localize('checkout');
4044
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.setAttribute('href', config.checkoutUrl!);
4145

42-
const cartEmptyMsg = this._host.querySelector<HTMLElement>('.ucc-cart-empty__message');
43-
if (cartEmptyMsg) {
44-
cartEmptyMsg.textContent = config.locales![config.lang].cart_empty;
45-
}
46+
Array.from(this._host.querySelectorAll<HTMLElement>('.ucc-cart-item')).forEach((el) => {
47+
48+
el.querySelector<HTMLElement>('.ucc-cart-item__remove')!.setAttribute('title', localize('remove'));
49+
50+
const basePriceEl = el.querySelector<HTMLElement>('.ucc-cart-item__bundle-item--base .ucc-cart-item__bundle-item-name');
51+
if (basePriceEl) {
52+
basePriceEl.textContent = localize('base_price');
53+
}
54+
55+
Array.from(el.querySelectorAll<HTMLElement>('.ucc-cart-item__property')).forEach((propertyEl) => {
56+
const key = propertyEl.dataset.propertyKey;
57+
propertyEl.querySelector<HTMLElement>('.ucc-cart-item__property-key')!.textContent = localize(`property_${key!.toLowerCase()}`);
58+
});
59+
60+
});
4661

4762
if (config.showPricesIncludingTax) {
4863
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.classList.add('ucc-cart-totals--inc-tax');
@@ -65,49 +80,80 @@ export class UccCartModalElement extends UccModalElement
6580

6681
const cart = this._context.cart.get();
6782
if (cart && cart.items.length > 0) {
68-
const activeEl = this._host.ownerDocument.activeElement as HTMLElement;
69-
const activeElSelector = activeEl ? getUniqueSelector(activeEl) : undefined;
70-
const activeElCaretPos = activeEl instanceof HTMLInputElement ? activeEl.selectionStart : undefined;
71-
this._host.querySelector('.ucc-cart-items')!.innerHTML = cart.items.map((item) => {
72-
const propsToDisplay = Object.keys(item.properties ?? {}).filter((x) => (config.properties ?? []).map(y => y.toLowerCase()).includes(x.toLowerCase()));
73-
return `
74-
<div class="ucc-cart-item" data-id="${ item.id }">
75-
${item.imageUrl ? `
76-
<figure class="ucc-cart-item__image">
77-
<img src="${item.imageUrl}" alt="${item.name}">
78-
</figure>` : ''}
79-
<div class="ucc-cart-item__body">
80-
<div class="ucc-cart-item__content ucc-split">
81-
<div class="ucc-split__left ucc-split__item--fill">
82-
<h3 class="ucc-cart-item__title">${item.name}</h3>
83-
${item.attributes ? `
84-
<div class="ucc-cart-item__attributes">
85-
${Object.keys(item.attributes).map((key) => `
86-
<div class="ucc-cart-item__attribute">
87-
<span class="ucc-cart-item__attribute-key">${key}</span>
88-
<span class="ucc-cart-item__attribute-value">${item.attributes![key]}</span>
89-
</div>
90-
`).join('')}
91-
</div>
92-
` : ''}
93-
${propsToDisplay.length > 0 ? `
94-
<div class="ucc-cart-item__properties">
95-
${propsToDisplay.map((key) => `
96-
<div class="ucc-cart-item__property">
97-
<span class="ucc-cart-item__property-key">${config?.locales![config.lang][`property_${key.toLowerCase()}`] ?? key}</span>
98-
<span class="ucc-cart-item__property-value">${item.properties![key]}</span>
99-
</div>
100-
`).join('')}
101-
</div>
102-
` : ''}
103-
${item.items && item.items.length ? `
104-
<div class="ucc-cart-item__bundle">
105-
<div class="ucc-cart-item__bundle-item ucc-cart-item__bundle-item--base">
106-
<span class="ucc-cart-item__bundle-item-name">${config?.locales![config.lang].base_price ?? 'Base price'}</span>
107-
<span class="ucc-cart-item__bundle-item-quantity"></span>
108-
<span class="ucc-cart-item__bundle-item-quantity">${config.showPricesIncludingTax ? item.basePrice.withTax : item.basePrice.withoutTax}</span>
83+
84+
// Compare cart items with DOM elements to determine what items have been added, removed or updated
85+
const domIds = Array.from(this._host.querySelectorAll<HTMLElement>('.ucc-cart-item')).map((el) => el.dataset.id);
86+
const cartIds = cart.items.map((item) => item.id);
87+
const { added, removed, intersects } = difference(cartIds, domIds);
88+
89+
// Remove items that are no longer in the cart
90+
removed.forEach((id) => {
91+
this._host.querySelector<HTMLElement>(`.ucc-cart-item[data-id="${id}"]`)?.remove();
92+
});
93+
94+
// Update existing items
95+
intersects.forEach((id) =>
96+
{
97+
const item = cart.items.find((item) => item.id === id)!;
98+
const el = this._host.querySelector<HTMLElement>(`.ucc-cart-item[data-id="${id}"]`)!;
99+
100+
// NB: We assume that only quantity and price can change
101+
102+
const qtyEl = el.querySelector<HTMLInputElement>('.ucc-cart-item__quantity')!;
103+
if (parseFloat(qtyEl.value) !== item.quantity) {
104+
qtyEl.value = item.quantity.toString();
105+
}
106+
107+
const newPrice = config.showPricesIncludingTax ? item.total.withTax : item.total.withoutTax;
108+
const priceEl = el.querySelector<HTMLElement>('.ucc-cart-item__price')!;
109+
if (priceEl.textContent !== newPrice) {
110+
priceEl.textContent = newPrice;
111+
}
112+
});
113+
114+
// Add new items to the cart
115+
added.forEach((id) => {
116+
117+
const item = cart.items.find((item) => item.id === id)!;
118+
119+
this._host.querySelector('.ucc-cart-items')!.insertAdjacentHTML('beforeend', `
120+
<div class="ucc-cart-item" data-id="${ item.id }">
121+
${item.imageUrl ? `
122+
<figure class="ucc-cart-item__image">
123+
<img src="${item.imageUrl}" alt="${item.name}">
124+
</figure>` : ''}
125+
<div class="ucc-cart-item__body">
126+
<div class="ucc-cart-item__content ucc-split">
127+
<div class="ucc-split__left ucc-split__item--fill">
128+
<h3 class="ucc-cart-item__title">${item.name}</h3>
129+
${item.attributes ? `
130+
<div class="ucc-cart-item__attributes">
131+
${Object.keys(item.attributes).map((key) => `
132+
<div class="ucc-cart-item__attribute">
133+
<span class="ucc-cart-item__attribute-key">${key}</span>
134+
<span class="ucc-cart-item__attribute-value">${item.attributes![key]}</span>
135+
</div>
136+
`).join('')}
137+
</div>
138+
` : ''}
139+
${item.properties ? `
140+
<div class="ucc-cart-item__properties">
141+
${Object.keys(item.properties).map((key) => `
142+
<div class="ucc-cart-item__property" data-property-key="${key}">
143+
<span class="ucc-cart-item__property-key">${ localize(`property_${key.toLowerCase()}`) }</span>
144+
<span class="ucc-cart-item__property-value">${item.properties![key]}</span>
109145
</div>
110-
${item.items.map((bundleItem) => `
146+
`).join('')}
147+
</div>
148+
` : ''}
149+
${item.items && item.items.length ? `
150+
<div class="ucc-cart-item__bundle">
151+
<div class="ucc-cart-item__bundle-item ucc-cart-item__bundle-item--base">
152+
<span class="ucc-cart-item__bundle-item-name">${localize('base_price')}</span>
153+
<span class="ucc-cart-item__bundle-item-quantity"></span>
154+
<span class="ucc-cart-item__bundle-item-quantity">${config.showPricesIncludingTax ? item.basePrice.withTax : item.basePrice.withoutTax}</span>
155+
</div>
156+
${item.items.map((bundleItem) => `
111157
<div class="ucc-cart-item__bundle-item">
112158
<span class="ucc-cart-item__bundle-item-name">${bundleItem.name}</span>
113159
<span class="ucc-cart-item__bundle-item-quantity">x${bundleItem.quantity}</span>
@@ -118,7 +164,7 @@ export class UccCartModalElement extends UccModalElement
118164
` : ''}
119165
</div>
120166
<div class="ucc-split__right">
121-
<button class="ucc-cart-item__remove" title="${config?.locales![config.lang].remove ?? 'Remove'}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg></button>
167+
<button class="ucc-cart-item__remove" title="${localize('remove')}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg></button>
122168
</div>
123169
</div>
124170
<div class="ucc-cart-item__foot ucc-split ucc-split--center">
@@ -131,34 +177,37 @@ export class UccCartModalElement extends UccModalElement
131177
</div>
132178
</div>
133179
</div>
134-
`
135-
}).join('');
136-
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.style.display = '';
137-
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.style.display = '';
180+
`);
181+
182+
});
183+
184+
// Toggle cart items and empty message
185+
this._host.querySelector<HTMLElement>('.ucc-cart-items')!.style.display = '';
186+
this._host.querySelector<HTMLElement>('.ucc-cart-empty')!.style.display = 'none';
187+
188+
// Populate totals
138189
this._host.querySelector<HTMLElement>('.ucc-cart-total--subtotal .ucc-cart-total-value')!.textContent = cart.subtotal.withoutTax;
139190
this._host.querySelector<HTMLElement>('.ucc-cart-total--taxes .ucc-cart-total-value')!.textContent = cart.subtotal.tax;
140191
this._host.querySelector<HTMLElement>('.ucc-cart-total--total .ucc-cart-total-value')!.textContent = cart.subtotal.withTax;
192+
193+
// Show totals
194+
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.style.display = '';
195+
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.style.display = '';
196+
197+
// Enable checkout button
141198
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.classList.remove('ucc-cart-checkout--disabled');
142-
if (activeElSelector) {
143-
const activeEl = this._host.querySelector<HTMLElement>(activeElSelector);
144-
if (activeEl) {
145-
activeEl.focus();
146-
if (activeEl instanceof HTMLInputElement && activeElCaretPos) {
147-
activeEl.setSelectionRange(activeElCaretPos!, activeElCaretPos!);
148-
}
149-
}
150-
}
199+
151200
} else {
152-
this._host.querySelector<HTMLElement>('.ucc-cart-items')!.innerHTML = `
153-
<div class="ucc-cart-empty">
154-
<div>
155-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-cart"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>
156-
<div class="ucc-cart-empty__message">${config?.locales![config.lang].cart_empty ?? 'Your cart is empty'}</div>
157-
</div>
158-
</div>
159-
`;
201+
202+
// Toggle cart items and empty message
203+
this._host.querySelector<HTMLElement>('.ucc-cart-items')!.style.display = 'none';
204+
this._host.querySelector<HTMLElement>('.ucc-cart-empty')!.style.display = '';
205+
206+
// Hide totals
160207
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.style.display = 'none';
161208
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.style.display = 'none';
209+
210+
// Disable checkout button
162211
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.classList.add('ucc-cart-checkout--disabled');
163212
}
164213
}
@@ -175,6 +224,12 @@ export class UccCartModalElement extends UccModalElement
175224
private _attachCartTemplate() {
176225
this.setBody(`
177226
<div class="ucc-cart-items"></div>
227+
<div class="ucc-cart-empty" style="display: none;">
228+
<div>
229+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-cart"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>
230+
<div class="ucc-cart-empty__message"></div>
231+
</div>
232+
</div>
178233
`)
179234
this.setFooter(`
180235
<div class="ucc-cart-totals">

0 commit comments

Comments
 (0)