(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 productdetails or vm-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

  • updateVirtueMartCartModule on document.body — after add to cart. Mini-cart modules bind this.
  • vmReinitChoices — after HTML inject if you have select.vm-chzn-select in 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[] or virtuemart_product_id[].
  • Do not style away .PriceSalesPrice by removing the span.
  • Do not change #cart-view to #my-cart without setting Virtuemart.containerSelector yourself.

9. Where core does this

  • sublayouts/bs5-products.php, bs5-default.php (details)
  • sublayouts/prices.php / bs5-prices.php
  • sublayouts/addtocart.php, addtocartbar.php
  • assets/js/product/vmprices.module.js, cvfind.module.js, toggleCartButton.module.js
  • assets/js/ajax/vmdynupdate.module.js