(written by Grok and reviewed by Max Milbers)
Audience: Template builders who override product browse, product details, or cart
Why: AJAX price update, add to cart, child variants, and dynupdate look for hooks, not for your grid classes. You may add Bootstrap. You may not drop the VM names.
Style the card as you like. Keep the contract below or prices freeze and cart JS binds the wrong product.
1. One product = one container
Every product card and the product-details page needs a wrapper JS can closest() to. Preferred in VirtueMart 5:
<div class="product-container …" data-vm="product-container">
… prices, form, custom fields …
</div>
Fallbacks if data-vm="product-container" is missing (do not rely on them in new overrides):
- class
product-container - class
productdetailsorvm-product-details-container(details page)
Several products on one category page: one container per product. Child-variant find and price recalc are scoped to that node. If you wrap the whole grid in one container, changing a dropdown on product A updates product B.
Details layout (core):
<div class="product-container productdetails-view productdetails" data-vm="product-container">
productdetails-view is also the dynupdate target for the whole details page (see below).
2. The add-to-cart form
<form method="post" class="product js-recalculate" action="…" autocomplete="off">
<div class="addtocart-bar">…</div>
<input type="hidden" name="virtuemart_product_id[]" value="123" />
<input type="hidden" name="<?php echo vRequest::getFormToken(); ?>" value="1" />
</form>
| Hook | Role |
|---|---|
form.product.js-recalculate |
Virtuemart.product(form) binds plus/minus, quantity, selects, radios, add-to-cart. js-recalculate is the parent toggleCartButton walks up to. |
.addtocart-bar |
Quantity ± looks for closest('td, .addtocart-bar, form'). |
input[name="virtuemart_product_id[]"] |
Product id for price AJAX. The [] is required. |
input.quantity-input name quantity[] |
Qty. Optional data-init, data-step, data-max (or attribute max). |
.quantity-plus / .quantity-minus |
Buttons. Type button, not submit. |
button[name="addtocart"] (or input / a) |
Add to cart. Class addtocart-button; disabled state uses addtocart-button-disabled. |
| CSRF token hidden | Cached layouts: vmtoken.module.js patches product and checkout forms. Still output the token in PHP. |
Selects and radios inside the form trigger price recalculation. To skip one control, add class no-vm-bind.
Prefer shopFunctionsF::renderVmSubLayout('addtocart') / addtocartbar / prices instead of reinventing the form. Override those sublayouts if you must change markup.
3. Prices that AJAX can rewrite
<div class="product-price" id="productPrice123" data-vm="product-prices">
<span class="PriceSalesPrice">…</span>
</div>
JS looks for [data-vm="product-prices"] or .product-price inside the product container. Fallback: #productPrice{id} on the page.
CurrencyDisplay::createPriceDiv() emits span.Price{Name} (PriceSalesPrice, PriceDiscountAmount, …). Recalculate replaces the inner HTML of those spans. If you print the price as a bare text node with no Price* class, AJAX has nowhere to write.
4. Dynupdate (category, details, cart)
Virtuemart.containerSelector is the node whose inner HTML is swapped after AJAX. Defaults:
| View | Selector you must keep on a wrapper |
|---|---|
| Category browse | .category-view |
| Product details | .productdetails-view |
| Cart | #cart-view |
| Orders | .vm-orders-information |
If the selector is missing, dynupdate cannot insert and may full-navigate. Child variants (cvfind) also set Virtuemart.container from the product wrapper above.
Cart fields that should refresh the cart without a full submit: data-dynamic-update="1" (country is the usual example). Checkout form id: #checkoutForm.
5. Child / Multivariant
- Options:
[data-cvsel](checked) or.avselection. - Must sit inside the same
data-vm="product-container"as the form.
6. Browse sort
Do not replace the order-by dropdown with a list of SEF links. Keep data-vm-browse-sort and the select ids from the orderby sublayout. See the browse-sort Technics article.
7. Events others listen to
updateVirtueMartCartModuleondocument.body— after add to cart. Mini-cart modules bind this.vmReinitChoices— after HTML inject if you haveselect.vm-chzn-selectin FE.
8. Do’s and don’ts
- Do add your own classes (
card,col-6, …) next to the VM hooks. - Do not drop
product-container/data-vm="product-container"for a pretty card wrapper with no hook. - Do not reuse one container for the whole listing.
- Do not rename
quantity[]orvirtuemart_product_id[]. - Do not style away
.PriceSalesPriceby removing the span. - Do not change
#cart-viewto#my-cartwithout settingVirtuemart.containerSelectoryourself.
9. Where core does this
sublayouts/bs5-products.php,bs5-default.php(details)sublayouts/prices.php/bs5-prices.phpsublayouts/addtocart.php,addtocartbar.phpassets/js/product/vmprices.module.js,cvfind.module.js,toggleCartButton.module.jsassets/js/ajax/vmdynupdate.module.js