How to Add Quantity Increment Decrement Button in Hyva Magento 2 Theme
Adding a quantity increment decrement button in Hyva Magento 2 theme can significantly improve your store’s user experience. By integrating this feature, customers can easily adjust the quantity of products with increment and decrement buttons before adding items to their cart. This step is essential for creating a user-friendly shopping environment in Hyva.
In this tutorial, we will walk you through how to add a quantity increment decrement button in Hyva using Alpine.js and Tailwind CSS. This simple yet powerful customization makes it easier for customers to interact with the quantity input field, enhancing overall usability. Implementing a quantity increment decrement button in Hyva will streamline the checkout process and help boost conversions in your Magento 2 store.
Whether you are new to Hyva or looking for advanced customizations, this guide will help you efficiently add quantity increment decrement buttons in Hyva and improve your product pages.
By implementing this solution, you improve the overall usability of your product pages, leading to a more seamless shopping experience and potentially boosting conversions.
For Hyva theme integration, just copy the code provided below and replace the quantity (QTY) section.
Product Detail Page :
app/design/frontend/Vendor/YourTheme/Magento_Catalog/templates/product/view/quantity.phtml
<?php
/**
* Hyvä Themes - https://hyva.io
* Copyright © Hyvä Themes 2020-present. All rights reserved.
* This product is licensed per Magento install
* See https://hyva.io/license
*/
declare(strict_types=1);
use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\ProductStockItem;
use Magento\Catalog\Block\Product\View;
use Magento\Catalog\Model\Product;
use Magento\Framework\Escaper;
// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect
/** @var View $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels */
/** @var Product $product */
$product = $block->getProduct();
/** @var ProductStockItem $stockItemViewModel */
$stockItemViewModel = $viewModels->require(ProductStockItem::class);
$minSalesQty = $stockItemViewModel->getMinSaleQty($product);
$maxSalesQty = $stockItemViewModel->getMaxSaleQty($product);
$defaultQty = $block->getProductDefaultQty() * 1;
$step = $stockItemViewModel->getQtyIncrements($product)
? $stockItemViewModel->getQtyIncrements($product)
: null;
/**
* sets minimum and maximum values taking into account the values set in the admin,
* but taking into account the value of Qty Increments
*/
if ($step) {
$minSalesQty = ceil($minSalesQty / $step) * $step;
$maxSalesQty = floor($maxSalesQty / $step) * $step;
$defaultQty = ceil($defaultQty / $step) * $step;
}
$maxSalesQtyLength = ($maxSalesQty ? strlen((string) $maxSalesQty) : 4)
+ (/* add one if decimal for separator */ (int) $stockItemViewModel->isQtyDecimal($product));
?>
<?php if ($block->shouldRenderQuantity()): ?>
<script>
function initQtyField() {
function findPathParam(key) {
// get all path pairs after BASE_URL/front_name/action_path/action
const baseUrl = (BASE_URL.substring(0, 2) === '//' ? 'http:' : '') + BASE_URL;
const baseUrlParts = (new URL(baseUrl)).pathname.replace(/\/$/, '').split('/');
const pathParts = window.location.pathname.split('/').slice(baseUrlParts.length + 3);
for (let i = 0; i < pathParts.length; i += 2) {
if (pathParts[i] === key && pathParts.length > i) {
return pathParts[i + 1];
}
}
}
return {
qty: <?= /** @noEscape */ $defaultQty ?>,
itemId: (new URLSearchParams(window.location.search)).get('id') || findPathParam('id'),
productId: '<?= (int)$product->getId() ?>',
<?php /* populate the qty when editing a product from the cart */ ?>
onGetCartData: function onGetCartData(data, $dispatch) {
const cart = data && data.data && data.data.cart;
if (this.itemId && cart && cart.items) {
const cartItem = cart.items.find((item) => {
return item.item_id === this.itemId && item.product_id === this.productId;
});
if (cartItem && cartItem.qty) {
this.qty = cartItem.qty;
$dispatch('update-qty-' + this.productId, this.qty);
}
}
}
};
}
</script>
<div x-data="initQtyField()"
x-init="$dispatch('update-qty-<?= (int)$product->getId() ?>', qty)"
>
<?php if ($product->isSaleable()): ?>
<div class="mr-2" x-data="{ counter: 0 }">
<label for="qty[<?= (int)$product->getId() ?>]" class="sr-only">
<?= $escaper->escapeHtml(__('Quantity')) ?>
</label>
<!-- Decrement Button -->
<button class="text-2xl px-3 py-2" @click="if(counter > 0) counter--">
<strong>-</strong>
</button>
<!-- Quantity Input -->
<input name="qty"
@private-content-loaded.window="onGetCartData($event.detail, $dispatch)"
id="qty[<?= (int)$product->getId() ?>]"
form="product_addtocart_form"
<?php if ($stockItemViewModel->isQtyDecimal($product)): ?>
type="text"
pattern="[0-9]+(\.[0-9]{1,<?= /** @noEscape */ $maxSalesQtyLength ?>})?"
inputmode="decimal"
<?php else: ?>
type="number"
pattern="[0-9]{0,<?= /** @noEscape */ $maxSalesQtyLength ?>}"
inputmode="numeric"
<?php if ($minSalesQty): ?>min="<?= /** @noEscape */ $minSalesQty ?>"<?php endif; ?>
<?php if ($maxSalesQty): ?>max="<?= /** @noEscape */ $maxSalesQty ?>"<?php endif; ?>
<?php if ($step): ?>step="<?= /** @noEscape */ $step ?>"<?php endif; ?>
<?php endif; ?>
x-model="counter"
class="form-input px-2 py-2 w-20 text-center invalid:ring-2 invalid:ring-red-500"
@input="$dispatch('update-qty-<?= (int)$product->getId() ?>', counter)"
/>
<!-- Increment Button -->
<button class="text-2xl px-3 py-2" @click="counter++">
<strong>+</strong>
</button>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
Explanation:
- Binding the Counter to the Input Field:
- The
x-model="counter"
directive binds thecounter
value to the input field. This ensures that whenever the value ofcounter
changes (through increment or decrement), the input field is updated automatically.
- The
- Decrement Button Logic:
- The decrement button now only reduces the value of
counter
if it is greater than 0:@click="if(counter > 0) counter--"
. This prevents negative values.
- The decrement button now only reduces the value of
- Dispatching Events:
- When the input changes, it dispatches an
update-qty-<?= (int)$product->getId() ?>
event with the current counter value, which should trigger any custom behavior you have for updating the quantity.
- When the input changes, it dispatches an
Output:
Cart Page:
app/design/frontend/Vendor/YourTheme/Magento_Checkout/templates/php-cart/item/default.phtml
<?php
/**
* Hyvä Themes - https://hyva.io
* Copyright © Hyvä Themes 2020-present. All rights reserved.
* This product is licensed per Magento install
* See https://hyva.io/license
*/
declare(strict_types=1);
// phpcs:disable Magento2.Files.LineLength.MaxExceeded
use Magento\Checkout\Block\Cart\Item\Renderer;
use Magento\Framework\Escaper;
/** @var Renderer $block */
/** @var Escaper $escaper */
$item = $block->getItem();
$product = $item->getProduct();
$isVisibleProduct = $product->isVisibleInSiteVisibility();
?>
<tbody class="cart item bg-white even:bg-container-darker">
<tr class="item-info align-top text-left lg:text-right flex flex-wrap lg:table-row">
<td data-th="<?= $escaper->escapeHtml(__('Item')) ?>" class="col item pt-6 px-4 flex flex-wrap gap-8 text-left w-full sm:flex-nowrap lg:w-auto">
<?php if ($block->hasProductUrl()): ?>
<a href="<?= $escaper->escapeUrl($block->getProductUrl()) ?>"
title="<?= $escaper->escapeHtmlAttr($block->getProductName()) ?>"
tabindex="-1"
class="product-item-photo shrink-0">
<?php else: ?>
<span class="product-item-photo shrink-0">
<?php endif;?>
<?= $block->getImage($block->getProductForThumbnail(), 'cart_page_product_thumbnail')
->setTemplate('Magento_Catalog::product/image.phtml')
->toHtml() ?>
<?php if ($block->hasProductUrl()): ?>
</a>
<?php else: ?>
</span>
<?php endif; ?>
<div class="product-item-details grow">
<strong class="product-item-name break-all">
<?php if ($block->hasProductUrl()): ?>
<a href="<?= $escaper->escapeUrl($block->getProductUrl()) ?>"><?= $escaper->escapeHtml($block->getProductName()) ?></a>
<?php else: ?>
<?= $escaper->escapeHtml($block->getProductName()) ?>
<?php endif; ?>
</strong>
<?php if ($options = $block->getOptionList()): ?>
<dl class="item-options w-full break-all mt-4 pb-2 text-sm clearfix">
<?php foreach ($options as $option): ?>
<?php $formatedOptionValue = $block->getFormatedOptionValue($option) ?>
<dt class="font-bold float-left clear-left mr-2 mb-2"><?= $escaper->escapeHtml($option['label']) ?>:</dt>
<dd class="float-left">
<?php if (isset($formatedOptionValue['full_view'])): ?>
<?= $escaper->escapeHtml($formatedOptionValue['full_view']) ?>
<?php else: ?>
<?= $escaper->escapeHtml($formatedOptionValue['value'], ['span', 'a']) ?>
<?php endif; ?>
</dd>
<?php endforeach; ?>
</dl>
<?php endif;?>
<?php if ($messages = $block->getMessages()): ?>
<?php foreach ($messages as $message): ?>
<div class= "cart item message <?= $escaper->escapeHtmlAttr($message['type']) ?>">
<div><?= $escaper->escapeHtml($message['text']) ?></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?>
<?php if ($addInfoBlock): ?>
<?= $addInfoBlock->setItem($item)->toHtml() ?>
<?php endif;?>
</div>
</td>
<td class="col price pt-6 px-4 block w-full sm:w-1/3 lg:w-auto lg:table-cell">
<span class="lg:hidden font-bold mt-2">
<?= $escaper->escapeHtml(__('Price')) ?>
</span>
<?= $block->getUnitPriceHtml($item) ?>
</td>
<td class="col qty pt-6 lg:pt-2 px-4 block w-full sm:w-1/3 lg:w-auto lg:table-cell">
<span class="lg:hidden font-bold">
<?= $escaper->escapeHtml(__('Qty')) ?>
</span>
<div class="field qty">
<div class="control qty" x-data="{ counter: <?= $block->getQty() ?> }">
<label for="cart-<?= $escaper->escapeHtmlAttr($item->getId()) ?>-qty">
<span class="label sr-only"><?= $escaper->escapeHtml(__('Qty')) ?></span>
<!-- Decrement Button -->
<button class="text-2xl px-3 py-2" @click="if(counter > 0) counter--">
<strong>-</strong>
</button>
<!-- Quantity Input -->
<input id="cart-<?= $escaper->escapeHtmlAttr($item->getId()) ?>-qty"
name="cart[<?= $escaper->escapeHtmlAttr($item->getId()) ?>][qty]"
value="<?= $escaper->escapeHtmlAttr($block->getQty()) ?>"
type="number"
size="4"
step="any"
title="<?= $escaper->escapeHtmlAttr(__('Qty')) ?>"
class="qty form-input px-2 py-2 w-20 text-center"
required
min="0"
x-model="counter"
data-role="cart-item-qty"/>
<!-- Increment Button -->
<button class="text-2xl px-3 py-2" @click="counter++">
<strong>+</strong>
</button>
</label>
</div>
</div>
</td>
<td class="col subtotal pt-6 px-4 block w-full sm:w-1/3 lg:w-auto lg:table-cell">
<span class="lg:hidden font-bold">
<?= $escaper->escapeHtml(__('Subtotal')) ?>
</span>
<?= $block->getRowTotalHtml($item) ?>
</td>
</tr>
<tr class="item-actions">
<td colspan="4">
<div class="flex justify-end gap-4 p-4 pt-2">
<?= /* @noEscape */ $block->getActions($item) ?>
</div>
</td>
</tr>
</tbody>
Leave a Reply