-
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
712 additions
and
312 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
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
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,86 @@ | ||
# Infinite scroll | ||
|
||
In this recipe, we'll add infinite scroll to our application. Superglue doesn't | ||
have an infinite scroll component, but it has tools that make it easy to | ||
work with React's ecosystem. | ||
|
||
Lets begin by adding `react-infinite-scroll-hook` | ||
|
||
``` | ||
yarn add react-infinite-scroll-hook | ||
``` | ||
|
||
And continue off from our [pagination] recipe. | ||
|
||
!!! tip | ||
We'll use the `beforeSave` callback to modify the payload before superglue | ||
saves it to the store. This callback is an option for both `visit` and | ||
`remote` functions. See the | ||
[beforeSave reference](../reference/types.requests/#beforesave-2) for more details. | ||
|
||
```diff | ||
// app/views/posts/index.js | ||
|
||
import React from 'react' | ||
- import {useContent} from '@thoughtbot/superglue' | ||
+ import {useContent, NavigationContext} from '@thoughtbot/superglue' | ||
import PostList from './PostList' | ||
import Header from './Header' | ||
+ import useInfiniteScroll from 'react-infinite-scroll-hook'; | ||
|
||
export default PostIndex = () => { | ||
const { | ||
posts, | ||
header, | ||
pathToNextPage, | ||
pathToPrevPage | ||
} = useContent() | ||
|
||
+ const { remote, pageKey } = useContext(NavigationContext) | ||
+ const { loading, setLoading } = useState(false) | ||
+ const hasNextPage = !!pathToNextPage | ||
+ | ||
+ const beforeSave = (prevPage, receivedPage) => { | ||
+ const prevPosts = prevPage.data.posts | ||
+ const receivedPosts = receivedPage.data.posts | ||
+ receivedPage.data.posts = prevPosts + receivedPosts | ||
+ | ||
+ return receivedPage | ||
+ } | ||
+ | ||
+ const loadMore = () => { | ||
+ setLoading(true) | ||
+ remote(pathToNextPage, {pageKey, beforeSave}) | ||
+ .then(() => setLoading(false)) | ||
+ } | ||
+ | ||
+ const [sentryRef] = useInfiniteScroll({ | ||
+ loading, | ||
+ hasNextPage, | ||
+ onLoadMore: loadMore, | ||
+ }); | ||
|
||
return ( | ||
<> | ||
<Header {...header}/> | ||
<div> | ||
{ | ||
posts.list.map(({id, body}) => ( | ||
<p key={id}>{body}</p> | ||
)) | ||
} | ||
+ {(loading || hasNextPage) && ( | ||
+ <p ref={sentryRef}> | ||
+ loading | ||
+ </p> | ||
+ )} | ||
</div> | ||
- {pathToPrevPage && <a href={pathToPrevPage} data-sg-visit>Prev Page</a>} | ||
- {pathToNextPage && <a href={pathToNextPage} data-sg-visit>Next Page</a>} | ||
</> | ||
) | ||
} | ||
|
||
``` | ||
|
||
[pagination]: ./spa-pagination.md |
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,76 @@ | ||
# Shopping cart | ||
|
||
In this recipe, we'll look at how to build a global shopping cart state. One | ||
that can be used in a header for a count of all quantity, a shopping cart | ||
panel, optimistic updates, etc. Here's how to achieve that: | ||
|
||
Render the cart in your props [across all pages] in your | ||
`application.json.props` and mark it as a fragment. | ||
|
||
```ruby | ||
json.data do | ||
json.cart partial: ['cart', fragment: true] do | ||
end | ||
|
||
yield | ||
end | ||
``` | ||
|
||
[across all pages]: ../cross-cutting-concerns.md#layouts | ||
|
||
Add a slice | ||
|
||
```javascript | ||
import { createSlice, createAction } from '@reduxjs/toolkit' | ||
import { updateFragments } from '@thoughtbot/superglue' | ||
|
||
export const cartSlice = createSlice({ | ||
name: 'cart', | ||
initialState: {}, | ||
reducers: { | ||
addToCart: (state, action) => { | ||
....logic to add something to the cart ... | ||
} | ||
}, | ||
extraReducers: (builder) => { | ||
builder.addCase(updateFragments, (state, action) => { | ||
// Update the slice with the latest and greatest. | ||
return action.value | ||
}) | ||
} | ||
}) | ||
``` | ||
|
||
With `fragment` enabled, the above will populate the slice whenever a page | ||
is received, while allowing you the flexibility to make local edits using | ||
the custom `addToCart` reducer. | ||
|
||
You can use this cart slice as you normally would with Redux selectors | ||
|
||
``` | ||
// For the cart component | ||
const cart = useSelector(state => state.cart) | ||
// For a header quantity component | ||
const cartCount = cart.lineItems.reduce((memo, line) => memo + line.qty, 0) | ||
``` | ||
|
||
For updates to the backend, add a ujs attribute to a normal form. | ||
|
||
```javascript | ||
<form action='/add_to_cart?props_at=data.header.cart' method='POST' data-sg-remote={true}> | ||
``` | ||
|
||
```ruby | ||
def create | ||
... add to cart logic here... | ||
|
||
# This helper will retain the `props_at` param when redirecting, which allows the | ||
# partial rendering of the `show` page. | ||
redirect_back_with_props_at fallback_url: '/' | ||
end | ||
``` | ||
|
||
The above will `POST`, and get redirected back to the original page while | ||
fetching only the cart to update. This will be picked up by `extraReducers` and | ||
update the entire cart state. |
Oops, something went wrong.