Создаем корзину покупателя на чистом JavaScript и Local Storage

Вариантов создания корзины с использованием jQuery, на просторах интернета достаточно, но так как не все хотят подключать громоздкие библиотеки, особенно для каких-то разовых задач, я хочу показать вариант реализации на чистом JS. К тому же, хранить выбранные пользователем товары, мы будем не в cookie, а Local Storage (локальное хранилище). Эта технология поддерживается практически во всех современных браузерах и даже в IE8.

Буквально два слова о Local Storage для тех, кто с этим способом хранения данных на стороне клиента не знаком. Объем хранимой информации в LS по сравнению с cookie значительно выше: около 5Мб(!) против 4Кб. К тому же, в LS данные хранятся в зашифрованном виде. Однако, как и в cookie, так и в LocalStorage, мы можем записывать только строковые данные. Если нужно добавить массив или объект, то его можно предварительно преобразовать в JSON-строку (JSON.stringify(obj)), а после получения данных из LS - производим обратное преобразование (JSON.parse(json_string)). Работать с Local Storage не просто, а очень просто. Вот его основные методы:

localStorage.setItem('key', 'value');
Обновляет или создает новую запись с ключом "key" и строковым значением "value"
var lsData = localStorage.getItem('key');
Возвращает данные связанные с ключом "key" или "null", если записи с таким ключом не обнаружено
localStorage.removeItem('key');
Удаляет данные со связанным ключом "key"
localStorage.clear();
Удаляет все записи из Local Storage

Переходим к делу и для примера, создадим такую HTML-структуру для вывода товара:

<div class="item_box">
  <h3 class="item_title">Samsung Galaxy S10</h3>
  <p>Цена: <span class="item_price">20</span>$</p>
  <button class="add_item" data-id="7">Добавить в корзину</button>
</div>
<div class="item_box">
  <h3 class="item_title">LG Optimus G E100500</h3>
  <p>Цена: <span class="item_price">100</span>$</p>
  <button class="add_item" data-id="2">Добавить в корзину</button>
</div>
  <div class="item_box">
  <h3 class="item_title">Nokia 2110</h3>
  <p>Цена: <span class="item_price">1000</span>$</p>
  <button class="add_item" data-id="5">Добавить в корзину</button>
</div>
<button id="checkout">Оформить заказ</button>
<button id="clear_cart">Очистить корзину</button>
<div id="cart_content"></div>

Все необходимые данные, такие как наименование или цена товара, мы можем брать прямо из элементов страницы. Остается важная составляющая - ID товара, которую можно выводить в каком-нибудь атрибуте. Для таких целей, я предпочитаю атрибут data-*, который я уже упоминал в других статьях. Его-то и добавим в кнопку "Добавить в корзину" каждого из товаров.
Теперь в дело вступает JavaScript. Ничего сверхъестественного тут нет и большую часть, я прокомментирую прямо в коде:

var d = document,
    itemBox = d.querySelectorAll('.item_box'), // блок каждого товара
    cartCont = d.getElementById('cart_content'); // блок вывода данных корзины
// Функция кроссбраузерной установка обработчика событий
function addEvent(elem, type, handler){
  if(elem.addEventListener){
    elem.addEventListener(type, handler, false);
  } else {
    elem.attachEvent('on'+type, function(){ handler.call( elem ); });
  }
  return false;
}
// Получаем данные из LocalStorage
function getCartData(){
  return JSON.parse(localStorage.getItem('cart'));
}
// Записываем данные в LocalStorage
function setCartData(o){
  localStorage.setItem('cart', JSON.stringify(o));
  return false;
}
// Добавляем товар в корзину
function addToCart(e){
  this.disabled = true; // блокируем кнопку на время операции с корзиной
  var cartData = getCartData() || {}, // получаем данные корзины или создаём новый объект, если данных еще нет
      parentBox = this.parentNode, // родительский элемент кнопки "Добавить в корзину"
      itemId = this.getAttribute('data-id'), // ID товара
      itemTitle = parentBox.querySelector('.item_title').innerHTML, // название товара
      itemPrice = parentBox.querySelector('.item_price').innerHTML; // стоимость товара
  if(cartData.hasOwnProperty(itemId)){ // если такой товар уже в корзине, то добавляем +1 к его количеству
    cartData[itemId][2] += 1;
  } else { // если товара в корзине еще нет, то добавляем в объект
    cartData[itemId] = [itemTitle, itemPrice, 1];
  }
  if(!setCartData(cartData)){ // Обновляем данные в LocalStorage
    this.disabled = false; // разблокируем кнопку после обновления LS
  }
 return false;
}
// Устанавливаем обработчик события на каждую кнопку "Добавить в корзину"
for(var i = 0; i < itemBox.length; i++){
  addEvent(itemBox[i].querySelector('.add_item'), 'click', addToCart);
}
// Открываем корзину со списком добавленных товаров
function openCart(e){
  var cartData = getCartData(), // вытаскиваем все данные корзины
      totalItems = '';
  // если что-то в корзине уже есть, начинаем формировать данные для вывода
  if(cartData !== null){
    totalItems = '<table class="shopping_list"><tr><th>Наименование</th><th>Цена</th><th>Кол-во</th></tr>';
    for(var items in cartData){
      totalItems += '<tr>';
      for(var i = 0; i < cartData[items].length; i++){
        totalItems += '<td>' + cartData[items][i] + '</td>';
      }
      totalItems += '</tr>';
    }
    totalItems += '</table>';
    cartCont.innerHTML = totalItems;
  } else {
    // если в корзине пусто, то сигнализируем об этом
    cartCont.innerHTML = 'В корзине пусто!';
  }
  return false;
}
/* Открыть корзину */
addEvent(d.getElementById('checkout'), 'click', openCart);
/* Очистить корзину */
addEvent(d.getElementById('clear_cart'), 'click', function(e){
  localStorage.removeItem('cart');
  cartCont.innerHTML = 'Корзина очишена.';
});

Объект "cartData" собираем по следующей схеме: ключ к товару - его ID и данные в виде массиве [название_товара, цена_товара, количество_товара]. Если бы вы вывели такой объект средствами php, то получили бы примерно следующее:

stdClass Object (
  [2] => Array (
    [0] => LG Optimus G E100500
    [1] => 100
    [2] => 1
  )
  [7] => Array (
    [0] => Samsung Galaxy S10
    [1] => 20
    [2] => 2
  )
)

Это я показал, чтобы было понимание того, как потом можно работать с этими данными на стороне сервера. И плавно подошли к тому, как же эти данные отправить на сервер. В отличии от cookie, Local Storage работает только на стороне клиента. Кто-то может и записать это в минусы LS, но я не вижу проблемы, т.к. есть достаточно способов превратить минус в плюсы. Легко и непринужденно, мы можем отправить данные Ajax-запросом, а это гораздо приятней посетителю, т.к. его не перебрасывает на другую страницу, экономит время и трафик, что немаловажно, если пользователь зашёл с мобильного устройства или скорость подключения не такая высокая.


Как видите, нет ничего сложного и объем кода, без использования сторонних библиотек, получился совсем небольшим. Если кому-то нужно учитывать более старые версии Internet Explorer, то он может добавить cookie, как "fallback" к Local Storage. То есть, проверять в функциях "getCartData" и "setCartData" возможности браузера и, если он не поддерживает LS, то в качестве хранилища использовать Cookie, а остальной код останется без изменений.

Incode Pro logo

153 комментария

Страница 4 из 7  
Гость 06.02.2016 16:24
Добрый день, скажите пожалуйста, а над корзиной jQuery которую Вы обещали, Вы не работали ещё? Я думаю все были бы очень Вам признательны.
Incode 06.02.2016 18:15
над корзиной jQuery которую Вы обещали, Вы не работали ещё?
В общих чертах уже сделал. Если успею, то завтра выложу в разделе jQuery
Serzhi 06.02.2016 20:26
Помогите пожалуйста вопрос вот в чем.
Необходимо создать карзину катораю формирует запрос в фомате json передает их на сервер методом
HTTP POST.
для передачи используеться метод "CreateOrder"

Пример запроса

{ «token»: «541c4a59bdad87039c7ae8df6af14785сaf7526f01a613f5bd894d615ee811e5», «qnit_id»: 1, «method»: «CreateOrder», «params»: { «name»: «Иванов Иван», «phone»: «79123211232», «external_id»: «2643», "products": [ { «id»: 1000587, «quantity»: 1, }, { «id»: 3044, «quantity»: 2 } ] } }
Incode 06.02.2016 20:54
@Serzhi, данные в localStorage уже находятся в формате JSON. Поэтому можете просто добавить их в объект с другими данными.
Incode 06.02.2016 21:09
@Serzhi, Вот примерно, как должно выглядеть у вас:
var obj = {
  token: 'some_token',
  qnit_id: 1,
  method: 'CreateOrder',
  params: {
    name: 'Иванов Иван',
    phone: '79123211232',
    external_id: 2643,
    products: getCartData() // данные заказываемых товаров
  }
};
Это готовый объект со всеми вашими данными. Если нужно его преобразовать в JSON-строку:
var json = JSON.stringify(obj);
Данные, естественно, подставляете свои, функция getCartData() описана в статье.
Serzhi 11.02.2016 21:15
Подскажите как методом HTTP POST этот объект передать на адрес: http://adres.ru/api. Возможно ли сделать так чтобы при нажати кнопки в корзине и на почту отправлялась письмо и этот объект отправлялся на сервер
Incode 11.02.2016 23:58
@Serzhi, тут, как говорится, одно из двух: то ли вы не понимаете принципы работы, то ли я не понимаю, что именно вы хотите. Давайте начнем с того, что я пару дней назад выложил простенький плагин, который передает данные на сервер и отправляет данные на почту. Возможно, что вам он подойдет и вопросы отпадут сами собой. Если же нет, то продолжим обсуждение, но только вы более конкретно объясните суть задачи.
Гость 19.02.2016 08:53
Добрый день - Огромное спасибо за скрипт корзины, давно искал такой вариант исполнения.
Возможно ли прикрутить выпадающий список для кнопки добавить в корзину?
Или вместо списка выбор цвета.
Спасибо.
Incode 19.02.2016 09:44
прикрутить выпадающий список для кнопки
Если вам надо передавать на сервер какие-то дополнительные данные, то их можно без проблем добавить к запросу, обратившись к определенному элементу и достав из него нужную информацию. Простенький пример, я вам набросал, НО в нём не учитывается то, что человек может выбрать два и более товара одного типа, но с разными цветами. Для этого нужны более глобальные изменения в коде.
Гость 28.02.2016 10:01
C пустым заказом разобрался. Если дернуть /php/handler.php придет пустой заказ. Как это поправить?
Incode 28.02.2016 11:08
Если дернуть /php/handler.php придет пустой заказ. Как это поправить?
Не дергать его (ваш К.О.). Если серьёзно, то нужно проверять и фильтровать данные перед тем, как их использовать. Для проверки на "пустоту", достаточно функции empty()
Гость 24.03.2016 08:25
А как реализовать например такую вещь. Когда кликаем добавить в корзину, справа появляется кнопка удалить товар из корзины плюс она работает и по нажатию удаляет и пропадает
Incode 24.03.2016 12:36
кликаем добавить в корзину, справа появляется кнопка удалить товар
Вся работа корзины крутится вокруг объекта. Если вам так будет проще, то этот объект в то же PHP, был бы ассоциативным массивом, где ключами выступали бы id товаров. Кнопка удаления из корзины, может иметь в каком-нибудь атрибуте (я ратую за data-атрибуты) этот id. По клику на эту кнопку, получаем id и удаляем соответствующий элемент из объекта (Оператор delete), который хранится в LocalStorage, Cookie или другом хранилище.
Абстрактный пример:
// Объект с даннми корзины
var obj = {
  10: 'Item 10',
  20: 'Item 20',
  50: 'Item 50'
};
// Полученный ID
var id = 20;
// Удаление
delete obj[id];
// Проверим в консоли
console.log(obj); // Осталось два элемента со свойствами 10 и 50
После удаления, прячете кнопку, пересчитываете кол-во товаров в корзине и производите любые другие действия. которые считаете нужными.
Гость 29.03.2016 15:40
totalItems += '<table>';


Наверное, </table>?
Гость 29.03.2016 21:29
Замечательный пример!
Спасибо, автор.

Incode, пытаюсь колонку с ценой выравнять по правому краю, но что-то костыли слишком корявые получаются :-)
У вас есть идеи, как это сделать?
Гость 29.03.2016 22:28
function openCart(e){
	var cartData = getCartData(), // вытаскиваем все данные корзины
			totalItems = '',
			totalCount = 0,
			totalSum = 0;
	// если что-то в корзине уже есть, начинаем формировать данные для вывода
	if(cartData !== null){
		totalItems = '<table class="shopping_list"><tr><th>Наименование</th><th>Цена</th><th>Кол-во</th><th></th></tr>';
		for(var items in cartData){
			totalItems += '<tr>';
			for(var i = 0; i < cartData[items].length; i++){
			
			if ( i == 1 ) {
				totalItems += '<td align="right">' + cartData[items][i] + '</td>';
				} else {
				totalItems += '<td>' + cartData[items][i] + '</td>';
 				};
			}
			totalSum += cartData[items][1] * cartData[items][2];
			totalCount += cartData[items][2];
			totalItems += '<td><span class="del_item" data-id="'+ items +'">X</span></td>';
			totalItems += '</tr>';
		}
		totalItems += '<tr><td><strong>Итого</strong></td><td align="right">$ <span id="total_sum">'+ totalSum +'</span></td><td><span id="total_count">'+ totalCount +'</span> шт.</td><td></td></tr>';
		totalItems += '</table>';
		cartCont.innerHTML = totalItems;
	} else {
		// если в корзине пусто, то сигнализируем об этом
		cartCont.innerHTML = 'В корзине пусто!';
	}
	return false;
}
dunakov 31.03.2016 10:51
Показать весь код
Спасибо автору за помощь! Очень помогло.
А сможете еще вот с чем помочь. Как видите у меня есть пустые боксы, в которые можно вводить количество товара. А как сделать, чтобы оно при нажатии Добавить товар считывало и введенное количество. Чтобы например 4 раза не кликать на товар. И например после слова цена добавить еще один текс бокс, в который будет выводиться конечная цена товара. Ну и естественно обработчик всё верно пересчитывает при формировании заказа.

Заранее спасибо!
Incode 03.04.2016 22:04
Наверное, </table>?
Спасибо, поправил.
Incode 03.04.2016 22:43
чтобы оно при нажатии Добавить товар считывало и введенное количество
@dunakov, не очень удобная разметка, но в принципе, достаточно добраться до поля ввода количества, вытащить значение и записать в переменную, приведя к числовому типу. Если значения нет, то в эту переменную записать единицу. Далее подставляем переменную вместо "1" и в if, и в else:
// Добавляем товар в корзину
function addToCart(e) {
    this.disabled = true;
    var cartData = getCartData() || {},
        parentBox = this.parentNode,
        itemId = this.getAttribute('data-id'),
        itemTitle = parentBox.querySelector('.item_title').innerHTML,
        itemPrice = parentBox.querySelector('.item_price').innerHTML,
        itemAmount = +(this.previousElementSibling.previousElementSibling.querySelector('input').value || 1);
    if (cartData.hasOwnProperty(itemId)) {
        cartData[itemId][2] += itemAmount; // Тут подставили переменную
    } else {
        cartData[itemId] = [itemTitle, itemPrice, itemAmount]; // И  тут подставили переменную
    }
    if (!setCartData(cartData)) {
        this.disabled = false;
    }
    return false;
}
Собственно, вот та часть кода, исходя из вашей разметки:

itemAmount = +(this.previousElementSibling.previousElementSibling.querySelector('input').value || 1);
Incode 03.04.2016 22:47
пытаюсь колонку с ценой выравнять по правому краю
Неужели text-align: right; не помогает? Или вы про что-то другое говорили?
dunakov 07.04.2016 09:15
Показать весь код

Спасибо большое, всё сделал.Только я переделал кнопку добавить товар, чтобы каждый раз было видно. А как сделать активными радиобоксы? И чтобы от выбора радиобокса изменялась цена. Т е например 8 гб стоит 5 долларов, 16 гб 7. И при выборе добавляет товар именно выбарнной цены. И идеально было бы , чтобы при выборе радиобокса добавляло например 16 гб.

Например Flash Kingston 50 mb 16 GB . Flash Kingston 50 mb 8GB заранее спасибо)
Incode 07.04.2016 10:30
@dunakov, Т.к. цена и характеристики одной подгруппы товара разные, то по сути это и товары разные. Я думаю, что и в БД они хранятся у вас, как разные записи, пусть и объединенные одним брендом. В таких случаях, я использую data-атрибуты, т.е. у каждого радиобокса есть свой набор, состоящий из ID, цены, названия характеристики и других данных, которые могут понадобится. Я вам набросал простенький пример в песочнице. Попробуйте разобраться, там ничего сложного. Как увидите, я убрал data-id у кнопки и это должно быть сигналом, что у вас в данном случае подгруппа товаров. Если бы у кнопки data-id был, то работаем, как с обычным добавлением.
P.S. Прячьте, пожалуйста, большие простыни кода под спойлер, кнопочка с плюсиком ))
dunakov 07.04.2016 13:46
@Incode, Cпасибо, попробую разобраться, правда гиперссылка на прсотенький пример не работает) Замечание учту, прошу прощения)
Incode 07.04.2016 14:01
@dunakov, Ссылку проверил, вроде бы всё нормально. Ну, скопируйте так: https://jsfiddle.net/fnk8fs52/
dunakov 07.04.2016 14:09
@Incode, Спасибо большое, будем разбираться)
Страница 4 из 7  
Ваш комментарий:
X