Recently, we published our first Magento micromodule – Magently CheckoutSwatches. Let’s take a look at how we made it!
Displaying a color-filled frame along with the product’s size instead of plain text definitely improves the experience of the store users. In this article, we’ll create a module allowing us to display swatches instead of the text label of the product on checkout. We used the UI Component included in the Magento structure that will display the elements for us after just adding two parameters.
Action plan
Let’s start by creating a class that will use the session to return the config needed to run the component for every cart item. Then we’ll inject the configuration using the checkout LayoutProcessor
plugin. The component responsible for displaying the cart item details is Magento_Checkout/js/view/summary/item/details
, so we will need to overwrite its *.js or *.html files. Let’s go!
First steps
As always, we will start by creating a new module. Let us call it Magently_CheckoutSwatches
.
// file etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Magently_CheckoutSwatches" setup_version="0.1.0">
<sequence>
<module name="Magento_Swatches"/>
<module name="Magento_Checkout"/>
</sequence>
</module>
</config>
// file registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magently_CheckoutSwatches',
__DIR__
);
The above is the standard procedure, so now you’ll only have to run bin/magento setup:upgrade
to install the module.
Creating the LayoutProcessor class for swatches
The class below will let us get the required config without a need to make unnecessary AJAX requests from the checkout level. Now, let’s inject the output array with configurations to the JS component.
// file Model/Swatches/LayoutProcessor.php
<?php
namespace Magently\CheckoutSwatches\Model\Swatches;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Quote\Api\Data\CartItemInterface;
use Magento\Swatches\Block\Product\Renderer\ConfigurableFactory;
use Magento\Checkout\Model\Session;
/**
* Class LayoutProcessor
*
* This class is responsible for getting swatches js layout for every cart item to render swatches on checkout.
*/
class LayoutProcessor
{
/**
* @var Json $json
*/
private $json;
/**
* @var ConfigurableFactory $configurableFactory
*/
private $configurableFactory;
/**
* @var Session $session
*/
private $session;
/**
* LayoutProcessor constructor.
*
* @param Json $json
* @param ConfigurableFactory $configurableFactory
* @param Session $session
*/
public function __construct(
Json $json,
ConfigurableFactory $configurableFactory,
Session $session
) {
$this->json = $json;
$this->configurableFactory = $configurableFactory;
$this->session = $session;
}
/**
* @return array
* @throws LocalizedException
* @throws NoSuchEntityException
*/
public function getJsLayout()
{
$jsLayout = [];
foreach ($this->session->getQuote()->getItems() as $item) {
$isConfigurable = $item->getProductType() == Configurable::TYPE_CODE;
$jsLayout[$item->getItemId()] = [
'jsonConfig' => $isConfigurable ? $this->getJsonConfig($item) : null,
'jsonSwatchConfig' => $isConfigurable ? $this->getJsonSwatchConfig($item) : null
];
}
return $jsLayout;
}
/**
* @param CartItemInterface $item
* @return string
*/
private function getJsonConfig(CartItemInterface $item)
{
$configurable = $this->configurableFactory->create()
->setProduct($item->getProduct());
return $configurable->getJsonConfig();
}
/**
* @param CartItemInterface $item
* @return string
*/
private function getJsonSwatchConfig(CartItemInterface $item)
{
$configurable = $this->configurableFactory->create()
->setProduct($item->getProduct());
return $this->filterJsonSwatchConfig($configurable->getJsonSwatchConfig(), $item);
}
/**
* @param CartItemInterface $item
* @return array
*/
private function getOptions(CartItemInterface $item)
{
$output = [];
$options = $item->getProduct()->getTypeInstance()->getOrderOptions($item->getProduct());
$attributesInfo = $options['attributes_info'];
if (!empty($attributesInfo)) {
foreach ($attributesInfo as $info) {
$output[] = $info['value'];
}
}
return $output;
}
/**
* @param string $jsonSwatchConfig
* @param CartItemInterface $item
* @return string
*/
private function filterJsonSwatchConfig(string $jsonSwatchConfig, CartItemInterface $item)
{
$output = [];
$options = $this->getOptions($item);
foreach ($this->json->unserialize($jsonSwatchConfig) as $primary => $config) {
foreach ($config as $secondary => $option) {
if (is_array($option) && in_array($option['label'], $options)) {
$output[$primary][$secondary] = $option;
}
}
}
return $this->json->serialize($output);
}
}
// file etc/frontend/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magently\CheckoutSwatches\Model\Swatches\LayoutProcessor">
<arguments>
<argument name="session" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument>
</arguments>
</type>
</config>
Let’s go over this class functionality. It has one public method – getJsLayout
. It returns an array filled with data necessary to run the swatches renderer on the front end. This array has information about every item inside the cart. These items are stored in the session. The getJsonConfig
function returns the main configuration for a given product. It contains all possible swatches options, their assets’ paths, etc. In a case when there are no product options (a product isn’t configurable), it returns null.
The getJsonSwatchConfig
function returns the config of a picked option when it’s added to a cart. It fetches the configuration of all available options and then filters them to only leave those chosen by a user. Needless to say, if we didn’t filter this array on the front end, we would see all available product options as swatches!
As mentioned earlier, the filterJsonSwatchConfig
function filters the input JSON configuration for a swatch. The output array only receives the options chosen by a user. This happens through the array returned by the getOptions
function. It may be confusing to see a label being compared to the returned option’s value. But it’s not a mistake. It’s all because the color value is stored in the options by its name, e.g. Blue
, while in the config, it’s in the hexadecimal system, e.g. #1857f7
. Hence, we have to map a value from the label. In the case of the size, it doesn’t matter. Both fields hold the same value. Just like that, we’ve received the proper configurations for the swatches renderer for every product inside the cart.
LayoutProcessor plugin on Checkout
/ file Plugin/Checkout/Block/Checkout/LayoutProcessor.php
<?php
namespace Magently\CheckoutSwatches\Plugin\Checkout\Block\Checkout;
use Magently\CheckoutSwatches\Model\Swatches\LayoutProcessor as SwatchesLayoutProcessor;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
/**
* Class LayoutProcessor
*
* This class is a plugin for Checkout LayoutProcessor to add swatches JSON configs to details Ui Component.
*/
class LayoutProcessor
{
/**
* @var SwatchesLayoutProcessor $swatchesLayoutProcessor
*/
private $swatchesLayoutProcessor;
/**
* LayoutProcessor constructor.
*
* @param SwatchesLayoutProcessor $swatchesLayoutProcessor
*/
public function __construct(SwatchesLayoutProcessor $swatchesLayoutProcessor)
{
$this->swatchesLayoutProcessor = $swatchesLayoutProcessor;
}
/**
* @param \Magento\Checkout\Block\Checkout\LayoutProcessor $subject
* @param array $result
* @return mixed
* @throws LocalizedException
* @throws NoSuchEntityException
*/
public function afterProcess(\Magento\Checkout\Block\Checkout\LayoutProcessor $subject, array $result)
{
$result['components']['checkout']['children']['sidebar']['children']['summary']['children']['cart_items']
['children']['details']['config']['swatchesJsLayout'] = $this->swatchesLayoutProcessor->getJsLayout();
return $result;
}
}
// file etc/frontend/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
...
<type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
<plugin name="magently_checkout_swatches_layout_processor_plugin"
type="Magently\CheckoutSwatches\Plugin\Checkout\Block\Checkout\LayoutProcessor"
sortOrder="10"/>
</type>
</config>
This plugin has a very simple task. It injects the result of calling the getJsLayout
function from the previously created processor class for swatches. Since the product options are displayed in the Magento_Checkout/summary/item/details
template, we need to inject the configuration into the UI Component details. First, choose a config and assign a name to a new array item. In this case, it will be swatchesJsLayout
. You can find detailed information on how to follow the jsLayout
path in the *.xml files. In this case, we used the Magento_Checkout/layout/checkout_index_index.xml
file, because that’s where a modified component is being created. Finally, we need to define the created plugin in the di.xml file.
Displaying the swatch on the front end
// file view/frontend/web/js/view/summary/item/details.js
define([], function () {
var mixin = {
defaults: {
template: 'Magently_CheckoutSwatches/summary/item/details',
},
getSwatchesJsLayoutByItemId: function (itemId) {
return this.swatchesJsLayout[itemId];
}
};
return function (Component) {
return Component.extend(mixin);
};
});
// file view/frontend/web/template/summary/item/details.html
<!-- ko foreach: getRegion('before_details') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->
<div class="product-item-details">
<div class="product-item-inner">
<div class="product-item-name-block">
<strong class="product-item-name" data-bind="html: $parent.name"></strong>
<div class="details-qty">
<span class="label"><!-- ko i18n: 'Qty' --><!-- /ko --></span>
<span class="value" data-bind="text: $parent.qty"></span>
</div>
</div>
<!-- ko foreach: getRegion('after_details') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->
</div>
<!-- ko if: (JSON.parse($parent.options).length > 0)-->
<div class="product options" data-bind="mageInit: {'collapsible':{'openedState': 'active'}}">
<span data-role="title" class="toggle"><!-- ko i18n: 'View Details' --><!-- /ko --></span>
<div data-role="content" class="content">
<strong class="subtitle"><!-- ko i18n: 'Options Details' --><!-- /ko --></strong>
<!-- ko if: (getSwatchesJsLayoutByItemId($parent.item_id))-->
<div data-bind='mageInit: {
"Magento_Swatches/js/swatch-renderer": {
"jsonConfig": JSON.parse(getSwatchesJsLayoutByItemId($parent.item_id).jsonConfig),
"jsonSwatchConfig": JSON.parse(getSwatchesJsLayoutByItemId($parent.item_id).jsonSwatchConfig)
}
}'></div>
<!-- /ko -->
</div>
</div>
<!-- /ko -->
</div>
<!-- ko foreach: getRegion('item_message') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- /ko -->
// file view/frontend/requirejs-config.js
var config = {
config: {
'mixins': {
'Magento_Checkout/js/view/summary/item/details': {
'Magently_CheckoutSwatches/js/view/summary/item/details': true
}
}
}
};
Let’s start by creating a mixin for the UI component details. We can and we will define the *.html template directly from the *.js file. Alternatively, we could create a checkout_index_index.xml file in the view/frontend/layout
catalogue and inject the details template into the config like this:
<item name="config" xsi:type="array">
<item name="template"
xsi:type="string">Magently_CheckoutSwatches/summary/item/details</item>
</item>
In this case, this option is less efficient and creates extra lines of code. We would have to create a path one element at a time until the target (details), where we would put the above chunk of code. It would look like this:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="sidebar" xsi:type="array">
<item name="children" xsi:type="array">
<item name="summary" xsi:type="array">
<item name="children" xsi:type="array">
<item name="cart_items" xsi:type="array">
<item name="children" xsi:type="array">
<item name="details" xsi:type="array">
...
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</referenceContainer>
</body>
</page>
It’s way more efficient to do it in a mixin, right? Things would be different if we only didn’t need to modify the *.js file and only the *.html file. Then, we would use the option to change the template using the layout.
One function has been added to the component and its job is to return the desired configuration of a given cart item. As you can see, we refer to the variable that stores the config for the swatches renderer with a name assigned earlier to a new array element in the plugin, i.e. in swatchesJsLayout
.
The *.html file hasn’t been changed much. The only real change was to replace the code responsible for rendering the product options with an element launching the swatches renderer. It’s very important to remember to pair the configuration being passed because it is stored as a string. Fishing out the itemId
parameter is peanuts because we know that we can refer to the $parent
variable, which stores all important information about the cart item being rendered.
There’s also render error security. Namely, checking if getSwatchesJsLayoutByItemId()
doesn’t return null
, which can be the case if a product isn’t configurable. It prevents knockout errors and the inability to finalize the order.
The final thing to do is to declare a minix in the requirejs-config.js
file.
Final touches
// file view/frontend/web/css/source/_module.less
& when (@media-common = true) {
.checkout-index-index {
.swatch {
&-opt {
margin: 0;
}
&-option {
margin: 0;
pointer-events: none;
&.text {
margin: 0;
min-width: 18px;
padding: 1px 8px;
}
}
&-attribute {
line-height: 32px;
display: flex;
align-items: center;
justify-content: space-between;
&-options.clearfix {
margin-top: 0;
}
}
}
}
}
The styles above will make the swatches unclickable and will place them next to the option name instead of below it. You only have to run bin/magento setup:static-content:deploy
and bin/magento cache:clean
.
Let’s see how it looks.
That’s it! The module is published on our Github and we encourage you to comment or contribute.