Введение
Всем привет! Завтра у меня дедлайн по проекту, который я делаю для местной Камчатской компании по доставки еды. И поэтому у меня есть две причины написать эту статью, первая — прокрастинация перед дедлайном, а вторая — я не нашёл на Хабре какого-либо обучающего мануала по написанию корзины товаров на 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. Работа над ошибками».
