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

146 комментариев

Страница 3 из 6  
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, Спасибо большое, будем разбираться)
dunakov 07.04.2016 19:31
@Incode, Я так покапался и понял, что половину старого скрипта можно удалять. Потому что мне нужно чтобы все радиобоксы так работали. Правда всё же хочется сделать формирование данных табличное. И чтобы формула сохранилась общего подсчёта как тут. И еще вопрос. Почему мы при работе с группой товаров уже используем псевдосторэж)? Сразу у вас в функции addtoCart использовался LS, а при группе товаров уже используем грубо говоря созданный нами Storage

Показать код
Incode 07.04.2016 20:12
@dunakov, псевдохранилище, другое название функции и т.д. - это только в контексте примера и не более :) Делая пример по теме, я всегда стараюсь уйти от реального кода, чтобы человек не зацикливался, а посмотрел на ситуацию с другой стороны без каких-то стереотипов.
всё же хочется сделать формирование данных табличное
Дык, кто же не даёт ) Делайте на здоровье. Вывод списком в примере - это опять же, только для примера. И больше думать надо не о том, как выводить, а как правильно хранить данные. Если вопрос хранения будет качественно продуман, то выводить потом будет легко и просто в любом виде: хоть списком, хоть таблицей, хоть по диагонали :D
dunakov 14.04.2016 15:36
Намудрил я беды в общем) Итак и сяк делал, не работает. А если работает, то только для 1 блока, остальные добавлять не желает. Не могли бы вы пожалуйста сделать для моего примера? Ну хотябы чтобы два блока работало, был LS, и табличный вывод данных. Бесится уже начинаю

Показать код
Incode 15.04.2016 09:49
@dunakov, внимательно пересмотрите код на предмет изменений.
Показать код
dunakov 25.04.2016 19:31
Просто по другому много чего реализовано в варианте, где радиобоксы работаю, но ладно попробую внимательно посмотреть как модифицировать мой вариант до варианта с работающими радиобоксами
dunakov 28.04.2016 19:21
Вроде бы разобрался, кое что добавил еще. Спасибо. Еще вопрос такой. Теперь хочу чтоли создать отдельную страничку. На которую будет заходить по клику, оформить заказ. Там нужно буддет ввести имя, фамилию, например способ оплаты выбрать, и способ доставки, а так же мыло. И чтобы оно либо на почту(Так прощу) Или на сервер(так сложнее) Отправляло данные. То что он выбрал в корзине+ его данные. Это реально сделать даже используя пхп?
Гость 25.05.2016 15:20
Привет! Столкнулся с проблемой: totalSum в 69 строке показывает NaN. Помогите, пожалуйста, исправить.

var d = document,
    itemBox = d.querySelectorAll('.item_box'), // блок каждого товара
    cartCont = d.getElementById('cart_content'); // блок вывода данных корзины
    cartContCount = d.getElementById('cart_content_count'); // блок вывода данных счетчика
// Функция кроссбраузерной установка обработчика событий
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][1] += 1;
  } else { // если товара в корзине еще нет, то добавляем в объект
    cartData[itemId] = [itemTitle, 1, itemPrice];
  }
  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 = '',
                        totalSum = 0,
                        totalCount = 0;
        // если что-то в корзине уже есть, начинаем формировать данные для вывода
        if(cartData !== null){
                totalItems = '<p><table class="shopping_list"><tr class="shopping_list_titles"><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>' + cartData[items][i] + '</td>';
                                } else {
                                totalItems += '<td>' + cartData[items][i] + ' </td>';
                                };
                        }
                        totalCount += cartData[items][1];
                        totalSum += cartData[items][1] * cartData[items][2];
                        totalItems += '<td><button class="del_item" data-id="'+ items +'" onclick=""></button></td>';
                        totalItems += '</tr>';
                }
                totalItems += '<tr><th></th><th></th><th><strong id="total_sum">'+ totalSum +' ₽</strong></th></tr></table>';
                cartCont.innerHTML = totalItems;
				cartContCount.innerHTML = '<p class="cart-count">' + totalCount + '</p>';		
				
        } else {
                // если в корзине пусто, то сигнализируем об этом
                cartCont.innerHTML = 'В корзине пусто!';
        }
        return false;
}
addEvent(d.getElementById('clear_cart'), 'click', function(e){
  localStorage.removeItem('cart');
  cartCont.innerHTML = 'Корзина очищена.';
});
/* Открыть корзину */
window.onload = openCart();
setInterval( openCart, 1000 );
Incode 25.05.2016 15:59
totalSum в 69 строке показывает NaN
Для начала, выведите в консоль объект корзины "cartData" и просмотрите данные. Возможно, что к числовым значениям, примешались какие-то лишние символы.
 // при таком варианте
var a = '45';
var b = 'x78';
console.log(a * b); // даст NaN
anubissk 02.06.2016 17:27
Добрый день! Подскажите, пожалуйста,как сделать описание товара невидимым? Приживляю кновку купить в ячейку таблицы
class="item_title">Samsung Galaxy S10
Incode 02.06.2016 19:47
как сделать описание товара невидимым?

@anubissk, не понимаю ваш вопрос. Что значит "невидимым"? Оберните это название в какой-нибудь элемент, например, <span>. Задайте этому элементу класс со свойством "opacity: 0;" или "display: none;" и текст станет невидимым. В конце концов, если он там не нужен, то его можно и не выводить вовсе.
anubissk 07.06.2016 10:42
anubissk, не понимаю ваш вопрос. Что значит "невидимым"? Оберните это название в какой-нибудь элемент, например, <span>. Задайте этому элементу класс со свойством "opacity: 0;" или "display: none;" и текст станет невидимым. В конце концов, если он там не нужен, то его можно и не выводить во

На странице размещена таблица, в первой колонке номер детали, во второй название, в третьей хочу сделать цену и кнопку "в корзину".
Получается так,что описание у меня уже есть, но убрать значение "itemTitle" я не могу, т.е. тогда название не будет отображаться в корзине.
Прошу сильно не ругаться, я лет 10ть назад писал сайт в блокноте, а сейчас друг попросил помочь накидать ему простенькую страничку с корзиной. Технологии уже давно опередили мои познания :(
anubissk 07.06.2016 13:41
@Incode, Вот таблица куда вживляю кнопку
Показать код

Помогите, пожалуйста советом, как сделать это красиво. Пробовал указать "itemTitle"в третьей колонке, но не получается. Что-то не правильно я делаю
Incode 07.06.2016 14:42
@anubissk, вам нужно спрятать элемент с классом "item_title"? Тогда в CSS ему можно задать display: none;
P.S. Может попробуете мой плагин jqCart? Думаю, что будет проще добиться нужного вам результат (который я всё равно понять не могу), т.к. там данные нужно размещать в самой кнопке. А кнопка, в свою очередь, вообще не зависит от разметки и видимой информации для пользователя.
anubissk 07.06.2016 15:13
@Incode, я пытаюсь сделать вот так https://yadi.sk/i/Q4A0Y0XIsKP2U
Т.е. кнопку отдельно от описания.
От безысходности, уже думал на костылях сделать, типо "item_title" не отображаемым
Incode 07.06.2016 15:27
От безысходности
Это у меня от безысходности уже крыша едет :) Я объясняю, что не могу понять, суть вашей проблемы, а вы мне о том, как думали на костылях сделать. Вот вы показали скрин... Что вас там не устраивает, что хотите скрыть и почему скрыть это "что-то" у вас не получается?
anubissk 07.06.2016 16:12
@Incode, Можно анекдот в тему? :)
Показать код
:) Суть в том, что у меня кнопка есть, но не работает :( Как мне кажется, из-за того, что я не туда приживляю "item_box" в теле таблицы.
Вот как это прописано сейчас (конечно там не правильно всё :( )
Показать код
anubissk 09.06.2016 12:24
@Incode, Я понимаю, что вопросы совсем тупые, помогите пожалуйста, совсем ступор из-за таблицы этой :(
Incode 09.06.2016 16:18
@anubissk, попробуйте задать класс "item_box" для <tr>, а не для <div>, как это сейчас у вас.
Страница 3 из 6  
Ваш комментарий:
X