-
Notifications
You must be signed in to change notification settings - Fork 293
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Product form implementation Shopify/hydrogen-internal#23
- Loading branch information
1 parent
a9cebd9
commit 61d2d98
Showing
19 changed files
with
1,467 additions
and
241 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'demo-store': patch | ||
'@shopify/hydrogen': patch | ||
--- | ||
|
||
Add a `<VariantSelector>` component to make building product forms easier. Also added `getFirstAvailableVariant` and `getSelectedProductOptions` helper functions. See the [proposal](https://gist.github.com/blittle/d9205d4ac72528005dc6f3104c328ecd) for examples. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
259 changes: 259 additions & 0 deletions
259
packages/hydrogen/docs/generated/generated_docs_data.json
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; | ||
|
||
const data: ReferenceEntityTemplateSchema = { | ||
name: 'VariantSelector', | ||
category: 'components', | ||
isVisualComponent: true, | ||
related: [ | ||
{ | ||
name: 'getSelectedProductOptions', | ||
type: 'utilities', | ||
url: '/docs/api/hydrogen/2023-04/utilities/getselectedproductoptions', | ||
}, | ||
{ | ||
name: 'getFirstAvailableVariant', | ||
type: 'utilities', | ||
url: '/docs/api/hydrogen/2023-04/utilities/getfirstavailablevariant', | ||
}, | ||
], | ||
description: `> Caution: | ||
> This component is in an unstable pre-release state and may have breaking changes in a future release. | ||
The \`VariantSelector\` component helps you build a form for selecting available variants of a product. It is important for variant selection state to be maintained in the URL, so that the user can navigate to a product and return back to the same variant selection. It is also important that the variant selection state is shareable via URL. The \`VariantSelector\` component provides a render prop that renders for each product option.`, | ||
type: 'component', | ||
defaultExample: { | ||
description: 'I am the default example', | ||
codeblock: { | ||
tabs: [ | ||
{ | ||
title: 'JavaScript', | ||
code: './VariantSelector.example.jsx', | ||
language: 'jsx', | ||
}, | ||
{ | ||
title: 'TypeScript', | ||
code: './VariantSelector.example.tsx', | ||
language: 'tsx', | ||
}, | ||
], | ||
title: 'Example code', | ||
}, | ||
}, | ||
definitions: [ | ||
{ | ||
title: 'Props', | ||
type: 'VariantSelectorProps', | ||
description: '', | ||
}, | ||
], | ||
}; | ||
|
||
export default data; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import {VariantSelector__unstable as VariantSelector} from '@shopify/hydrogen'; | ||
import {Link} from '@remix-run/react'; | ||
|
||
const ProductForm = ({product}) => { | ||
return ( | ||
<VariantSelector options={product.options} variants={product.variants}> | ||
{({option}) => ( | ||
<> | ||
<div>{option.name}</div> | ||
<div> | ||
{option.values.map(({value, isAvailable, path, isActive}) => ( | ||
<Link | ||
to={path} | ||
prefetch="intent" | ||
className={ | ||
isActive ? 'active' : isAvailable ? '' : 'opacity-80' | ||
} | ||
> | ||
{value} | ||
</Link> | ||
))} | ||
</div> | ||
</> | ||
)} | ||
</VariantSelector> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import {VariantSelector__unstable as VariantSelector} from '@shopify/hydrogen'; | ||
import type {Product} from '@shopify/hydrogen/storefront-api-types'; | ||
import {Link} from '@remix-run/react'; | ||
|
||
const ProductForm = ({product}: {product: Product}) => { | ||
return ( | ||
<VariantSelector options={product.options} variants={product.variants}> | ||
{({option}) => ( | ||
<> | ||
<div>{option.name}</div> | ||
<div> | ||
{option.values.map(({value, isAvailable, path, isActive}) => ( | ||
<Link | ||
to={path} | ||
prefetch="intent" | ||
className={ | ||
isActive ? 'active' : isAvailable ? '' : 'opacity-80' | ||
} | ||
> | ||
{value} | ||
</Link> | ||
))} | ||
</div> | ||
</> | ||
)} | ||
</VariantSelector> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import {useLocation} from '@remix-run/react'; | ||
import {flattenConnection} from '@shopify/hydrogen-react'; | ||
import {useMemo, createElement, Fragment} from 'react'; | ||
export function VariantSelector({ | ||
options = [], | ||
variants: _variants = [], | ||
children, | ||
defaultVariant, | ||
}) { | ||
const variants = | ||
_variants instanceof Array ? _variants : flattenConnection(_variants); | ||
const {pathname, search} = useLocation(); | ||
const {searchParams, path} = useMemo(() => { | ||
const isLocalePathname = /\/[a-zA-Z]{2}-[a-zA-Z]{2}\//g.test(pathname); | ||
const path = isLocalePathname | ||
? `/${pathname.split('/').slice(2).join('/')}` | ||
: pathname; | ||
const searchParams = new URLSearchParams(search); | ||
return { | ||
searchParams: searchParams, | ||
path, | ||
}; | ||
}, [pathname, search]); | ||
// If an option only has one value, it doesn't need a UI to select it | ||
// But instead it always needs to be added to the product options so | ||
// the SFAPI properly finds the variant | ||
const optionsWithOnlyOneValue = options.filter( | ||
(option) => option?.values?.length === 1, | ||
); | ||
return createElement( | ||
Fragment, | ||
null, | ||
...useMemo(() => { | ||
return ( | ||
options | ||
// Only show options with more than one value | ||
.filter((option) => option?.values?.length > 1) | ||
.map((option) => { | ||
let activeValue; | ||
let availableValues = []; | ||
for (let value of option.values) { | ||
// The clone the search params for each value, so we can calculate | ||
// a new URL for each option value pair | ||
const clonedSearchParams = new URLSearchParams(searchParams); | ||
clonedSearchParams.set(option.name, value); | ||
// Because we hide options with only one value, they aren't selectable, | ||
// but they still need to get into the URL | ||
optionsWithOnlyOneValue.forEach((option) => { | ||
clonedSearchParams.set(option.name, option.values[0]); | ||
}); | ||
// Find a variant that matches all selected options. | ||
const variant = variants.find((variant) => | ||
variant?.selectedOptions?.every( | ||
(selectedOption) => | ||
clonedSearchParams.get(selectedOption?.name) === | ||
selectedOption?.value, | ||
), | ||
); | ||
const currentParam = searchParams.get(option.name); | ||
const calculatedActiveValue = currentParam | ||
? // If a URL parameter exists for the current option, check if it equals the current value | ||
currentParam === value | ||
: defaultVariant | ||
? // Else check if the default variant has the current option value | ||
defaultVariant.selectedOptions?.some( | ||
(selectedOption) => | ||
selectedOption?.name === option.name && | ||
selectedOption?.value === value, | ||
) | ||
: false; | ||
if (calculatedActiveValue) { | ||
// Save out the current value if it's active. This should only ever happen once. | ||
// Should we throw if it happens a second time? | ||
activeValue = value; | ||
} | ||
availableValues.push({ | ||
value: value, | ||
isAvailable: variant ? variant.availableForSale : true, | ||
path: path + '?' + clonedSearchParams.toString(), | ||
isActive: Boolean(calculatedActiveValue), | ||
}); | ||
} | ||
return children({ | ||
option: { | ||
name: option.name, | ||
value: activeValue, | ||
values: availableValues, | ||
}, | ||
}); | ||
}) | ||
); | ||
}, [options, variants, children]), | ||
); | ||
} | ||
export const getSelectedProductOptions = (request) => { | ||
if (!(request instanceof Request)) | ||
throw new TypeError(`Expected a Request instance, got ${typeof request}`); | ||
const searchParams = new URL(request.url).searchParams; | ||
const selectedOptions = []; | ||
searchParams.forEach((value, name) => { | ||
selectedOptions.push({name, value}); | ||
}); | ||
return selectedOptions; | ||
}; | ||
export function getFirstAvailableVariant(variants = []) { | ||
return ( | ||
variants instanceof Array ? variants : flattenConnection(variants) | ||
).find((variant) => variant?.availableForSale); | ||
} |
Oops, something went wrong.