Введение
Всем привет! Завтра у меня дедлайн по проекту, который я делаю для местной Камчатской компании по доставки еды. И поэтому у меня есть две причины написать эту статью, первая — прокрастинация перед дедлайном, а вторая — я не нашёл на Хабре какого-либо обучающего мануала по написанию корзины товаров на AngularJS.
Я нашёл статью на стороннем блоге, которая частично помогла мне решить пару задач, которые стояли передо мной. Но оформление статьи оставляло желать лучшего, да и за 5 лет я уже отвык от кода в блокноте, без подсветки синтаксиса, поэтому нужно было как-то структурировать и сделать более читабельной эту полезную информацию.
Почему был выбран формат одностраничного магазина?
Кто-то из вас, наверное уже знает, что на Камчатке существует проблема с интернетом, так как наш полуостров ещё не связан с материком оптоволокном, и весь поток идёт через единственную вышку. К концу 2015 года планируется завершить работы по прокладке оптоволокна по дну Охотского моря, и возможно у нас появится наконец-то стабильный и быстрый интернет.
И соответственно чтобы исключить потери клиентов, переходящих по разным категориям блюд, из-за нестабильного канала, была выбрана подобная модель. То есть человеку достаточно один раз загрузить сайт и получить всю необходимую информацию, и сделать заказ.
Так же такой формат позволил избежать хранения товаров корзины в сессиях, localStorage или же в базе данных. Так как мы точно знаем, что человек никуда не уйдёт с этой страницы, мы храним данные корзины в объекте javascript. Ещё одним плюсом стало уменьшение времени заказа, так как нет нужды перемещаться по категориям, и загружать новые страницы. И так как позиций блюд не слишком много, нам даже не пришлось делать Ajax-подгрузку данных при нажатии на категорию, всё подгружалось из кеша базы данных.
База данных
После того, как был готов и свёрстан дизайн одностраничного сайта, пришло время создать структуру базы данных категорий и товаров. Это наверно самый быстрый и самый простой этап, учитывая наши потребности и направленность на простоту работы системы и взаимодействия с пользователями. У меня уже был набросок админки на Phalcon PHP Framework, поэтому поправить его для работы с двумя таблицами category и products, не составило особого труда.
Таблица category
Таблица products
Вот так выглядит админка сайта
Получаем категории и блюда из базы данных
Для работы с базой данных использовалась стандартная ORM система фреймворка Phalcon, если вы с ней не работаете, можете пропустить этот раздел, он для общего развития, чтоб дальше было понятно откуда ноги растут, точнее откуда и как берутся блюда на сайте.
В основном контроллере IndexController.php в модуле frontend, я написал функцию, которая сформирует данные нужным нам образом и выведет их на единственную нашу страницу.
public function indexAction()
{
//Получаем все категории в массив
$category = Category::find()->toArray();
$help = new \Lib\Url();
Перебираем массив и для каждой категории подгружаем блюда
foreach ($category as $key => $val)
{
$category[$key]['url'] = $help->translit_url($val['name']);
//Это не родительская категория? Тогда удаляем из массива
if ($val['pid'] != 0)
{
unset($category[$key]);
}
else
{
//Подгружаем массив подкатегорий, правда в нашем случае только одна категория имеет подкатегории
$category[$key]['sub'] = Category::find("pid = '".$val['id']."'")->toArray();
//Если есть подкатегории, цепляем к ним блюда, если нет, то цепляем блюда к основном категории
if(count($category[$key]['sub']) > 0)
{
foreach($category[$key]['sub'] as $i => $cat)
{
$category[$key]['sub'][$i]['products'] = Products::find("category = '" . $cat['id'] . "'")->toArray();
}
}
else
{
$category[$key]['products'] = Products::find("category = '" . $val['id'] . "'")->toArray();
}
}
}
$this->view->setVar('items',$category);
}
Возможно есть более изящные решения этой задачи, но у нас не будет больше 200 посетителей за день, и мы подключим кеширование запросов к базе данных, и в принципе не будет такой сильной нагрузки, тем более это Phalcon — «Самый быстрый PHP фреймворк». Но вопрос сейчас не о производительно и оптимизации, это пока рано, главное что пора выводить товар на странице.
Кому доверить рендеринг товаров на странице? AngularJS или Phalcon?
Сначала я реализовал всё на AngularJS, ну это было как-то изящнее и красивее, но потом задумался о СЕО-оптимизации, и индексации поисковыми системами, и подумал что лучше наверно не рисковать, и рендеринг доверить нашему старому доброму любимому PHP.
Приводить код выводящий товары на странице я тут не буду, как из соображений величины этого кода, так и из этических соображений, всё таки это продукт нашего заказчика. Да и думаю, кто читает Хабр знает как пользоваться функцией foreach в php.
Ну ладно, покажу как я выводил категории блюд, а там уже по примеру каждый разберётся и с товарами.
<div class="row">
<div class="large-12 columns">
<ul class="tabs small-block-grid-2 medium-block-grid-4 large-block-grid-6" data-tab>
<?php foreach ($items as $item): ?>
<li>
<a href="#<?= $item['url'] ?>">
<img ng-src="/uploads/<?= $item['images'] ?>" alt="<?= $item['name'] ?>">
</a>
<p><?= $item['name'] ?></p>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
Выглядит это всё дело очень и очень аппетитно.
Добавление товара в корзину
Нажимая на категорию, внизу открывается таб с блюдами из этой категории. Возможно кто-то из вас уже не может смотреть на эти аппетитные картинки, но для продолжения описания работы одностраничного магазина, мне просто необходимо показать вам, как выглядит карточка товара на сайте. Смотрите.
Так, а теперь
В основном для понимания основы вам нужно видеть только этот отрезок кода, который выводит кнопку «Добавить» с полем, куда можно ввести количество штук блюда.
<div class="add-cart">
<input type="number" ng-model="num<?=$p['id']?> value="1" min="1" max="50">
<button type="button" ng-click="addCart(<?=$p['id']?>, num<?=$p['id']?>, '<?=$p['title']?>', <?=$p['price']?>)"></button>
</div>
Как видно из исходного кода, функция addCart() выполняет добавление данных в объект javascript, давайте посмотрим исходный код на AngularJS.
$scope.carts = [];
$scope.addCart = function(id, num, title, price)
{
var nums = num || 1;
$scope.carts.push({
id : id,
num : nums,
title : title,
price : price
});
};
Я думаю тут нечего объяснять, всё предельно просто и понятно, а главное — работает! После того, как мы добавили данные в $scope.carts, нужно их куда-то вывести. В нашем случае, дизайнер решил сделать это в плавающее окошко справа, вот так.
И соответственно код.
<div class="cart ng-cloak" ng-cloak ng-show="carts.length > 0">
<div class="cart-items">
<h3 class="text-center">Корзина</h3>
<div class="row items" ng-repeat="item in carts">
<div class="small-5 columns text-right">
{{item.title}}
</div>
<div class="small-2 columns">
<input type="number" ng-model="item.num" min="1" max="50">
</div>
<div class="small-4 columns">
{{item.price}} руб.
</div>
<div class="small-1 columns">
<a href ng-click="removeItem(carts,item)">X</a>
</div>
</div>
</div>
<div class="cart-results text-center">
<div class="row">
<div class="small-12 columns">
<select ng-model="delivery" ng-init="delivery = 0">
<option value="0">Октябрьский район</option>
<option value="1">Ленинский район</option>
<option value="2">Долиновка, Завойко</option>
<option value="3">Самовывоз (-10%)</option>
</select>
</div>
</div>
<h3>{{total() | number : 0}} руб.</h3>
<button class="new_btn" data-reveal-id="order">Заказать</button>
</div>
</div>
Кстати, функция ng-cloak просто выручает в условиях Камчатского интернета, если её не использовать, пока человек будет ждать загрузку страницы, ему будет видна пустая корзина со страшными символами. Для тех кто не знаком с AngularJS, укажу на несколько ключевых моментов.
Это нужно, чтобы показывать корзину, только в случае если в ней есть товары.
ng-show="carts.length > 0"
Этот код выводит конечную сумму заказа, форматируя число, и убирая копейки, которые могут получится, при высчитывании 10% скидки, например в случае самовывоза.
{{total() | number : 0}}
У меня часто возникала задача удалить элемент ассоциативного массива, я каждый раз забывал как это делать, и обращался к Google, но надеюсь после этой публикации я наконец-то запомню, а те кто не знал, узнают.
$scope.removeItem = function(carts, item) {
carts.splice(item, 1);
};
А что, кто-то рассчитывал что будет больше кода? Можно кстати даже в одну строчку написать. Но не будем изгаляться, нам главное читабельность кода. И наверно последнюю функцию которую я хочу привести в этой статье — это подсчёт конечной суммы заказа. Он был выполнен исходя из условий и способов доставки.
$scope.total = function() {
var total = 0;
angular.forEach($scope.carts, function(item) {
total += item.num * item.price;
});
var delivery = 0;
if($scope.delivery == 0)
{
if(total >= 600)
{
delivery = 0;
}
else
{
delivery = 130;
}
}
if($scope.delivery == 1)
{
if(total >= 1500)
{
delivery = 0;
}
else
{
delivery = 130;
}
}
if($scope.delivery == 2)
{
delivery = 300;
}
if($scope.delivery == 3)
{
delivery = 10/total*100*(-1);
}
return total + delivery;
};
Чем объяснять откуда растут цифры, я просто покажу блок с информацией о доставке, а вы уже сами разгадаете откуда появились разные цифры в коде. Тем более у каждого эти данные будут разные, и будет разное количество районов, поэтому не вижу смысла заострят на этом внимание, тем более статья и так уже получилась достаточно объёмная.
Послесловие
Главная задача моего поста решена, теперь на Хабре есть материал, о том как сделать добавление товаров в корзину и подсчёт суммы заказа на AngularJS, а в интернете есть хорошо оформленный материал об этом, который дополнит запись из стороннего блога, ссылку на который я привёл в начале статьи. Ну а так же, я уже достаточно прокрастинировал, поэтому пора приступать снова к работе, и заканчивать отправку заказа нашему заказчику. Надеюсь статья поможет таким же как я. Если у вас всё же возникнут вопросы по приведённому исходному коду, или что-то будет не понятно, я с удовольствием отвечу в комментариях. К сожалению опыт работы с AngularJS всего пол года, поэтому чем смогу, тем помогу. Спасибо за ваше внимание.
Обновление
Спасибо всем за комментарии, я постарался исправить недочёты и описал проделанную работу в следующей публикации — «Одностраничный магазин на Phalcon PHP + AngularJS. Работа над ошибками».