├── README.md ├── etc └── module.xml ├── registration.php └── view └── frontend ├── layout └── catalog_product_view.xml ├── requirejs-config.js ├── templates └── product │ └── view │ └── addtocart.phtml └── web ├── css └── source │ └── _module.less ├── js ├── sidebar.js └── view │ └── minicart.js └── template └── minicart └── item └── default.html /README.md: -------------------------------------------------------------------------------- 1 | # magento2-qty 2 | How to create the buttons increase and decrease quantity in Magento 2 3 | 4 | # See the video about this practice 5 | 6 | ## How to create the buttons increase and decrease quantity on the product detail page in Magento 2 7 | - Youtube: https://www.youtube.com/watch?v=ihn9P0cLP80&t=576s&index=25&list=PL98CDCbI3TNvPczWSOnpaMoyxVISLVzYQ 8 | - Facebook: https://www.facebook.com/giaphugroupcom/videos/308028576449228/ 9 | ### The results of this lesson 10 | ![The results of this lesson](https://jvdeh29369.i.lithium.com/t5/image/serverpage/image-id/9531i2B046FA922226559/image-dimensions/2500) 11 | 12 | ## How to add the buttons increase and decrease quantity in Magento 2 mini cart 13 | - Youtube: https://www.youtube.com/watch?v=czt4WvHILa4&index=35&list=PL98CDCbI3TNvPczWSOnpaMoyxVISLVzYQ 14 | - Facebook: https://www.facebook.com/giaphugroupcom/videos/314071576055365/ 15 | ### The results of this lesson 16 | ![The results of this lesson](https://i.imgur.com/l7RaOBs.png) 17 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | 26 | 27 | PHPCuong_Qty::product/view/addtocart.phtml 28 | 29 | 30 | 31 | 32 | PHPCuong_Qty::product/view/addtocart.phtml 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /view/frontend/requirejs-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GiaPhuGroup Co., Ltd. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * This source file is subject to the GiaPhuGroup.com license that is 7 | * available through the world-wide-web at this URL: 8 | * https://www.giaphugroup.com/LICENSE.txt 9 | * 10 | * DISCLAIMER 11 | * 12 | * Do not edit or add to this file if you wish to upgrade this extension to newer 13 | * version in the future. 14 | * 15 | * @category PHPCuong 16 | * @package PHPCuong_Qty 17 | * @copyright Copyright (c) 2018-2019 GiaPhuGroup Co., Ltd. All rights reserved. (http://www.giaphugroup.com/) 18 | * @license https://www.giaphugroup.com/LICENSE.txt 19 | */ 20 | var config = { 21 | map: { 22 | '*': { 23 | // This code helps us to override the knockout HTML template file. 24 | 'Magento_Checkout/template/minicart/item/default.html': 'PHPCuong_Qty/template/minicart/item/default.html', 25 | // The bellow codes help us to override the JS files 26 | 'sidebar': 'PHPCuong_Qty/js/sidebar', 27 | 'Magento_Checkout/js/view/minicart': 'PHPCuong_Qty/js/view/minicart' 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /view/frontend/templates/product/view/addtocart.phtml: -------------------------------------------------------------------------------- 1 | 24 | getProduct(); ?> 25 | 26 | isSaleable()): ?> 27 |
28 |
29 | shouldRenderQuantity()): ?> 30 |
31 | 32 |
33 | 34 | 42 | 43 | 66 |
67 |
68 | 69 |
70 | 76 | getChildHtml('', true) ?> 77 |
78 |
79 |
80 | 81 | isRedirectToCartEnabled()) : ?> 82 | 91 | 92 | 99 | 100 | -------------------------------------------------------------------------------- /view/frontend/web/css/source/_module.less: -------------------------------------------------------------------------------- 1 | /** This CSS help us to display the buttons increase and decrease qty*/ 2 | .box-tocart { 3 | div.control { 4 | position: relative; 5 | width: 92px !important; 6 | .minus, 7 | .plus { 8 | &:hover { 9 | button { 10 | background: #dfdfdf; 11 | color: #252531; 12 | } 13 | } 14 | button { 15 | position: absolute; 16 | top: 0px; 17 | border-radius: 4px 0 0 4px; 18 | border: 1px solid #dfdfdf; 19 | background: #fff; 20 | height: 40px; 21 | line-height: 40px; 22 | padding: 0px; 23 | width: 28px; 24 | text-align: center; 25 | cursor: pointer; 26 | box-shadow: none; 27 | } 28 | } 29 | .minus { 30 | button { 31 | left: 0px; 32 | } 33 | } 34 | .plus { 35 | button { 36 | right: 0px; 37 | border-radius: 0px 4px 4px 0px; 38 | } 39 | } 40 | .input-text.qty { 41 | border-radius: 0px !important; 42 | height: 20px !important; 43 | height: 40px !important; 44 | width: 40px !important; 45 | text-align: center !important; 46 | margin-left: 26px !important; 47 | display: inline-block !important; 48 | z-index: 5 !important; 49 | padding: 0px 3px !important; 50 | border: 1px solid #dfdfdf !important; 51 | } 52 | } 53 | } 54 | .details-qty.qty { 55 | .item-qty.cart-item-qty { 56 | margin-right: 0px; 57 | } 58 | .decreasing-qty { 59 | vertical-align: top; 60 | margin-right: -5px; 61 | padding: 7px 10px; 62 | box-shadow: none; 63 | } 64 | .increasing-qty { 65 | vertical-align: top; 66 | margin-left: -5px; 67 | padding: 7px 8px; 68 | box-shadow: none; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /view/frontend/web/js/sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GiaPhuGroup Co., Ltd. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * This source file is subject to the GiaPhuGroup.com license that is 7 | * available through the world-wide-web at this URL: 8 | * https://www.giaphugroup.com/LICENSE.txt 9 | * 10 | * DISCLAIMER 11 | * 12 | * Do not edit or add to this file if you wish to upgrade this extension to newer 13 | * version in the future. 14 | * 15 | * @category PHPCuong 16 | * @package PHPCuong_Qty 17 | * @copyright Copyright (c) 2018-2019 GiaPhuGroup Co., Ltd. All rights reserved. (http://www.giaphugroup.com/) 18 | * @license https://www.giaphugroup.com/LICENSE.txt 19 | */ 20 | define([ 21 | 'jquery', 22 | 'Magento_Customer/js/model/authentication-popup', 23 | 'Magento_Customer/js/customer-data', 24 | 'Magento_Ui/js/modal/alert', 25 | 'Magento_Ui/js/modal/confirm', 26 | 'jquery/ui', 27 | 'mage/decorate', 28 | 'mage/collapsible', 29 | 'mage/cookies' 30 | ], function ($, authenticationPopup, customerData, alert, confirm) { 31 | 32 | $.widget('mage.sidebar', { 33 | options: { 34 | isRecursive: true, 35 | minicart: { 36 | maxItemsVisible: 3 37 | } 38 | }, 39 | scrollHeight: 0, 40 | 41 | /** 42 | * Create sidebar. 43 | * @private 44 | */ 45 | _create: function () { 46 | this._initContent(); 47 | }, 48 | 49 | /** 50 | * Update sidebar block. 51 | */ 52 | update: function () { 53 | $(this.options.targetElement).trigger('contentUpdated'); 54 | this._calcHeight(); 55 | this._isOverflowed(); 56 | }, 57 | 58 | _initContent: function () { 59 | var self = this, 60 | events = {}; 61 | 62 | this.element.decorate('list', this.options.isRecursive); 63 | 64 | events['click ' + this.options.button.close] = function (event) { 65 | event.stopPropagation(); 66 | $(self.options.targetElement).dropdownDialog('close'); 67 | }; 68 | events['click ' + this.options.button.checkout] = $.proxy(function () { 69 | var cart = customerData.get('cart'), 70 | customer = customerData.get('customer'); 71 | 72 | if (!customer().firstname && cart().isGuestCheckoutAllowed === false) { 73 | // set URL for redirect on successful login/registration. It's postprocessed on backend. 74 | $.cookie('login_redirect', this.options.url.checkout); 75 | if (this.options.url.isRedirectRequired) { 76 | location.href = this.options.url.loginUrl; 77 | } else { 78 | authenticationPopup.showModal(); 79 | } 80 | 81 | return false; 82 | } 83 | location.href = this.options.url.checkout; 84 | }, this); 85 | events['click ' + this.options.button.remove] = function (event) { 86 | event.stopPropagation(); 87 | confirm({ 88 | content: self.options.confirmMessage, 89 | actions: { 90 | confirm: function () { 91 | self._removeItem($(event.currentTarget)); 92 | }, 93 | always: function (event) { 94 | event.stopImmediatePropagation(); 95 | } 96 | } 97 | }); 98 | }; 99 | events['keyup ' + this.options.item.qty] = function (event) { 100 | self._showItemButton($(event.target)); 101 | }; 102 | events['click ' + this.options.item.button] = function (event) { 103 | event.stopPropagation(); 104 | self._updateItemQty($(event.currentTarget)); 105 | }; 106 | events['focusout ' + this.options.item.qty] = function (event) { 107 | self._validateQty($(event.currentTarget)); 108 | }; 109 | 110 | // The bellow codes will execute when you click on the decrease button 111 | events['click ' + this.options.item.qtyDecreasing] = function (event) { 112 | event.stopPropagation(); 113 | var itemId = $(event.currentTarget).data('cart-item'); 114 | var qtyElement = $('#cart-item-'+itemId+'-qty'); 115 | var qtyValue = parseInt(qtyElement.val()); 116 | qtyValue = qtyValue - 1; 117 | if (qtyValue <= 0) { 118 | qtyValue = 1; 119 | } 120 | qtyElement.val(qtyValue).trigger('keyup'); 121 | }; 122 | 123 | // The bellow codes will execute when you click on the increase button 124 | events['click ' + this.options.item.qtyIncreasing] = function (event) { 125 | event.stopPropagation(); 126 | var itemId = $(event.currentTarget).data('cart-item'); 127 | var qtyElement = $('#cart-item-'+itemId+'-qty'); 128 | var qtyValue = parseInt(qtyElement.val()); 129 | qtyValue = qtyValue + 1; 130 | if (qtyValue > 100) { 131 | qtyValue = 100; 132 | } 133 | qtyElement.val(qtyValue).trigger('keyup'); 134 | }; 135 | 136 | this._on(this.element, events); 137 | this._calcHeight(); 138 | this._isOverflowed(); 139 | }, 140 | 141 | /** 142 | * Add 'overflowed' class to minicart items wrapper element 143 | * 144 | * @private 145 | */ 146 | _isOverflowed: function () { 147 | var list = $(this.options.minicart.list), 148 | cssOverflowClass = 'overflowed'; 149 | 150 | if (this.scrollHeight > list.innerHeight()) { 151 | list.parent().addClass(cssOverflowClass); 152 | } else { 153 | list.parent().removeClass(cssOverflowClass); 154 | } 155 | }, 156 | 157 | _showItemButton: function (elem) { 158 | var itemId = elem.data('cart-item'), 159 | itemQty = elem.data('item-qty'); 160 | 161 | if (this._isValidQty(itemQty, elem.val())) { 162 | $('#update-cart-item-' + itemId).show('fade', 300); 163 | } else if (elem.val() == 0) { 164 | this._hideItemButton(elem); 165 | } else { 166 | this._hideItemButton(elem); 167 | } 168 | }, 169 | 170 | /** 171 | * @param origin - origin qty. 'data-item-qty' attribute. 172 | * @param changed - new qty. 173 | * @returns {boolean} 174 | * @private 175 | */ 176 | _isValidQty: function (origin, changed) { 177 | return (origin != changed) && 178 | (changed.length > 0) && 179 | (changed - 0 == changed) && 180 | (changed - 0 > 0); 181 | }, 182 | 183 | /** 184 | * @param {Object} elem 185 | * @private 186 | */ 187 | _validateQty: function (elem) { 188 | var itemQty = elem.data('item-qty'); 189 | 190 | if (!this._isValidQty(itemQty, elem.val())) { 191 | elem.val(itemQty); 192 | } 193 | }, 194 | 195 | _hideItemButton: function (elem) { 196 | var itemId = elem.data('cart-item'); 197 | $('#update-cart-item-' + itemId).hide('fade', 300); 198 | }, 199 | 200 | _updateItemQty: function (elem) { 201 | var itemId = elem.data('cart-item'); 202 | this._ajax(this.options.url.update, { 203 | item_id: itemId, 204 | item_qty: $('#cart-item-' + itemId + '-qty').val() 205 | }, elem, this._updateItemQtyAfter); 206 | }, 207 | 208 | /** 209 | * Update content after update qty 210 | * 211 | * @param elem 212 | */ 213 | _updateItemQtyAfter: function (elem) { 214 | this._hideItemButton(elem); 215 | }, 216 | 217 | _removeItem: function (elem) { 218 | var itemId = elem.data('cart-item'); 219 | 220 | this._ajax(this.options.url.remove, { 221 | item_id: itemId 222 | }, elem, this._removeItemAfter); 223 | }, 224 | 225 | /** 226 | * Update content after item remove 227 | * 228 | * @param {Object} elem 229 | * @private 230 | */ 231 | _removeItemAfter: function (elem) { 232 | var productData = customerData.get('cart')().items.find(function (item) { 233 | return Number(elem.data('cart-item')) === Number(item['item_id']); 234 | }); 235 | 236 | $(document).trigger('ajax:removeFromCart', productData['product_sku']); 237 | }, 238 | 239 | /** 240 | * @param {String} url - ajax url 241 | * @param {Object} data - post data for ajax call 242 | * @param {Object} elem - element that initiated the event 243 | * @param {Function} callback - callback method to execute after AJAX success 244 | */ 245 | _ajax: function (url, data, elem, callback) { 246 | $.extend(data, { 247 | 'form_key': $.mage.cookies.get('form_key') 248 | }); 249 | 250 | $.ajax({ 251 | url: url, 252 | data: data, 253 | type: 'post', 254 | dataType: 'json', 255 | context: this, 256 | beforeSend: function () { 257 | elem.attr('disabled', 'disabled'); 258 | }, 259 | complete: function () { 260 | elem.attr('disabled', null); 261 | } 262 | }) 263 | .done(function (response) { 264 | if (response.success) { 265 | callback.call(this, elem, response); 266 | } else { 267 | var msg = response.error_message; 268 | 269 | if (msg) { 270 | alert({ 271 | content: msg 272 | }); 273 | } 274 | } 275 | }) 276 | .fail(function (error) { 277 | console.log(JSON.stringify(error)); 278 | }); 279 | }, 280 | 281 | /** 282 | * Calculate height of minicart list 283 | * 284 | * @private 285 | */ 286 | _calcHeight: function () { 287 | var self = this, 288 | height = 0, 289 | counter = this.options.minicart.maxItemsVisible, 290 | target = $(this.options.minicart.list), 291 | outerHeight; 292 | 293 | self.scrollHeight = 0; 294 | target.children().each(function () { 295 | 296 | if ($(this).find('.options').length > 0) { 297 | $(this).collapsible(); 298 | } 299 | outerHeight = $(this).outerHeight(); 300 | 301 | if (counter-- > 0) { 302 | height += outerHeight; 303 | } 304 | self.scrollHeight += outerHeight; 305 | }); 306 | 307 | target.parent().height(height); 308 | } 309 | }); 310 | 311 | return $.mage.sidebar; 312 | }); 313 | -------------------------------------------------------------------------------- /view/frontend/web/js/view/minicart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GiaPhuGroup Co., Ltd. 3 | * 4 | * NOTICE OF LICENSE 5 | * 6 | * This source file is subject to the GiaPhuGroup.com license that is 7 | * available through the world-wide-web at this URL: 8 | * https://www.giaphugroup.com/LICENSE.txt 9 | * 10 | * DISCLAIMER 11 | * 12 | * Do not edit or add to this file if you wish to upgrade this extension to newer 13 | * version in the future. 14 | * 15 | * @category PHPCuong 16 | * @package PHPCuong_Qty 17 | * @copyright Copyright (c) 2018-2019 GiaPhuGroup Co., Ltd. All rights reserved. (http://www.giaphugroup.com/) 18 | * @license https://www.giaphugroup.com/LICENSE.txt 19 | */ 20 | define([ 21 | 'uiComponent', 22 | 'Magento_Customer/js/customer-data', 23 | 'jquery', 24 | 'ko', 25 | 'underscore', 26 | 'sidebar', 27 | 'mage/translate' 28 | ], function (Component, customerData, $, ko, _) { 29 | 'use strict'; 30 | 31 | var sidebarInitialized = false, 32 | addToCartCalls = 0, 33 | miniCart; 34 | 35 | miniCart = $('[data-block=\'minicart\']'); 36 | miniCart.on('dropdowndialogopen', function () { 37 | initSidebar(); 38 | }); 39 | 40 | /** 41 | * @return {Boolean} 42 | */ 43 | function initSidebar() { 44 | if (miniCart.data('mageSidebar')) { 45 | miniCart.sidebar('update'); 46 | } 47 | 48 | if (!$('[data-role=product-item]').length) { 49 | return false; 50 | } 51 | miniCart.trigger('contentUpdated'); 52 | 53 | if (sidebarInitialized) { 54 | return false; 55 | } 56 | sidebarInitialized = true; 57 | miniCart.sidebar({ 58 | 'targetElement': 'div.block.block-minicart', 59 | 'url': { 60 | 'checkout': window.checkout.checkoutUrl, 61 | 'update': window.checkout.updateItemQtyUrl, 62 | 'remove': window.checkout.removeItemUrl, 63 | 'loginUrl': window.checkout.customerLoginUrl, 64 | 'isRedirectRequired': window.checkout.isRedirectRequired 65 | }, 66 | 'button': { 67 | 'checkout': '#top-cart-btn-checkout', 68 | 'remove': '#mini-cart a.action.delete', 69 | 'close': '#btn-minicart-close' 70 | }, 71 | 'showcart': { 72 | 'parent': 'span.counter', 73 | 'qty': 'span.counter-number', 74 | 'label': 'span.counter-label' 75 | }, 76 | 'minicart': { 77 | 'list': '#mini-cart', 78 | 'content': '#minicart-content-wrapper', 79 | 'qty': 'div.items-total', 80 | 'subtotal': 'div.subtotal span.price', 81 | 'maxItemsVisible': window.checkout.minicartMaxItemsVisible 82 | }, 83 | 'item': { 84 | 'qty': ':input.cart-item-qty', 85 | 'button': ':button.update-cart-item', 86 | 'qtyDecreasing': '.decreasing-qty', 87 | 'qtyIncreasing': '.increasing-qty' 88 | }, 89 | 'confirmMessage': $.mage.__('Are you sure you would like to remove this item from the shopping cart?') 90 | }); 91 | } 92 | 93 | return Component.extend({ 94 | shoppingCartUrl: window.checkout.shoppingCartUrl, 95 | maxItemsToDisplay: window.checkout.maxItemsToDisplay, 96 | cart: {}, 97 | 98 | /** 99 | * @override 100 | */ 101 | initialize: function () { 102 | var self = this, 103 | cartData = customerData.get('cart'); 104 | 105 | this.update(cartData()); 106 | cartData.subscribe(function (updatedCart) { 107 | addToCartCalls--; 108 | this.isLoading(addToCartCalls > 0); 109 | sidebarInitialized = false; 110 | this.update(updatedCart); 111 | initSidebar(); 112 | }, this); 113 | $('[data-block="minicart"]').on('contentLoading', function (event) { 114 | addToCartCalls++; 115 | self.isLoading(true); 116 | }); 117 | if (cartData().website_id !== window.checkout.websiteId) { 118 | customerData.reload(['cart'], false); 119 | } 120 | 121 | return this._super(); 122 | }, 123 | isLoading: ko.observable(false), 124 | initSidebar: initSidebar, 125 | 126 | /** 127 | * @return {Boolean} 128 | */ 129 | closeSidebar: function () { 130 | var minicart = $('[data-block="minicart"]'); 131 | minicart.on('click', '[data-action="close"]', function (event) { 132 | event.stopPropagation(); 133 | minicart.find('[data-role="dropdownDialog"]').dropdownDialog('close'); 134 | }); 135 | 136 | return true; 137 | }, 138 | 139 | /** 140 | * @param {String} productType 141 | * @return {*|String} 142 | */ 143 | getItemRenderer: function (productType) { 144 | return this.itemRenderer[productType] || 'defaultRenderer'; 145 | }, 146 | 147 | /** 148 | * Update mini shopping cart content. 149 | * 150 | * @param {Object} updatedCart 151 | * @returns void 152 | */ 153 | update: function (updatedCart) { 154 | _.each(updatedCart, function (value, key) { 155 | if (!this.cart.hasOwnProperty(key)) { 156 | this.cart[key] = ko.observable(); 157 | } 158 | this.cart[key](value); 159 | }, this); 160 | }, 161 | 162 | /** 163 | * Get cart param by name. 164 | * 165 | * @param {String} name 166 | * @returns {*} 167 | */ 168 | getCartParam: function (name) { 169 | if (!_.isUndefined(name)) { 170 | if (!this.cart.hasOwnProperty(name)) { 171 | this.cart[name] = ko.observable(); 172 | } 173 | } 174 | 175 | return this.cart[name](); 176 | }, 177 | 178 | /** 179 | * Returns array of cart items, limited by 'maxItemsToDisplay' setting. 180 | * 181 | * @returns [] 182 | */ 183 | getCartItems: function () { 184 | var items = this.getCartParam('items') || []; 185 | items = items.slice(parseInt(-this.maxItemsToDisplay, 10)); 186 | 187 | return items; 188 | }, 189 | 190 | /** 191 | * Returns count of cart line items. 192 | * 193 | * @returns {Number} 194 | */ 195 | getCartLineItemsCount: function () { 196 | var items = this.getCartParam('items') || []; 197 | 198 | return parseInt(items.length, 10); 199 | } 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /view/frontend/web/template/minicart/item/default.html: -------------------------------------------------------------------------------- 1 | 22 |
  • 23 |
    24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
    51 | 52 | 53 |
    54 | 55 |
    56 | 57 |
    58 |
    59 | 60 | 61 | 62 | 63 | 64 | 65 |
    66 | 67 |
    68 |
    69 |
    70 | 71 | 72 |
    73 | 74 | 75 |
    76 | 77 | 78 |
    79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
    87 | 89 | 90 | 100 | 101 | 110 |
    111 |
    112 | 113 |
    114 | 115 |
    116 | 117 | 118 | 119 |
    120 | 121 |
    122 | 124 | 125 | 126 |
    127 |
    128 |
    129 |
    130 |
  • 131 | --------------------------------------------------------------------------------