
{{#capture "default_variant_id"}}
{{default product.selected_or_first_available_variant.id (get "id" (first product.variants))}}
{{/capture}}
<script src="{{asset_url 'component-product-form.js'}}" defer="defer"></script>
<div class="article-template__right-Products">
<h1>Products from the article</h1>
{{#if article.metafields.is_show_product.is_show_product.value}}
{{#each section.blocks as |block|}}
{{#if block.type == "product_picker_myself"}}
<div>
<div class="product-art-right-box">
<a href="{{block.settings.product_id.url}}">
<div class="product-image-right-box">
<img src="{{block.settings.product_id.featured_image}}" alt="Product Image">
<div class="product-right-favorite-icon" onclick="toggleFavorite()">❤</div>
</div>
{{#if block.settings.product_id.available}}
<div class="product-right-sale-status">Sale</div>
{{else}}
<div class=" product-right-sale-status-out">Sold out</div>
{{/if}}
<div class="product-right-product-title">{{block.settings.product_id.title}}</div>
<div class="product-right-product-description">{{{block.settings.product_id.description}}}</div>
<div class="product-right-reviews">
<div class="product-right-stars">★★★★★</div>
<div class="product-right-review-count">6 Reviews</div>
</div>
</a>
<div class="product-right-cart-module">
<div class="product-right-prices">
<div class="product-right-original-price">{{money block.settings.product_id.compare_at_price_max}}</div>
<div class="product-right-current-price">{{money block.settings.product_id.price}}</div>
</div>
{{!-- <button class="product-right-cart-button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="product-right-cart-icon">
<path d="M7 4h-2l-3 9v1c0 1.1.9 2 2 2h14v-2H4.4l1.1-3H16c.6 0 1.1-.4 1.2-.9l1.8-8.1H4.2l-.2-.8zM7 18c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm10 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
</svg>
</button> --}}
<div>
{{assign "product_form_id" block.settings.product_id.id}}
<product-form
id="{{append product_form_id '__wrapper'}}"
class="product-form"
data-default-error-message="{{t 'products.general.no_product_data_found'}}"
>
<form method="post" action="/api/carts/ajax-cart/add.js" id="productFrom" id={{product_form_id}}>
<input type="number" required step="1" form="product-form-{{section.id}}" name="quantity" min='1' max='999' value='1'/ hidden>
<input type="hidden" name="id" value="{{default block.settings.product_id.selected_or_first_available_variant.id (get 'id' (first block.settings.product_id.variants))}}" disabled class="ids"/>
<button class="product-right-cart-button" type="submit" name="add" id="{{product_form_id}}-submit">
{{snippet "carticon"}}
</button>
<div class="loading-overlay__spinner ">
<svg class="icon icon-loading" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3337 9.99984C18.3337 14.6022 14.6027 18.3332 10.0003 18.3332C5.39795 18.3332 1.66699 14.6022 1.66699 9.99984C1.66699 5.39746 5.39795 1.6665 10.0003 1.6665" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"></path>
</svg>
</div>
</form>
</product-form>
</div>
</div>
</div>
</div>
{{/if}}
{{/each}}
{{/if}}
</div>
按钮:
<div>
{{assign "product_form_id" block.settings.product_id.id}}
<product-form
id="{{append product_form_id '__wrapper'}}"
class="product-form"
data-default-error-message="{{t 'products.general.no_product_data_found'}}"
>
<form method="post" action="/api/carts/ajax-cart/add.js" id="productFrom" id={{product_form_id}}>
<input type="number" required step="1" form="product-form-{{section.id}}" name="quantity" min='1' max='999' value='1'/ hidden>
<input type="hidden" name="id" value="{{default block.settings.product_id.selected_or_first_available_variant.id (get 'id' (first block.settings.product_id.variants))}}" disabled class="ids"/>
<button class="product-right-cart-button" type="submit" name="add" id="{{product_form_id}}-submit">
{{snippet "carticon"}}
</button>
<div class="loading-overlay__spinner ">
<svg class="icon icon-loading" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.3337 9.99984C18.3337 14.6022 14.6027 18.3332 10.0003 18.3332C5.39795 18.3332 1.66699 14.6022 1.66699 9.99984C1.66699 5.39746 5.39795 1.6665 10.0003 1.6665" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"></path>
</svg>
</div>
</form>
</product-form>
</div>
js代码:
defineCustomElement('product-form', () => {
return class ProductForm extends HTMLElement {
constructor() {
super();
this.form = this.querySelector('form');
if (!this.form) {
return;
}
this.options = {
onErrorShowToast: this.dataset.onErrorShowToast || false,
};
this.fetchInstance = Promise.resolve();
this.form.querySelector('[name=id]').disabled = false;
this.cart = document.querySelector('cart-notification') || document.querySelector('cart-drawer-entry');
this.submitButton = this.querySelector('[type="submit"]');
this.submitButton.addEventListener('click', this.submitButtonClickHandler.bind(this));
}
// Because the editor hijack the a tag click event, the click event needs to be bound to prevent bubbling
submitButtonClickHandler(e) {
e.preventDefault();
e.stopPropagation();
this.onSubmitHandler();
}
onSubmitHandler() {
if (this.submitButton.classList.contains('disabled') || this.submitButton.classList.contains('loading')) {
return;
}
const formData = new FormData(this.form);
const currentVariantId = formData.get('id');
if (!currentVariantId) {
window.Shopline.utils.toast.open({
duration: 2000,
content: t('products.product_list.select_product_all_options'),
});
return;
}
this.handleErrorMessage();
this.submitButton.classList.add('loading');
this.querySelector('.loading-overlay__spinner').classList.add('display-flex');
const config = window.fetchConfig();
config.headers['X-Requested-With'] = 'XMLHttpRequest';
delete config.headers['Content-Type'];
this.ensureQuantity(formData);
formData.delete('returnTo');
const isCartPage = document.body.getAttribute('data-template') === 'cart';
if (this.cart && !isCartPage) {
formData.append(
'sections',
this.cart.getSectionsToRender().map((section) => section.id),
);
formData.append('sections_url', window.location.pathname);
}
config.body = formData;
const fetchInstance = fetch(`${window.routes.cart_add_url}`, config).then((response) => response.json());
this.fetchInstance = fetchInstance;
fetchInstance
.then((response) => {
if (response.message) {
this.handleErrorMessage(response.message);
const isQuickAdd = this.submitButton.classList.contains('quick-add__submit');
if (!isQuickAdd) return response;
this.submitButton.classList.add('disabled');
this.submitButton.querySelector('span').classList.add('hidden');
this.error = true;
return response;
}
if (!this.cart || isCartPage) {
window.location = window.routes.cart_url;
return response;
}
this.error = false;
const quickAddModal = this.closest('quick-add-modal');
const SLQuickAddModal = (window.Shopline.utils || {}).quickAddModal;
if (quickAddModal) {
document.body.addEventListener(
'modalClosed',
() => {
setTimeout(() => {
this.cart.renderContents(response);
});
},
{ once: true },
);
quickAddModal.close(true);
} else if (SLQuickAddModal) {
SLQuickAddModal.close().then(() => this.cart.renderContents(response));
} else {
this.cart.renderContents(response);
}
return response;
})
.catch((err) => {
console.error('product form err', err);
this.handleErrorMessage(this.getAttribute('data-default-error-message'));
})
.finally((response) => {
this.submitButton.classList.remove('loading');
this.querySelector('.loading-overlay__spinner').classList.remove('display-flex');
return response;
});
}
ensureQuantity(formData) {
if (!formData.has('quantity')) {
formData.set('quantity', '1');
}
}
handleErrorMessage(errorMessage = false) {
if (this.options.onErrorShowToast && errorMessage) {
window.Shopline.utils.toast.open({
content: errorMessage,
});
}
this.errorMessageWrapper = this.errorMessageWrapper || this.querySelector('.product-form__error-message-wrapper');
if (!this.errorMessageWrapper) return;
this.errorMessage = this.errorMessage || this.errorMessageWrapper.querySelector('.product-form__error-message');
this.errorMessageWrapper.toggleAttribute('hidden', !errorMessage);
if (errorMessage) {
this.errorMessage.textContent = errorMessage;
}
}
};
});
window.Shopline.loadFeatures(
[
{
name: 'component-toast',
version: '0.1',
},
],
function (error) {
if (error) {
throw error;
}
},
);