├── 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 | 
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 | 
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 |
120 |
121 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------