5

In Magento 2.1.1 CE visual swatches works with select-type attributes out of box, but not witth multiselect.

Is there any simple way to make it work also with multiselect-type?

Can it be achieved by some configuration or small modification of core files? Or is it more complicated?

5
  • 1
    As a workaround, i map the select swatch text value with the multiselect value. Dec 16, 2016 at 20:37
  • thats a good point, but are they editable from admin html? Dec 19, 2016 at 21:57
  • I edit them from backoffice and use cutom code for FO Dec 20, 2016 at 14:09
  • @frank, can you please add your changes here as solution?
    – mjdevloper
    May 25, 2018 at 6:41
  • I also want to make multiselect in visual swatch type attribute, Is there any solution for this? Thank you in advance. Nov 19, 2018 at 9:16

1 Answer 1

5

Let me preface this by saying the solution below is far more invasive than I initially thought it would have needed to be. Most notably the total overwrite of Magento_Swatches/js/product-attributes. The code below was testing and functioned on Magento 2.2.4.

Model/Swatch.php
This class just holds the constant values for the new frontend input types

<?php

namespace Pmclain\MultiSelectSwatch\Model;

class Swatch
{
    const SWATCH_MULTISELECT_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT = 'swatch_multiselect_text';
    const SWATCH_MULTISELECT_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT = 'swatch_multiselect_visual';
}

Observer/AddMultiSelectSwatchType.php
The observer will add the new frontend input type to the product attribute edit form.

<?php

namespace Pmclain\MultiSelectSwatch\Observer;

use Magento\Framework\Module\Manager;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;

class AddMultiSelectSwatchType implements ObserverInterface
{
    protected $moduleManager;

    public function __construct(Manager $moduleManager)
    {
        $this->moduleManager = $moduleManager;
    }

    public function execute(EventObserver $observer)
    {
        if (!$this->moduleManager->isOutputEnabled('Pmclain_MultiSelectSwatch')) {
            return;
        }

        $response = $observer->getEvent()->getResponse();
        $types = $response->getTypes();
        $types[] = [
            'value' => \Pmclain\MultiSelectSwatch\Model\Swatch::SWATCH_MULTISELECT_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT,
            'label' => __('Visual Swatch - Multiselect'),
            'hide_fields' => [
                'is_unique',
                'is_required',
                'frontend_class',
                '_scope',
                '_default_value',
            ],
        ];
        $types[] = [
            'value' => \Pmclain\MultiSelectSwatch\Model\Swatch::SWATCH_MULTISELECT_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT,
            'label' => __('Text Swatch - Multiselect'),
            'hide_fields' => [
                'is_unique',
                'is_required',
                'frontend_class',
                '_scope',
                '_default_value',
            ],
        ];

        $response->setTypes($types);
    }
}

etc/adminhtml/events.xml
Register the observer created above.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="adminhtml_product_attribute_types">
        <observer name="multiselect_swatch_attribute_type" instance="Pmclain\MultiSelectSwatch\Observer\AddMultiSelectSwatchType" />
    </event>
</config>

view/adminhtml/web/js/product-attributes.js
Add the frontend input values to the product attribute javascript. I couldn't find a clean way for doing this via a mixin, so this is a full rewrite of the core javascript.

define([
    'jquery',
    'Magento_Ui/js/modal/alert',
    'Magento_Ui/js/modal/prompt',
    'uiRegistry',
    'collapsable'
], function ($, alert, prompt, rg) {
    'use strict';

    return function (optionConfig) {
        var swatchProductAttributes = {
                frontendInput: $('#frontend_input'),
                isFilterable: $('#is_filterable'),
                isFilterableInSearch: $('#is_filterable_in_search'),
                backendType: $('#backend_type'),
                usedForSortBy: $('#used_for_sort_by'),
                frontendClass: $('#frontend_class'),
                isWysiwygEnabled: $('#is_wysiwyg_enabled'),
                isHtmlAllowedOnFront: $('#is_html_allowed_on_front'),
                isRequired: $('#is_required'),
                isUnique: $('#is_unique'),
                defaultValueText: $('#default_value_text'),
                defaultValueTextarea: $('#default_value_textarea'),
                defaultValueDate: $('#default_value_date'),
                defaultValueYesno: $('#default_value_yesno'),
                isGlobal: $('#is_global'),
                useProductImageForSwatch: $('#use_product_image_for_swatch'),
                updateProductPreviewImage: $('#update_product_preview_image'),
                usedInProductListing: $('#used_in_product_listing'),
                isVisibleOnFront: $('#is_visible_on_front'),
                position: $('#position'),
                attrTabsFront: $('#product_attribute_tabs_front'),

                get tabsFront() {
                    return this.attrTabsFront.length ? this.attrTabsFront.closest('li') : $('#front_fieldset-wrapper');
                },
                selectFields: ['select', 'multiselect', 'price', 'swatch_text', 'swatch_visual', 'swatch_multiselect_text', 'swatch_multiselect_visual'],

                toggleApplyVisibility: function (select) {
                    if ($(select).val() === 1) {
                        $(select).next('select').removeClass('no-display');
                        $(select).next('select').removeClass('ignore-validate');
                    } else {
                        $(select).next('select').addClass('no-display');
                        $(select).next('select').addClass('ignore-validate');
                        $(select).next('select option:selected').each(function () {
                            this.selected = false;
                        });
                    }
                },

                checkOptionsPanelVisibility: function () {
                    var selectOptionsPanel = $('#manage-options-panel'),
                        visualOptionsPanel = $('#swatch-visual-options-panel'),
                        textOptionsPanel = $('#swatch-text-options-panel');

                    this._hidePanel(selectOptionsPanel);
                    this._hidePanel(visualOptionsPanel);
                    this._hidePanel(textOptionsPanel);

                    switch (this.frontendInput.val()) {
                        case 'swatch_visual':
                        case 'swatch_multiselect_visual':
                            this._showPanel(visualOptionsPanel);
                            break;

                        case 'swatch_text':
                        case 'swatch_multiselect_text':
                            this._showPanel(textOptionsPanel);
                            break;

                        case 'select':
                        case 'multiselect':
                            this._showPanel(selectOptionsPanel);
                            break;
                    }
                },

                bindAttributeInputType: function () {
                    this.checkOptionsPanelVisibility();
                    this.switchDefaultValueField();

                    if (!~$.inArray(this.frontendInput.val(), this.selectFields)) {
                        // not in array
                        this.isFilterable.selectedIndex = 0;
                        this._disable(this.isFilterable);
                        this._disable(this.isFilterableInSearch);
                    } else {
                        // in array
                        this._enable(this.isFilterable);
                        this._enable(this.isFilterableInSearch);
                        this.backendType.val('int');
                    }

                    if (this.frontendInput.val() === 'multiselect' ||
                        this.frontendInput.val() === 'gallery' ||
                        this.frontendInput.val() === 'textarea'
                    ) {
                        this._disable(this.usedForSortBy);
                    } else {
                        this._enable(this.usedForSortBy);
                    }

                    if (this.frontendInput.val() === 'swatch_text') {
                        $('.swatch-text-field-0').addClass('required-option');
                    } else {
                        $('.swatch-text-field-0').removeClass('required-option');
                    }

                    this.setRowVisibility(this.isWysiwygEnabled, false);
                    this.setRowVisibility(this.isHtmlAllowedOnFront, false);

                    switch (this.frontendInput.val()) {
                        case 'textarea':
                            this.setRowVisibility(this.isWysiwygEnabled, true);

                            if (this.isWysiwygEnabled.val() === '0') {
                                this._enable(this.isHtmlAllowedOnFront);
                            }
                            this.frontendClass.val('');
                            this._disable(this.frontendClass);
                            break;

                        case 'text':
                            this.setRowVisibility(this.isHtmlAllowedOnFront, true);
                            this._enable(this.frontendClass);
                            break;

                        case 'select':
                        case 'multiselect':
                            this.setRowVisibility(this.isHtmlAllowedOnFront, true);
                            this.frontendClass.val('');
                            this._disable(this.frontendClass);
                            break;
                        default:
                            this.frontendClass.val('');
                            this._disable(this.frontendClass);
                    }

                    this.switchIsFilterable();
                },

                switchIsFilterable: function () {
                    if (this.isFilterable.selectedIndex === 0) {
                        this._disable(this.position);
                    } else {
                        this._enable(this.position);
                    }
                },

                /**
                 * @this {swatchProductAttributes}
                 */
                switchDefaultValueField: function () {
                    var currentValue = this.frontendInput.val(),
                        defaultValueTextVisibility = false,
                        defaultValueTextareaVisibility = false,
                        defaultValueDateVisibility = false,
                        defaultValueYesnoVisibility = false,
                        scopeVisibility = true,
                        useProductImageForSwatch = false,
                        defaultValueUpdateImage = false,
                        optionDefaultInputType = '',
                        isFrontTabHidden = false,
                        thing = this;

                    if (!this.frontendInput.length) {
                        return;
                    }

                    switch (currentValue) {
                        case 'select':
                            optionDefaultInputType = 'radio';
                            break;

                        case 'multiselect':
                            optionDefaultInputType = 'checkbox';
                            break;

                        case 'date':
                            defaultValueDateVisibility = true;
                            break;

                        case 'boolean':
                            defaultValueYesnoVisibility = true;
                            break;

                        case 'textarea':
                            defaultValueTextareaVisibility = true;
                            break;

                        case 'media_image':
                            defaultValueTextVisibility = false;
                            break;

                        case 'price':
                            scopeVisibility = false;
                            break;

                        case 'swatch_visual':
                        case 'swatch_multiselect_visual':
                            useProductImageForSwatch = true;
                            defaultValueUpdateImage = true;
                            defaultValueTextVisibility = false;
                            break;

                        case 'swatch_text':
                        case 'swatch_multiselect_text':
                            useProductImageForSwatch = false;
                            defaultValueUpdateImage = true;
                            defaultValueTextVisibility = false;
                            break;
                        default:
                            defaultValueTextVisibility = true;
                            break;
                    }

                    delete optionConfig.hiddenFields['swatch_visual'];
                    delete optionConfig.hiddenFields['swatch_text'];

                    if (currentValue === 'media_image') {
                        this.tabsFront.hide();
                        this.setRowVisibility(this.isRequired, false);
                        this.setRowVisibility(this.isUnique, false);
                        this.setRowVisibility(this.frontendClass, false);
                    } else if (optionConfig.hiddenFields[currentValue]) {
                        $.each(optionConfig.hiddenFields[currentValue], function (key, option) {
                            switch (option) {
                                case '_front_fieldset':
                                    thing.tabsFront.hide();
                                    isFrontTabHidden = true;
                                    break;

                                case '_default_value':
                                    defaultValueTextVisibility = false;
                                    defaultValueTextareaVisibility = false;
                                    defaultValueDateVisibility = false;
                                    defaultValueYesnoVisibility = false;
                                    break;

                                case '_scope':
                                    scopeVisibility = false;
                                    break;
                                default:
                                    thing.setRowVisibility($('#' + option), false);
                            }
                        });

                        if (!isFrontTabHidden) {
                            thing.tabsFront.show();
                        }

                    } else {
                        this.tabsFront.show();
                        this.showDefaultRows();
                    }

                    this.setRowVisibility(this.defaultValueText, defaultValueTextVisibility);
                    this.setRowVisibility(this.defaultValueTextarea, defaultValueTextareaVisibility);
                    this.setRowVisibility(this.defaultValueDate, defaultValueDateVisibility);
                    this.setRowVisibility(this.defaultValueYesno, defaultValueYesnoVisibility);
                    this.setRowVisibility(this.isGlobal, scopeVisibility);

                    /* swatch attributes */
                    this.setRowVisibility(this.useProductImageForSwatch, useProductImageForSwatch);
                    this.setRowVisibility(this.updateProductPreviewImage, defaultValueUpdateImage);

                    $('input[name=\'default[]\']').each(function () {
                        $(this).attr('type', optionDefaultInputType);
                    });
                },

                /**
                 * @this {swatchProductAttributes}
                 */
                showDefaultRows: function () {
                    this.setRowVisibility(this.isRequired, true);
                    this.setRowVisibility(this.isUnique, true);
                    this.setRowVisibility(this.frontendClass, true);
                },

                /**
                 * @param {Object} el
                 * @param {Boolean} isVisible
                 * @this {swatchProductAttributes}
                 */
                setRowVisibility: function (el, isVisible) {
                    if (isVisible) {
                        el.show();
                        el.closest('.field').show();
                    } else {
                        el.hide();
                        el.closest('.field').hide();
                    }
                },

                /**
                 * @param {Object} el
                 * @this {swatchProductAttributes}
                 */
                _disable: function (el) {
                    el.attr('disabled', 'disabled');
                },

                /**
                 * @param {Object} el
                 * @this {swatchProductAttributes}
                 */
                _enable: function (el) {
                    if (!el.attr('readonly')) {
                        el.removeAttr('disabled');
                    }
                },

                /**
                 * @param {Object} el
                 * @this {swatchProductAttributes}
                 */
                _showPanel: function (el) {
                    el.closest('.fieldset').show();
                    this._render(el.attr('id'));
                },

                /**
                 * @param {Object} el
                 * @this {swatchProductAttributes}
                 */
                _hidePanel: function (el) {
                    el.closest('.fieldset').hide();
                },

                /**
                 * @param {String} id
                 * @this {swatchProductAttributes}
                 */
                _render: function (id) {
                    rg.get(id, function () {
                        $('#' + id).trigger('render');
                    });
                },

                /**
                 * @param {String} promptMessage
                 * @this {swatchProductAttributes}
                 */
                saveAttributeInNewSet: function (promptMessage) {

                    prompt({
                        content: promptMessage,
                        actions: {

                            /**
                             * @param {String} val
                             * @this {actions}
                             */
                            confirm: function (val) {
                                var rules = ['required-entry', 'validate-no-html-tags'],
                                    newAttributeSetNameInputId = $('#new_attribute_set_name'),
                                    editForm = $('#edit_form'),
                                    newAttributeSetName = val,
                                    i;

                                if (!newAttributeSetName) {
                                    return;
                                }

                                for (i = 0; i < rules.length; i++) {
                                    if (!$.validator.methods[rules[i]](newAttributeSetName)) {
                                        alert({
                                            content: $.validator.messages[rules[i]]
                                        });

                                        return;
                                    }
                                }

                                if (newAttributeSetNameInputId.length) {
                                    newAttributeSetNameInputId.val(newAttributeSetName);
                                } else {
                                    editForm.append(new Element('input', {
                                            type: 'hidden',
                                            id: newAttributeSetNameInputId,
                                            name: 'new_attribute_set_name',
                                            value: newAttributeSetName
                                        })
                                    );
                                }
                                // Temporary solution will replaced after refactoring of attributes functionality
                                editForm.triggerHandler('save');
                            }
                        }
                    });
                }
            };

        $(function () {
            $('#frontend_input').bind('change', function () {
                swatchProductAttributes.bindAttributeInputType();
            });
            $('#is_filterable').bind('change', function () {
                swatchProductAttributes.switchIsFilterable();
            });

            swatchProductAttributes.bindAttributeInputType();

            // @todo: refactor collapsable component
            $('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]')
                .collapsable()
                .collapse('hide');
        });

        window.saveAttributeInNewSet = swatchProductAttributes.saveAttributeInNewSet;
        window.toggleApplyVisibility = swatchProductAttributes.toggleApplyVisibility;
    };
});

view/adminhtml/web/js/type-change.js
Add the ability for switching between multiselect, visual swatch multiselect, and text swatch multiselect.

require([
    'jquery',
    'mage/translate'
], function ($) {
    'use strict';

    $(function () {

        $('select#frontend_input:disabled').each(function () {
            var select = $(this),
                currentValue = select.find('option:selected').val(),
                enabledTypes = ['multiselect', 'swatch_multiselect_visual', 'swatch_multiselect_text'],
                warning = $('<label>')
                    .hide()
                    .text($.mage.__('These changes affect all related products.'))
                    .addClass('mage-error')
                    .attr({
                        generated: true, for: select.attr('id')
                    }),

                toggleWarning = function () {
                    if (select.find('option:selected').val() === currentValue) {
                        warning.hide();
                    } else {
                        warning.show();
                    }
                },

                removeOption = function () {
                    if (!~enabledTypes.indexOf($(this).val())) {
                        $(this).remove();
                    }
                };

            if (!~enabledTypes.indexOf(currentValue)) {
                return;
            }

            select.removeAttr('disabled').find('option').each(removeOption);

            select.after(warning).on('change', toggleWarning);
        });
    });
});

view/adminhtml/requirejs-config.js
Register the rewrite of the core product attribute javascript above.

var config = {
    map: {
        '*': {
            swatchesProductAttributes: 'Pmclain_MultiSelectSwatch/js/product-attributes'
        }
    }
};

view/adminhtml/templates/catalog/product/attribute/js.phtml
For adding all the javascript about on the attribute edit forms.

<script type="text/x-magento-init">
    {
        "*": {
            "swatchesProductAttributes": {
                "hiddenFields": <?= /* @escapeNotVerified */ json_encode($this->helper('Magento\Catalog\Helper\Data')->getAttributeHiddenFields()) ?>
            },
            "swatchesTypeChange": { },
            "Pmclain_MultiSelectSwatch/js/type-change": { }
        }
    }
</script>

view/adminhtml/layout/catalog_product_attribute_edit_popup.xml

<?xml version="1.0"?>
<page layout="admin-1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="attribute_edit_js">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">Pmclain_MultiSelectSwatch::catalog/product/attribute/js.phtml</argument>
            </action>
        </referenceBlock>
    </body>
</page>

view/adminhtml/layout/catalog_product_attribute_edit.xml

<?xml version="1.0"?>
<page layout="admin-1column" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="attribute_edit_js">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">Pmclain_MultiSelectSwatch::catalog/product/attribute/js.phtml</argument>
            </action>
        </referenceBlock>
    </body>
</page>

Plugin/Controller/Adminhtml/Product/Attribute/Save.php
Handle the new frontend types when saving product attributes.

<?php

namespace Pmclain\MultiSelectSwatch\Plugin\Controller\Adminhtml\Product\Attribute;

use Magento\Catalog\Controller\Adminhtml\Product\Attribute;
use Magento\Framework\App\RequestInterface;
use Magento\Swatches\Model\Swatch;
use Pmclain\MultiSelectSwatch\Model\Swatch as MultiselectSwatch;

class Save
{
    public function beforeDispatch(Attribute\Save $subject, RequestInterface $request)
    {
        $data = $request->getPostValue();
        if (isset($data['frontend_input'])) {
            switch ($data['frontend_input']) {
                case MultiselectSwatch::SWATCH_MULTISELECT_TYPE_VISUAL_ATTRIBUTE_FRONTEND_INPUT:
                    $data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_VISUAL;
                    $data['frontend_input'] = 'multiselect';
                    $request->setPostValue($data);
                    break;
                case MultiselectSwatch::SWATCH_MULTISELECT_TYPE_TEXTUAL_ATTRIBUTE_FRONTEND_INPUT:
                    $data[Swatch::SWATCH_INPUT_TYPE_KEY] = Swatch::SWATCH_INPUT_TYPE_TEXT;
                    $data['use_product_image_for_swatch'] = 0;
                    $data['frontend_input'] = 'multiselect';
                    $request->setPostValue($data);
                    break;
            }
        }
        return [$request];
    }
}

Block/Adminhtml/Product/Attribute/Edit/Form.php
Ensure the proper frontend type is pre-selected when loading an existing attribute with the multiselect swatch type.

<?php

namespace Pmclain\MultiSelectSwatch\Block\Adminhtml\Product\Attribute\Edit;

use Magento\Swatches\Model\Swatch;

class Form extends \Magento\Swatches\Block\Adminhtml\Product\Attribute\Edit\Form
{
    public function addValues($values)
    {
        if (!is_array($values)) {
            return $this;
        }
        $values = array_merge(
            $values,
            $this->getAdditionalData($values)
        );
        if (isset($values['frontend_input']) && 'multiselect' == $values['frontend_input']
            && isset($values[Swatch::SWATCH_INPUT_TYPE_KEY])
        ) {
            $values['frontend_input'] = 'swatch_multiselect_' . $values[Swatch::SWATCH_INPUT_TYPE_KEY];
        }
        return parent::addValues($values);
    }
}
4
  • wow, it seems for a quite complicated thing... Jun 9, 2018 at 20:51
  • Getting the admin form working to create new multiselect swatch attributes was extremely involved. If you create the multiselect swatch attribute with a Setup|Upgrade script you can ditch all the javascript and it just works. I put the above code on Github in case anyone wants to quickly copy/test it. github.com/pmclain/module-multiselect-swatch
    – Pmclain
    Jun 10, 2018 at 20:38
  • @Pmclain I have downloaded this module from github, but it is not working fine for me. Issue: Use in Layered Navigation = Filterable (with results) I have configured above setting in my attribute, still it is not showing on category page (layered navigation filter options). Please help in this issue. Nov 19, 2018 at 7:01
  • @Pmclain I tried your code from Github. I can create a visual swatch multiselect attribute but attribute option section does showing. seems hidden all the time. Could you help me to overcome this issue? Jul 6, 2021 at 15:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.