3
\$\begingroup\$

I'm very new to jQuery and I managed to make this code work but I'm pretty sure that it could be made in a more efficient way. For example, it should be a way to send the id as a parameter instead of writing the same code for each id.

But beside of that, are there other things that could be made in a better way?

// selecting elements
$('#0')
  .find('.cart-quantity-input')
  .change(function () {
    var quantity = parseFloat($('#0').find('.cart-quantity-input').val());
    var price = parseFloat(
      $('#0').find('.cart-price').text().replace('€', '')
    );

    $('#0')
      .find('.cart-subtotal')
      .text(quantity * price + '€');
  });

$('#1')
  .find('.cart-quantity-input')
  .change(function () {
    var quantity = parseFloat($('#1').find('.cart-quantity-input').val());
    var price = parseFloat(
      $('#1').find('.cart-price').text().replace('€', '')
    );

    $('#1')
      .find('.cart-subtotal')
      .text(quantity * price + '€');
  });

$('#2')
  .find('.cart-quantity-input')
  .change(function () {
    var quantity = parseFloat($('#2').find('.cart-quantity-input').val());
    var price = parseFloat(
      $('#2').find('.cart-price').text().replace('€', '')
    );

    $('#2')
      .find('.cart-subtotal')
      .text(quantity * price + '€');
  });

// compute subtotal sum
function subTotal(element) {
  var quantity = element.siblings('.cart-quantity-input').val();
  var price = parseFloat(
    element.closest('.cart-row').find('.cart-price').text().replace('€', '')
  );

  $(element)
    .closest('.cart-row')
    .find('.cart-subtotal')
    .text(price * quantity + '€');
}

// compute total sum and number of items
function computeTotalValues() {
  var total = [...$('.cart-items .cart-subtotal')]
    .map((subtotalElm) => Number(subtotalElm.textContent.replace('€', '')))
    .reduce((a, b) => a + b, 0);

  $('.cart-total-price').text(total + '€');

  const totalQuantity = $.map(
    $('.cart-items .cart-quantity-input'),
    (input) => +input.value
  ).reduce((a, b) => a + b, 0);

  $('.items-number').text(totalQuantity + ' items');
}

// increase number of items
$('.plus').on('click', function () {
  var increment = parseInt($(this).siblings('input').val());

  increment++;
  $(this).siblings('input').val(increment);
  subTotal($(this));
  computeTotalValues();
});

// decrease number of items
$('.minus').on('click', function () {
  var decrement = parseInt($(this).siblings('input').val());

  if (decrement) {
    decrement--;
  }
  $(this).siblings('input').val(decrement);
  subTotal($(this));
  computeTotalValues();
});

// checkout button clicked
$('.btn-primary').on('click', function () {
  if (
    confirm('Are you sure you want to checkout? \nYour products will disappear')
  ) {
    $('.cart-total-price').text('0 €');
    $('.items-number').text('0 items');
    $('.cart-quantity-input').val('0');
  }
});
* {
  box-sizing: border-box;
  font-family: sans-serif;
  color: #777;
}

html,
body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background-color: #261741;
}

.container {
  display: flex;
  width: 55%;
  min-width: 700px;
  margin: 0 auto;
  border-radius: 10px;
  overflow: hidden;
  margin-top: 50px;
  min-height: 500px;
}

.first-section {
  background-color: #ffffff;
  position: relative;
  width: 70%;
  padding-left: 40px;
  padding-top: 10px;
}

.second-section {
  background-color: #f3f3f3;
  position: relative;
  width: 30%;
  padding: 10px 20px 20px 20px;
}

.section-header {
  font-weight: bold;
  color: #333;
  font-size: 18px;
}

.summary-title {
  display: flex;
}

.cart-items {
  margin-bottom: 60px;
}

.btn {
  text-align: center;
  vertical-align: middle;
  padding: 0.67em 0.67em;
  cursor: pointer;
}

.btn-primary {
  color: white;
  position: absolute;
  background-color: #9100ff;
  border: none;
  border-radius: 4px;
  font-weight: bold;
  display: block;
  font-size: 16px;
  bottom: 20px;
  width: 83%;
}

.btn-primary:hover {
  background-color: #000099;
}

.main-line {
  margin-right: 20px;
}

.cart-header {
  font-weight: bold;
  font-size: 15 px;
}

.cart-column {
  display: flex;
  align-items: center;
  color: black;
  margin-right: 1.5em;
  padding-bottom: 10px;
  margin-top: 10px;
}

.cart-column-title {
  display: flex;
  align-items: center;
  color: lightgray;
  margin-right: 1.5em;
  padding-bottom: 10px;
  margin-top: 10px;
}

.cart-row {
  display: flex;
}

.cart-item {
  width: 55%;
  font-size: 14px;
  text-transform: uppercase;
}

.cart-price {
  display: flex;
  justify-content: center;
  width: 15%;
  font-size: 14px;
  text-transform: uppercase;
}

.cart-quantity {
  display: flex;
  justify-content: center;
  width: 15%;
  font-size: 14px;
  text-transform: uppercase;
}

.cart-subtotal {
  display: flex;
  justify-content: center;
  width: 15%;
  font-size: 14px;
  text-transform: uppercase;
}

.item-info {
  display: flex;
  flex-direction: column;
}

.cart-item-title {
  color: #bc6cff;
  margin-left: 10px;
  font-size: 15px;
  font-weight: bold;
}

.cart-item-description {
  color: lightslategray;
  margin-left: 10px;
  font-size: 10px;
}

.cart-item-image {
  width: 75px;
  height: auto;
  border-radius: 10px;
}

.cart-quantity-input {
  height: 34px;
  width: 34px;
  border-radius: 5px;
  border: 2px solid lightgray;
  color: #333;
  padding: 0;
  text-align: center;
  font-size: 1.2em;
  margin-right: 15px;
  margin-left: 15px;
}

.cart-row:last-child .cart-column {
  border: none;
}

.cart-total {
  text-align: end;
  margin-top: 10px;
  margin-right: 10px;
}

.cart-total-title {
  font-size: 14px;
  color: black;
  margin-right: 20px;
  text-transform: uppercase;
}

.cart-total-price {
  color: #333;
  font-size: 1.1em;
}

.numerical-values {
  color: #bc6cff;
  font-size: 34px;
}

.numerical-values:hover {
  cursor: pointer;
}

.items-number {
  display: flex;
}

.items-and-total {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 17px 0px 0px 17px;
}

.items-and-total-final {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 0px 0px 0px 0px;
}

.items-discount {
  display: flex;
  text-transform: uppercase;
  padding-top: 15px;
}

.final-part {
  margin-bottom: 70px;
  bottom: 0px;
  position: absolute;
  width: 82%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!DOCTYPE html>
<html>
  <head>
    <title>Shopping cart</title>
    <meta name="description" content="This is the description" />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="container">
      <div class="first-section">
        <h2 class="section-header">Shopping cart</h2>
        <hr class="main-line" />
        <div class="cart-row">
          <span class="cart-item cart-header cart-column-title"
            >Product details</span
          >
          <span class="cart-quantity cart-header cart-column-title"
            >Quantity</span
          >
          <span class="cart-price cart-header cart-column-title">Price</span>
          <span class="cart-subtotal cart-header cart-column-title">Total</span>
        </div>
        <div class="cart-items">
          <div class="cart-row" id="0">
            <div class="cart-item cart-column">
              <img
                class="cart-item-image"
                src="Images/goku.png"
                width="100"
                height="100"
              />
              <div class="item-info">
                <span class="cart-item-title">Goku POP</span>
                <span class="cart-item-description">Product code GOKU</span>
              </div>
            </div>
            <div class="cart-quantity cart-column">
              <div class="numerical-values minus">-</div>
              <input
                class="cart-quantity-input"
                disabled
                type="text"
                value="0"
              />
              <div class="numerical-values plus">+</div>
            </div>
            <span class="cart-price cart-column">5€</span>
            <span class="cart-subtotal cart-column">0€</span>
          </div>
          <div class="cart-row" id="1">
            <div class="cart-item cart-column">
              <img
                class="cart-item-image"
                src="Images/naruto.png"
                width="100"
                height="100"
              />
              <div class="item-info">
                <span class="cart-item-title">Naruto POP</span>
                <span class="cart-item-description">Product code NARUTO</span>
              </div>
            </div>
            <div class="cart-quantity cart-column">
              <div class="numerical-values minus">-</div>
              <input
                class="cart-quantity-input"
                id="naruto"
                disabled
                type="text"
                value="0"
              />
              <div class="numerical-values plus">+</div>
            </div>
            <span class="cart-price cart-column">20€</span>
            <span class="cart-subtotal cart-column">0€</span>
          </div>
          <div class="cart-row" id="2">
            <div class="cart-item cart-column">
              <img
                class="cart-item-image"
                src="Images/luffy.png"
                width="100"
                height="100"
              />
              <div class="item-info">
                <span class="cart-item-title">Luffy POP</span>
                <span class="cart-item-description">Product code LUFFY</span>
              </div>
            </div>
            <div class="cart-quantity cart-column">
              <div class="numerical-values minus">-</div>
              <input
                class="cart-quantity-input"
                id="luffy"
                disabled
                type="text"
                value="0"
              />
              <div class="numerical-values plus">+</div>
            </div>
            <span class="cart-price cart-column">7.5€</span>
            <span class="cart-subtotal cart-column">0€</span>
          </div>
        </div>
      </div>
      <div class="second-section">
        <div class="cart-total">
          <h2 class="section-header summary-title">Order Summary</h2>
          <hr />
          <div class="items-and-total">
            <span class="items-number">0 items</span>
            <span class="cart-total-price">0€</span>
          </div>
          <hr />
          <div class="final-part">
            <hr />
            <div class="items-and-total-final">
              <div class="cart-total-title">Total cost</div>
              <span class="cart-total-price">0€</span>
            </div>
          </div>
        </div>
        <button class="btn btn-primary" type="button">Checkout</button>
      </div>
    </div>
  </body>
  <script
    src="https://code.jquery.com/jquery-3.5.1.min.js"
    integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
    crossorigin="anonymous"
  ></script>
  <script src="store.js" async></script>
</html>

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

DRYing

  • The separate numeric-indexed IDs are used only to get to their children .cart-quantity-input elements, so you can just select those elements directly instead and remove the IDs. When you have to select something else in the same row, first use:

    const $row = $(this).closest('.cart-row');
    

    and you'll get to the row, from which you can, with .find, navigate to the price, quantity, and total elements, whichever is needed.

    That said, given only the code in the question, the change handler looks to be entirely superfluous, since the plus and minus buttons already calculate and render the new item subtotals - there doesn't seem to be any need for a separate handler on top of those, unless some other part of the page can change it too.

  • Also, rather than having a separate handler for plus and minus buttons, you can combine those into a single handler instead, and check the class of the clicked element to determine whether to add or subtract one from the current quantity:

    $('.plus, .minus').on('click', function () {
      const currentValue = parseInt($(this).siblings('input').val());
      const addValue = $(this).is('.plus') ? 1 : -1;
      $(this).siblings('input').val(Math.max(0, currentValue + addValue));
    

Other suggestions:

The + and - buttons are selectable currently, which looks weird if one clicks more than once in quick succession. They act as buttons, not as plain text, so having the text in the button be selectable doesn't seem right. Consider adding:

.numerical-values {
  user-select: none;
}

After content? Rather than having to add and replace out every time you want to set or get a price, consider using :after instead:

.cart-price:after, .cart-subtotal:after {
  content: "€";
}

Precise names Imagine you hadn't seen the code before and saw this line: subTotal($(this)); What does it do? It's not very clear. Consider calling it something more precise, like calculateAndRenderSubtotal, also making the comment above the function entirely superfluous:

// (Current code is:)

// compute subtotal sum
function subtotal(element) {

Use modern syntax everywhere You're using ES2015 in a number of places in the code. If you want to use it (which you should, it makes things readable and concise!), best to use ES2015+ syntax everywhere you can - in particular, use const instead of var (or, use let when the variable needs to be reassigned).

Checkout bug? You have:

$('.btn-primary').on('click', function () {
  if (
    confirm('Are you sure you want to checkout? \nYour products will disappear')
  ) {
    $('.cart-total-price').text('0 €');
    $('.items-number').text('0 items');
    $('.cart-quantity-input').val('0');
  }
});

But this doesn't clear the item subtotals. Reset them all to 0.

$('.cart-subtotal').text(0);

Also consider replacing the confirm with a proper modal - the browsers built-in alert boxes block the browser, preventing JavaScript from running on the page, and making the page inaccessible until the box is cleared. It's not user-friendly.

Combine select + .find I refactored it out while DRYing, but whenever you have code like:

$('#0')
  .find('.cart-quantity-input')

This simplifies to:

$('#0 .cart-quantity-input')

using a space, the descendant combinator.

Suggested new code:

/*
// This section can be removed entirely if 
// calculateAndRenderSubtotal is the only place
// where a row's quantity can change from

$('.cart-quantity-input').on('change', function() {
  const quantity = this.value;
  const $row = $(this).closest('.cart-row');
  const price = $row.find('.cart-price').text();
  $row.find('.cart-subtotal').text(quantity * price);
});
*/

function calculateAndRenderSubtotal(element) {
  const quantity = element.siblings('.cart-quantity-input').val();
  const price = parseFloat(
    element.closest('.cart-row').find('.cart-price').text()
  );

  $(element)
    .closest('.cart-row')
    .find('.cart-subtotal')
    .text(price * quantity);
}

// compute total sum and number of items
function computeTotalValues() {
  const total = [...$('.cart-items .cart-subtotal')]
    .map((subtotalElm) => Number(subtotalElm.textContent))
    .reduce((a, b) => a + b, 0);

  $('.cart-total-price').text(total);

  const totalQuantity = $.map(
    $('.cart-items .cart-quantity-input'),
    (input) => +input.value
  ).reduce((a, b) => a + b, 0);

  $('.items-number').text(totalQuantity + ' items');
}

// increase number of items
$('.plus, .minus').on('click', function() {
  const currentValue = parseInt($(this).siblings('input').val());
  const addValue = $(this).is('.plus') ? 1 : -1;
  $(this).siblings('input').val(Math.max(0, currentValue + addValue));
  calculateAndRenderSubtotal($(this));
  computeTotalValues();
});

// checkout button clicked
$('.btn-primary').on('click', function() {
  if (
    confirm('Are you sure you want to checkout? \nYour products will disappear')
  ) {
    $('.cart-total-price').text('0');
    $('.items-number').text('0 items');
    $('.cart-quantity-input').val('0');
    $('.cart-subtotal').text(0);
  }
});
* {
  box-sizing: border-box;
  font-family: sans-serif;
  color: #777;
}

html,
body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  background-color: #261741;
}

.container {
  display: flex;
  width: 55%;
  min-width: 700px;
  margin: 0 auto;
  border-radius: 10px;
  overflow: hidden;
  margin-top: 50px;
  min-height: 500px;
}

.first-section {
  background-color: #ffffff;
  position: relative;
  width: 70%;
  padding-left: 40px;
  padding-top: 10px;
}

.second-section {
  background-color: #f3f3f3;
  position: relative;
  width: 30%;
  padding: 10px 20px 20px 20px;
}

.section-header {
  font-weight: bold;
  color: #333;
  font-size: 18px;
}

.summary-title {
  display: flex;
}

.cart-items {
  margin-bottom: 60px;
}

.btn {
  text-align: center;
  vertical-align: middle;
  padding: 0.67em 0.67em;
  cursor: pointer;
}

.btn-primary {
  color: white;
  position: absolute;
  background-color: #9100ff;
  border: none;
  border-radius: 4px;
  font-weight: bold;
  display: block;
  font-size: 16px;
  bottom: 20px;
  width: 83%;
}

.btn-primary:hover {
  background-color: #000099;
}

.main-line {
  margin-right: 20px;
}

.cart-header {
  font-weight: bold;
  font-size: 15 px;
}

.cart-column {
  display: flex;
  align-items: center;
  color: black;
  margin-right: 1.5em;
  padding-bottom: 10px;
  margin-top: 10px;
}

.cart-column-title {
  display: flex;
  align-items: center;
  color: lightgray;
  margin-right: 1.5em;
  padding-bottom: 10px;
  margin-top: 10px;
}

.cart-row {
  display: flex;
}

.cart-item {
  width: 55%;
  font-size: 14px;
  text-transform: uppercase;
}

.cart-price:after,
.cart-subtotal:after {
  content: "€";
}

.cart-price {
  display: flex;
  justify-content: center;
  width: 15%;
  font-size: 14px;
  text-transform: uppercase;
}

.cart-quantity {
  display: flex;
  justify-content: center;
  width: 15%;
  font-size: 14px;
  text-transform: uppercase;
}

.cart-subtotal {
  display: flex;
  justify-content: center;
  width: 15%;
  font-size: 14px;
  text-transform: uppercase;
}

.item-info {
  display: flex;
  flex-direction: column;
}

.cart-item-title {
  color: #bc6cff;
  margin-left: 10px;
  font-size: 15px;
  font-weight: bold;
}

.cart-item-description {
  color: lightslategray;
  margin-left: 10px;
  font-size: 10px;
}

.cart-item-image {
  width: 75px;
  height: auto;
  border-radius: 10px;
}

.cart-quantity-input {
  height: 34px;
  width: 34px;
  border-radius: 5px;
  border: 2px solid lightgray;
  color: #333;
  padding: 0;
  text-align: center;
  font-size: 1.2em;
  margin-right: 15px;
  margin-left: 15px;
}

.cart-row:last-child .cart-column {
  border: none;
}

.cart-total {
  text-align: end;
  margin-top: 10px;
  margin-right: 10px;
}

.cart-total-title {
  font-size: 14px;
  color: black;
  margin-right: 20px;
  text-transform: uppercase;
}

.cart-total-price {
  color: #333;
  font-size: 1.1em;
}

.numerical-values {
  color: #bc6cff;
  font-size: 34px;
  user-select: none;
}

.numerical-values:hover {
  cursor: pointer;
}

.items-number {
  display: flex;
}

.items-and-total {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 17px 0px 0px 17px;
}

.items-and-total-final {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 0px 0px 0px 0px;
}

.items-discount {
  display: flex;
  text-transform: uppercase;
  padding-top: 15px;
}

.final-part {
  margin-bottom: 70px;
  bottom: 0px;
  position: absolute;
  width: 82%;
}
<div class="container">
  <div class="first-section">
    <h2 class="section-header">Shopping cart</h2>
    <hr class="main-line" />
    <div class="cart-row">
      <span class="cart-item cart-header cart-column-title">Product details</span
          >
          <span class="cart-quantity cart-header cart-column-title"
            >Quantity</span
          >
          <span class="cart-price cart-header cart-column-title">Price</span>
      <span class="cart-subtotal cart-header cart-column-title">Total</span>
    </div>
    <div class="cart-items">
      <div class="cart-row" id="0">
        <div class="cart-item cart-column">
          <img class="cart-item-image" src="Images/goku.png" width="100" height="100" />
          <div class="item-info">
            <span class="cart-item-title">Goku POP</span>
            <span class="cart-item-description">Product code GOKU</span>
          </div>
        </div>
        <div class="cart-quantity cart-column">
          <div class="numerical-values minus">-</div>
          <input class="cart-quantity-input" disabled type="text" value="0" />
          <div class="numerical-values plus">+</div>
        </div>
        <span class="cart-price cart-column">5</span>
        <span class="cart-subtotal cart-column">0</span>
      </div>
      <div class="cart-row" id="1">
        <div class="cart-item cart-column">
          <img class="cart-item-image" src="Images/naruto.png" width="100" height="100" />
          <div class="item-info">
            <span class="cart-item-title">Naruto POP</span>
            <span class="cart-item-description">Product code NARUTO</span>
          </div>
        </div>
        <div class="cart-quantity cart-column">
          <div class="numerical-values minus">-</div>
          <input class="cart-quantity-input" id="naruto" disabled type="text" value="0" />
          <div class="numerical-values plus">+</div>
        </div>
        <span class="cart-price cart-column">20</span>
        <span class="cart-subtotal cart-column">0</span>
      </div>
      <div class="cart-row" id="2">
        <div class="cart-item cart-column">
          <img class="cart-item-image" src="Images/luffy.png" width="100" height="100" />
          <div class="item-info">
            <span class="cart-item-title">Luffy POP</span>
            <span class="cart-item-description">Product code LUFFY</span>
          </div>
        </div>
        <div class="cart-quantity cart-column">
          <div class="numerical-values minus">-</div>
          <input class="cart-quantity-input" id="luffy" disabled type="text" value="0" />
          <div class="numerical-values plus">+</div>
        </div>
        <span class="cart-price cart-column">7.5</span>
        <span class="cart-subtotal cart-column">0</span>
      </div>
    </div>
  </div>
  <div class="second-section">
    <div class="cart-total">
      <h2 class="section-header summary-title">Order Summary</h2>
      <hr />
      <div class="items-and-total">
        <span class="items-number">0 items</span>
        <span class="cart-total-price">0€</span>
      </div>
      <hr />
      <div class="final-part">
        <hr />
        <div class="items-and-total-final">
          <div class="cart-total-title">Total cost</div>
          <span class="cart-total-price">0€</span>
        </div>
      </div>
    </div>
    <button class="btn btn-primary" type="button">Checkout</button>
  </div>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

\$\endgroup\$

Your Answer

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

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