​Делаем корзину покупок с помощью Codeigniter и JQuery. Часть 6.

В шестой части статьи, как и обещал, сделаем нашу корзину аяксовой, а так же стилизуем её с помощью CSS.

Статья обещает быть длинной, поэтому запаситесь терпением, нам надо много изучить и сделать.

Т.к. изучение CSS не входит в планы этих уроков, то я просто приведу уже готовый код без пояснений. В первых уроках мы создавали файл assets/css/styles.css, который был пустым. В этом файле и будет храниться весь CSS код сайта:

assets/css/styles.css

body {
    font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif;
    font-size: 16px;
    color: black;
}

.content {
    width: 700px;
    margin: 20px auto;
}

.cart-items,
.products-list {
    width: 100%;
    text-align: center;
}

.cart-items thead {
    font-weight: bold;
}

.products-list .product-title,
.products-list .price {
    margin-bottom: 5px;
}

.products-list .product-title {
    font-weight: bold;
}

.products-list .price {
    color: red;
}

.cart {
    background: #eee;
    padding: 10px;
}

.cart .controls {
    margin-top: 10px;
}

.message-success,
.message-error {
    width: 100%;
    padding: 10px;
    border-radius: 8px;
    border-width: 1px;
    color: white;
    box-sizing: border-box;
    margin-bottom: 10px;
}

.message-success {
    background: #5db954;
    border-color: #51a34a;
}
.message-error {
    background: #b94545;
    border-color: #ab4040;
}

/* Стили HTML форм */
*:focus {
    outline: none;
}

button,
input[type=submit],
input[type=button] {
    position: relative;
    display: inline-block;
    padding: 7px 12px;
    font-size: 13px;
    font-weight: bold;
    color: #333;
    text-shadow: 0 1px 0 rgba(255,255,255,0.9);
    white-space: nowrap;
    vertical-align: middle;
    cursor: pointer;
    background-color: #eeeeee;
    background-image: -moz-linear-gradient(#fcfcfc, #eee);
    background-image: -webkit-linear-gradient(#fcfcfc, #eee);
    background-image: linear-gradient(#fcfcfc, #eee);
    background-repeat: repeat-x;
    border: 1px solid #d5d5d5;
    border-radius: 3px;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    -webkit-appearance: none;
}

button:hover,
input[type=submit]:hover,
input[type=button]:hover {
    text-decoration: none;
    background-color: #dddddd;
    background-image: -moz-linear-gradient(#eee, #ddd);
    background-image: -webkit-linear-gradient(#eee, #ddd);
    background-image: linear-gradient(#eee, #ddd);
    background-repeat: repeat-x;
    border-color: #ccc;
}

button:active,
input[type=submit]:active,
input[type=button]:active {
    background-color: #dcdcdc;
    background-image: none;
    border-color: #b5b5b5;
    box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);
}

AJAX корзина

Вот мы и добрались до самого интересного - написание клиентского js кода функиционала ajax корзины. В основном шаблоне уже из первых уроков подключена библиотека jQuery и скрипт cart.js, в котором мы будеим писать весь js код.

application/views/template.php

...
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="<?php print base_url('assets/js/cart.js'); ?>"></script>
</body>
</html>

Открывайте файл cart.js и напишите следующее:

assets/js/cart.js

;(function($) {

    // Здесь будет весь JS код

})(jQuery);

Это анонимная функция, которая сразу же вызывается, чтобы исключить конфликты с другими скриптами js (хоть у нас их и нет, но нужно привыкать к хорошему).

Нам нужно сделать формы (добавления в корзину, удаления товара из корзины и остальные) аяксовыми. Поэтому при клике на кнопку, мы будет посылать AJAX запрос на сервер, и в зависимости от результата, который возвратил сервер, выполнять какие то действия (например выводить сообщение пользователю).

Давайте добавим эту возможность кнопкам "купить":

assets/js/cart.js

;(function($) {

// Форма с кнопкой "Купить"
$('.products-list').on('submit', 'form', function(e) {

    var $form = $(this); // текущая форма добавления в корзину
    var formData = $form.serialize(); // Данные формы в строку

    // POST запрос на сервер
    $.post($form.attr('action'), formData, function(resp) {

        // Выводим сообщения пользователю на 2сек и скрываем его

        // Создаём HTML сообщения, помещаем её в выборку jQuery и сразу скрываем, чтобы потом применить к нему
        // эффект появления
        var $messageHTML = $('<div class="message-' + resp.status + '">' + resp.message + '</div>').hide();

        // Добавляем сообщение внутрь блока .content и плавно его показываем
        $messageHTML.prependTo('.content').fadeIn();

        // Через 2 секунды удалим сообщение, чтобы не "плодились"
        setTimeout(function() {

            // Сначала запускаем эффект slideUp,
            // после того как эффект закончится,
            // удалим сообщение из DOM
            $messageHTML.slideUp(function() {
                $(this).remove();
            });
        }, 2000);

    });

    // Отменяет действие по умолчанию
    e.preventDefault();
});

})(jQuery);

Итак, разберём что тут получилось. Для начала с помощью js необходимо изменить событие отправки формы на своё. Для этого в

$('.products-list').on('submit', 'form', function(e) {

мы "слушаем" все события submit на элементе form внутри блока .products-list, т.е. отправку формы добавления в корзину товара. Чтобы запретить обычную отправку формы мы используем функцию

e.preventDefault();

Далее необходимо отправить POST запрос к серверу. Для этого надо подготовить данные, которые будет отправлять. В jQuery есть функция serialize(), котороя преобзазует данные формы в строку (см. официальную документацию).

После того, как ответ от сервера получен, мы подготавливаем HTML код сообщения, которое будем выводить пользователю, показываем его и скрываем через 2 секунды. В коде выше всё подробно прокоментировано. Если есть какие-то опросы - пишите в комментариях.

Вот что у нас получилось:

add-to-cart.gif

Т.к. вывод сообщений пользователю с помощью javascript нам ещё понадобиться, то предлагаю сделать функцию setMessage(), чтобы не плодить код. Добавьте её в самое начало cart.js:

assets/js/cart.js

/**
 * Выводит сообщение пользователю
 * @param text
 * @param status
 */
function setMessage(text, status)
{
    // Создаём HTML сообщения, помещаем её в выборку jQuery и сразу скрываем, чтобы потом применить к нему
    // эффект появления
    var $messageHTML = $('<div class="message-' + status + '">' + text + '</div>').hide();

    // Добавляем сообщение внутрь блока .content и плавно его показываем
    $messageHTML.prependTo('.content').fadeIn();

    // Через 2 секунды удалим сообщение, чтобы не "плодились"
    setTimeout(function() {

        // Сначала запускаем эффект slideUp,
        // после того как эффект закончится,
        // удалим сообщение из DOM
        $messageHTML.slideUp(function() {
            $(this).remove();
        });
    }, 2000);
}

И изменим код добавления в корзину:

assets/js/cart.js

// Форма с кнопкой "Купить"
$('.products-list').on('submit', 'form', function(e) {

    var $form = $(this); // текущая форма добавления в корзину
    var formData = $form.serialize(); // Данные формы в строку

    // POST запрос на сервер
    $.post($form.attr('action'), formData, function(resp) {
        // Выводим сообщения пользователю на 2сек и скрываем его
        setMessage(resp.message, resp.status);
    });

    // Отменяет действие по умолчанию
    e.preventDefault();
});

Советы

Совет 1

Хорошей практикой является именно добавление прослушки события для родительского элемента. Ведь Можно было написать и так:

$('.products-list form').submit(function(e) {

    // Тут код отправки формы

    e.preventDefault();

});

Здесь для каждой формы добавляется событие submit. И, когда на странице например 1000 товаров, мы бы добавили 1000 событий, что явно не добавит производительности. Так что вседа нужно добавлять именно прослушку события.

Без прослушки событий не обойтись, если контент динамически меняется на странице (как у нас корзина. В неё постоянно добавляеются строки, удаляеются, очищаются).

Совет 2

Если Вы пользуетесь браузером Chrome (а я надеюсь вы делаете это), то наверно уже знаете про средства разработчика, вызываемые по нажитию клавиши F12. Если нет, то давайте попробуем на нашей корзине проследить за AJAX запросом, который посылает наше приложение серверу. Откройте средства разработчика и перейдите на вкладку Network (если там не пусто, то очистите список):

clear-network.png

В этом окне будут оторажаться все запросы к серверу от приложения. Теперь нажмите на кнопку "Купить". Должен добавиться новый запрос, и если на него нажать, то получим полную информацию о нём. На скриншоте ниже можно увидеть текст, который возвращает сервер:

chrome-network.png

Если открыть вкладку Headers, то можно увидеть данные, которые мы передаём серверу, тип ответа и ещё много информации, которая позволит находить ошибки.

headers.png

Но ведь добавленный товар не появляется в корзине!

Как вы уже заметили, товар в корзину добавляется, но не появляется там. Он появится только после перезагрузки страницы. Почему? Да потому что мы не написали javascript функционал добавления строки с товаром в корзину.

Реализовывать это будем самым простым способом:

При добавлении товара в корзину, в случае успеха, сервер возвращает весь HTML код корзины (с уже добавленной в неё строкой нового товара), а мы с помощью js просто обновим её.

Этот способ простой, но не лишён недостатков. Один из них - нужно получать весь HTML корзины, а не только ту часть, которую нужно обновить. Т.е. мы будет "гонять" больше трафика. Но думаю это не такая большая проблема. Во-первых скорость интернета позволяет, во-вторых корзина не такой большой элемент на странице, чтобы мы почувствовали какие-то тормоза в работе интерфейса. В-третьих - вы посмотрите исходный код opencart, и код в этой стетье покажется вам идеальным.

Итак, поправим немного код в методе add_to_cart() контроллера cart. Я не буду приводить полный код метода, только небольшой кусок:

application/controllers/cart.php

...
// Получает данные товара
$product_data = $this->cart_model->get_product($product_id);

// Если товар существует
if ($product_data) {
    $this->cart->insert(array(
        'id' => $product_id,
        'qty' => 1,
        'price' => $product_data['price'],
        'name' => $product_data['name'],
    ));

    // Получаем корзину пользователя
    $user_cart = $this->cart->contents();

    // Подгружаем шаблоны корзины и сетки товаров.
    // Последним параметром передаём true, чтобы шаблон выводился
    // в переменную
    $cart_html = $this->load->view('cart-view', array('cart_items' => $user_cart), true);

    $result = array(
        'status' => 'success',
        'message' => 'Товар добавлен в корзину',
        'cart_html' => $cart_html,
    );

После добавления товара в корзину методом $this->cart->insert(), получаем в массив $user_cart данные корзины пользователя. Далее передаём этом пассив в шаблон. В итоге, весь HTML код корзины будет храниться в переменной $cart_html. Впринципе, мы повторили обычные получение и вывод корзины, которые использователи в методе index() контроллера cart.

Поле этого в массив $result добавляем новый элемент cart_html, в котором будет весь HTML код корзины, и который мы возвратим вместе с сообщением о результате.

Теперь, если с помощью средств разработчка, мы посмотрим на результат, котоый сервер возвращает нам после добавления в корзину мы увидим HTML код корзины:

ci_cart-add_to_cart.png

Далее, по плану - обновление корзины на странице. Вот изменённый js код добавления товара в корзину и её обновления:

// Форма с кнопкой "Купить"
$('.products-list').on('submit', 'form', function(e) {

    var $form = $(this); // текущая форма добавления в корзину
    var formData = $form.serialize(); // Данные формы в строку

    // POST запрос на сервер
    $.post($form.attr('action'), formData, function(resp) {

        // Заменяет HTML корзины на новый
        $('.cart').replaceWith(resp.cart_html);

        // Выводим сообщения пользователю на 2сек и скрываем его
        setMessage(resp.message, resp.status);
    });

    // Отменяет действие по умолчанию
    e.preventDefault();
});

jQuery методом replaceWith() (ссылка на документацию) мы заменяем блок .cart на новый, который возвратил сервер. И так же выводим сообщение пользователю.

Очищаем корзину

В прошлом уроке мы не доделали до конца функционал корзины, а в частности её очистку. Кнопка очистки корзины это обычная форма с кнопкой, которая отсылает POST запрос на адрес /cart/cart_empty. Давайте создадим в контроллере cart метод cart_empty()

/**
 * Очистка корзины
 */
public function cart_empty()
{
    // Очищает корзину
    $this->cart->destroy();

    $result = array(
        'status' => 'success',
        'message' => 'Корзина очищена',
    );

    // Проверяем тип запроса. Если true, то запрос передан с помощью
    // AJAX, если false то обычный POST запрос
    $is_ajax = $this->input->is_ajax_request();

    if ($is_ajax) {
        // Если был AJAX запрос, возвращаем JSON ответ
        $this->output->set_content_type('application/json');
        $this->output->set_output(json_encode($result));
    } else {
        // Выводим сообщение
        $this->session->set_flashdata('message', $result);

        // Редирект на страницу с товарами
        redirect(base_url());
    }
}

Здесь многое уже знакомо. С помощью стандартного метода

$this->cart->destroy();

мы очищаем корзину пользователя и выводим ему сообщение.

Добавим немного javascript'a, чтобы сделать очистку корзины без перезагрузки страницы.

Что нужно сделать:

  • По нажатию на кнопку "Очистить корзину" отослать AJAX запрос на сервер
  • После получения ответа удалить таблицу с корзиной из DOM
  • Вывести сообщение пользвателю.

Приступим к реализации:

// Очистка корзины
$('.content').on('submit', '.controls form', function(e) {
    var $form = $(this);
    $.post($form.attr('action'), function(resp) {
        if (resp.status === 'success') {
            $('.cart')
                .prepend('<p>В корзине нет товаров</p>')
                .find('.cart-items').remove(); // Удаляем таблицу с корзиной
        }

        setMessage(resp.message, resp.status); // Выводим сообщения пользователю на 2сек и скрываем его
    });
    e.preventDefault();
});

По событии submit формы очистки корзины, посылаем AJAX запрос на сервер (на URL, который указан в атрибуте action формы), затем, если сервер вернул статус success, удаляем таблицу с корзиной, выводим надпись "В корзине нет товаров", и выводим сообщение пользователю с помощью нашей функции setMessage().

Почему я повесил прослушку события аж на блок .content, а не на, например, .cart. Потому что при очистке, добавлении товара и его удаления, мы полностью заменяем блок .cart, и событие с него "слетает". Поэтому самый ближайший родительский неизменяемый динамически элемент к корзине это блок .content.

AJAX удаление товара из корзины

Чтобы уже логически закончить статью, давайте напишем обработчик для кнопок удаления товара из корзины.

Алгоритм действий:

  • Вешаем прослушку события отправки формы с кнопкой удаления из корзины
  • Отправляем POST запрос на сервер для удаления товара из корзины
  • Сервер возвращает результат, и, в случае успеха, обновлённый HTML корзины
  • С помощью js мы обновляем корзину и выводим сообщение пользователю, что товар удалён из корзины

Открываем файл cart.js и пишем новый обраточик:

assets/js/cart.js

// Удаление товара из корзины
$('.content').on('submit', '.cart-items form', function(e) {
    var $form = $(this);

    // Данные формы. Нужно передать серверу rowid товара в корзине,
    // который нужно удалить
    var formData = $form.serialize();

    // Отсылаем AJAX запрос
    $.post($form.attr('action'), formData, function(resp) {

        // Обновляем корзину
        if (resp.status === 'success') {
            // Заменяет HTML корзины на новый
            $('.cart').replaceWith(resp.cart_html);
        }
        setMessage(resp.message, resp.status); // Выводим сообщения пользователю на 2сек и скрываем его
    });

    e.preventDefault(); // Отменяет действие по умолчанию
});

Код очень похож на добавление товара в корзину. Мы так же собираем данные формы с помощью метода $form.serialize(), отсылает данные на URL, который указан в атрибуте action формы. Далее, если сервер возвратис статус success, обновляем корзину и выводит сообщение пользователю.

Теперь доработает код метода удаления товара из корзины delete_item(). Догадываетесь что нужно сделать? Получить данные корзины пользователя, пропустить их через шаблон и возвратить в ответе.

Код для получения HTML корзины точно такой же, как и при обновлении или добавлении товара в корзину:

// Получаем корзину пользователя
$user_cart = $this->cart->contents();

// Подгружаем шаблоны корзины и сетки товаров.
// Последним параметром передаём true, чтобы шаблон выводился
// в переменную
$cart_html = $this->load->view('cart-view', array('cart_items' => $user_cart), true);

Листинг метода delete_item():

application/controllers/cart.php

/**
* Удаление товара из корзины
*/
public function delete_item()
{
    // Уникальный rowid товара в корзине
    $rowid = $this->input->post('rowid', true);

    // Проверяем тип запроса. Если true, то запрос передан с помощью
    // AJAX, если false то обычный POST запрос
    $is_ajax = $this->input->is_ajax_request();

    // Массив с результатами работы скрипта
    $result = array(
        'status' => 'error',
        'message' => 'Такого товара нет в корзине',
    );

    if ($rowid) {
        $data = array(
            'rowid' => $rowid,
            'qty'   => 0,
        );

        // Обновляем корзину пользователя
        $this->cart->update($data);

        // Получаем корзину пользователя
        $user_cart = $this->cart->contents();

        // Подгружаем шаблоны корзины и сетки товаров.
        // Последним параметром передаём true, чтобы шаблон выводился
        // в переменную
        $cart_html = $this->load->view('cart-view', array('cart_items' => $user_cart), true);

        $result = array(
            'status' => 'success',
            'message' => 'Товар успешно удалён из корзины',
            'cart_html' => $cart_html,
        );
    }

    if ($is_ajax) {
        // Если был AJAX запрос, возвращаем JSON ответ
        $this->output->set_content_type('application/json');
        $this->output->set_output(json_encode($result));
    } else {
        // Выводим сообщение
        $this->session->set_flashdata('message', $result);

        // Редирект на страницу с товарами
        redirect(base_url());
    }
}

Теперь посмотрим что получилось:

remove-from-cart.gif

На этом основной этам разработки корзины завершён. Далее можно будет совершенствовать корзину. Например добавить туда:

  • Увеличение количества товара в корзине при повторном добавление товара в корзину.
  • Возможность изменять количество товара прямо в блоке с корзиной.
  • Строка с общей суммой заказа.

В общем много ещё чего. Вы держитесь там, успехов, удачи.