Reputation: 3422
I am building a cart with stimulus. The cart recap page displays multiple cart items. A cart item has a unit price and a quantity.
Each cart item component has a quantity selector, when the quantity number changes the total price of the cartitem element is recalculated totalCartItem = unitPrice x Quantity
and the total price if the cart must also be recalculated totalCartPrice = sum(totalCartItem)
Here's a simplification of the html structure
<div class="cart" data-controller='cart'>
<div class="cart-item" data-controller='cart-item'>
<p class='unit-price'>20</p>
<input type="number" name="quantity" value="1">
<p class='total-price' data-target='cart-item.totalPrice' data-action="change->cart-item#update" data-target='cart-item.quantity'>20</p>
</div>
<div class="cart-item" data-controller='cart-item'>
<p class='unit-price'>10</p>
<input type="number" name="quantity" value="2" data-action="change->cart-item#update" data-target='cart-item.quantity'>
<p class='total-price' data-target='cart-item.totalPrice'>20</p>
</div>
<div class="cart-total">
40
</div>
</div>
My cart item controller works perfectly fine and updates correctly the totalPriceFor a cart item in the UI.
export default class extends Controller {
static targets = ["totalPrice", "quantity"]
static values = {
unitPrice: String,
}
connect() {
console.log("hello from StimulusJS")
}
update(){
this.totalPriceTarget.textContent = parseFloat(this.unitPriceValue) * parseInt(this.quantityTarget.value)
}
}
However, I am now lost on how to update the totalCartPrice
. I feel like this should be the responsability of the cartController
that wrapps the cartItems elements, but I have no idea on what id the correct way to achieve this.
I feel like I should add change->cart#update
on each number input selector for quantity for each cart-item, but then what should I add to the cart#update method to recalculate the total from each individual cart item ?
export default class extends Controller {
static targets = ["total"]
connect() {
console.log("hello from StimulusJS")
}
update(){
// let details = event.detail;
// this.cartItemChildren.forEach((item) => console.log(item.totalValue))
}
}
Upvotes: 2
Views: 1257
Reputation: 5186
As per the Stimulus documentation, it is always best to start with the HTML. By extension it is best to structure your HTML in an accessible way and see what the browser gives you without reinventing things.
When you are building forms, it is best to use the form
element, even if this form is not being submitted. https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
There is also a very useful output
element that helps you assign an output value based on some input value. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output#attr-for
Finally, there is a DOM API for the form element that lets you access various parts of the form via their name
attribute, this is form.elements
. https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
Bringing this together we can start with the following HTML...
input
has a unique ID and each output
element references the input via this id using the for
attribute - this will be better for accessibility but comes in handy when we want to parse the values and update outputs.<form class="cart" data-controller='cart' data-action="change->cart#updateTotals">
<div class="cart-item">
<span class='unit-price' data-price-for="cart-item-1">20</span>
x
<input type="number" name="quantity" value="1" id="cart-item-1">
=
<output class="total-price" for="cart-item-1" name="item-total">20</output>
</div>
<div class="cart-item">
<span class='unit-price' data-price-for="cart-item-2">10</span>
x
<input type="number" name="quantity" value="1" id="cart-item-2">
=
<output class="total-price" for="cart-item-2" name="item-total">10</output>
</div>
<div class="cart-total">
Total: <span data-cart-target="total">30</span>
</div>
</form>
this.element
on the base form's attached controller.update
method and trying to be more specific with updateTotals
.elements
API on the form to get the elements with the name 'item-total'
(note that output names do not get submitted). https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elementsfor
and the input's id and then the data attribute to find the price.import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['total'];
connect() {
this.updateTotals();
}
updateTotals() {
// parse the form elements to get all data
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
const cartItems = [...this.element.elements['item-total']].map(
(outputElement) => {
const forId = outputElement.getAttribute('for');
const priceElement = document.querySelector(`[data-price-for='${forId}']`);
const price = Number(priceElement.textContent);
const quantity = Number(document.getElementById(forId).value);
return { outputElement, total: quantity * price };
}
);
// calculate the grand total
const grandTotal = cartItems.reduce(
(runningTotal, { total }) => runningTotal + total,
0
);
// update the grand total
this.totalTarget.textContent = grandTotal;
// update totals for each cart item
cartItems.forEach(({ outputElement, total }) => {
outputElement.textContent = total;
});
console.log('form updated', { grandTotal, cartItems });
}
}
Upvotes: 3