Создаем корзину покупателя на чистом 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

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

Страница 6 из 6  
Гость 25.10.2014 20:23
Очень хороший пример спасибо!
А как точно такое можно прописать посредством jquery?
Incode 25.10.2014 21:06
А как точно такое можно прописать посредством jquery?
На самом деле, изменений не так уж и много:
Показать код
$(function() {
  // блок вывода данных корзины
  var cartCont = $('#cart_content');
  // Получаем данные из LocalStorage
  function getCartData() {
      return JSON.parse(localStorage.getItem('cart'));
    }
    // Записываем данные в LocalStorage
  function setCartData(o) {
      localStorage.setItem('cart', JSON.stringify(o));
      return false;
    }
    // Функция добавления товара в корзину
  function addToCart() {
      var $that = $(this),
        cartData = getCartData() || {}, // получаем данные корзины или создаём новый объект, если данных еще нет
        parentBox = $that.parent(), // родительский элемент кнопки Добавить в корзину
        itemId = $that.data('id'), // ID товара
        itemTitle = $('.item_title', parentBox).text(), // название товара
        itemPrice = $('.item_price', parentBox).text(); // стоимость товара
      $that.prop('disabled', true); // блокируем кнопку на время операции с корзиной
      if (cartData.hasOwnProperty(itemId)) { // если такой товар уже в корзине, то добавляем +1 к его количеству
        cartData[itemId][2] += 1;
      } else { // если товара в корзине еще нет, то добавляем в объект
        cartData[itemId] = [itemTitle, itemPrice, 1];
      }
      // Обновляем данные в LocalStorage
      if (!setCartData(cartData)) {
        $that.prop('disabled', false); // разблокируем кнопку после обновления LS
        cartCont.text('Товар добавлен в корзину.');
        setTimeout(function() {
          cartCont.text('Продолжить покупки...');
        }, 1000);
      }
      return false;
    }
    // Открываем корзину со списком добавленных товаров
  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.html(totalItems);
      } else {
        // если в корзине пусто, то сигнализируем об этом
        cartCont.text('В корзине пусто!');
      }
      return false;
    }
    /* Добавляем товар в корзину */
  $('.add_item').on('click', addToCart);
  /* Открыть корзину */
  $('#checkout').on('click', openCart);
  /* Очистить корзину */
  $('#clear_cart').on('click', function(e) {
    localStorage.removeItem('cart');
    cartCont.text('Корзина очишена.');
  });
});
Гость 07.01.2015 10:09
День добрый! А не могли бы Вы привести тот же пример с кукисами? LS все же штука не очень стабильная, у меня кстати в ИЕ8 так и не заработала корзина.
А за пример спасибо!
Incode 07.01.2015 13:00
LS все же штука не очень стабильная, у меня кстати в ИЕ8 так и не заработала
А вы уверены, что проблема именно в LS? Я практически уверен, что проблемка где-то в другом месте, т.к. LocalStorage отлично поддерживается в IE8+. И не соглашусь по поводу стабильности этого API... не настолько кроссбраузерный - это да, но за мою практику, сбоев не замечалось.  Кстати, вы тестировали вариант на чистом JS или jQuery?
Впрочем, можно и cookie использовать. Если используете jQuery? то есть плагин jquery.cookie. Очень прост в использовании. И по сути, вам нужно будет только заменить функции getCartData() и setCartData().
function getCartData(){
  return JSON.parse($.cookie('cart'));
}
function setCartData(o){
  $.cookie('cart', JSON.stringify(o), { expires: 365, path: '/' });
  return false;
}
Но опять же повторюсь, что причина не правильной работы - явно не в LocalStorage
Гость 07.01.2015 13:30
Спасибо! Кстати, пока по Вашему совету с куками чето не заработало (плагин подключил), но я попробую разобраться. А вот с LocalStorage я пробовал и другие скрипты, ни один не работал в 8 осле. Возможно, это просто в настройках браузера надо покопаться, но как заставить пользователей это делать?? А куки все таки включены "из коробки", и мало кто будет их отключать.
Incode 07.01.2015 13:41
Кстати, пока по Вашему совету с куками чето не заработало
Ну, не особо удивился )) Я же предположил, что проблема явно в чем-то другом. Смотрите ошибки в консоли. И вообще, пока вы занимаетесь отладкой, то консоль у вас не должна закрываться.

Кстати, вы пробовали демо страницу открывать в IE8 и проверить работу?
Гость 13.01.2015 08:58
Доброго дня! Это снова я) Проблема такая: если использовать кукисы, а не LS, то приведенный Вами код не работает - мне кажется потому, что кукисы хранятся в виде строки, а не объекта. Первая ошибка в консоли вот тут
return JSON.parse($.cookie('cart'));


меняю на

return $.cookie('cart');

ошибка пропадает, но из трех товаров добавляется только один, а потом в кукисах в строку добавляется просто куча символов.
Если не трудно, подскажите как на Вашем коде реализовать следующее: мне нужно добавлять в кукисы только ID товара, при этом атрибут data-id использовать не хочется, можно ли работать только с чистым id товара? строка value в кукисах должна быть такого вида: item_1, item_25, item_998 - то есть содержать идентификаторы товаров через запятую. Для понятности - мне по сути нужна не корзина, а блокнот, куда пользователь может добавить товары для дальнейшего ознакомления. В идеале ( если уж быть совсем нескромным) то хотелось бы еще такой функционал - возможность удалить из блокнота каждый конкретный товар.
Кстати, это можно было бы оформить как отдельную статью. Ну а уж если будет код с проверкой ( браузер поддерживает LS ? сохраняем в LS : сохраняем в кукисы ) то будет просто шикарно, я думаю.
Заранее спасибо!
Incode 13.01.2015 13:55
а потом в кукисах в строку добавляется просто куча символов.
Я же не просто так ссылку вам давал на плагин jquery.cookie. Там описано, что если необходимо работать с объектами, то для автоматического их преобразования, установить для свойства json значение true:
$.cookie.json = true; // это мы добавляем где-нибудь в начале кода
В функциях же, преобразовывать данные в json-строку и обратно в объект, нам уже не требуется.
function getCartData() {
    return $.cookie('cart');
}
function setCartData(o) {
    $.cookie('cart', o);
    return false;
}
можно ли работать только с чистым id товара?
Конечно можно. Только не забывайте, что значение атрибута id не должно начинаться с цифры.
Ну а уж если будет код с проверкой ( браузер поддерживает LS ? сохраняем в LS : сохраняем в кукисы ) то будет просто шикарно, я думаю.
Fallback - это обычная и хорошая практика. Просто не хотелось раздувать статью, за счет включения всех возможных и невозможных планов "А" и планов "Б".
По поводу отдельной статьи для описания удаления отдельных позиций - как бы нет особого смысла. Но в ближайшее время, я лучше дополню такой функциона в эту статью.
P.S. Сделал вам примерчик работы с кукисаим. В исходном коде, смотрите файл script.js
Гость 13.01.2015 14:57
$.cookie.json = true; // это мы добавляем где-нибудь в начале кода

Вот жеж а....
а я голову сломал, почему не работает)). Спасибо огромное!
Гость 30.03.2015 16:18
сделал кнопку минус один товар, скопировав кусок функции function addToCart и заменив там плюс на минус))) ну и кнопку новую привязал к этой новой функции. а как сделать чтобы если товар в корзине 1шт и жмем минус, то соответственно удалить строку с этим товаром полностью?
Гость 30.03.2015 16:27
delete cartData[itemId];
так чтоли?
Incode 30.03.2015 17:51
@Гость,
delete cartData[itemId];
так чтоли?
Нет, нужно очистить кукисы или LocalStorage. Можно туда просто записать пустое значение или, если брать мой пример, то вызвать событие (триггер) на кнопке "Очистить корзину", т.к. функция уже для этого предусмотрена и писать что-либо повторно не имеет смысла.
$('.clear_cart').click();
// или
$('.clear_cart').trigger('click');
Гость 30.03.2015 19:21
а у меня delete исправно работает)))... странно
Гость 30.03.2015 19:22
 function addToCartMinus() {
		var $that = $(this),
        cartData = getCartData() || {}, // получаем данные корзины или создаём новый объект, если данных еще нет
        parentBox = $that.parent(), // родительский элемент кнопки Добавить в корзину
        itemId = $that.data('id'), // ID товара
        itemTitle = $('.item_title', parentBox).text(), // название товара
        itemPrice = $('.item_price', parentBox).text(); // стоимость товара
    $that.prop('disabled', true); // блокируем кнопку на время операции с корзиной
    if (cartData.hasOwnProperty(itemId)) { // если такой товар уже в корзине, то вычитаем -1 к его количеству
      if (cartData[itemId][3]>1) { // если товар в количестве больше 1, то сминусовывааем а если 1шт то удаляем строку всю
		cartData[itemId][3] -= 1;
	  } else {
		delete cartData[itemId];
	  }
    }
    // Обновляем данные в LocalStorage
    if (!setCartData(cartData)) {
      $that.prop('disabled', false); // разблокируем кнопку после обновления LS
      //cartCont.text('Товар добавлен в корзину.');
     // setTimeout(function() {
     //  cartCont.text('Продолжить покупки...');
      //}, 1000);
	  openCart();
		}
		return false;
	}
Гость 30.03.2015 19:23
у меня еще сделано что корзина постоянно видна, вон нижний код закомментил
Гость 30.03.2015 19:24
и я имел ввиду, что удалить ОДНУ строку с товаром, а остальные товары должны остаться в корзине!!!
Гость 30.03.2015 19:31
эх, еще бы чтоб плюс-минус прямо в строчке с товаром делать... ну в моем варианте когда корзина постоянно видна. а кстати надо всегда так вызывать её, как я сделал - т.е. нажал добавить - корзина заново подгружается?
Incode 30.03.2015 21:37
а кстати надо всегда так вызывать её, как я сделал - т.е. нажал добавить - корзина заново подгружается?
Мне сложно судить по отрывку кода и после бессонной ночи )) В принципе, "загнав" единожды данные из хранилища в переменную cartData и до следующей перезагрузки, больше обращаться к хранилищу не нужно, при условии, что эта переменная находится в области видимости для всех функций, которые с ней работают. С другой стороны (а случаи бывают разные), обращаться к хранилищу каждый раз - не накладно и на скорость работы практически не влияет.
эх, еще бы чтоб плюс-минус прямо в строчке с товаром делать
Будет чуть больше свободного времени - сделаю примерчик ;)
Гость 13.06.2015 12:40
не хватает вывода суммы)
Страница 6 из 6  
Ваш комментарий:
X