Skip to content

Commit

Permalink
Update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
jho406 committed Jan 6, 2025
1 parent 1fd0762 commit f8c554d
Show file tree
Hide file tree
Showing 20 changed files with 712 additions and 312 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ app/
|-- controllers/
|-- views/
| |-- dashboard/
| | |-- index.js # The React page component
| | |-- index.jsx # The React page component
| | |-- index.json.props # The json for the page component
| | |-- index.html.erb
```

### PropsTemplate
Powering the JSON responses is PropsTemplate, a digable JSON templating DSL
Powering the JSON responses is PropsTemplate, a diggable JSON templating DSL
inspired by JBuilder. With PropsTemplate you can specify a path of the node you
want, and PropsTemplate will walk the tree to it, skipping the execution of nodes
that don't match the keypath.
Expand Down Expand Up @@ -85,15 +85,15 @@ new JS code, etc.
With Superglue, this can be done with a simple `onChange`

```js
import {NavigationContext} from '@thoughtbot/superglue'

const {remote} = useContext(NavigationContext)

const onChange = (e) => (
remote(`/dashboard?qry=${e.target.value}&props_at=data.header.search`)}
)
```

?> `remote` and `visit` is a thunk [passed] to every page component.

[passed]: ./navigation.md

With `props_at`, the above will make a request to `/dashboard?qry=haircut`,
dig your template for the `data.header.search` node, return it in the response,
and immutably graft it in the exact same path on the redux store before finally
Expand Down
21 changes: 5 additions & 16 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
You've installed Superglue and now you're ready to configure your app.
Chances are, the first thing you'll want to add a progress bar to your app.

## `application_visit.js`

!!! tip
If you want a progress bar, this is likely the first thing you'll
want to configure after installation.
If you want a [progress bar], this is the first thing you'll want to
configure after installation.

This file contains the factory that builds the [remote] and [visit]
function that will be passed to your page components and used by the
Expand All @@ -28,8 +27,7 @@ when the internet is down.
Stop by the [tutorial] to learn how to work with this file.

**Vite Users** This step can be entirely optional if you're using Vite. See
the recipie for more information.

the [recipe](recipe/vite.md) for more information.

This file exports a mapping between a `componentIdentifier` to an imported page
component. This gets used in your `application.js` so that superglue knows
Expand All @@ -46,17 +44,6 @@ const pageIdentifierToPageComponent = {
}
```

It's not uncommon to have multiple identifiers pointing to the same component.
This can be used when building `index` pages use modals instead of a new page for
`show`.

```js
const pageIdentifierToPageComponent = {
'posts/index': PostsIndex,
'posts/show': PostsIndex,
}
```

[tutorial]: tutorial.md

## `application.js`
Expand All @@ -66,6 +53,8 @@ component. There's nothing to do here, but if you need finer control of
how redux is setup, you can build your own Application using the [source] as
inspiration.

[source]: https://github.com/thoughtbot/superglue/blob/main/superglue/lib/index.tsx#L114

<div class="grid cards" markdown>
- [:octicons-arrow-right-24: See complete reference](reference/index.md#application)
for `Application`
Expand Down
31 changes: 20 additions & 11 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,12 @@ end
```

Familiar Rails conveniences include form_props (a fork of `form_with` made for React),
flash messages integrated as a [Redux slice], and [Unobtrusive Javascript]
(UJS) helpers.
flash messages integrated as a Redux [slice], and [Unobtrusive Javascript](UJS) helpers.

### It’s React

But there are no APIs! The above is injected as a script tag in the DOM so everything
loads in the initial request. Its added to [your Redux state] and passed to
loads in the initial request. Its added to your [Redux state] and passed to
the page component in a hook, for example:

```js
Expand Down Expand Up @@ -140,15 +139,19 @@ your React components to SPA transition to the next page.
<a href=/next_page” data-sg-visit> Next Page </a>
```

### It’s more!
## A thoughtful pairing

Being able to easily use React in place of ERB isn't enough. Superglue’s secret
sauce is that your `foobar.json.props` is diggable; making any part of your page
dynamic by using a query string. It’s a simpler approach to Turbo Frames and
Turbo Stream.
Superglue is about thoughfully pairing React and Rails that brings out the best
of both frameworks.

### The return of UJS and diggable templates

Superglue’s secret sauce is that your `foobar.json.props` is diggable; making
any part of your page dynamic by using a query string. It’s a simpler approach
to Turbo Frames and Turbo Stream.

Need to reload a part of the page? Just add a query parameter and combine with
the [UJS helper] attribute `data-sg-remote`:
the [UJS] helper attribute `data-sg-remote`:

```jsx
<Header {...header}>
Expand All @@ -168,7 +171,7 @@ This works well for [modals], chat, streaming, and [more]!
[secret sauce]: digging.md
[UJS]: ujs.md

## One-stop shop
### One-stop shop

We know first hand how complex React can be, but we don't shy away from
complexity. We want to make things better for everyone and to that end, we have
Expand All @@ -192,7 +195,7 @@ your team.

Integration helpers, and generators for installation and scaffolding.

[:octicons-arrow-right-24: SuperglueJs](https://github.com/thoughtbot/superglue/tree/main/superglue_rails)
[:octicons-arrow-right-24: superglue_rails](https://github.com/thoughtbot/superglue/tree/main/superglue_rails)

- __PropsTemplate__

Expand Down Expand Up @@ -230,3 +233,9 @@ your team.
[:octicons-arrow-right-24: candy_wrapper](https://github.com/thoughtbot/candy_wrapper)

</div>


[Redux state]: ./redux-state-shape.md
[modals]: ./recipes/modals.md
[more]: ./recipes
[slice]: ./cross-cutting-concerns.md#slices
5 changes: 1 addition & 4 deletions docs/page-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ used Superglue's generators, this would be all set for you in
```

### `data`
Passed to your page component as its props. In a Superglue application, this would
be the contents of your templates, e.g., `index.json.props`. Note that `csrfToken`,
`fragments`, and `pageKey` will be merged with your props. `ownProps` are also
merged when [navigating](functions-passed.md#navigateto)
Your page's content. This can be accessed using the

### `componentIdentifier`
A `string` to instruct Superglue which component to render. The generated
Expand Down
86 changes: 86 additions & 0 deletions docs/recipes/infinite-scroll.md
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
2 changes: 1 addition & 1 deletion docs/recipes/modals.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ controller and the `page_to_page_mapping.js` the same way.
Similarly, we tie the `componentIdentifier` to the same page component.

**Vite Users** This step can be entirely optional if you're using Vite. See
the recipie for more information.
the [recipe](recipe/vite.md) for more information.

```js
import PostIndex from '../views/posts/index'
Expand Down
76 changes: 76 additions & 0 deletions docs/recipes/shopping-cart.md
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.
Loading

0 comments on commit f8c554d

Please sign in to comment.