@ribajs/shopify
Version:
Shopify extension for Riba.js
306 lines (267 loc) • 8.15 kB
text/typescript
import { Component, ScopeBase } from "@ribajs/core";
import {
ShopifyProductVariant,
ShopifyProduct,
ShopifyProductVariantOption,
ShopifyCartService,
ShopifyProductService,
} from "@ribajs/shopify";
import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
export interface Scope extends ScopeBase {
handle: string | null;
product: ShopifyProduct | null;
variant: ShopifyProductVariant | null;
quantity: number;
showDetailMenu: boolean;
detailMenuPadding: string;
// showAddToCartButton: boolean;
chooseOption: ShopifyProductItemComponent["chooseOption"];
addToCart: ShopifyProductItemComponent["addToCart"];
toggleDetailMenu: ShopifyProductItemComponent["toggleDetailMenu"];
decrease: ShopifyProductItemComponent["decrease"];
increase: ShopifyProductItemComponent["increase"];
colorOption: ShopifyProductVariantOption | null;
sizeOption: ShopifyProductVariantOption | null;
available: boolean;
}
/**
* TODO minify this, create a general product service instead of extend from ShopifyProductItemComponent
* or create a product list for all products
* or just get the attributes we need like the options
* or render the most with liquid
*/
export class ShopifyProductItemComponent extends Component {
public static tagName = "shopify-product-item";
protected autobind = true;
/**
* handle is the product handle to get the product json object
* extras are product data which is only available over liquid and not over the product json object
* product is the product object itself if you want to pass it directly
*/
static get observedAttributes(): string[] {
return ["handle", "extras", "product"];
}
protected requiredAttributes(): string[] {
return ["handle"];
}
public scope: Scope = {
handle: null,
product: null,
variant: null,
quantity: 1,
showDetailMenu: false,
detailMenuPadding: "60px",
// showAddToCartButton: false,
chooseOption: this.chooseOption,
addToCart: this.addToCart,
toggleDetailMenu: this.toggleDetailMenu,
decrease: this.decrease,
increase: this.increase,
colorOption: null,
sizeOption: null,
/**
* If the variant is available, used to disable the add to cart button
*/
available: false,
};
/**
* Array with all selected product options
*/
private selectedOptions: string[] = [];
/**
* Number of detail menü padding without px
*/
private _menuPadding = 60;
/**
* Is true if the user has chosen an option
*/
private optionChosen = false;
protected set menuPadding(padding: number) {
this._menuPadding = padding;
this.scope.detailMenuPadding = this._menuPadding + "px";
}
/**
* available is only true if the variant is available and the user has clicked on an option
*/
protected set available(available: boolean) {
this.scope.available = available && this.optionChosen;
}
protected set showMenu(show: boolean) {
if (show) {
this.menuPadding = 215;
} else {
this.menuPadding = 60;
}
this.scope.showDetailMenu = show;
}
protected get showMenu() {
return this.scope.showDetailMenu;
}
protected set product(product: ShopifyProduct | null) {
if (product) {
this.scope.product = ShopifyProductService.prepare(product);
this.scope.handle = this.scope.product.handle;
this.scope.colorOption =
ShopifyProductService.getOption(this.scope.product, "color") || null;
this.scope.sizeOption =
ShopifyProductService.getOption(this.scope.product, "size") || null;
// set the first variant to the selected one
this.variant = this.scope.product.variants[0];
}
}
protected get product(): ShopifyProduct | null {
return this.scope.product;
}
protected set variant(variant: ShopifyProductVariant | null) {
if (variant === null) {
return;
}
this.scope.variant = variant;
if (this.scope.variant) {
this.selectedOptions = this.scope.variant.options.slice();
this.available = this.scope.variant.available;
this.activateOptions();
}
}
protected get variant() {
return this.scope.variant;
}
constructor() {
super();
}
protected connectedCallback() {
super.connectedCallback();
this.init(ShopifyProductItemComponent.observedAttributes);
}
public chooseOption(
optionValue: string | number,
position1: number,
optionName: string,
event: MouseEvent,
) {
optionValue = optionValue.toString();
if (!this.scope.product) {
throw new Error("Product not set!");
}
this.selectedOptions[position1 - 1] = optionValue.toString();
const variant = ShopifyProductService.getVariantOfOptions(
this.scope.product,
this.selectedOptions,
);
if (variant) {
// Option chosen -> so enable add to cart button
this.optionChosen = true;
this.variant = variant as ShopifyProductVariant;
}
event.stopPropagation();
}
public addToCart() {
if (!this.variant) {
return;
}
ShopifyCartService.add(this.variant.id, this.scope.quantity)
.then((response: any /** TODO not any */) => {
console.debug("addToCart response", response);
})
.catch((error: Error) => {
console.debug("addToCart error", error);
});
}
public toggleDetailMenu() {
this.showMenu = !this.showMenu;
}
public increase() {
this.scope.quantity++;
}
public decrease() {
this.scope.quantity--;
if (this.scope.quantity <= 0) {
this.scope.quantity = 1;
}
}
/**
* Workaround because `rv-class-active="isOptionActive | call size"` is not updating if selectedOptions changes
* @param optionValue
* @param optionName
*/
protected activateOption(optionValue: string, optionName: string) {
optionValue = optionValue.toString().replace("#", "");
this.querySelector<HTMLElement>(
`.option-${optionName.toLocaleLowerCase()}`,
)?.classList.remove("active");
this.querySelector<HTMLElement>(
`.option-${optionName.toLocaleLowerCase()}-${optionValue}`,
)?.classList.add("active");
}
/**
* Activate option by selected options (scope.selectedOptions)
* This method sets the active class to the options elements
*/
protected activateOptions() {
for (const position0 in this.selectedOptions) {
if (this.selectedOptions[position0]) {
const optionValue = this.selectedOptions[position0];
if (this.scope.product) {
const optionName = this.scope.product.options[position0].name;
// Only activate size if it was clicked by the user
if (optionName === "size") {
if (this.optionChosen) {
this.activateOption(optionValue, optionName);
}
} else {
this.activateOption(optionValue, optionName);
}
}
}
}
}
protected async beforeBind() {
await super.beforeBind();
if (this.scope.handle === null) {
throw new Error("Product handle not set");
}
if (!this.product) {
this.fetchProduct(this.scope.handle);
}
}
protected async fetchProduct(handle: string) {
const product = await ShopifyProductService.get(handle);
if (product) {
this.product = product;
}
return product;
}
protected parsedAttributeChangedCallback(
attributeName: string,
oldValue: any,
newValue: any,
namespace: string | null,
) {
super.parsedAttributeChangedCallback(
attributeName,
oldValue,
newValue,
namespace,
);
switch (attributeName) {
case "product":
this.product = newValue;
break;
}
}
protected async afterBind() {
await super.afterBind();
this.activateOptions();
}
protected async template() {
// Only set the component template if there no childs already
if (this && hasChildNodesTrim(this)) {
return null;
} else {
const { default: template } = await import(
"./product-item.component.html?raw"
);
return template;
}
}
}