Начинаем 12 часть нашего курса создания интернет-магазина на MODx Revo. В данном уроке мы с вами реализуем корзину. Если прочитать документацию к miniShop2, то мы увидим, что корзина в данном компоненте два вида корзины:
- Мини-корзина (за это отвечает сниппет msMiniCart).
- Большая корзина (ее выводит сниппет msCart).
В нашем же случае видов корзины будет 3:
- Корзина в шапке
- Выпадающая корзина
- Страница с корзиной
Начнем с большой, потому что так нам будет проще. Советую посмотреть дефолтный чанк корзины miniShop2 – tpl.msCart. Те, кто смотрел мой предыдущий курс, сразу найдут разницу – здесь мы впервые столкнемся с шаблонизатором Fenom (документация Fenom).
Для того, чтобы начать, нам необходимо интегрировать шаблон корзины (cart.html). Я надеюсь, вы помните, как мы это делали на прошлых уроках. Также необходимо создать ресурс “корзина” с данным шаблоном. У меня получилось вот так:

Дальше мы приступим к созданию чанка. В данном чанке у нас будет полностью все, что находится в элементе “section#cart-page”. Для того, чтобы посмотреть все доступные плейсхолдеры, нужно вызвать сниппет msCart с пустым параметром tpl:
<pre>[[!msCart?tpl=``]]</pre>

Теперь соединим дефолтный чанк с нашим и у нас получится следующее:
<div class="container" id="msCart">
{if !count($products)}
<div class="alert alert-info" role="alert">{'ms2_cart_is_empty' | lexicon}</div>
{else}
<div class="col-xs-12 col-md-9 items-holder no-margin">
{foreach $products as $product}
<div class="row no-margin cart-item" id="{$product.key}">
<div class="col-xs-12 col-sm-2 no-margin">
<a href="{$product.id | url}" class="thumb-holder">
{if $product.image?}
<img class="lazy" src="{$product.image}" alt="{$product.pagetitle}" title="{$product.pagetitle}"/>
{else}
<img class="lazy" src="{'assets_url' | option}components/minishop2/img/web/ms2_small.png"
srcset="{'assets_url' | option}components/minishop2/img/web/ms2_small@2x.png 2x"
alt="{$product.pagetitle}" title="{$product.pagetitle}"/>
{/if}
</a>
</div>
<div class="col-xs-12 col-sm-5 ">
<div class="title">
<a href="{$product.id | url}">{$product.pagetitle}</a>
</div>
<div class="brand">{$product['vendor.name']}</div>
{if $product.options?}
<div class="small">
{$product.options | join : '; '}
</div>
{/if}
</div>
<div class="col-xs-12 col-sm-3 no-margin count">
<div class="quantity">
<div class="le-quantity">
<form method="post" class="ms2_form form-inline" role="form">
<input type="hidden" name="key" value="{$product.key}"/>
<a class="minus" href="#reduce"></a>
<input type="number" name="count" class="counter" value="{$product.count}"/>
<a class="plus" href="#add"></a>
<button class="btn btn-default" type="submit" name="ms2_action" value="cart/change">
<i class="glyphicon glyphicon-refresh"></i>
</button>
</form>
</div>
</div>
</div>
<div class="col-xs-12 col-sm-2 no-margin">
<div class="price">
{$product.price} руб.
</div>
<form method="post" class="ms2_form">
<input type="hidden" name="key" value="{$product.key}">
<button class="close-btn" type="submit" name="ms2_action" value="cart/remove">
</button>
</form>
</div>
</div>
{/foreach}
</div>
<div class="col-xs-12 col-md-3 no-margin sidebar ">
<div class="widget cart-summary">
<h1 class="border">Корзина</h1>
<div class="body">
<ul id="total-price" class="tabled-data inverse-bold no-border">
<li>
<label>Стоимость</label>
<div class="value pull-right">
<span class="ms2_total_cost">{$total.cost}</span> руб.
</div>
</li>
</ul>
<div class="buttons-holder">
<a class="le-button big" href="checkout.html" >Оформить заказ</a>
<a class="simple-link block" href="category-grid.html" >Продолжить покупки</a>
</div>
</div>
</div>
</div>
{/if}
</div>
Прочитать, как работает шаблонизатор и какие у него есть операторы и фишки вы можете в документации Fenom. В видео я поясню откуда я взял конкретные значения. Кроме того, мне пришлось поправить стили и скрипты шаблона. В целом, у нас сейчас все работает: изменяется количество товара в корзине и его можно удалить из корзины.
Теперь шаблон корзины у меня выглядит следующим образом:
<!DOCTYPE html>
<html lang="ru">
<head>
[[$meta]]
</head>
<body>
<div class="wrapper">
[[$headerInner]]
<section id="cart-page">
[[!msCart?tpl=`cartTpl`]]
</section>
[[$footer]]
</div>
[[$scripts]]
</body>
</html>
Неплохо не правда ли? Приступим к реализации маленькой корзины. Поступим тем же самым способом, как и с большой – будем сравнивать дефолтный чанк (tpl.msMiniCart) с нашим и будем вставлять необходимые классы и плейсхолдеры. Также я немного опередил события и добавил классы, для нашей выпадающей корзины (подробнее смотрите на видео). Не забывайте, что мини корзина у нас находится в двух чанках: header и headerInner. У меня чанк получился такой:
<a class="dropdown-toggle {$total_count > 0 ? 'full' : ''}" data-toggle="dropdown" href="[[~10]]" id="msMiniCart">
<div class="empty">
<div class="basket-item-count">
<span class="count ms2_total_count">0</span>
<img src="assets/images/icon-cart.png" alt="" />
</div>
<div class="total-price-basket">
<span class="lbl">Корзина:</span>
<span class="total-price">
<span class="value ms2_total_cost">0</span><i class="fa fa-ruble"></i>
</span>
</div>
</div>
<div class="not_empty">
<div class="basket-item-count">
<span class="count ms2_total_count">{$total_count}</span>
<img src="assets/images/icon-cart.png" alt="" />
</div>
<div class="total-price-basket">
<span class="lbl">Корзина:</span>
<span class="total-price">
<span class="value ms2_total_cost">{$total_cost}</span><i class="fa fa-ruble"></i>
</span>
</div>
</div>
</a>
Теперь вызываем нашу мини корзину в двух чанках шапки нашу корзину:
[[!msMiniCart?tpl=`miniCartTpl`]]
Теперь у нас есть мини корзина! Поздравляю!
И в данной части будет глава, которую достаточно много человек у меня просили в прошлом курсе – выпадающая корзина. Реализовывать будем с помощью технологии AJAX. Для начала нам нужно сделать чанк подобный большой корзине. У меня он получился следующий:
{if !count($products)}
<div class="alert alert-info" role="alert">{'ms2_cart_is_empty' | lexicon}</div>
{else}
{foreach $products as $product}
<li>
<div class="basket-item">
<div class="row">
<div class="col-xs-4 col-sm-4 no-margin text-center">
<div class="thumb">
<a href="{$product.id | url}" class="thumb-holder">
{if $product.image?}
<img class="lazy" src="{$product.image}" alt="{$product.pagetitle}" title="{$product.pagetitle}"/>
{else}
<img class="lazy" src="{'assets_url' | option}components/minishop2/img/web/ms2_small.png"
srcset="{'assets_url' | option}components/minishop2/img/web/ms2_small@2x.png 2x"
alt="{$product.pagetitle}" title="{$product.pagetitle}"/>
{/if}
</a>
</div>
</div>
<div class="col-xs-8 col-sm-8 no-margin">
<div class="title">{$product.pagetitle}</div>
<div class="price">{$product.price} <i class="fa fa-ruble"></i></div>
</div>
</div>
<form method="post" class="ms2_form">
<input type="hidden" name="key" value="{$product.key}">
<button class="close-btn" type="submit" name="ms2_action" value="cart/remove">
</button>
</form>
</div>
</li>
{/foreach}
<li class="checkout">
<div class="basket-item">
<div class="row">
<div class="col-xs-12 col-sm-6">
<a href="[[~10]]" class="le-button inverse">Корзина</a>
</div>
<div class="col-xs-12 col-sm-6">
<a href="checkout.html" class="le-button">Оформить заказ</a>
</div>
</div>
</div>
</li>
{/if}
Заметили, что он получается подобный крупной корзине? Теперь на нужно создать несколько ресурсов, к которым мы будем обращаться по ajax. Обычно для таких ресурсов я создаю контейнер “технические ресурсы”/”ajax” и создаем ресурс с пустым шаблоном, отключаем текстовый редактор на вкладке настройки.

В содержимом ресурса вызываем уже знакомы сниппет:
[[!msCart?tpl=`toggleCartTpl` &sortdir=`DESC`]]
Только мы сменили сортировку, последние добавленные товары будут первыми. И нам осталось написать скрипт JS, который мы разместим в чанке “scripts”:
<script>
$(document).on('click', '#msMiniCart', function(e) {
e.preventDefault();
$.ajax({
type: "POST",
url: '[[~13]]',
data: {parent: '[[*id]]'},
success: function(data) {
if (data){
$('.basket .ajax-data').html(data);
}else{
miniShop2.Message.error('Что-то пошло не так, попробуйте позже!');
}
}
});
});
</script>
Ура! У нас работает всплывающая корзина! Делается все очень просто, не правда ли? На этом мы закончили реализацию наших корзин. Осталось дело за малым – настроить стили, а то они немного подслетели. До следующих уроков!
miniShop2.Callbacks.add('Cart.clean.success', 'restrict_cart', function() { location.href('ссылка для переадресации'); return false; });<form method="post"> <button class="btn btn-default" type="submit" name="ms2_action" value="cart/clean"> <i class="glyphicon glyphicon-remove"></i> {'ms2_cart_clean' | lexicon} </button> </form>// Quantity Spinner $('.le-quantity a').click(function(e){ e.preventDefault(); var elem = $(this).parent().parent().find('input.counter'); var currentQty= elem.val(); if( $(this).hasClass('minus') && currentQty>0){ elem.val(parseInt(currentQty, 10) - 1); elem.trigger("change"); }else{ if( $(this).hasClass('plus')){ elem.val(parseInt(currentQty, 10) + 1); elem.trigger("change"); } } });Проверьте соответствие в файле assets/js/scripts.js примерно 316 строка$('.basket .ajax-data').html(data);не совсем понятно куда пишется содержимое ответа, селекторы .basket .ajax-data нигде не встречаются.$(document).on('mouseover', '#msMiniCart', function(e) { e.preventDefault(); $.ajax({ type: "POST", url: '[[~13]]', data: {parent: '[[*id]]'}, success: function(data) { if (data){ $('.basket .ajax-data').html(data); }else{ miniShop2.Message.error('Что-то пошло не так, попробуйте позже!'); } } }); });Я тут поменял событие click на mouseover в первой строке. Также нужно добавить стили:a#msMiniCart.dropdown-toggle:hover + ul.dropdown-menu { display: block; }И должно выпадать при наведении мыши. Не забудьте после изменений почистить кеш (CTRL + F5).$(function(){ $("#msMiniCart").on("click", function(e){ e.preventDefault(); $.ajax({ type: "POST", url: '[[~67]]', data: {parent: '[[*id]]'}, success: function(data) { if (data){ $('#msMiniCart .ajax-data').html(data); }else{ miniShop2.Message.error('Что-то пошло не так, попробуйте позже!'); } } }); }); });//den812++ var elem= $(this).parent().parent().find('input.counter'); var currentQty= elem.val(); //var currentQty= $(this).parent().parent().find('input.counter').val(); if( $(this).hasClass('minus') && currentQty>0){ //$(this).parent().parent().find('input').val(parseInt(currentQty, 10) - 1); elem.val(parseInt(currentQty, 10) - 1); elem.trigger("change"); }else{ if( $(this).hasClass('plus')){ //$(this).parent().parent().find('input').val(parseInt(currentQty, 10) + 1); elem.val(parseInt(currentQty, 10) + 1); elem.trigger("change"); } } //den812--{if $product.parent == 17 || $product.parent == 18 || $product.parent == 19 || $product.parent == 20 || $product.parent == 23 || $product.parent == 249 || $product.id == 311 || $product.id == 312 || $product.id == 313}то применяется это только к последнему добавленному товару. если добавляю:{foreach $products as $product}то естественно доп.блоков появляется столько, сколько товаров с нужными родителями есть в корзине, нам же нужно, чтобы этот блок был один хоть для одного товара, хоть для многих. может кто-то подсказать ответ на этот (казалось бы простой) вопрос?<a class="dropdown-toggle {$total_count > 0 ? 'full' : ''}" data-toggle="dropdown" href="[[~40]]" id="msMiniCart"> <div class="empty"> <div class="basket-item-count"> <span class="count ms2_total_count">0</span> <img src="assets/images/icon-cart.png" alt="" /> </div> <div class="total-price-basket"> <span class="lbl">Корзина:</span> <span class="total-price"> <span class="value ms2_total_cost">0</span><i class="fa fa-ruble"></i> </span> </div> </div> <div class="not_empty"> <div class="basket-item-count"> <span class="count ms2_total_count">{$total_count}</span> <img src="assets/images/icon-cart.png" alt="" /> </div> <div class="total-price-basket"> <span class="lbl">Корзина:</span> <span class="total-price"> <span class="value ms2_total_cost">{$total_cost}</span><i class="fa fa-ruble"></i> </span> </div> </div> </a> <ul class="ajax-data"> </ul>вызов сниппета тоже такой жеjs только немного поменяла, так как у меня контейнера с классом basket нет, то его только удалила:$(document).on('click', '#msMiniCart', function(e) { e.preventDefault(); $.ajax({ type: "POST", url: '[[~42]]', success: function(data) { if (data){ $('.ajax-data').html(data); }else{ miniShop2.Message.error('Что-то пошло не так, попробуйте позже!'); } } }); });<form method="post" class="ms2_form" role="form"> <input type="hidden" name="key" value="{$product.key}"/> <input type="number" name="count" value="{$product.count}" class="form-control"/> </form>$status = array( 'total_pos' => 0, 'total_count' => 0, 'total_cost' => 0, 'total_weight' => 0, ); foreach ($this->cart as $item) { if (empty($item['ctx']) || $item['ctx'] == $this->ctx) { $status['total_pos'] += 1; $status['total_count'] += $item['count']; $status['total_cost'] += $item['price'] * $item['count']; $status['total_weight'] += $item['weight'] * $item['count']; } }И тогда вы сможете использовать [[+total_pos]] в своем чанке. Если вам не понятно как это работает, то вы можете взять платную консультацию в skype — я расскажу.$status = array( 'total_pos' => 0, //add pos count 'total_count' => 0, 'total_cost' => 0, 'total_weight' => 0, ); foreach ($this->cart as $item) { if (empty($item['ctx']) || $item['ctx'] == $this->ctx) { $status['total_pos'] += 1; //add pos count $status['total_count'] += $item['count']; $status['total_cost'] += $item['price'] * $item['count']; $status['total_weight'] += $item['weight'] * $item['count']; } }Миникорзина<a href="[[~4659]]"> <div id="msMiniCart" class="{$total_pos > 0 ? 'full' : ''}"> <div class="miniCart__basket"> <div class="miniCart__basket-circle empty"> <span>0</span> </div> <div class="miniCart__basket-circle not_empty"> <!--<span class="ms2_total_count">{*$total_count*}</span>--> <span class="ms2_total_count">{$total_pos}</span> </div> </div> </div> </a>Но происходит всё так: при добавлении допустим 10 метров трубы в значке появляется число 10. По клику на корзину, когда переходим на страницу при загрузке её уже получается 1setup: function () { miniShop2.Cart.cart = '#msCart'; miniShop2.Cart.miniCart = '#msMiniCart'; miniShop2.Cart.miniCartNotEmptyClass = 'full'; miniShop2.Cart.countInput = 'input[name=count]'; miniShop2.Cart.totalWeight = '.ms2_total_weight'; miniShop2.Cart.totalCount = '.ms2_total_count'; miniShop2.Cart.totalCost = '.ms2_total_cost'; miniShop2.Cart.totalPos = '.ms2_total_pos'; // add pos count },status: function (status) { if (status['total_count'] < 1) { location.reload(); } else { //var $cart = $(miniShop2.Cart.cart); var $miniCart = $(miniShop2.Cart.miniCart); if (status['total_count'] > 0 && !$miniCart.hasClass(miniShop2.Cart.miniCartNotEmptyClass)) { $miniCart.addClass(miniShop2.Cart.miniCartNotEmptyClass); } $(miniShop2.Cart.totalWeight).text(miniShop2.Utils.formatWeight(status['total_weight'])); $(miniShop2.Cart.totalCount).text(status['total_count']); $(miniShop2.Cart.totalPos).text(status['total_pos']); // add pos count $(miniShop2.Cart.totalCost).text(miniShop2.Utils.formatPrice(status['total_cost'])); if ($(miniShop2.Order.orderCost, miniShop2.Order.order).length) { miniShop2.Order.getcost(); } } },И чанк миникорзины<a href="[[~4659]]"> <div id="msMiniCart" class="{$total_pos > 0 ? 'full' : ''}"> <div class="miniCart__basket"> <div class="miniCart__basket-circle empty"> <span>0</span> </div> <div class="miniCart__basket-circle not_empty"> <!--<span class="ms2_total_count">{*$total_count*}</span>--> <span class="ms2_total_pos">{$total_pos}</span> </div> </div> </div> </a>Вдруг кому пригодится. Ещё раз благодарю$('.basket__item-count a').click(function(e){ e.preventDefault(); var elem = $(this).parent().parent().find('input.counter'); var currentQty= elem.val(); var productCost = $(this).parent().parent().parent().find(".basket__item-cost .ms2_cost"); var productPrice = $(this).parent().parent().parent().find(".basket__item-price span").text(); productPrice = +productPrice; if( $(this).hasClass('minus') && currentQty>0){ elem.val(parseInt(currentQty, 10) - 1); elem.trigger("change"); productCost.html(productPrice * currentQty - productPrice); }else{ if( $(this).hasClass('plus')){ elem.val(parseInt(currentQty, 10) + 1); elem.trigger("change"); productCost.html(productPrice * currentQty + productPrice); } } });$('.basket__item-count a').click(function(e){ e.preventDefault(); var elem = $(this).closest('.basket__item-count').find('input.counter'); var currentQty= elem.val(); var productCost = $(this).closest('.basket__item').find(".basket__item-cost .ms2_cost"); var productPrice = $(this).closest('.basket__item').find(".basket__item-price span").text(); productPrice = +productPrice; if( $(this).hasClass('minus') && currentQty>0){ elem.val(parseInt(currentQty, 10) - 1); elem.trigger("change"); productCost.html(productPrice * currentQty - productPrice); }else{ if( $(this).hasClass('plus')){ elem.val(parseInt(currentQty, 10) + 1); elem.trigger("change"); productCost.html(productPrice * currentQty + productPrice); } } });Так вы избавитесь от вложенности и код переиспользовать будет проще.