Кошик товарів
Приклад. Дано перелік товарів у кошику. При зміні кількості одиниць товару збільшувати загальну вартість. Створити клас Product, що призначений для маніпуляцій товаром та клас ProductManager що оперує з усіма товарами (через подію передвати ідентифікатор товару та операцію, що зроблена).
Рішення:
'use strict'
class Product {
constructor(data) {
const requiredKeys = [
'id',
'title',
'link',
'imgSrc',
'amount',
'amountAvailable',
'price',
]
for (const key of requiredKeys) {
if (!(key in data)) throw new TypeError(`Missing field "${key}".`)
}
Object.assign(this, data)
}
}
class Counter {
static events = {
change: 'counter:change',
}
#value
minValue
maxValue
inputEl
minusBtn
plusBtn
$el
constructor(value, minValue = 1, maxValue = Infinity, options = {}) {
this.minValue = minValue
this.maxValue = maxValue
this.value = value
this.options = {
baseClass: 'u-flex u-gap-100',
btnClass:
'c-button | u-flex-none u-inline-grid u-place-content-center u-is-900 u-font-bolder u-leading-none u-text-400',
valueClass: 'c-input | u-is-1100 u-text-center',
...options,
}
}
get value() {
return this.#value
}
set value(newValue) {
if (!isFinite(newValue)) return
this.#value =
newValue >= this.maxValue
? this.maxValue
: newValue <= this.minValue
? this.minValue
: newValue
}
changeValueBy(amount = 1) {
this.value += amount
}
dispatchChange() {
const changeEvent = new CustomEvent(Counter.events.change, {
detail: {value: this.value},
bubbles: true,
})
this.$el.dispatchEvent(changeEvent)
}
updateBtnUI(btnEl, limitValue) {
btnEl.disabled = this.value === limitValue
}
updateUI() {
this.inputEl.value = String(this.value)
this.updateBtnUI(this.minusBtn, this.minValue)
this.updateBtnUI(this.plusBtn, this.maxValue)
}
handleValueChange(amount) {
this.value = amount
this.updateUI()
this.dispatchChange()
}
handleValueChangeBy(amount = 1) {
this.changeValueBy(amount)
this.updateUI()
this.dispatchChange()
}
renderInput() {
const inputEl = document.createElement('INPUT')
inputEl.className = this.options.valueClass
inputEl.type = 'number'
inputEl.min = String(this.minValue)
inputEl.max = String(this.maxValue)
inputEl.step = '1'
inputEl.addEventListener('change', () =>
this.handleValueChange(Number(inputEl.value)),
)
return inputEl
}
renderBtn(label, handleClick) {
const btnEl = document.createElement('BUTTON')
btnEl.className = this.options.btnClass
btnEl.type = 'button'
btnEl.textContent = label
btnEl.addEventListener('click', handleClick)
return btnEl
}
render(cssSelector) {
this.inputEl = this.renderInput()
this.minusBtn = this.renderBtn('-', () => this.handleValueChangeBy(-1))
this.plusBtn = this.renderBtn('+', () => this.handleValueChangeBy(1))
this.updateUI()
this.$el = document.createElement('DIV')
this.$el.className = this.options.baseClass
this.$el.append(this.minusBtn, this.inputEl, this.plusBtn)
if (cssSelector) document.querySelector(cssSelector).append(this.$el)
return this.$el
}
}
class ProductCard {
static events = {
change: 'product:change',
delete: 'product:delete',
}
id
title
link
imgSrc
amount
amountAvailable
price
options
priceEl
deleteBtn
$el
constructor(
{id, title, link, imgSrc, amount, amountAvailable, price},
options = {},
) {
this.id = id
this.title = title
this.link = link
this.imgSrc = imgSrc
this.amount = amount
this.amountAvailable = amountAvailable
this.price = price
this.options = {
baseClass: 'product | u-flow-400 u-pb-400',
upperContainerClass: 'product__upper | u-flex u-gap-400 u-items-start',
lowerContainerClass: 'u-flex u-gap-400 u-items-center',
pictureClass:
'product__picture | u-is-3/12 u-flex-none u-relative u-aspect-video',
imgClass: 'u-ibg u-object-contain',
headerClass: 'product__header | u-text-300 u-flex-auto',
counterOptions: {},
priceClass: 'u-text-400 u-text-red-600 u-font-bolder',
deleteBtnClass:
'c-button | u-flex-none u-inline-grid u-place-content-center u-is-900 u-font-bolder u-leading-none u-text-400 u-bg-red-600 u-self-start',
...options,
}
}
get totalPrice() {
return this.price * this.amount
}
updateUI() {
this.priceEl.textContent = this.totalPrice.toLocaleString('uk-UA') + ' ₴'
}
dispatchEvent(type) {
const customEvent = new CustomEvent(type, {
detail: {
id: this.id,
amount: this.amount,
},
bubbles: true,
})
this.$el.dispatchEvent(customEvent)
}
handleAmountChange({detail: {value}}) {
this.amount = value
this.updateUI()
this.dispatchEvent(ProductCard.events.change)
}
handleProductRemove() {
this.dispatchEvent(ProductCard.events.delete) // must come before the remove method or there will be no one to hear this 👂
this.$el.remove()
}
renderPicture() {
const imgEl = document.createElement('IMG')
imgEl.className = this.options.imgClass
imgEl.alt = ''
imgEl.src = this.imgSrc
const anchorEl = document.createElement('A')
anchorEl.href = this.link
anchorEl.className = this.options.pictureClass
anchorEl.append(imgEl)
return anchorEl
}
renderHeader() {
const anchorEl = document.createElement('A')
anchorEl.href = this.link
anchorEl.textContent = this.title
const headerEl = document.createElement('H3')
headerEl.className = this.options.headerClass
headerEl.append(anchorEl)
return headerEl
}
renderPrice() {
const priceEl = document.createElement('DIV')
priceEl.className = this.options.priceClass
return priceEl
}
renderDeleteBtn() {
const deleteBtn = document.createElement('BUTTON')
deleteBtn.type = 'button'
deleteBtn.className = this.options.deleteBtnClass
deleteBtn.textContent = '✖'
deleteBtn.addEventListener('click', () => this.handleProductRemove())
return deleteBtn
}
render(cssSelector) {
const pictureEl = this.renderPicture()
const headerEl = this.renderHeader()
this.deleteBtn = this.renderDeleteBtn()
const upperContainerEl = document.createElement('DIV')
upperContainerEl.className = this.options.upperContainerClass
upperContainerEl.append(pictureEl, headerEl, this.deleteBtn)
const counterEl = new Counter(
this.amount,
1,
this.amountAvailable,
this.options.counterOptions,
).render()
this.priceEl = this.renderPrice()
const lowerContainerEl = document.createElement('DIV')
lowerContainerEl.className = this.options.lowerContainerClass
lowerContainerEl.append(counterEl, this.priceEl)
this.$el = document.createElement('ARTICLE')
this.$el.className = this.options.baseClass
this.$el.append(upperContainerEl, lowerContainerEl)
this.$el.addEventListener(Counter.events.change, (e) =>
this.handleAmountChange(e),
)
this.updateUI()
if (cssSelector) document.querySelector(cssSelector).append(this.$el)
return this.$el
}
}
class ProductCart {
productsList
options
orderEl
priceEl
orderBtn
$el
constructor(productsList, options) {
this.productsList = structuredClone(productsList)
this.options = {
baseClass: 'cart | u-flow-400',
listClass: 'cart__list',
makeOrderClass:
'u-flex u-gap-400 u-flex-wrap u-items-center u-bg-zinc-700 u-p-200 u-rounded-lg',
priceClass: 'u-text-500 u-font-bolder u-text-neutral-50',
btnClass: 'c-button | u-text-400',
orderText: 'Оформити замовлення',
productOptions: {},
...options,
}
}
get totalPrice() {
return this.productsList.reduce(
(sum, {amount, price}) => amount * price + sum,
0,
)
}
updateUI() {
this.priceEl.textContent = `Загалом: ${this.totalPrice.toLocaleString('uk-UA')} ₴`
this.orderBtn.disabled = this.totalPrice === 0
}
handleAmountChange({detail: {id, amount}}) {
for (const product of this.productsList) {
if (product.id === id) {
product.amount = amount
break
}
}
this.updateUI()
}
handleDelete({detail: {id: idToRemove}}) {
const productIndex = this.productsList.findIndex(
({id}) => id === idToRemove,
)
if (productIndex === -1) return
this.productsList.splice(productIndex, 1)
this.updateUI()
}
renderMakeOrderSection() {
const makeOrderEl = document.createElement('DIV')
makeOrderEl.className = this.options.makeOrderClass
const priceEl = document.createElement('DIV')
priceEl.className = this.options.priceClass
const orderBtn = document.createElement('BUTTON')
orderBtn.type = 'button'
orderBtn.textContent = this.options.orderText
orderBtn.className = this.options.btnClass
makeOrderEl.append(priceEl, orderBtn)
return {orderEl: makeOrderEl, priceEl, orderBtn}
}
renderList() {
const listEl = document.createElement('DIV')
listEl.className = this.options.listClass
listEl.addEventListener(ProductCard.events.change, (e) =>
this.handleAmountChange(e),
)
listEl.addEventListener(ProductCard.events.delete, (e) =>
this.handleDelete(e),
)
for (const product of this.productsList) {
const card = new ProductCard(product, this.options.productOptions)
listEl.append(card.render())
}
return listEl
}
render(cssSelector) {
this.$el = document.createElement('DIV')
this.$el.className = this.options.baseClass
const listEl = this.renderList()
;({
orderEl: this.orderEl,
priceEl: this.priceEl,
orderBtn: this.orderBtn,
} = this.renderMakeOrderSection())
this.$el.append(listEl, this.orderEl)
this.updateUI()
if (cssSelector) document.querySelector(cssSelector).append(this.$el)
return this.$el
}
}
// =============================================================================
const productsList = [
new Product({
id: 0,
title:
'Монітор 34" Samsung Odyssey G5 C34G55T (LC34G55TWWIXCI) WQHD / VA 1000R Curved / 165Hz / 1ms / 8-Bit / DisplayHDR 10 / G-Sync Compatible / FreeSync Premium / Black eQualizer / Picture-by-Picture',
link: 'https://hard.rozetka.com.ua/ua/samsung_lc34g55twwixci/p271707421/',
imgSrc: './images/monitor_samsung.webp',
amount: 2,
amountAvailable: 5,
price: 14199,
}),
new Product({
id: 1,
title:
'SSD диск Samsung 990 Pro 4TB M.2 PCIe 4.0 x4 V-NAND 3-bit MLC (MZ-V9P4T0GW)',
link: 'https://hard.rozetka.com.ua/ua/samsung-mz-v9p4t0gw/p426281556/',
imgSrc: './images/ssd_samsung.webp',
amount: 1,
amountAvailable: 4,
price: 15199,
}),
new Product({
id: 2,
title:
'Відеокарта ASUS PCI-Ex GeForce RTX 5080 ROG Astral OC Edition 16GB GDDR7 (256bit) (2 x HDMI, 3 x DisplayPort) (ROG-ASTRAL-RTX5080-O16G-GAMING)',
link: 'https://hard.rozetka.com.ua/ua/asus-rog-astral-rtx5080-o16g-gaming/p471415334/',
imgSrc: './images/graphic_card_asus.webp',
amount: 1,
amountAvailable: 2,
price: 74769,
}),
new Product({
id: 3,
title:
'Ноутбук Apple MacBook Air 15.3" M4 16/256GB 2025 (MC7A4UA/A) Sky Blue',
link: 'https://rozetka.com.ua/ua/apple-mc7a4ua-a/p489486629/',
imgSrc: './images/notebook_apple.webp',
amount: 1,
amountAvailable: 10,
price: 66499,
}),
]
const productCart = new ProductCart(productsList)
productCart.render('.js-app')
