ProductAvailabilityAction
Purpose#
ProductAvailabilityAction is the shared primary action button for a product or variant. It picks one of several states (select options, add to cart, notify me, pending, unavailable) from the product's hydrated quantity constraints and the store's Stock Alerts setting.When the item is out of stock and Stock Alerts is active, it shows a Notify me button that collects the shopper's email for a back-in-stock notification.Where it's rendered#
In product cards and variant pickers, inside the parent's Alpine scope (Reusables/ProductGridItem, Reusables/ProductGridItemClassic, Reusables/VariantContent, Reusables/VariantContentClassic):{% render 'Reusables\\ProductAvailabilityAction\\Default', id: product.id, title: product.title, suppressSelectState: true %}
In Reusables/ShoppingLists/Default.liquid, notify-only and icon-only for one row:{% render 'Reusables\\ProductAvailabilityAction\\Default', idJs: 'item.product.id', forceNotifyOnly: true, explicitVariantJs: 'getVariant(item, item.productVariantId)', iconOnly: true %}
id: product id (string literal). Ignored when idJs is set.
idJs: JS expression evaluated in the parent scope that returns the product id.
title: product title, appended to the button aria-labels.
suppressSelectState (default false): shows a disabled incomplete button instead of select when dimensions are pickable inline.
forceNotifyOnly (default false): only resolves pending / cart / notify / unavailable for the variant returned by explicitVariantJs.
explicitVariantJs: JS expression that returns the variant; wrapped in a resolver function.
closeModalOnAdd (default false): passed to the parent handleAddToCart.
iconOnly: renders small icon buttons without text.
btnStyle: outline for btn--primary-outline, otherwise btn--primary.
Global data read: GlobalData.Settings.isStockAlertsActive, and GlobalData.User.Email when authenticated (pre-fills the email field).Model shape (storefront example)#
{
"id": "Product Id",
"title": "Sample title",
"suppressSelectState": true,
"translations": {
"selectOptions": "Sample translation",
"addToCart": "Sample translation",
"notifyMe": "Sample translation",
"...": "..."
}
}
Template behavior (Liquid + Alpine)#
Renders one x-if template per availabilityState:pending: loading placeholder (aria-hidden) while constraints are hydrating, so the wrong state never flashes.
select: opens the shared product modal (openSelect()).
incomplete: disabled add-to-cart button until every dimension is selected.
cart: calls the parent handleAddToCart(...); disabled while isAdding.
notify: opens the email modal (openNotifyModal()); disabled for 3 seconds after a successful subscription.
unavailable: disabled add-to-cart button (out of stock and Stock Alerts off).
Outside forceNotifyOnly mode the reusable relies on the parent scope for product, selectedVariant, hasMultipleVariants, allDimensionsSelected, constraintsPending, isAdding, handleAddToCart and uniqueKey.Data contract (JS runtime)#
The state is computed from the variant's quantityConstraints.additive.isValid. A variant without quantityConstraints is treated as still hydrating (pending).JavaScript#
Global object#
Global object: productavailabilityactionreusabledefaultx-data="productavailabilityactionreusabledefault.initComponent(productId, notifyModalTitle, notifyEmailLabel, notifyEmailPlaceholder, notifySubmitLabel, notifyErrorMessage, notifySuccessMessage, suppressSelectState, stockAlertsActive, userEmail, explicitVariantResolver, forceNotifyOnly, closeModalOnAdd)"
resolveEffectiveVariant#
resolveEffectiveVariant(product, selectedVariant) returns selectedVariant, or the only variant of a single-variant product, or null.
initComponent#
Returns the Alpine state (productId, flags, userEmail, notifySubmitted) and the members below.
explicitVariant#
Getter that calls explicitVariantResolver() on every read (or returns null). The result is never cached, so Alpine picks up constraints hydrated after the first paint.
availabilityState#
Getter that returns select | incomplete | pending | notify | cart | unavailable.forceNotifyOnly: pending without constraints, cart when orderable, otherwise notify or unavailable.
Multi-variant with an incomplete selection: incomplete when suppressSelectState, else select.
pending while the parent's constraintsPending is set or constraints are missing.
Out of stock: notify when Stock Alerts is active, otherwise unavailable.
canAddToCart#
Returns true when any variant is orderable (products without dimensions) or the effective variant is orderable.
openSelect#
Opens $store.productModal for the parent product (keyed by uniqueKey or the product id).
openNotifyModal#
Clears toasts and opens Alpine.store("modal") with a required email input, pre-filled with userEmail, and a submit button that calls submitNotify.
submitNotify#
submitNotify(modalData, resetLoading) (async) calls servicesreusabledefault.subscribeStockAlert({ productId, productVariantId, email }) for the explicit or effective variant.On failure: error toast and resetLoading().
On success: closes the modal, shows a success toast and sets notifySubmitted for 3 seconds.
Global Alpine stores#
Alpine.store("modal"): email collection modal.
Alpine.store("toast"): success/error feedback.
$store.productModal: variant selection modal.
Services / API calls#
servicesreusabledefault.subscribeStockAlert(data) → POST /api/stockalert/subscribe.
Dependencies#
Reusables/Services (servicesreusabledefault)
A parent Alpine scope (product card / variant picker) unless forceNotifyOnly is used.
Notes#
Stock Alerts is a gated module plus a per-store setting. When it is off, out-of-stock items show a disabled add-to-cart button instead of a notify button that would fail silently.
Modified at 2026-10-06 10:11:35