@brytdesigns/shopify-product-info-components 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -2,12 +2,43 @@ import { useProduct } from "./useProduct.js";
|
|
|
2
2
|
import { createGuardedEffect, createRemoteContext, getContextFromProvider } from "@brytdesigns/web-component-core/utils";
|
|
3
3
|
import { merge, omit, untrack } from "solid-js";
|
|
4
4
|
import { consume, createContext, provide } from "component-register";
|
|
5
|
+
import { Events } from "@brytdesigns/shopify-utils";
|
|
5
6
|
//#region src/hooks/useProductOptions.ts
|
|
7
|
+
/** A variant in the shape Shopify's converter takes. */
|
|
8
|
+
var liquidVariant = (variant) => {
|
|
9
|
+
if (variant.price === void 0) throw new Error(`Variant ${variant.id} has no price, so shopify:product:select cannot report one.`);
|
|
10
|
+
return {
|
|
11
|
+
id: variant.id,
|
|
12
|
+
title: variant.title ?? variant.options.join(" / "),
|
|
13
|
+
available: variant.available ?? false,
|
|
14
|
+
price: variant.price,
|
|
15
|
+
options: variant.options
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `shopify:product:select` for options a shopper has just chosen, resolving
|
|
20
|
+
* with the variant they point at, or `null` when no variant matches.
|
|
21
|
+
*/
|
|
22
|
+
var announceSelection = (target, product, options) => {
|
|
23
|
+
const names = product.options ?? [];
|
|
24
|
+
Events.productSelect({
|
|
25
|
+
target,
|
|
26
|
+
product: {
|
|
27
|
+
id: Events.gid("Product", product.id),
|
|
28
|
+
title: product.title ?? "",
|
|
29
|
+
handle: product.handle ?? ""
|
|
30
|
+
},
|
|
31
|
+
selectedOptions: Events.selectedOptions(names, options)
|
|
32
|
+
}, () => product.variants.find((variant) => options.every((option) => variant.options.includes(option))) ?? null, { toResult: (variant) => ({ variant: variant === null ? null : Events.variantFromJson(liquidVariant(variant), names) }) }).catch(() => {});
|
|
33
|
+
};
|
|
6
34
|
function initializeProductOptionsContext(props) {
|
|
7
35
|
const publicProps = omit(props, "root");
|
|
8
36
|
const [context, methods] = useProduct(props.root);
|
|
37
|
+
Events.preload();
|
|
9
38
|
function updateSelectedOptions(options) {
|
|
39
|
+
const previous = untrack(() => publicProps.selectedOptions);
|
|
10
40
|
props.root.setAttribute("selected-options", JSON.stringify(options));
|
|
41
|
+
if (JSON.stringify(previous) !== JSON.stringify(options)) announceSelection(props.root, untrack(() => context.product), options);
|
|
11
42
|
}
|
|
12
43
|
function updateSelectedOption(option, index) {
|
|
13
44
|
const options = [...untrack(() => publicProps.selectedOptions)];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useProductOptions.js","names":[],"sources":["../../../src/hooks/useProductOptions.ts"],"sourcesContent":["import {\n\tcreateGuardedEffect,\n\tcreateRemoteContext,\n\tgetContextFromProvider,\n} from \"@brytdesigns/web-component-core/utils\";\nimport {\n\tconsume,\n\tcreateContext,\n\ttype ICustomElement,\n\tprovide,\n} from \"component-register\";\nimport { merge, omit, untrack } from \"solid-js\";\n\nimport { useProduct } from \"./useProduct.js\";\n\ntype CreateContextOptions = {\n\troot: HTMLElement & ICustomElement;\n\tselectedOptions: string[];\n};\n\ntype WalkableNode = Parameters<typeof provide>[2];\n\ntype ProductOptionsContext = ReturnType<typeof initializeProductOptionsContext>;\n\nfunction initializeProductOptionsContext(props: CreateContextOptions) {\n\t// `omit` returns only the rest; `root` is read straight off `props`.\n\tconst publicProps = omit(props, \"root\");\n\tconst [context, methods] = useProduct(props.root);\n\n\tfunction updateSelectedOptions(options: string[]) {\n\t\tprops.root.setAttribute(\"selected-options\", JSON.stringify(options));\n\t}\n\n\tfunction updateSelectedOption(option: string, index: number) {\n\t\tconst current = untrack(() => publicProps.selectedOptions);\n\t\tconst options = [...current];\n\t\toptions[index] = option;\n\t\treturn updateSelectedOptions(options);\n\t}\n\n\tcreateGuardedEffect(\n\t\t() => {\n\t\t\tconst selectedOptions = publicProps.selectedOptions;\n\t\t\t// Resolved in the COMPUTE phase. `.find` walks the store's variant\n\t\t\t// array, and walking it here is what subscribes to it — so swapping\n\t\t\t// the product re-resolves the variant. Done from the apply phase, as\n\t\t\t// it was, those reads are untracked: the options were a dependency\n\t\t\t// but the variants were not, and a product replaced underneath this\n\t\t\t// context kept resolving against the previous one's list.\n\t\t\treturn context.product.variants.find((variant) =>\n\t\t\t\tselectedOptions.every((option) => variant.options.includes(option)),\n\t\t\t);\n\t\t},\n\t\t(variant) => {\n\t\t\tif (!variant) return;\n\t\t\t// The currently-selected id is read as a snapshot, deliberately, and\n\t\t\t// must NOT join the dependency list: `methods.variant.update` below\n\t\t\t// is what changes it, so tracking it would re-run this effect on its\n\t\t\t// own result.\n\t\t\tconst current = untrack(\n\t\t\t\t() => context.product.selected_or_first_available_variant.id,\n\t\t\t);\n\t\t\tif (variant.id === current) return;\n\t\t\tmethods.variant.update(variant.id);\n\t\t},\n\t);\n\n\treturn [\n\t\tpublicProps,\n\t\t{\n\t\t\toptions: {\n\t\t\t\tupdate: updateSelectedOptions,\n\t\t\t\tupdateOption: updateSelectedOption,\n\t\t\t},\n\t\t},\n\t] as const;\n}\n\nconst ProductOptionsContextState = createContext(\n\tinitializeProductOptionsContext,\n);\n\nexport const provideProductOptionsContext = (\n\tinitialState: Omit<CreateContextOptions, \"root\">,\n\telement: WalkableNode,\n): ProductOptionsContext => {\n\tconst props = merge(initialState, { root: element });\n\treturn provide(ProductOptionsContextState, props, element);\n};\n\nexport const useProductOptionsContext = (context: ProductOptionsContext) => {\n\treturn context;\n};\n\nexport const useProductOptions = (element: HTMLElement & ICustomElement) => {\n\tconst context: ProductOptionsContext = consume(\n\t\tProductOptionsContextState,\n\t\telement,\n\t);\n\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"ProductOptionsContext not found! Please ensure to wrap your custom element with product-options element.\",\n\t\t);\n\t}\n\n\treturn useProductOptionsContext(context);\n};\n\nexport const getProductOptionsContext = (element: Element) => {\n\tconst context = getContextFromProvider<ProductOptionsContext>(\n\t\tProductOptionsContextState,\n\t\telement,\n\t);\n\treturn useProductOptionsContext(context);\n};\n\nexport const useProductOptionsAt = createRemoteContext<ProductOptionsContext>(\n\tProductOptionsContextState,\n);\n"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"useProductOptions.js","names":[],"sources":["../../../src/hooks/useProductOptions.ts"],"sourcesContent":["import { Events } from \"@brytdesigns/shopify-utils\";\nimport {\n\tcreateGuardedEffect,\n\tcreateRemoteContext,\n\tgetContextFromProvider,\n} from \"@brytdesigns/web-component-core/utils\";\nimport {\n\tconsume,\n\tcreateContext,\n\ttype ICustomElement,\n\tprovide,\n} from \"component-register\";\nimport { merge, omit, untrack } from \"solid-js\";\n\nimport type { Product, Variant } from \"../types.js\";\nimport { useProduct } from \"./useProduct.js\";\n\n/** A variant in the shape Shopify's converter takes. */\nconst liquidVariant = (variant: Variant): Events.LiquidVariant => {\n\tif (variant.price === undefined)\n\t\tthrow new Error(\n\t\t\t`Variant ${variant.id} has no price, so shopify:product:select cannot report one.`,\n\t\t);\n\treturn {\n\t\tid: variant.id,\n\t\ttitle: variant.title ?? variant.options.join(\" / \"),\n\t\tavailable: variant.available ?? false,\n\t\tprice: variant.price,\n\t\toptions: variant.options,\n\t};\n};\n\n/**\n * `shopify:product:select` for options a shopper has just chosen, resolving\n * with the variant they point at, or `null` when no variant matches.\n */\nconst announceSelection = (\n\ttarget: HTMLElement,\n\tproduct: Product,\n\toptions: string[],\n) => {\n\tconst names = product.options ?? [];\n\tEvents.productSelect(\n\t\t{\n\t\t\ttarget,\n\t\t\tproduct: {\n\t\t\t\tid: Events.gid(\"Product\", product.id),\n\t\t\t\ttitle: product.title ?? \"\",\n\t\t\t\thandle: product.handle ?? \"\",\n\t\t\t},\n\t\t\tselectedOptions: Events.selectedOptions(names, options),\n\t\t},\n\t\t() =>\n\t\t\tproduct.variants.find((variant) =>\n\t\t\t\toptions.every((option) => variant.options.includes(option)),\n\t\t\t) ?? null,\n\t\t{\n\t\t\ttoResult: (variant) => ({\n\t\t\t\tvariant:\n\t\t\t\t\tvariant === null\n\t\t\t\t\t\t? null\n\t\t\t\t\t\t: Events.variantFromJson(liquidVariant(variant), names),\n\t\t\t}),\n\t\t},\n\t).catch(() => {});\n};\n\ntype CreateContextOptions = {\n\troot: HTMLElement & ICustomElement;\n\tselectedOptions: string[];\n};\n\ntype WalkableNode = Parameters<typeof provide>[2];\n\ntype ProductOptionsContext = ReturnType<typeof initializeProductOptionsContext>;\n\nfunction initializeProductOptionsContext(props: CreateContextOptions) {\n\t// `omit` returns only the rest; `root` is read straight off `props`.\n\tconst publicProps = omit(props, \"root\");\n\tconst [context, methods] = useProduct(props.root);\n\n\tEvents.preload();\n\n\tfunction updateSelectedOptions(options: string[]) {\n\t\tconst previous = untrack(() => publicProps.selectedOptions);\n\t\tprops.root.setAttribute(\"selected-options\", JSON.stringify(options));\n\t\tif (JSON.stringify(previous) !== JSON.stringify(options))\n\t\t\tannounceSelection(\n\t\t\t\tprops.root,\n\t\t\t\tuntrack(() => context.product),\n\t\t\t\toptions,\n\t\t\t);\n\t}\n\n\tfunction updateSelectedOption(option: string, index: number) {\n\t\tconst current = untrack(() => publicProps.selectedOptions);\n\t\tconst options = [...current];\n\t\toptions[index] = option;\n\t\treturn updateSelectedOptions(options);\n\t}\n\n\tcreateGuardedEffect(\n\t\t() => {\n\t\t\tconst selectedOptions = publicProps.selectedOptions;\n\t\t\t// Resolved in the COMPUTE phase. `.find` walks the store's variant\n\t\t\t// array, and walking it here is what subscribes to it — so swapping\n\t\t\t// the product re-resolves the variant. Done from the apply phase, as\n\t\t\t// it was, those reads are untracked: the options were a dependency\n\t\t\t// but the variants were not, and a product replaced underneath this\n\t\t\t// context kept resolving against the previous one's list.\n\t\t\treturn context.product.variants.find((variant) =>\n\t\t\t\tselectedOptions.every((option) => variant.options.includes(option)),\n\t\t\t);\n\t\t},\n\t\t(variant) => {\n\t\t\tif (!variant) return;\n\t\t\t// The currently-selected id is read as a snapshot, deliberately, and\n\t\t\t// must NOT join the dependency list: `methods.variant.update` below\n\t\t\t// is what changes it, so tracking it would re-run this effect on its\n\t\t\t// own result.\n\t\t\tconst current = untrack(\n\t\t\t\t() => context.product.selected_or_first_available_variant.id,\n\t\t\t);\n\t\t\tif (variant.id === current) return;\n\t\t\tmethods.variant.update(variant.id);\n\t\t},\n\t);\n\n\treturn [\n\t\tpublicProps,\n\t\t{\n\t\t\toptions: {\n\t\t\t\tupdate: updateSelectedOptions,\n\t\t\t\tupdateOption: updateSelectedOption,\n\t\t\t},\n\t\t},\n\t] as const;\n}\n\nconst ProductOptionsContextState = createContext(\n\tinitializeProductOptionsContext,\n);\n\nexport const provideProductOptionsContext = (\n\tinitialState: Omit<CreateContextOptions, \"root\">,\n\telement: WalkableNode,\n): ProductOptionsContext => {\n\tconst props = merge(initialState, { root: element });\n\treturn provide(ProductOptionsContextState, props, element);\n};\n\nexport const useProductOptionsContext = (context: ProductOptionsContext) => {\n\treturn context;\n};\n\nexport const useProductOptions = (element: HTMLElement & ICustomElement) => {\n\tconst context: ProductOptionsContext = consume(\n\t\tProductOptionsContextState,\n\t\telement,\n\t);\n\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"ProductOptionsContext not found! Please ensure to wrap your custom element with product-options element.\",\n\t\t);\n\t}\n\n\treturn useProductOptionsContext(context);\n};\n\nexport const getProductOptionsContext = (element: Element) => {\n\tconst context = getContextFromProvider<ProductOptionsContext>(\n\t\tProductOptionsContextState,\n\t\telement,\n\t);\n\treturn useProductOptionsContext(context);\n};\n\nexport const useProductOptionsAt = createRemoteContext<ProductOptionsContext>(\n\tProductOptionsContextState,\n);\n"],"mappings":";;;;;;;AAkBA,IAAM,iBAAiB,YAA2C;CACjE,IAAI,QAAQ,UAAU,KAAA,GACrB,MAAM,IAAI,MACT,WAAW,QAAQ,GAAG,4DACvB;CACD,OAAO;EACN,IAAI,QAAQ;EACZ,OAAO,QAAQ,SAAS,QAAQ,QAAQ,KAAK,KAAK;EAClD,WAAW,QAAQ,aAAa;EAChC,OAAO,QAAQ;EACf,SAAS,QAAQ;CAClB;AACD;;;;;AAMA,IAAM,qBACL,QACA,SACA,YACI;CACJ,MAAM,QAAQ,QAAQ,WAAW,CAAC;CAClC,OAAO,cACN;EACC;EACA,SAAS;GACR,IAAI,OAAO,IAAI,WAAW,QAAQ,EAAE;GACpC,OAAO,QAAQ,SAAS;GACxB,QAAQ,QAAQ,UAAU;EAC3B;EACA,iBAAiB,OAAO,gBAAgB,OAAO,OAAO;CACvD,SAEC,QAAQ,SAAS,MAAM,YACtB,QAAQ,OAAO,WAAW,QAAQ,QAAQ,SAAS,MAAM,CAAC,CAC3D,KAAK,MACN,EACC,WAAW,aAAa,EACvB,SACC,YAAY,OACT,OACA,OAAO,gBAAgB,cAAc,OAAO,GAAG,KAAK,EACzD,GACD,CACD,CAAC,CAAC,YAAY,CAAC,CAAC;AACjB;AAWA,SAAS,gCAAgC,OAA6B;CAErE,MAAM,cAAc,KAAK,OAAO,MAAM;CACtC,MAAM,CAAC,SAAS,WAAW,WAAW,MAAM,IAAI;CAEhD,OAAO,QAAQ;CAEf,SAAS,sBAAsB,SAAmB;EACjD,MAAM,WAAW,cAAc,YAAY,eAAe;EAC1D,MAAM,KAAK,aAAa,oBAAoB,KAAK,UAAU,OAAO,CAAC;EACnE,IAAI,KAAK,UAAU,QAAQ,MAAM,KAAK,UAAU,OAAO,GACtD,kBACC,MAAM,MACN,cAAc,QAAQ,OAAO,GAC7B,OACD;CACF;CAEA,SAAS,qBAAqB,QAAgB,OAAe;EAE5D,MAAM,UAAU,CAAC,GADD,cAAc,YAAY,eACtB,CAAO;EAC3B,QAAQ,SAAS;EACjB,OAAO,sBAAsB,OAAO;CACrC;CAEA,0BACO;EACL,MAAM,kBAAkB,YAAY;EAOpC,OAAO,QAAQ,QAAQ,SAAS,MAAM,YACrC,gBAAgB,OAAO,WAAW,QAAQ,QAAQ,SAAS,MAAM,CAAC,CACnE;CACD,IACC,YAAY;EACZ,IAAI,CAAC,SAAS;EAKd,MAAM,UAAU,cACT,QAAQ,QAAQ,oCAAoC,EAC3D;EACA,IAAI,QAAQ,OAAO,SAAS;EAC5B,QAAQ,QAAQ,OAAO,QAAQ,EAAE;CAClC,CACD;CAEA,OAAO,CACN,aACA,EACC,SAAS;EACR,QAAQ;EACR,cAAc;CACf,EACD,CACD;AACD;AAEA,IAAM,6BAA6B,cAClC,+BACD;AAEA,IAAa,gCACZ,cACA,YAC2B;CAC3B,MAAM,QAAQ,MAAM,cAAc,EAAE,MAAM,QAAQ,CAAC;CACnD,OAAO,QAAQ,4BAA4B,OAAO,OAAO;AAC1D;AAEA,IAAa,4BAA4B,YAAmC;CAC3E,OAAO;AACR;AAEA,IAAa,qBAAqB,YAA0C;CAC3E,MAAM,UAAiC,QACtC,4BACA,OACD;CAEA,IAAI,CAAC,SACJ,MAAM,IAAI,MACT,0GACD;CAGD,OAAO,yBAAyB,OAAO;AACxC;AAEA,IAAa,4BAA4B,YAAqB;CAK7D,OAAO,yBAJS,uBACf,4BACA,OAE+B,CAAO;AACxC;AAEA,IAAa,sBAAsB,oBAClC,0BACD"}
|
package/dist/main/index.d.ts
CHANGED
|
@@ -21,11 +21,18 @@ export type Variant = {
|
|
|
21
21
|
*/
|
|
22
22
|
title?: string;
|
|
23
23
|
available?: boolean;
|
|
24
|
+
/** In Liquid's cents. Needed for `shopify:product:select` to report a price. */
|
|
25
|
+
price?: number | string;
|
|
24
26
|
};
|
|
25
27
|
export type Product = {
|
|
26
28
|
id: string | number;
|
|
27
29
|
variants: Variant[];
|
|
28
30
|
selected_or_first_available_variant: Variant;
|
|
31
|
+
/** The rest are what `shopify:product:select` reports; Shopify sends them all. */
|
|
32
|
+
title?: string;
|
|
33
|
+
handle?: string;
|
|
34
|
+
/** Option names, in order: `["Color", "Size"]`. */
|
|
35
|
+
options?: string[];
|
|
29
36
|
};
|
|
30
37
|
export type Props = {
|
|
31
38
|
product?: Product;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brytdesigns/shopify-product-info-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Headless web components used to render product information based on active product variants",
|
|
5
5
|
"main": "./dist/main/index.js",
|
|
6
6
|
"module": "./dist/main/index.js",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"author": "Bryt Designs",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@brytdesigns/
|
|
22
|
+
"@brytdesigns/shopify-utils": "3.2.0",
|
|
23
|
+
"@brytdesigns/web-component-core": "3.2.0",
|
|
23
24
|
"@solidjs/web": "2.0.0-rc.14",
|
|
24
25
|
"component-register": "0.8.7",
|
|
25
26
|
"solid-js": "2.0.0-rc.14"
|