I want my main menu to not include any categories that are empty. I've done this for the layered navigation very easily in the relevant phtml file by using
$_category->getProductCount()
However, for the navigation menu, I'm finding it impossible to do this as easily (I have seen the Prattski example but it does seem rather OTT).
The main menu seems to be built in Mage_Page_Block_Html_Topmenu.php, specifically in the function _getHtml. This gets all the children in the menu and if I try something like $child->getId(), I get something like "category-node-36".
It doesn't seem like I'm too far from being able to use getProductCount() and so test if it's more than zero.
Is it possible to do this? Can somebody point me to how?
If I can, I'll extend the class with my version.
To do this, go to:
app/code/core/Mage/Catalog/Block Folder and copy Navigation.php and override it in your local package.
Open Navigation.php of your package and paste below code in this file:
if ($category->getIsActive()) {
$cat = Mage::getModel('catalog/category')->load($category->getId());
$products = Mage::getResourceModel('catalog/product_collection')->addCategoryFilter($cat);
Mage::getSingleton('catalog/product_status')->addVisibleFilterToCollection($products);
Mage::getSingleton('catalog/product_visibility')->addVisibleInCatalogFilterToCollection($products);
Mage::getSingleton('cataloginventory/stock')->addInStockFilterToCollection($products);
if(count($products)==0)
return;
}
I hope my code will help you.
I finally cracked it although I'm far from convinced it's an optimum solution. Anyway, I'll described what I did here and hopefully somebody can make it more efficient. I'll give a blow-by-blow description as I was ufamiliar with quite a few areas. So apologies for the length.
As I said, in my case at least, the main menu is built via Topmenu.php in app/code/core/Mage/Page/Block/Html, specifically the method _getHtml. I very definitely don't want to modify a core file so I found out how to extend this method via a new module. (You can skip this bit if you're familiar with creating new modules.)
Configuring a new module
I needed to create a new module (I'll call it MYMOD below). As I'm overwriting the core magento page block, I had to create new folders: app/code/local/MYMOD/Page and in there two sub-folders, Block and etc (I believe they are case sensitive). And within Block another subfolder Html. You can see this is exactly mirroring the folder structure from app/code/core/Mage.
The etc folder holds the specification for the new module in a config.xml file. This is what mine looks like:
<?xml version="1.0" encoding="UTF-8"?>
<!-- The root node for Magento module configuration -->
<config>
<!--
The module's node contains basic
information about each Magento module
-->
<modules>
<!--
This must exactly match the namespace and module's folder
names, with directory separators replaced by underscores
-->
<MYMOD_Page>
<!-- The version of our module, starting at 0.0.1 -->
<version>0.0.1</version>
</MYMOD_Page>
</modules>
<global>
<blocks>
<page>
<rewrite>
<html_topmenu>MYMOD_Page_Block_Html_Topmenu</html_topmenu>
</rewrite>
</page>
</blocks>
</global>
</config>
You can find out about the whys and wherefors of this elsewhere.
Unfortunately (in my opinion!), that's not all you have to do to specify a new module in Magento. You also have to create a file called "MYMOD_Page.xml" in app/etc/modules. This is just telling Magento about your module and where to look for it. Mine looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<config>
<modules>
<MYMOD_Page>
<!-- Whether our module is active: true or false -->
<active>true</active>
<!-- Which code pool to use: core, community or local -->
<codePool>local</codePool>
</MYMOD_Page>
</modules>
</config>
OK, sorry for the irrelevant instructions on modules but I do like self-contained blow-by-blow explanations.
Overwriting the method
I can now create a new file in my module with a subclass in which I can have methods (functions) that will be used instead of the core Magento ones. The file name has to be the same as the original file, Topmenu.php and it goes in app/code/local/MYMOD/Page/Block/Html.
Remember, the object oriented structure means that all of the functions in the original core version of Topmenu.php are available to me, I don't have to copy them in my new version (great for maintainability). My version of Topmenu.php only has to contain any new functions I might want (in this case I didn't need any) and redeclare any functions I want to overwite with my own version. In my case, I needed to modify two functions: _getHtml and _getMenuItemClasses.
_getHtml needs additional checks so any empty categories aren't included.
_getMenuItemClasses needs additional checks so that the class "parent" isn't added to categories whose children are empty.
Here's how I did it (with comments). I'm sure there are better ways but I'm still fairly new to Magento.
class MYMOD_Page_Block_Html_Topmenu extends Mage_Page_Block_Html_Topmenu
// Create my subclass, in accordance with how I've defined the new module
{
/**
* Recursively generates top menu html from data that is specified in $menuTree
*
* #param Varien_Data_Tree_Node $menuTree
* #param string $childrenWrapClass
* #return string
*/
protected function _getHtml(Varien_Data_Tree_Node $menuTree, $childrenWrapClass)
{
$html = '';
$children = $menuTree->getChildren();
$parentLevel = $menuTree->getLevel();
$childLevel = is_null($parentLevel) ? 0 : $parentLevel + 1;
$counter = 1;
$childrenCount = $children->count();
$parentPositionClass = $menuTree->getPositionClass();
$itemPositionClassPrefix = $parentPositionClass ? $parentPositionClass . '-' : 'nav-';
foreach ($children as $child) {
$child->setLevel($childLevel);
$child->setIsFirst($counter == 1);
$child->setIsLast($counter == $childrenCount);
$child->setPositionClass($itemPositionClassPrefix . $counter);
$outermostClassCode = '';
$outermostClass = $menuTree->getOutermostClass();
if ($childLevel == 0 && $outermostClass) {
$outermostClassCode = ' class="' . $outermostClass . '" ';
$child->setClass($outermostClass);
}
/*
* Find out if this category has any products. I don't know an easier way.
* The id of every child returned by getID is of the form "category-node-nnn"
* where nnn is the id that can be used to select the category details.
* substr strips everything leaving just the nnn.
* Then use getModel-> getCollection with a filter on the id. Although this looks
* like it will return many, obviously category ids are unique so in fact it only
* returns the category we're currently looking at.
*/
$_gcategoryId = substr($child->getId(), 14, 6);
$_gcategories = Mage::getModel('catalog/category')->getCollection()->addFieldToFilter('entity_id', array('eq', $_gcategoryId));
foreach ($_gcategories as $_gcategory) {
$_gcategoryCount = $_gcategory->getProductCount();
}
/*
* Now only include those categories that have products.
* In my case I also wanted to include the top level categories come what may.
*/
if (($childLevel == 0) || ($_gcategoryCount > 0)) {
$html .= '<li ' . $this->_getRenderedMenuItemAttributes($child) . '>';
$html .= '<a href="' . $child->getUrl() . '" ' . $outermostClassCode . '><span>'
. $this->escapeHtml($child->getName()) . '</span></a>';
if ($child->hasChildren()) {
if (!empty($childrenWrapClass)) {
$html .= '<div class="' . $childrenWrapClass . '">';
}
$html .= '<ul class="level' . $childLevel . '">';
$html .= $this->_getHtml($child, $childrenWrapClass);
$html .= '</ul>';
if (!empty($childrenWrapClass)) {
$html .= '</div>';
}
}
$html .= '</li>';
}
$counter++;
}
return $html;
}
/**
* Returns array of menu item's classes
*
* #param Varien_Data_Tree_Node $item
* #return array
*/
protected function _getMenuItemClasses(Varien_Data_Tree_Node $item)
{
$classes = array();
$classes[] = 'level' . $item->getLevel();
$classes[] = $item->getPositionClass();
if ($item->getIsFirst()) {
$classes[] = 'first';
}
if ($item->getIsActive()) {
$classes[] = 'active';
}
if ($item->getIsLast()) {
$classes[] = 'last';
}
if ($item->getClass()) {
$classes[] = $item->getClass();
}
if ($item->hasChildren()) {
/*
* Don't just check if there are children but, if there are, are they all empty?
* If so, then the changes in _getHtml will mean none of them will be included
* and so this one has no children displayed and so the "parent" class is not appropriate.
*/
$children = $item->getChildren(); // Get all the children from this menu category
foreach ($children as $child) { // Loop over each child and find out how many products (see _getHtml)
$_gcategoryId = substr($child->getId(), 14, 6);
$_gcategories = Mage::getModel('catalog/category')->getCollection()->addFieldToFilter('entity_id', array('eq', $_gcategoryId));
foreach ($_gcategories as $_gcategory) { // Remember, there's actually only one category that will match the child's id
$_gcategoryCount = $_gcategory->getProductCount();
}
if ($_gcategoryCount > 0) { // As soon as one child has products, then we have a parent and can stop looking
$classes[] = 'parent';
break;
}
}
}
return $classes;
}
}
I hope this is clear. It does what I want (my store is small) but any suggestions for improvement welcome.
path: app/design/frontend/rwd/default/template/page/html/topmenu/renderer.phtml
Make this query (it is 0.0004 sec), under the foreach ($children as $child) {
$mageconnection = Mage::getSingleton("core/resource")->getConnection("core_read");
$query="select count(cataloginventory_stock_item.is_in_stock) as subcount,catalog_category_flat_store_1.`name` from catalog_category_flat_store_1 INNER JOIN
catalog_category_product_index on catalog_category_product_index.category_id=catalog_category_flat_store_1.entity_id INNER JOIN
cataloginventory_stock_item on cataloginventory_stock_item.product_id=catalog_category_product_index.product_id
where cataloginventory_stock_item.is_in_stock=1 and catalog_category_product_index.category_id=";
$subCatqueryId = str_replace('category-node-', '', $child->getId());
$prodCollection = $mageconnection->fetchAll("$query'{$subCatqueryId}'");
if($prodCollection[0]["subcount"] > 0) {
$child->setLevel($childLevel);
$child->setIsFirst($counter == 1);
// these are the existing code ..
...
...
$counter++;
}
}
It is very fast and secure way to control product count.
Related
I need to auto-generate different aliases from two articles with the same title in Joomla 3.3. The user will add articles in the front end. I found this code:
<?php
defined( '_JEXEC' ) or die;
class plgContentRandom_Alias extends JPlugin
{
function onContentBeforeSave($context, &$article, $isNew) {
if(!$isNew){
return;
}
$alias = $article->alias;
$n = substr( "abcdefghijklmnopqrstuvwxyz" ,mt_rand( 0 ,25 ) ,1 ) .substr( md5( time( ) ) ,1 );
$table = JTable::getInstance('content');
while ($table->load(array('alias' => $alias))) {
$new_alias = $alias . $n;
}
$article->alias = $new_alias;
return true;
}
}
?>
, and made a plugin for Joomla, but the plugin not working in Joomla 3.3.
Any suggestions?
You can write your own plugin using the following code, although this functionality should be already part of joomla core.
I've used this because of Seblod error when using its insert content form.
Files:
Joomla installer descriptor:
uniqueAliasGenerator.xml
<?xml version="1.0" encoding="utf-8"?>
<extension version="3.1" type="plugin" group="content" method="upgrade">
<name>Content - Unique alias generator</name>
<author>McGiogen</author>
<creationDate>May 2015</creationDate>
<copyright></copyright>
<license></license>
<authorEmail>mcgiogen#hotmail.it</authorEmail>
<authorUrl>www.joomla.org</authorUrl>
<version>1.0</version>
<description>
Automatic generator of unique alias.
At save time it append "-X" (where X is a numeric identifier)
if article alias is already in database.
</description>
<files>
<filename plugin="uniqueAliasGenerator">uniqueAliasGenerator.php</filename>
<filename>index.html</filename>
</files>
<config>
</config>
</extension>
Plugin code:
uniqueAliasGenerator.php
<?php
// no direct access
defined( '_JEXEC' ) or die( 'Restricted access' );
class plgContentUniqueAliasGenerator extends JPlugin
{
/**
* Alias check and generation before save content method.
* Content is passed by reference. Method is called before the content is saved.
*
* #param string $context The context of the content passed to the plugin (added in 1.6).
* #param object $article A JTableContent object.
* #param bool $isNew If the content is just about to be created.
*
* #return void
*/
public function onContentBeforeSave($context, $article, $isNew)
{
if ($context == 'com_content.article' && $article->alias && $isNew) {
$oldAlias = $article->alias;
$categoryId = $article->catid; //An alias must be unique only in its category
$article->alias = $this->getUniqueAlias($oldAlias, $categoryId);
}
return true;
}
/**
* Find unique Alias name if current doesn't exist.
* #param string $alias Alias of the article
* #param string $catId Id of article's category
*
* #return string Return the unique alias value.
*/
protected function getUniqueAlias($alias, $catId)
{
$alias_ini = $alias;
for ($i = 2; $this->isAliasExist($alias, $catId); $i++) {
$alias = $alias_ini . '-' . $i;
}
return $alias;
}
/**
* Check the 'alias' in the database.
*
* #return boolean If found return true else false.
*/
protected function isAliasExist($alias, $catId)
{
$db = JFactory::getDBO();
$query = $db->getQuery(true);
$query
->select('COUNT(*)')
->from($db->quoteName('#__content')) //Articles table
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('catid') . ' = ' . $db->quote($catId)); //Category ID
$db->setQuery($query);
return ($db->loadResult() ? true : false);
}
}
?>
index.html
<!DOCTYPE html><title></title>
How to use:
Create files with the same names, put them in a folder called "uniqueAliasGenerator", zip in "uniqueAliasGenerator.zip", upload and install on your joomla.
Compatible with Joomla 3.x, tested on Joomla 3.4.1
Update 11 Nov 2017
Added check of $isNew. Thanks #robert-drygas.
In McGiogen code the line
if ($context == 'com_content.article' && $article->alias) {
can be write as
if ($context == 'com_content.article' && $article->alias && $isNew) {
so unique alias will be ganerated only for the new articles (without altered existing aliases when editing old articles).
I am using Zend2 and i am creating menu items dynamicly.
This is the function i am using:
public static function getAdminMenu() {
$config = \App\Application::getInstance()->getConfig();
$menuItems = $config['menu_items'];
$html = '<ul>';
foreach ($menuItems as $section => $menuItem) {
$html .= '<div class="user-menu-section">' . $section . '</div>';
foreach ($menuItem as $subSection => $params) {
$html .= '<li>' . $subSection . '</li>';
}
}
$html .= '</ul>';
return $html;
}
How can i create divs with different class user-menu-section for each menu item. It should be something like 'user-menu-section1', 'user-menu-section2'...
Or maybe better to use something like this:
<div class="' . $section . '">;
but in this case, if $section is a string of two words i would need '-' in between words and both words small caps, if it is possible.
Well, just use your $section and modify this. Using ZF2, you'd use the Filter CamelCaseToDash
$filter = new \Zend\Filter\Word\CamelCaseToDash();
$classFiltered = strtolower($filter->filter($class);)
Now you can use $classFiltered for your CSS-Class assignment.
And since you've mentioned both frameworks in your tags. In case you are using ZF2, that code is horrible :D You should create yourself a ViewHelper that renders the Menu. Evan Coury has written a very easy introduction on how to do that.
Aside from that, you don't need a static call to some Application::getInstance(). If you want to gain access to the config you do this via the ServiceLocator. In a Controller this would look like this:
$config = $this->getServiceLocator()->get('config');
If you need the config in another class outside of the Controller, you create the class from the ServiceLocator and inject the config into this class.
What is the correct way to print a drupal block assigned to a region on a node template ex(node.tpl.php)
You can use module_invoke($module, $hook) to do so.
$module: The name of the module (without the .module extension).
$hook: The name of the hook to invoke.
Code snippet:
$block = module_invoke('module_name', 'block_view', 'block_delta');
print $block['content'];
OR
use the following code snippet:
function block_print_html($module, $block_id)
{
$block_html = "";
$block = block_load($module, $block_id);
$block_content = _block_render_blocks(array($block));
$block_build = _block_get_renderable_array($block_content);
return = drupal_render($build);
}
I am trying to implement embedded widget. Administrators will be able to configure this widget and embed it inside WYSIWYG editor. Two of the many configuration options are list of products that should show up on frontend and list of categories.
I want to allow this selection with "adminhtml/catalog_product_widget_chooser" and "adminhtml/catalog_category_widget_chooser". I tried to implement these widgets with sparse documentation available on the web but all I managed to accomplish is implementation for selecting one product or selecting one category. I need multiselect behavior.
As far as I can see no multiselection possibility is allowed by the current implementation. I checked code for both classes and grid.phtml template and it seams it is badly written and not extensible beyond current intention of use. For example this is how you would suppose to initialize helper block for a widget parameter to allow multiple select:
<helper_block>
<type>adminhtml/catalog_product_widget_chooser</type>
<data>
<button translate="open">
<open>Select Products...</open>
</button>
<use_massaction>1</use_massaction>
</data>
</helper_block>
But product chooser is hard coded for use without mass actions with this part of the code:
public function prepareElementHtml(Varien_Data_Form_Element_Abstract $element)
{
$uniqId = Mage::helper('core')->uniqHash($element->getId());
$sourceUrl = $this->getUrl('*/catalog_product_widget/chooser', array(
'uniq_id' => $uniqId,
'use_massaction' => false,
));
...
And grid.phtml template that is supposed to have some kind of button to confirm multiple selection is just showing "Search" and "Reset filter" buttons. And there is no handling of adding another button. For example here is the default code responsible for printing button html:
public function getMainButtonsHtml()
{
$html = '';
if($this->getFilterVisibility()){
$html.= $this->getResetFilterButtonHtml();
$html.= $this->getSearchButtonHtml();
}
return $html;
}
Only these two buttons are going to be printed by default.
So I started my own implementation based on two implementations mentioned above and it is getting ugly and could end up as an unmaintainable mess of copy-pasta. And I work by principle that if things start to look ugly then I am doing something wrong.
So is there a straightforward way to implement multiple product and multiple category selection on widget configuration screen by using grid widget?
I've found a quick way to get category multiselects on widget parameters using a source model based on adminhtml/system_config_source_category. I've removed the root-level filter and added indentation for subcategories.
widget.xml:
<widgets>
<my_widget type="mymodule/block" translate="name" module="mymodule">
<name>Widget with Multiselect Categories</name>
<parameters>
<category_ids translate="label description">
<visible>1</visible>
<required>1</required>
<label>Categories</label>
<type>multiselect</type>
<source_model>mymodule/system_config_source_category</source_model>
</category_ids>
</parameters>
</my_widget>
</widgets>
The source model:
class Mynamespace_Mymodule_Model_System_Config_Source_Category
{
public function toOptionArray()
{
$collection = Mage::getResourceModel('catalog/category_collection');
$collection->addAttributeToSelect('name')
->addFieldToFilter('path', array('neq' => '1'))
->load();
$options = array();
foreach ($collection as $category) {
$depth = count(explode('/', $category->getPath())) - 2;
$indent = str_repeat('-', max($depth * 2, 0));
$options[] = array(
'label' => $indent . $category->getName(),
'value' => $category->getId()
);
}
return $options;
}
}
The result:
Source: http://www.magentocommerce.com/knowledge-base/entry/tutorial-creating-a-magento-widget-part-2
I have added an answer to this question.
Implement multiple product chooser widget Magento
I have checked the Module that is under https://github.com/dio5/magento-multiproducts-widget.
Use the FORK option instead of Download ZIP.
It works and gives us the exact results i.e Multiple Products Selection in WIDGET. If there are any errors do let me know.
Let me know if that works for you.
Thanks!
[Editing my previous comment, upon request of code here directly]
/Namespace/Modulename/etc/widget.xml
<widgets>
<catalog_product_multiproducts type="namespace_modulename/widget_catalog_product_multiproducts" translate="name description" module="namespace_modulename">
<name>Catalog Multiple Products Widget</name>
<description>Select multiple products for display</description>
<parameters>
<title translate="label">
<visible>1</visible>
<label>Title</label>
<type>text</type>
</title>
<products_count translate="label">
<visible>1</visible>
<required>1</required>
<label>No of Products</label>
<type>text</type>
</products_count>
<ids translate="label">
<visible>1</visible>
<required>1</required>
<label>Products</label>
<type>label</type>
<helper_block>
<type>namespace_modulename/adminhtml_catalog_product_widget_multiproducts_chooser</type>
<data>
<button translate="open">
<open>Select Products...</open>
</button>
</data>
</helper_block>
<sort_order>10</sort_order>
</ids>
<template translate="label description">
<required>1</required>
<visible>1</visible>
<label>Product Carousel Template</label>
<type>text</type>
<value>catalog/product/widget/products_carousel.phtml</value>
<values>
<default translate="label"> <value>catalog/product/widget/products_carousel.phtml</value>
<label>New Products Grid Template</label>
</default>
<list translate="label">
<value>catalog/product/widget/new/content/new_list.phtml</value>
<label>New Products List Template</label>
</list>
</values>
<description>Template path cannot be changed/updated</description>
</template>
</parameters>
</catalog_product_multiproducts>
</widgets>
/NameSpace/ModuleName/Block/Adminhtml/Catalog/Product/MultiProducts/Chooser.php
This function will call the DOCHOOSE() function which will help "Choose" the checked/selected products.
/**
* prepare layout for products grid
*
* #return type Mage_Adminhtml_Block_Catalog_Product_Widget_Chooser
*/
protected function _prepareLayout()
{
$this->setChild('choose_button', $this->getLayout()->createBlock('adminhtml/widget_button')
->setData(array(
'label' => Mage::helper('adminhtml')->__('Choose Selected Products'),
'onclick' => $this->getJsObjectName() . '.doChoose()'
))
);
return parent::_prepareLayout();
}
The below function needs to be used to prepare the product element's HTML, in the format {1}{2}
/**
* Prepare chooser element HTML
*
* #param Varien_Data_Form_Element_Abstract $element Form Element
* #return Varien_Data_Form_Element_Abstract
*/
public function prepareElementHtml(Varien_Data_Form_Element_Abstract $element)
{
$uniqueId = Mage::helper('core')->uniqHash($element->getId());
$sourceUrl = $this->getUrl('*/multiproducts/chooser', array(
'uniq_id' => $uniqueId,
'use_massaction' => true,
));
$chooser = $this->getLayout()->createBlock('widget/adminhtml_widget_chooser')
->setElement($element)
->setTranslationHelper($this->getTranslationHelper())
->setConfig($this->getConfig())
->setFieldsetId($this->getFieldsetId())
->setSourceUrl($sourceUrl)
->setUniqId($uniqueId);
if ($element->getValue())
{
$label = "";
$ids = explode('}{', $element->getValue());
$cleanIds = array();
foreach ($ids as $id)
{
$id = str_replace('{', '', $id);
$id = str_replace('}', '', $id);
$cleanIds[] = $id;
}
$products = $this->_getProductsByIDs($cleanIds);
if ($products)
{
$label .= '<ul>';
foreach ($products as $product)
{
$label .= '<li>' . $product->getName() . '</li>';
}
$label .= '</ul>';
$chooser->setLabel($label);
}
}
$element->setData('after_element_html', $chooser->toHtml());
return $element;
}
JS for checkbox checked/unchecked
/**
* Checkbox Check JS Callback
*
* #return string
*/
public function getCheckboxCheckCallback()
{
if ($this->getUseMassaction())
{
return "function (grid, element) {
$(grid.containerId).fire('product:changed', {element: element});
}";
}
}
JS for Row/Product Clicked/Checked/Selected
/**
* Grid Row JS Callback
*
* #return string
*/
public function getRowClickCallback()
{
if (!$this->getUseMassaction())
{
$chooserJsObject = $this->getId();
return '
function (grid, event) {
var trElement = Event.findElement(event, "tr");
var productId = trElement.down("td").innerHTML;
var productName = trElement.down("td").next().next().innerHTML;
var optionLabel = productName;
var optionValue = "product/" + productId.replace(/^\s+|\s+$/g,"");
if (grid.categoryId) {
optionValue += "/" + grid.categoryId;
}
if (grid.categoryName) {
optionLabel = grid.categoryName + " / " + optionLabel;
}
' . $chooserJsObject . '.setElementValue(optionValue);
' . $chooserJsObject . '.setElementLabel(optionLabel);
' . $chooserJsObject . '.close();
}
';
}
}
JS code, if user is interested in selecting products from specific category.
/**
* Category Tree node onClick listener js function
*
* #return string
*/
public function getCategoryClickListenerJs()
{
$js = '
function (node, e) {
{jsObject}.addVarToUrl("category_id", node.attributes.id);
{jsObject}.reload({jsObject}.url);
{jsObject}.categoryId = node.attributes.id != "none" ? node.attributes.id : false;
{jsObject}.categoryName = node.attributes.id != "none" ? node.text : false;
}
';
$js = str_replace('{jsObject}', $this->getJsObjectName(), $js);
return $js;
}
Additional JS for preparing the POST element with product ids.
/**
* return additional JS for controls
*
* #return JS
*/
public function getAdditionalJavascript()
{
$chooserJsObject = $this->getId();
$js = '
{jsObject}.initChecked = function() {
$$("#' . $chooserJsObject . '_table tbody input:checkbox").each(function(element, i) {
var values = ' . $chooserJsObject . '.getElementValue();
var capture = values.replace("{"+element.value+"}", "match");
var searchValue = "match";
if(capture.search(searchValue) != -1)
{
element.checked = true;
}
});
}
{jsObject}.initChecked();
var values = ' . $chooserJsObject . '.getElementValue();
$("' . $chooserJsObject . '").insert({bottom: "<div class=\"filter\"><input type=\"hidden\" value=\"+values+\" name=\"selected_products\" /></div>"});
$$("#' . $chooserJsObject . '_table tbody input:checkbox").invoke("observe", "change", function(event) {
var element = Event.element(event);
var label = element.up("td").next().next().next().innerHTML;
label = label.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
if(element.checked)
{
{jsObject}.addValue(element.value);
{jsObject}.addLabel(label);
} else {
{jsObject}.removeValue(element.value);
{jsObject}.removeLabel(label);
}
});
{jsObject}.removeValue = function(value) {
var currentValue = ' . $chooserJsObject . '.getElementValue();
currentValue = currentValue.replace("{"+value+"}", "");
' . $chooserJsObject . '.setElementValue(currentValue);
}
{jsObject}.addValue = function(value) {
var currentValue = ' . $chooserJsObject . '.getElementValue();
currentValue = currentValue.replace("{"+value+"}", "");
currentValue = currentValue + "{"+value+"}";
' . $chooserJsObject . '.setElementValue(currentValue);
}
{jsObject}.removeLabel = function(label) {
var currentLabel = ' . $chooserJsObject . '.getElementLabelText();
currentLabel = currentLabel.replace("<li>"+label+"</li>", "");
' . $chooserJsObject . '.setElementLabel(currentLabel);
}
{jsObject}.addLabel = function(label) {
var currentLabel = ' . $chooserJsObject . '.getElementLabelText();
if(currentLabel.search("ul") != -1)
{
currentLabel = currentLabel.replace("</ul>", "");
currentLabel = currentLabel.replace("<li>"+label+"</li>", "");
} else {
currentLabel = "<ul>";
}
currentLabel = currentLabel +"<li>"+label+"</li></ul>";
' . $chooserJsObject . '.setElementLabel(currentLabel);
}
{jsObject}.doChoose = function(node,e) {
' . $chooserJsObject . '.close();
}
';
$js = str_replace('{jsObject}', $this->getJsObjectName(), $js);
return $js;
}
The above are the major functions which will help you select multiple products from the GRID within a pop up.
There is a bit more to the code which can be checked here: https://github.com/dio5/magento-multiproducts-widget
Steps:
Navigate to a CMS Page in ADMIN panel
Click on "Insert Widget" in WYSIWYG editor
Select Widget Type - Catalog Multiple Products Widget
Enter Title, Products Count
Choose a template (One can add as many templates as required as an option)
Click on "Select Products" button
Select products from the GRID
Click on "Choose Selected Products" button
Hope this helps someone!
Happy Coding...
It looks as though you're not the first to go down the path of developing your own implementation for this.
David Manners appears to have tackled the same issue, with his Manners_Widgets.
Features of the Manners_Widgets Extension:
Multiple select for products and categories
I've not had contact with David, nor have I used this solution, so can't comment on quality (or completeness) of this code... but if you haven't already seen this solution, it might save you some time (or at worst, give you a point of contact for collaboration on this issue).
Hope this helps you, good luck with it!
Here's a quick fix: don't use the product widget chooser, but use a textfield instead that allows for comma separated SKUs.
Then in your code explode the skus and get the products by sku. Return that to your template. Much easier :)
Try to https://github.com/dio5/magento-multiproducts-widget.
It seems very useful.
My client is requesting that each simple product within a bundled product they are selling (Clothing Top and Bottom) be added as a separate line item in the cart whenever a user adds it. Can anyone direct me in how to accomplish this? I am fairly good with MVC and the Zend Framework, but I need a bit of help finding the exact files that control adding bundled products to the cart, or an alternate method for getting these items added separately. Please assume that the only possible product type for this clothing is the Bundled product type.
You will need an observer:
<checkout_cart_product_add_after>
<observers>
<reporting>
<type>singleton</type>
<class>Yourcompany_yourmodelname_Model_Observer</class>
<method>onCartProductAdd</method>
</reporting>
</observers>
</checkout_cart_product_add_after>
Then do the observer:
class ourcompany_yourmodelname_Model_Observer extends Mage_Core_Model_Abstract
{
/**
* Binds to the checkout_cart_product_add_after Event and passes control to the helper function to process the quote
*
* #param Varien_Event_Observer $observer
* #return void
*/
public function onCartProductAdd($observer){
$product = $observer->getProduct();
$isProductBundle = ($product->getTypeId() == 'bundle');
$items_to_add = array();
$oTypeInstance = $oProduct->getTypeInstance(true);
$aSelections = $oTypeInstance->getSelectionsCollection($aOptionIds, $product );
$aOptions = $oTypeInstance->getOptionsByIds($aOptionIds, $product);
$bundleOptions = $aOptions->appendSelections($aSelections, true);
foreach ($bundleOptions as $bundleOption) {
if ($bundleOption->getSelections()) {
$bundleSelections = $bundleOption->getSelections();
foreach ($bundleSelections as $bundleSelection) {
$items_to_add[] = $bundleSelection.getID();
}
}
}
insertExtractedProducts($items_to_add);
}
/**
* Add extracted products into quote
*
* #param array $items_to_add
*/
public function insertExtractedProducts($items_to_add){
/**#var $cart Mage_Checkout_Model_Cart**/
$cart = Mage::helper('checkout/cart')->getCart();
$ids_to_add = array();
foreach($items_to_add as $item_to_be_added){
$ids_to_add[] = $item_to_be_added->getProductId();
}
$cart->addProductsByIDs($ids_to_add);
$cart->save();
Mage::getSingleton('checkout/session')->setCartWasUpdated(true);
}
}
Just a simple sample, but it might help.
Bundled products can be complicated to understand, when working with it via code:
Here is a sample image:
Each Bundled product, have one to many options, which in the end will be links to products to be added to the bundle in the Shopping Cart.
Each Option consists of one to many Selections, which will be the linked products that will end up in the Shopping cart, under this bundled product. One Selection, can typically be set as the default, and will already be selected on the product page. More information can be found at this link on how to create and configure bundled products, because in this document, we will only discuss the programming side of it.
The bundled product’s display page, will look like this:
It could look like this in the shopping cart, once you clicked on “Add to Cart”:
Bundle Sample
Sample Shampoo
1 x Moisturiser-125ml $29.95
Default Conditioner
1 x Moisturiser-60g $99.95
When interrogating this product through code, you load it like any normal product:
$oProduct->load($vProductId);
Once it is loaded, you need to get a product type instance, so that you can load the options for this product type.
$oTypeInstance = $oProduct->getTypeInstance(true);
Now we can get the list of option ID’s for this product in this manner:
$oTypeInstance = $oProduct->getTypeInstance(true);
To interrogate the Selections, we will add the list of options to a collection, then get the Options collection, as well as their respective Selections:
$aSelections = $oTypeInstance->getSelectionsCollection($aOptionIds, $oProduct );
$aOptions = $oTypeInstance->getOptionsByIds($aOptionIds, $oProduct);
$bundleOptions = $aOptions->appendSelections($aSelections, true);
Now we have a Collection of Options, and each Option will have a collection of Selections. We can now loop through the options, and look at their Selctions.
foreach ($bundleOptions as $bundleOption) {
if ($bundleOption->getSelections()) {
$bundleSelections = $bundleOption->getSelections();
foreach ($bundleSelections as $bundleSelection) {
// get some data here
$vName = $bundleOption->getTitle();
}
}
}
To get a list of the Required Selection Products for the Bundled product, you can use the following code:
$requiredChildren = $this->getChildrenIds($product->getId(),$required=true);
You can then loop through the array of ID’s and load the products by their ID’s to get more information regarding those products.
Bundled products in the shopping cart
In order to loop through the selected options of a Bundled product in the shopping card, you can use something like this:
/**
* #var Mage_Bundle_Model_Product_Type
*/
$typeInstance = $this->getProduct()->getTypeInstance(true);
// get bundle options
$optionsQuoteItemOption = $this->getItem()->getOptionByCode('bundle_option_ids');
$bundleOptionsIds = unserialize($optionsQuoteItemOption->getValue());
if ($bundleOptionsIds) {
/**
* #var Mage_Bundle_Model_Mysql4_Option_Collection
*/
$optionsCollection = $typeInstance->getOptionsByIds($bundleOptionsIds, $this->getProduct());
// get and add bundle selections collection
$selectionsQuoteItemOption = $this->getItem()->getOptionByCode('bundle_selection_ids');
$selectionsCollection = $typeInstance->getSelectionsByIds(
unserialize($selectionsQuoteItemOption->getValue()),
$this->getProduct()
);
$bundleOptions = $optionsCollection->appendSelections($selectionsCollection, true);
foreach ($bundleOptions as $bundleOption) {
if ($bundleOption->getSelections()) {
$label = $bundleOption->getTitle()
$bundleSelections = $bundleOption->getSelections();
foreach ($bundleSelections as $bundleSelection) {
$sName = $bundleSelection->getName();
}
// some more code here to do stuff
}
}
}
This code gets the Options from the Quote Item Bundled Product, and then gets the Options for that product in a collection, and then finds the “Selected” Option Selection.
hth,
Shaun
Here's how you can do this in Magento 2.3 via an "around" plugin (interceptor) for the Magento\Checkout\Model\Cart->addProduct() method.
The addProduct() method is called when the customer adds a product to the cart. By adding code to this method via a plugin, you can alter the way the bundle product is added to the cart.
Define the plugin in Vendor/Module/etc/frontend/di.xml:
<?xml version="1.0"?>
<!--
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="\Magento\Checkout\Model\Cart">
<plugin name="add-bundle-products-separate" type="Vendor\Module\Plugin\Checkout\Model\CartPlugin" sortOrder="1"/>
</type>
</config>
This is what tells Magento a plugin exists for this particular method.
Reference:
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html
Create the plugin's class.
In Vendor/Module/Plugin/Checkout/Model/CartPlugin.php:
<?php
namespace Vendor\Module\Plugin\Checkout\Model;
use \Magento\Catalog\Model\Product;
use \Magento\Framework\DataObject;
use \Magento\Checkout\Model\Cart;
/**
* Class CartPlugin
*
* #package Ppwd\CrossSell\Plugin\Checkout\Model
*
*/
class CartPlugin {
/**
* #param Cart $subject
* #param \Closure $proceed
* #param Product $productInfo
* #param DataObject|int|array $requestInfo
* #return Cart $subject
* #SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function aroundAddProduct(
$subject,
$proceed,
$productInfo,
$requestInfo = null
) {
// Detect if we are adding a bundle product to cart
if (!is_numeric($productInfo) && $productInfo->getTypeId() == 'bundle') {
$buyRequest = new DataObject($requestInfo);
// List of products selected as part of the bundle
$cartCandidates = $productInfo->getTypeInstance()->prepareForCartAdvanced($buyRequest, $productInfo);
$productIds = [];
// Add each item in bundle as if it were separately added to cart
/** #var Product $cartCandidate */
foreach ($cartCandidates as $cartCandidate) {
if ($cartCandidate->getTypeId() != 'bundle') {
for ($i = 0; $i < $cartCandidate->getCartQty(); $i++) {
$productIds[] = $cartCandidate->getId();
}
}
}
$subject->addProductsByIds($productIds);
return $subject;
}
// Return original result from addProduct() as if plugin didn't exist
$result = $proceed($productInfo, $requestInfo);
return $result;
}
}
When done, if you add the bundle to the cart the line items will appear separately instead of grouped together like a bundle product normally is.
I did this to solve the customer request to add bundle content as separate items in the cart. Just replace
$cart->addProduct($product, $params)
with
if ($product->getTypeId() == 'bundle') {
$request = new Varien_Object($params);
$cartCandidates = $product->getTypeInstance(true)->prepareForCartAdvanced($request, $product, Mage_Catalog_Model_Product_Type_Abstract::PROCESS_MODE_FULL);
$idstoadd = array();
foreach ($cartCandidates as $cartCandidate) {
if ($cartCandidate->getTypeId() == 'simple') {
for ($i = 0; $i < $cartCandidate->getCartQty(); $i++) {
$idstoadd[] = $cartCandidate->getId();
}
}
}
$cart->addProductsByIds($idstoadd);
} else {
$cart->addProduct($product, $params);
}
in the file cartController.