FENOM'енальный курс. Часть 9. Сравнение товара с сгруппированными опциями.

Шаблон MAGAZINE вы можете скачать по ссылке.

Как и обещал в начале данного урока я вам расскажу, как можно создавать (не через админку) и другие опции товара и легко их добавлять в корзину. Обычно данная операция требуется на сайтах с нестандартным функционалом.

Смотрите, в корзине опции у товара содержатся в массиве $product[‘options’], и те, кто воспользовался моей подсказкой, прекрасно поняли, что, если мы хотим передать какую-либо опцию в корзину, нам необходимо, чтобы она попала в этот массив $product[‘options’]. А как сделать так, чтобы она попала? Вот:

<input type="hidden" name="option[custom_option]" value="Значание опции"/>
	

Где custom_option – это имя опции.

Перейдем с вами к нашему новому функционалу. В данном уроке мы будем с вами использовать компонент Comparison из modstore.pro. Данный компонент я ужет расматривал в “Сравнение товаров в miniShop2”, однако в данном уроке мы с вами будем модернизировать код сниппета CompareList, для того, чтобы опции товара у нас выводились сгрупированными, как в карточке товара.

Я думаю, что вы уже создали ресурс и шаблон “Сравнения товара”, поэтому не будем на это отвлекаться. Модифицированный сниппет CompareListGroup будет выглядеть следующим образом:

<?php
/** @var array $scriptProperties */
/** @var Comparison $Comparison */
$Comparison = $modx->getService('comparison','Comparison',$modx->getOption('comparison_core_path',null,$modx->getOption('core_path').'components/comparison/').'model/comparison/',$scriptProperties);
if (!($Comparison instanceof Comparison)) return '';
$Comparison->initialize($modx->context->key);
/** @var pdoFetch $pdoFetch */
$pdoFetch = $modx->getService('pdoFetch');
$pdoFetch->setConfig($scriptProperties);

$list = !empty($_REQUEST['list'])
  ? (string) $_REQUEST['list']
  : 'default';

if (isset($_SESSION['Comparison'][$modx->context->key][$list]['ids'])) {
  $ids = array_keys($_SESSION['Comparison'][$modx->context->key][$list]['ids']);
}
elseif (!empty($_REQUEST['cmp_ids'])) {
  $ids = explode(',', preg_replace('/[^0-9\,]/', '', $_REQUEST['cmp_ids']));
}
else {
  return $modx->lexicon('comparison_err_no_list');
}

if (empty($fields)) {$fields = '{"default":["price","article","vendor.name","color","size"]}';}
if (empty($tplRow)) {$tplRow = 'tpl.Comparison.row';}
if (empty($tplParam)) {$tplParam = 'tpl.Comparison.param';}
if (empty($tplCell)) {$tplCell = 'tpl.Comparison.cell';}
if (empty($tplHead)) {$tplHead = 'tpl.Comparison.head';}
if (empty($tplCorner)) {$tplCorner = 'tpl.Comparison.corner';}
if (empty($tplOuter)) {$tplOuter = 'tpl.Comparison.outer';}
if (empty($tplGroup)) {$tplGroup ='compareGroupTpl';}
if (empty($minItems)) {$minItems = 1;}
if (empty($maxItems)) {$maxItems = 10;}
if (!isset($scriptProperties['showUnpublished'])) {$scriptProperties['showUnpublished'] = false;}
if (!isset($scriptProperties['showDeleted'])) {$scriptProperties['showDeleted'] = false;}

$fields = $modx->fromJSON($fields);
if (empty($fields) || !is_array($fields)) {
  return $modx->lexicon('comparison_err_wrong_fields');
}
elseif (!isset($fields[$list])) {
  if ($modx->user->isAuthenticated('mgr')) {
	return $modx->lexicon('comparison_err_wrong_list', array('list' => $list));
  }
  else {
	return $modx->lexicon('comparison_err_no_list');
  }
}
$fields = $fields[$list];

$format = null;
if (!empty($formatSnippet)) {
  /** @var modSnippet $format */
  $format = $modx->getObject('modSnippet', array('name' => $formatSnippet));
}

// Joining MS2 tables
if (in_array('msProduct', $modx->classMap['modResource'])) {
  $class = 'msProduct';
  $leftJoin = array(
	'Data' => array('class' => 'msProductData'),
	'Vendor' => array('class' => 'msVendor', 'on' => 'Vendor.id = Data.vendor'),
  );
  
  $select = array(
	$class => !empty($includeContent) ?  $modx->getSelectColumns($class, $class) : $modx->getSelectColumns($class, $class, '', array('content'), true),
	'Data' => $modx->getSelectColumns('msProductData', 'Data', '', array('id'), true),
	'Vendor' => $modx->getSelectColumns('msVendor', 'Vendor', 'vendor.', array('id'), true),
  );
  
  $thumbsSelect = array();
  if (!empty($includeThumbs)) {
	$thumbs = array_map('trim',explode(',',$includeThumbs));
	if(!empty($thumbs[0])){
	  foreach ($thumbs as $thumb) {
		$leftJoin[$thumb] = array(
		  'class' => 'msProductFile',
		  'on' => "`$thumb`.`product_id` = `{$class}`.`id` AND `$thumb`.`parent` != 0 AND `$thumb`.`path` LIKE '%/$thumb/'"
		);
		$select[$thumb] = "`$thumb`.`url` as `$thumb`";
	  }
	}
  }
}
else {
  $class = 'modResource';
  $leftJoin = $select = array();
}

// Add custom parameters
foreach (array('leftJoin','select') as $v) {
  if (!empty($scriptProperties[$v])) {
	$tmp = $modx->fromJSON($scriptProperties[$v]);
	if (is_array($tmp)) {
	  $$v = array_merge($$v, $tmp);
	}
  }
  unset($scriptProperties[$v]);
}

$properties = array(
  'class' => $class,
  'parents' => 0,
  'resources' => implode(',', $ids),
  'includeTVs' => implode(',', $fields),
  'leftJoin' => $leftJoin,
  'select' => $select,
  'groupby' => $class . '.id',
  'limit' => $maxItems,
  'return' => 'data',
  'nestedChunkPrefix' => 'comparison_'
);
$pdoFetch->setConfig(array_merge($scriptProperties, $properties), false);
$resources = $pdoFetch->run();

$output = $rows = '';
if (count($ids) < $minItems) {
  $output = $modx->lexicon('comparison_err_min_count');
}
elseif (count($ids) > $maxItems) {
  $output = $modx->lexicon('comparison_err_max_resource');
}
else {
  $row_idx = 1;
  $grouper = array();
  foreach ($fields as $field) {
	//$cells = $pdoFetch->getChunk($tplParam, array('field' => $field, 'option' => $data["option"]));
	$cells = '';
	$cell_idx = 1;
	$previous_value = null;
	$same = true;
	foreach ($resources as $resource) {
	  $value = '';
	  if (array_key_exists($field, $resource)) {
		$value = $resource[$field];
	  }
	  elseif (stripos($field, 'option.') === 0) {
		$tmp_field = substr($field, 7);
		$values = $pdoFetch->getCollection(
		  'msProductOption',
		  array('key' => $tmp_field, 'product_id' => $resource['id']),
		  array('select' => 'value', 'sortby' => 'value')
		);
		if (!empty($values)) {
		  if (count($values) > 1) {
			$value = array();
			foreach ($values as $tmp) {
			  $value[] = $tmp['value'];
			}
		  }
		  else {
			$value = $values[0]['value'];
		  }
		}
		$option_data = array();
		$option = $modx->getObject('msOption', array('key' => $tmp_field));
		if($option){
		  $cat = $modx->getObject("modCategory", $option->get("category"));
		  $optid = $option->get("id");
		  $option_data["option_key"] = $option->get("key");
		  $option_data["option_measure_unit"] = $option->get("measure_unit");
		  $option_data["option_category"] = $option->get("category");
		  $option_data["option_category_name"] = $cat->get("category");
		  $option_data["option_type"] = $option->get("type");
		  $option_data["option_caption"] = $option->get("caption");
		  $option_data["option_description"] = $option->get("description");
		}
		$pdoFetch->addTime('Get product option "' . $tmp_field . '" for product "' . $resource['id'] . '"');
	  }
	  
	  // Send value to special snippet
	  if ($format) {
		$format->_cacheable = false;
		$format->_processed = false;
		$format->_content = '';
		$value = $format->process(array(
		  'name' => $field,
		  'field' => $field,
		  'input' => $value,
		  'value' => $value,
		  'resource' => $resource,
		  'pdoTools' => $pdoFetch,
		  'pdoFetch' => $pdoFetch,
		));
	  }
	  else {
		if (is_array($value)) {
		  natsort($value);
		  $value = implode(',', $value);
		}
		if ($class == 'msProduct' && in_array($field, array('price', 'old_price', 'weight'))) {
		  /** @var miniShop2 $miniShop2 */
		  if ($miniShop2 = $modx->getService('miniShop2')) {
			switch ($field) {
			  case 'price':
			  case 'old_price':
			  $value = $miniShop2->formatPrice($value) . ' ' . $modx->lexicon('ms2_frontend_currency');
			  break;
			  case 'weight':
			  $value = $miniShop2->formatWeight($value) . ' ' . $modx->lexicon('ms2_frontend_weight_unit');
			  break;
			}
		  }
		}
	  }
	  
	  if ($same && $cell_idx > 1) {
		$same = $previous_value == $value;
	  }

	  $cells .= $pdoFetch->getChunk($tplCell, array(
	  	'value' => $value, 
		'product_id' => $resource['id'],
		'cell_idx' => $cell_idx ++, 
		'classes' => 
		' field-'.$field, 'option_measure_unit' => $option_data["option_measure_unit"], 
		'option_type' => $option_data["option_type"])
	  );
	  $previous_value = $value;
	}

	$data = array(
	  'cells' => $cells, 
	  'row_idx' => $row_idx++, 
	  'same' => $same
	);
	$option = $modx->getObject('msOption', array('key' => $tmp_field));
	if($option){
	  $cat = $modx->getObject("modCategory", $option->get("category"));
	  $optid = $option->get("id");
	  $data["option_key"] = $option_data["option_key"];
	  $data["option_measure_unit"] = $option_data["option_measure_unit"];
	  $data["option_category"] = $option_data["option_category"];
	  $data["option_category_name"] = $option_data["option_category_name"];
	  $data["option_type"] = $option_data["option_type"];
	  $data["option_caption"] = $option_data["option_caption"];
	  $data["option_description"] = $option_data["option_description"];
	  $grouper[$data["option_category"]]["name"] = $data["option_category_name"]; 
	  $grouper[$data["option_category"]]["row"] .= $pdoFetch->getChunk($tplRow, $data);
	}else{
	  if ($miniShop2 = $modx->getService('miniShop2')) {
		switch ($field) {
		  case 'price':
		  case 'old_price':
		  $data["option_caption"] = "Цена";
		  break;
		  case 'weight':
		  $data["option_caption"] = "Вес";
		  break;
		  case 'vendor.name':
		  $data["option_caption"] = "Производитель";
		  break;
		}
	  }
	  $grouper[0]["name"] = "Основные характеристики"; 
	  $grouper[0]["row"] .= $pdoFetch->getChunk($tplRow, $data);
	}
	
	//$rows .= $pdoFetch->getChunk($tplRow, $data);
  }
  foreach($grouper as $group){
	$rows .= $pdoFetch->getChunk($tplGroup, $group);
  }

  $head = '';
  foreach ($resources as $resource) {
	$resource['list'] = $list;
	$head .= $pdoFetch->getChunk($tplHead, $resource);
  }

  $output = $pdoFetch->getChunk($tplOuter, array('head' => $head, 'rows' => $rows));
}

if ($modx->user->hasSessionContext('mgr') && !empty($showLog)) {
  $output .= '<pre class="CompareListLog">' . print_r($pdoFetch->getTime(),1) . '</pre>';
}

$modx->regClientScript('<script type="text/javascript">Comparison.list.initialize(".comparison-table", {minItems:'.$minItems.'});</script>', true);
return $output;
	

Над данным сниппетом также можно поработать и сделать количество чанков гораздо меньше. Но в данном случае мы с вами будем работать с тем, что есть. Создаем новый сниппет compare_list_group.php в нашей файловой директории. Вызов сниппета в шаблоне у нас будет следующий:

{'@FILE snippets/compare_list_group.php' | snippet : [
	'fields' => '{"default":["price","vendor.name"]}',
	'tplGroup' => '@FILE chunks/compare_group.tpl',
	'tplRow' => '@FILE chunks/compare_row.tpl',
	'tplHead' => '@FILE chunks/compare_product.tpl',
	'tplCell' => '@FILE chunks/compare_cell.tpl',
	'tplOuter' => '@FILE chunks/compare_outer.tpl',
]}
	

Конечно, нам еще нужны и чанки. Чанк группы compare_group.tpl:

<div class="compare-table">
  <div class="title">
	<span>{$name} <i class="fa fa-angle-up"></i></span>
  </div>
  {$row}
</div>
	

Чанк строки compare_row.tpl:

<div class="table-row {if $same != 1?}different{else}same{/if}">
  <div class="table-title">{$option_caption}
  	{if $option_description?}
	<span data-toggle="tooltip" data-placement="right" title="{$option_description}">
	  <i class="fa fa-question-circle"></i>
	</span>
	{/if}
  </div>
  {$cells}
</div>
	

Чанк товара – compare_product.tpl:

<div class="product" data-product-id="{$id}">
  <a href="#" class="comparison-remove comparison-link close"></a>
  <div class="item eqh">
	<div class="image">
	  <div class="labels">
		{if $favorite?}
		<div>
		  <span class="labels-hits">Хит продаж</span>
		</div>
		{/if}
		{if $new?}
		<div>
		  <span class="labels-new">Новинка</span>
		</div>
		{/if}
		{if $old_price > 0?}
		<div>
		  <span class="labels-action">Акция</span>
		</div>
		{/if}
	  </div>
	  <div class="bigger">
		<a href="{$id | url}" class="text-center" style="display:block">
		  {if $image?}
			<img src="{$image}" alt="{$pagetitle | htmlent}" title="{$pagetitle | htmlent}" class="img-fluid">
		  {else}
			<img src="{$_modx->getPlaceholder('+noimage')}" alt="{$pagetitle | htmlent}" title="{$pagetitle | htmlent}" class="img-fluid">
		  {/if}
		</a>
		{if $_pls['vendor.logo']?}
			<img src="{$_pls['vendor.logo']}" class="vendor" alt="{$_pls['vendor.name']}">
		{/if}
	  </div>	  
  </div>
  <div class="text">
	<a href="{$id | url}" class="title">{$pagetitle}</a>
	<div class="characters">
	    {$_modx->runSnippet('msProductOptions', [
			'onlyOptions' => $parent | resource: 'options',
			'product' => $id,
			'tpl' => '@FILE chunks/main_opts.tpl'
		])}
	  </div>
	  <div class="prices-block">
		<div class="prices">
		  {if $old_price > 0?}
		  <div class="old_price">
			<span>{$old_price}</span> <i class="fa fa-ruble"></i>
		  </div>
		  {/if}
		  {if $price > 0?}
		  <div class="price">
			<span>{$price}</span> <i class="fa fa-ruble"></i>
		  </div>
		  {/if}
		</div>
		<form method="post" class="ms2_form">
		  <input type="hidden" name="id" value="{$id}">
		  <input type="hidden" name="count" value="1">
		  <input type="hidden" name="options" value="[]">
		  <button type="submit" name="ms2_action" value="cart/add" class="btn btn-blue btn-rounded">В корзину</button>
		</form>
	  </div>
	</div>
  </div>
</div>
	

Чанк ячейки – compare_cell.tpl:

<div class="table-value {$classes}" data-product-id="{$product_id}" data-type="{$option_type}">
  <p>
	{if $option_type == "combo-boolean"?}
		{if $value == 1?}
			<span class="green">Да</span>
		{else}
			<span class="red">Нет</span>
		{/if}
	{else}
		{if $option_type == "combo-options"?}
			{var $values = $value|split}
			{foreach $values as $val}
				<span class="label label-default">{$val}</span>
			{/foreach}
			{$option_measure_unit}
		{else}
			{$value} {$option_measure_unit}
		{/if}
	{/if}
	</p>
</div>
	

Чанк обертки – compare_outer.tpl:

<div class="compare-table-header comparison-table" id="compareHeader">
  <div class="slider-container">
	<div class="compare-slider products owl-carousel">
	 {$head}
	</div>
  </div>
</div>
<div class="compare-tables">
  {$rows}  
</div>
	

Если подойти грамотно и с помощью всех возможностей Fenom, то мы можем обойтись одним чанком вместо пяти. Есть кто-нибудь, кто хочет это сделать? Отлично, если кто-то нашелся.

Следующим шагом нам необходимо реализовать механизм добавления товаров в сравнение. Как это будет выглядеть в карточке товара:

{$_modx->runSnippet('!addComparison', [
 'list_id' => 10,
 'id' => $_modx->resource.id
])}
	

В параметре list_id содержится идентификатор страницы сравнения товаров. Изменим стандартный чанк tpl.Comparison.add на следующий:

<div class="comparison comparison-[[+list]][[+added]][[+can_compare]]" data-id="[[+id]]" data-list="[[+list]]">
	<a href="#" class="comparison-add comparison-link compare" data-text="[[%comparison_updating_list]]">[[%comparison_add_to_list]]</a>
	<a href="#" class="comparison-remove comparison-link compare" data-text="[[%comparison_updating_list]]">[[%comparison_remove_from_list]]</a>
	<a href="[[+link]]" class="comparison-go compare">[[%comparison_go_to_list]]</a>
	<span class="comparison-total">[[+total]]</span>
</div>

<!--comparison_can_compare  can_compare-->
<!--comparison_added  added-->
	

В шапке сайта нам нужно вывести блок с ссылкой на страницу стравнения. Для этого в чанке header выводим следующую конструкцию:

{$_modx->runSnippet('!addComparison', [
	'list_id' => 10,
	'tpl' => '@FILE chunks/compare_total.tpl'
])}
	

Чанк compare_total.tpl:

<div data-id="{$id}" data-list="{$list}" class="comparison-default">
  <a href="{10 | url}" data-toggle="tooltip" data-placement="bottom" title="Сравнение товаров">
	<i class="fa fa-sliders-h">
	  <span class="comparison-total">{$total}</span>
	</i>
  </a>
</div>
	

Последний шаг – нужно модернизировать assets/components/comparison/js/default.js у Comparison:

Comparison = {
	add: {
		options: {
			add: '.comparison-add',
			remove: '.comparison-remove',
			go: '.comparison-go',
			total: '.comparison-total',
			added: 'added',
			can_compare: 'can_compare',
			loading: 'loading'
		},
		initialize: function(selector, params) {
			if (!$(selector).length) {return;}

			var options = this.options;
			var minItems = !params.minItems
				? 2
				: params.minItems;
			$(document).on('click', selector + ' ' + options.add + ',' + selector + ' ' + options.remove, function() {
				var $this = $(this);
				var $parent = $this.parents(selector);
				var text = $this.data('text');
				var list = $parent.data('list');
				var id = $parent.data('id');
				var action = $this.hasClass(options.add.substr(1))
					? 'add'
					: 'remove';

				if ($this.hasClass(options.loading)) {return false;}
				else {$this.addClass(options.loading);}
				if (text.length) {
					$this.attr('data-text', Comparison.utils.encode($this.html())).html(text);
				}
				$.post(document.location.href, {cmp_action: action, list: list, resource: id}, function(response) {
					if (text.length) {
						text = Comparison.utils.decode($this.attr('data-text'));
						$this.attr('data-text', Comparison.utils.encode($this.html())).html(text);
					}
					$this.removeClass(options.loading);
					if (response.success) {
						$(options.total, selector).text(response.data.total);

						if (response.data.link) {
							$(options.go, selector).attr('href', response.data.link);
						}
						if (response.data.total >= minItems) {
							$(selector).addClass(options.can_compare);
						}
						else {$(selector).removeClass(options.can_compare);}

						if (action == 'add') {$parent.addClass(options.added);}
						else {$parent.removeClass(options.added);}
					}
					else {
						if (typeof miniShop2 != 'undefined') {miniShop2.Message.error(response.message);}
						else {alert(response.message);}
					}
				}, 'json');
				return false;
			});
		}
	},

	list: {
		options: {
			all: '.comparison-params-all',
			unique: '.comparison-params-unique',
			remove: '.comparison-remove',
			same_class: 'same',
			active_class: 'active'
		},
		initialize: function(selector, params) {
			if (!$(selector).length) {return;}

			var options = this.options;
			var minItems = !params.min ? 1 : params.min;

			// Switch parameters
			$(document).on('click', selector + ' ' + options.all + ',' + selector + ' ' + options.unique, function() {
				var $this = $(this);
				var $parent = $this.parents(selector);

				if ($this.hasClass(options.active_class)) {
					return false;
				}
				else if ($this.hasClass(options.all.substr(1))) {
					$(options.unique, $parent).removeClass(options.active_class);
					$this.addClass(options.active_class);
					$('.'+options.same_class, $parent).show();
				}
				else if ($this.hasClass(options.unique.substr(1))) {
					$(options.all, $parent).removeClass(options.active_class);
					$this.addClass(options.active_class);
					$('.'+options.same_class, $parent).hide();
				}
				return false;
			});

			// Remove from list
			$(document).on('click', selector + ' ' + options.remove, function(e) {
				e.preventDefault();
				var $this = $(this);
				var par = $(this).parent().parent();
				var $parent = $this.parents(selector);
				var text = $this.data('text');
				var list = $this.parent().data('list');
				var id = $this.parent().data('product-id');
				var index = $(options.remove, selector).index(this) + 1;

				$.post(document.location.href, {cmp_action: 'remove', list: list, resource: id}, function(response) {
					$this.removeClass(options.loading);
					if (response.success) {
						if (response.data.total < minItems) {
							document.location = document.location.pathname;
						}

						$parent.find('.compare-table .table-row').each(function() {
							$(this).find('.table-value[data-product-id='+id+']').remove();
						});
						var carousel = $('.compare-slider').data('owl.carousel');
						var owlindex = carousel.relative(par.index());
						$(".compare-slider").trigger('remove.owl.carousel', [owlindex]).trigger('refresh.owl.carousel');
					}
					else {
						if (typeof miniShop2 != 'undefined') {miniShop2.Message.error(response.message);}
						else {alert(response.message);}
					}
				}, 'json');

				return false;
			});
		}
	},

	utils: {
		encode: function(string) {
			return $('<pre/>').text(string).html();
		},
		decode: function(string) {
			return $("<pre/>").html(string).text();
		}
	}
};
	

И у нас получился отличный сервис для сравнения товаров. Да, тут нужно еще поработать над группами, но в целом – очень даже неплохо.

FENOM'енальный курс. Часть 9. Сравнение товара с сгруппированными опциями.

0 Число голосов: 5
5
5
1
5

Комментарии ()

  1. Алексей 27 января 2020, 14:51 # 0
    Было здорово конечно 6 чанков в один запихнуть. Но и на этом спасибо.
    1. Михаил 07 мая 2021, 02:40 # 0
      А как проблему решили с глюком при перезагрузке страницы каталога? Т.е. когда после добавления в избранное из общего каталога карточек мы перегружаем страницу и видим снова кнопки «добавить», которые не срабатывают, а редиректят на #.
      1. Алексей 21 марта 2023, 19:23(Комментарий был изменён) # 0
        Привет
        $(document).on('click', selector + ' ' + options.remove, function(e) { e.preventDefault(); var $this = $(this); var par = $this.parent().parent(); var $parent = $this.parents(selector); var text = $this.data('text'); var list = $this.parent().data('list'); var id = $this.parent().data('product-id'); var index = $(options.remove, selector).index(this) + 1;  $.post(document.location.href, {cmp_action: 'remove', list: list, resource: id}, function(response) { $this.removeClass(options.loading); if (response.success) { if (response.data.total < minItems) { document.location = document.location.pathname; } $(options.total).text(response.data.total); $parent.find('.compare-table .table-row').each(function() { $(this).find('.table-value[data-product-id='+id+']').remove(); }); var carousel = $('.compare-slider').data('owl.carousel'); var owlindex = carousel.relative(par.index()); $('.compare-slider').trigger('remove.owl.carousel', [owlindex]).trigger('refresh.owl.carousel'); } else { if (typeof miniShop2 != 'undefined') {miniShop2.Message.error(response.message);} else {alert(response.message);} } }, 'json'); return false; });

      Наши клиенты

      Многие компании уже доверяют нам. Будьте в их числе!

      Хотите реализовать проект?

      Контакты

      Напишите нам - мы расскажем вам много интересного!


      Пермь, ул. Крупской 34, офис 510