Skip to content

Commit

Permalink
Merge pull request #205 from Shopify/@juanpprieto/cart-lines-remove
Browse files Browse the repository at this point in the history
Cart LinesRemove
  • Loading branch information
juanpprieto authored Nov 18, 2022
2 parents 804b698 + 6bc3afb commit 2504f9c
Show file tree
Hide file tree
Showing 6 changed files with 644 additions and 72 deletions.
42 changes: 32 additions & 10 deletions docs/resources/cart/LinesAdd.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# LinesAdd

The `LinesAdd` cart resource is a full-stack component that provides a set of utilities to facilitate adding line(s) to the cart. It also provides a set of hooks to help you handle optimistic and pending UI.
The `LinesAdd` cart resource is a full-stack component that provides a set of utilities to add line(s) to the cart. It also provides a set of hooks to help you handle optimistic and pending UI.

## `LinesAddForm`

A Remix `fetcher.Form` that adds a set of line(s) to the cart. This form mutates the cart via the [cartLinesAdd](https://shopify.dev/api/storefront/2022-10/mutations/cartLinesAdd) mutation and provides
`error`, `state` and `event` instrumentation for analytics.

| Prop | Type | Description |
| ------------ | ----------------------- | --------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------ |
| `lines` | `{quantity, variant}[]` | `lines items to add to the cart` |
| `onSuccess?` | `(event) => void` | `A callback that runs after every successful event` |
| `children` | `({ state: 'idle' | 'submitting' | 'loading'; error: string})` | `A render prop that provides the state and errors for the current submission.` |
| Prop | Type | Description |
| ------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `lines` | `{quantity, variant}[]` | `lines items to add to the cart` |
| `onSuccess?` | `(event) => void` | `A callback that runs after every successful event` |
| `children` | `({ state: 'idle' or 'submitting' or 'loading'; error: string})` | `A render prop that provides the state and errors for the current submission.` |

Basic use:

Expand Down Expand Up @@ -47,7 +47,6 @@ function AddToCartButton({selectedVariant, quantity}) {
]}
onSuccess={(event) => {
navigator.sendBeacon('/events', JSON.stringify(event))
toggleNotification()
}}
>
{(state, error) => (
Expand Down Expand Up @@ -83,15 +82,38 @@ const {linesAdd, linesAdding, linesAddingFetcher} = useLinesAdd(onSuccess);
| :----------- | :---------------- | :-------------------------------------------------- |
| `onSuccess?` | `(event) => void` | `A callback that runs after every successful event` |
Example use
Example use: reacting to add to cart event
```jsx
// Toggle a cart drawer when adding to cart
function Layout() {
const {linesAdding} = useLinesAdd();
const [drawer, setDrawer] = useState(false);

useEffect(() => {
if (drawer || !linesAdding) return;
setDrawer(true);
}, [linesAdding, drawer, setDrawer]);

return (
<div>
<Header />
<CartDrawer className={drawer ? '' : 'hidden'} setDrawer={setDrawer} />
</div>
);
}
```
Example use: programmatic add to cart
```jsx
// A hook that adds a free gift variant to the cart, if there are 3 items in the cart
// A hook that programmatically adds a free gift variant to the cart,
// if there are 3 or more items in the cart
function useAddFreeGift({cart}) {
const {linesAdd, linesAdding} = useLinesAdd();
const giftInCart = cart.lines...
const freeGiftProductVariant = ...
const shouldAddGift = !linesAdding && !giftInCart && cart.lines.edges.length !== 3;
const shouldAddGift = !linesAdding && !giftInCart && cart.lines.edges.length >= 3;

useEffect(() => {
if (!shouldAddGift) return;
Expand Down
171 changes: 171 additions & 0 deletions docs/resources/cart/LinesRemove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# LinesRemove

The `LinesRemove` cart resource is a full-stack component that provides a set of utilities to remove line(s) from the cart. It also provides a set of hooks to help you handle optimistic and pending UI.

## `LinesRemoveForm`

A Remix `fetcher.Form` that removes a set of line(s) from the cart. This form mutates the cart via the [cartLinesRemove](https://shopify.dev/api/storefront/2022-10/mutations/cartLinesRemove) mutation and provides
`error`, `state` and `event` instrumentation for analytics.

| Prop | Type | Description |
| :----------- | :--------------------------------------------------------------- | :----------------------------------------------------------------------------- |
| `lineIds` | `['lineId..]` | `line ids to remove from the cart` |
| `onSuccess?` | `(event) => void` | `A callback that runs after every successful event` |
| `children` | `({ state: 'idle' or 'submitting' or 'loading'; error: string})` | `A render prop that provides the state and errors for the current submission.` |

Basic use:

```jsx
function RemoveFromCart({lindeIds}) {
return (
<LinesRemoveForm lineIds={lindeIds}>
{(state, error) => <button>Remove</button>}
</LinesRemoveForm>
);
}
```

Advanced use:

```jsx
// Advanced example
function RemoveFromCart({lindeIds}) {
return (
<LinesRemoveForm
lineIds={lindeIds}
onSuccess={(event) => {
navigator.sendBeacon('/events', JSON.stringify(event))
}}
>
{(state, error) => (
<button>{state === 'idle' ? 'Remove' : 'Removing'}</button>
{error ? <p>{error}</p>}
)}
</LinesRemoveForm>
)
}
```
## `useLinesRemove`
This hook provides a programmatic way to remove line(s) from the cart;
Hook signature
```jsx
function onSuccess(event) {
console.log('lines removed');
}

const {linesRemove, linesRemoving, linesRemoveFetcher} =
useLinesRemove(onSuccess);
```
| Action | Description |
| :------------------- | :--------------------------------------------------------------------------------------------------------- |
| `linesRemove` | A utility that submits the lines remove mutation via fetcher.submit() |
| `linesRemoving` | The lines being submitted. If the fetcher is idle it will be null. Useful for handling optimistic updates. |
| `linesRemoveFetcher` | The Remix `fetcher` handling the current form submission |
| Prop | Type | Description |
| :----------- | :---------------- | :-------------------------------------------------- |
| `onSuccess?` | `(event) => void` | `A callback that runs after every successful event` |
Example use
```jsx
// A hook that removes a free gift variant from the cart, if there are less than 3 items in the cart
function useRemoveFreeGift({cart}) {
const {linesRemove, linesRemoving} = useLinesRemove();
const freeGiftLineId = cart.lines.filter;
const shouldRemoveGift =
!linesRemoving && freeGiftLineId && cart.lines.edges.length < 3;

useEffect(() => {
if (!shouldRemoveGift) return;
linesRemove({
lineIds: [freeGiftLineId],
});
}, [shouldRemoveGift, freeGiftLineId]);
}
```
## `useOptimisticLineRemove`
A utility hook to easily implement optimistic UI when a specific line is being removed.
Hook signature
```jsx
const {optimisticLineRemove, linesRemoving} = useOptimisticLineRemove(lines);
```
| Action | Description |
| :--------------------- | :--------------------------------------------------------------------------------------------------------- |
| `optimisticLineRemove` | A boolean indicating if the line is being removed |
| `linesRemoving` | The lines being submitted. If the fetcher is idle it will be null. Useful for handling optimistic updates. |
Example use
```jsx
function CartLineItem({line}) {
const {optimisticLineRemove} = useOptimisticLineRemove(line);
const {id: lineId, merchandise} = line;

return (
<li
key={lineId}
// Optimistically hide the line while its being removed.
// It will be automatically removed when the cart lines are updated
className={optimisticLineRemove ? 'hidden' : ''}
>
<h4>{{merchandise.product.title}}</h4>
<img
width={112}
height={112}
src={merchandise.image.url}
className="object-cover object-center w-24 h-24 border rounded md:w-28 md:h-28"
alt={merchandise.title}
/>
...
</li>
);
}
```
## `useOptimisticLinesRemove`
A utility hook to easily implement optimistic UI when a any cart line is being removed.
Hook signature
```jsx
const {optimisticLastLineRemove, linesRemoving} =
useOptimisticLinesRemove(lines);
```
| Action | Description |
| :------------------------- | :--------------------------------------------------------------------------------------------------------- |
| `optimisticLastLineRemove` | A boolean indicating that the last line in cart is being removed |
| `linesRemoving` | The lines being submitted. If the fetcher is idle it will be null. Useful for handling optimistic updates. |
Example use
```jsx
function Cart({cart}) {
const {lines} = cart;
const {optimisticLastLineRemove} = useOptimisticLinesRemove(lines);

// optimistically show an empty cart if removing the last line
const cartEmpty = lines.length === 0 || optimisticLastLineRemove;

return (
<div>
{cartEmpty
? <CartEmpty>
: <CartLines lines={lines}>
}
</div>
);
}
```
73 changes: 35 additions & 38 deletions templates/demo-store/app/components/CartDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {useMemo, useRef} from 'react';
import clsx from 'clsx';
import {useRef} from 'react';
import {useScroll} from 'react-use';
import {flattenConnection, Money} from '@shopify/hydrogen-react';
import {
type FetcherWithComponents,
useFetcher,
useLocation,
useFetchers,
} from '@remix-run/react';
import {
Button,
Expand All @@ -24,6 +24,11 @@ import type {
ProductConnection,
} from '@shopify/hydrogen-react/storefront-api-types';
import {useOptimisticLinesAdd} from '~/routes/__resources/cart/LinesAdd';
import {
LinesRemoveForm,
useOptimisticLineRemove,
useOptimisticLinesRemove,
} from '~/routes/__resources/cart/LinesRemove';

enum Action {
SetQuantity = 'set-quantity',
Expand All @@ -46,14 +51,11 @@ export function CartDetails({
const {y} = useScroll(scrollRef);
const lineItemFetcher = useFetcher();
const {optimisticLinesAdd} = useOptimisticLinesAdd(lines);
const optimisticallyDeletingLastLine =
lines.length === 1 &&
lineItemFetcher.submission &&
lineItemFetcher.submission.formData.get('intent') === Action.RemoveLineItem;
const {optimisticLastLineRemove} = useOptimisticLinesRemove(lines);

const cartIsEmpty = Boolean(
(lines.length === 0 && !optimisticLinesAdd.length) ||
optimisticallyDeletingLastLine,
optimisticLastLineRemove,
);

if (cartIsEmpty) {
Expand Down Expand Up @@ -164,10 +166,8 @@ function CartLineItem({
optimistic?: boolean;
}) {
const {id: lineId, quantity, merchandise} = line;

const location = useLocation();
const {optimisticLineRemove} = useOptimisticLineRemove(line);
let optimisticQuantity = quantity;
let optimisticallyDeleting = false;

if (
fetcher.submission &&
Expand All @@ -180,16 +180,14 @@ function CartLineItem({
);
break;
}

case Action.RemoveLineItem: {
optimisticallyDeleting = true;
break;
}
}
}

return optimisticallyDeleting ? null : (
<li key={lineId} className="flex gap-4">
return (
<li
key={lineId}
className={clsx(['flex gap-4', optimisticLineRemove ? 'hidden' : ''])}
>
<div className="flex-shrink">
{merchandise.image && (
<img
Expand Down Expand Up @@ -231,27 +229,7 @@ function CartLineItem({
optimistic={optimistic}
/>
</div>
<fetcher.Form method="post" action="/cart">
<input
type="hidden"
name="intent"
value={Action.RemoveLineItem}
/>
<input type="hidden" name="lineId" value={lineId} />
<input
type="hidden"
name="redirect"
value={location.pathname + location.search}
/>
<button
type="submit"
className="flex items-center justify-center w-10 h-10 border rounded"
disabled={optimistic}
>
<span className="sr-only">Remove</span>
<IconRemove aria-hidden="true" />
</button>
</fetcher.Form>
<CartLineRemove lineIds={[lineId]} />
</div>
</div>
<Text>
Expand All @@ -262,6 +240,25 @@ function CartLineItem({
);
}

function CartLineRemove({lineIds}: {lineIds: CartLine['id'][]}) {
return (
<LinesRemoveForm lineIds={lineIds}>
{({state}) => (
<button
className="flex items-center justify-center w-10 h-10 border rounded"
type="submit"
disabled={state !== 'idle'}
>
<span className="sr-only">
{state === 'loading' ? 'Removing' : 'Remove'}
</span>
<IconRemove aria-hidden="true" />
</button>
)}
</LinesRemoveForm>
);
}

function CartLineQuantityAdjust({
lineId,
quantity,
Expand Down
Loading

0 comments on commit 2504f9c

Please sign in to comment.