├── .github
└── workflows
│ ├── build_js.yml
│ ├── dynamic-security.yml
│ └── publish_docs.yml
├── .gitignore
├── .tool-versions
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── NEWS.md
├── README.md
├── SECURITY.md
├── docs
├── configuration.md
├── cross-cutting-concerns.md
├── deferments.md
├── demo.md
├── digging.md
├── images
│ ├── demo.jpg
│ ├── haircuts.png
│ ├── no_apis.png
│ ├── props_template.png
│ ├── s-color.svg
│ ├── superglue--one-color.svg
│ ├── superglue--word-mark-one-color.svg
│ ├── superglue--word-mark.svg
│ ├── superglue.png
│ └── superglue.svg
├── index.md
├── installation.md
├── navigation-context.md
├── page-response.md
├── rails-utils.md
├── recipes
│ ├── infinite-scroll.md
│ ├── modals.md
│ ├── progress-bar.md
│ ├── shopping-cart.md
│ ├── spa-pagination.md
│ ├── ssr.md
│ ├── turbo.md
│ └── vite.md
├── redux-state-shape.md
├── reference
│ ├── README.md
│ ├── components.Navigation.md
│ ├── hooks.md
│ ├── index.md
│ ├── types.actions.md
│ ├── types.md
│ └── types.requests.md
├── requests.md
├── stylesheets
│ └── extra.css
├── tutorial.md
└── ujs.md
├── mkdocs.yml
├── package-lock.json
└── superglue
├── .babelrc.js
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── LICENSE
├── lib
├── action_creators
│ ├── index.ts
│ └── requests.ts
├── actions.ts
├── components
│ └── Navigation.tsx
├── config.ts
├── hooks
│ └── index.ts
├── index.tsx
├── reducers
│ └── index.ts
├── types
│ ├── actions.ts
│ ├── index.ts
│ └── requests.ts
└── utils
│ ├── helpers.ts
│ ├── immutability.ts
│ ├── index.ts
│ ├── request.ts
│ ├── ujs.ts
│ ├── url.ts
│ └── window.ts
├── package.json
├── spec
├── features
│ └── navigation.spec.jsx
├── fixtures.js
├── helpers
│ ├── polyfill.js
│ └── setup.js
├── lib
│ ├── NavComponent.spec.jsx
│ ├── action_creators.spec.js
│ ├── hooks.spec.jsx
│ ├── reducers.spec.js
│ └── utils
│ │ ├── helpers.spec.js
│ │ ├── immutability.spec.js
│ │ ├── request.spec.js
│ │ ├── ujs.spec.js
│ │ ├── url.spec.js
│ │ └── window.spec.js
└── support
│ └── jasmine.json
├── tsconfig.json
├── tsup.config.ts
├── typedoc.json
└── vitest.config.ts
/.github/workflows/build_js.yml:
--------------------------------------------------------------------------------
1 | name: Test superglue_js
2 | on:
3 | push:
4 | pull_request:
5 | workflow_dispatch:
6 |
7 | jobs:
8 | build:
9 | name: Test superglue.js
10 | strategy:
11 | fail-fast: false
12 |
13 | runs-on: 'ubuntu-latest'
14 |
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: 18
20 | - name: Setup project
21 | working-directory: ./superglue
22 | run: npm install
23 | - name: Lint
24 | working-directory: ./superglue
25 | run: npm run lint
26 | - name: Test
27 | working-directory: ./superglue
28 | run: npm run test
29 |
--------------------------------------------------------------------------------
/.github/workflows/dynamic-security.yml:
--------------------------------------------------------------------------------
1 | name: update-security
2 |
3 | on:
4 | push:
5 | paths:
6 | - SECURITY.md
7 | branches:
8 | - main
9 | workflow_dispatch:
10 |
11 | jobs:
12 | update-security:
13 | permissions:
14 | contents: write
15 | pull-requests: write
16 | pages: write
17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main
18 | secrets:
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 |
--------------------------------------------------------------------------------
/.github/workflows/publish_docs.yml:
--------------------------------------------------------------------------------
1 | name: Publish docs
2 | on: workflow_dispatch
3 |
4 | permissions:
5 | contents: write
6 | pages: write
7 | id-token: write
8 |
9 | concurrency:
10 | group: "pages"
11 | cancel-in-progress: false
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions/setup-node@v4
19 | with:
20 | node-version: 18
21 |
22 | - name: npm install
23 | working-directory: ./superglue
24 | run: npm install
25 |
26 | - name: Build typedoc
27 | working-directory: ./superglue
28 | run: npx typedoc
29 |
30 | - uses: actions/setup-python@v5
31 | with:
32 | python-version: 3.x
33 | - run: pip install mkdocs-material
34 | - name: Build mkdoc
35 | run: mkdocs build --site-dir ./_site
36 | - name: Upload artifact
37 | uses: actions/upload-pages-artifact@v3
38 |
39 | deploy:
40 | environment:
41 | name: github-pages
42 | url: ${{ steps.deployment.outputs.page_url }}
43 | runs-on: ubuntu-latest
44 | needs: build
45 | steps:
46 | - name: Deploy to GitHub Pages
47 | id: deployment
48 | uses: actions/deploy-pages@v4
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.lock
2 | node_modules
3 | dist
4 | pkg/
5 | vendor/bundle
6 | test/log/
7 | tmp/
8 | .byebug_history
9 | log
10 | blade.yml
11 | breezy/build/**/*.js
12 | *.gem
13 | props_template/performance/**/*.png
14 | testapp/
15 |
16 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 18.20.6
2 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Lines starting with '#' are comments.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # More details are here: https://help.github.com/articles/about-codeowners/
5 |
6 | # The '*' pattern is global owners.
7 |
8 | # Order is important. The last matching pattern has the most precedence.
9 | # The folders are ordered as follows:
10 |
11 | # In each subsection folders are ordered first by depth, then alphabetically.
12 | # This should make it easy to add new rules without breaking existing ones.
13 |
14 | # Global rule:
15 | * @jho406
16 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of conduct
2 |
3 | By participating in this project, you agree to abide by the
4 | [thoughtbot code of conduct][1].
5 |
6 | [1]: https://thoughtbot.com/open-source-code-of-conduct
7 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | ## Code of Conduct
4 |
5 | We welcome pull requests from everyone. By participating in this project, you
6 | agree to abide by the thoughtbot [code of conduct].
7 |
8 | We expect everyone to follow the code of conduct anywhere in thoughtbot's
9 | project codebases, issue trackers, chat-rooms, and mailing lists.
10 |
11 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct
12 |
13 | ### Opening a PR
14 |
15 | 1. Fork the repo,
16 | 2. Navigate to the `superglue` directory,
17 | 2. Run `npm install` to install the base dependencies,
18 | 3. Run the test suite: `npm run test`,
19 | 4. Make your changes,
20 | 5. Push your fork and open a pull request.
21 |
22 | A good PR will solve the smallest problem it possibly can and have good test
23 | coverage.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016-2025 Johny Ho
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Superglue
6 |
7 | Use classic Rails to build rich React Redux applications with **NO APIs** and
8 | **NO client-side routing**.
9 |
10 | [](https://github.com/thoughtbot/superglue/actions/workflows/build_js.yml)
11 |
12 | Superglue makes React and Redux as productive as Hotwire, Turbo and Stimulus.
13 | Its inspired by Turbolinks and designed to feel like a natural extension of
14 | Rails. Enjoy the benefits of Redux state management and React components
15 | without giving up the productivity of Rails form helpers, UJS, tag helpers,
16 | flash, cookie auth, and more.
17 |
18 | ### No APIs
19 |
20 | Instead of APIs, Superglue leans on Rails' ability to respond to different
21 | [mime types](https://apidock.com/rails/ActionController/MimeResponds/InstanceMethods/respond_to)
22 | on the same route. In a Superglue application, if you direct your browser to
23 | `/dashboard.html`, you would see the HTML version of the content, and if you
24 | went to `/dashboard.json` you would see the JSON version of the exact same
25 | content down to the footer.
26 |
27 | The end result would be something like this:
28 |
29 | 
30 |
31 | ### Powered by Classic Rails
32 | Superglue leans on Rails. Features like the flash, cookie auth, and URL
33 | helpers continue to be useful. Here's a look at the directory structure of a
34 | typical Rails application with Superglue.
35 |
36 | ```treeview
37 | app/
38 | |-- controllers/
39 | |-- views/
40 | | |-- dashboard/
41 | | | |-- index.jsx # The React page component
42 | | | |-- index.json.props # The json for the page component
43 | | | |-- index.html.erb
44 | ```
45 |
46 | ### PropsTemplate
47 | Powering the JSON responses is PropsTemplate, a diggable JSON templating DSL
48 | inspired by JBuilder. With PropsTemplate you can specify a path of the node you
49 | want, and PropsTemplate will walk the tree to it, skipping the execution of nodes
50 | that don't match the keypath.
51 |
52 | 
53 |
54 | ### All together now!
55 | Superglue comes with batteries that bring all the above concepts together to make
56 | building popular SPA features easy, painless, and productive.
57 |
58 | #### SPA Navigation
59 | A popular ask of SPAs is page-to-page navigation without reloading. This is
60 | easily done with Superglue's own UJS attributes inspired by Turbolinks:
61 |
62 | ```jsx
63 |
64 | ```
65 |
66 | The above will request for `/posts` with an `accept` of `application/json`, and
67 | when the client receives the response, swap out the current component for the
68 | component the response asks for, and `pushState` on history.
69 |
70 |
71 | #### Easy Partial updates
72 | Some features rely on updating some parts of the existing page. Imagine
73 | implementing type-ahead search. In traditional applications, you may need a new
74 | controller, routes, a discussion over versioning, JSON serializer, plenty of
75 | new JS code, etc.
76 |
77 | 
78 |
79 | With Superglue, this can be done with a simple `onChange`
80 |
81 | ```js
82 | import {NavigationContext} from '@thoughtbot/superglue'
83 |
84 | const {remote} = useContext(NavigationContext)
85 |
86 | const onChange = (e) => (
87 | remote(`/dashboard?qry=${e.target.value}&props_at=data.header.search`)}
88 | )
89 | ```
90 |
91 | With `props_at`, the above will make a request to `/dashboard?qry=haircut`,
92 | dig your template for the `data.header.search` node, return it in the response,
93 | and immutably graft it in the exact same path on the redux store before finally
94 | letting React re-render.
95 |
96 | For more on what you can do, check out our documentation.
97 |
98 | #### Server-Side Rendering
99 | Server-Side Rendering is supported via [Humid](https://github.com/thoughtbot/humid).
100 | See the [documentation for server-side rendering][ssr docs].
101 |
102 | [ssr docs]: ./recipes/server-side-rendering.md
103 |
104 | ## Documentation
105 |
106 | Documentation is hosted on [GitHub pages](https://thoughtbot.github.io/superglue).
107 |
108 | ## Contributing
109 |
110 | Please see [CONTRIBUTING.md](CONTRIBUTING.md).
111 |
112 | Thank you, [contributors]!
113 |
114 | [contributors]: https://github.com/thoughtbot/superglue/graphs/contributors
115 |
116 | ## Special Thanks
117 |
118 | Thanks to [jbuilder](https://github.com/rails/jbuilder),
119 | [scour](https://github.com/rstacruz/scour),
120 | [turbolinks3](https://github.com/turbolinks/turbolinks-classic),
121 | [turbograft](https://github.com/Shopify/turbograft/),
122 | [turbostreamer](https://github.com/malomalo/turbostreamer)
123 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 | # Security Policy
3 |
4 | ## Supported Versions
5 |
6 | Only the the latest version of this project is supported at a given time. If
7 | you find a security issue with an older version, please try updating to the
8 | latest version first.
9 |
10 | If for some reason you can't update to the latest version, please let us know
11 | your reasons so that we can have a better understanding of your situation.
12 |
13 | ## Reporting a Vulnerability
14 |
15 | For security inquiries or vulnerability reports, visit
16 | .
17 |
18 | If you have any suggestions to improve this policy, visit .
19 |
20 |
21 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | You've installed Superglue and now you're ready to configure your app.
2 |
3 | ## `application_visit.js`
4 |
5 | !!! tip
6 | If you want a [progress bar], this is the first thing you'll want to
7 | configure after installation.
8 |
9 | This file contains the factory that builds the [remote] and [visit]
10 | function that will be passed to your page components and used by the
11 | [data-sg-visit] and [data-sg-remote] UJS attributes.
12 |
13 | This file is meant for you to customize. For example, you'll likely
14 | want to add a [progress bar], control how visits work, or flash
15 | when the internet is down.
16 |
17 | [remote]: requests.md#remote
18 | [visit]: requests.md#visit
19 | [data-sg-remote]: ujs.md#data-sg-remote
20 | [data-sg-visit]: ujs.md#data-sg-visit
21 | [progress bar]: recipes/progress-bar.md
22 |
23 |
24 | ## `page_to_page_mapping.js`
25 |
26 | !!! info
27 | Stop by the [tutorial] to learn how to work with this file.
28 |
29 | **Vite Users** This step can be entirely optional if you're using Vite. See
30 | the [recipe](recipes/vite.md) for more information.
31 |
32 | This file exports a mapping between a `componentIdentifier` to an imported page
33 | component. This gets used in your `application.js` so that superglue knows
34 | which component to render with which identifier.
35 |
36 | For example:
37 |
38 | ```js
39 | const pageIdentifierToPageComponent = {
40 | 'posts/edit': PostsEdit,
41 | 'posts/new': PostsNew,
42 | 'posts/show': PostsShow,
43 | 'posts/index': PostsIndex,
44 | }
45 | ```
46 |
47 | [tutorial]: tutorial.md
48 |
49 | ## `application.js`
50 |
51 | This is the entry point of your application and uses Superglue's [Application]
52 | component. There's nothing to do here, but if you need finer control of
53 | how redux is setup, you can build your own Application using the [source] as
54 | inspiration.
55 |
56 | [source]: https://github.com/thoughtbot/superglue/blob/main/superglue/lib/index.tsx#L114
57 |
58 |
59 | - [:octicons-arrow-right-24: See complete reference](reference/index.md#application)
60 | for `Application`
61 |
62 |
63 | ## `flash.js`
64 |
65 | The installation generator will add a `flash.js` slice to `app/javascript/slices`
66 | and will work with the Rails `flash`. You can modify this however you like, out of the box:
67 |
68 | - When using `data-sg-visit`, all data in the flash slice will be cleared before the request.
69 | - When using `data-sg-visit` or `data-sg-remote`, the recieved flash
70 | will be merged with the current flash. You can change this behavior
71 | by modifying the flash slice.
72 |
73 |
74 | !!! hint
75 | If you're curious how this works, in your layout, `application.json.props`,
76 | the flash is serialized using `flash.to_h`
77 |
78 |
79 | To use in your page components, simply use a selector.
80 |
81 | ```jsx
82 | import { useSelector } from 'react-redux'
83 |
84 | ...
85 |
86 | const flash = useSelector((state) => state.flash)
87 | ```
88 |
89 | then use the flash as you would normally in a controller
90 |
91 | ```ruby
92 | def create
93 | flash[:success] = "Post was saved!"
94 | end
95 | ```
96 |
97 | [buildStore]: reference/index.md#buildstore
98 | [visitAndRemote]: requests.md
99 | [mapping]: reference/index.md#mapping
100 | [installation]: installation.md
101 |
102 |
--------------------------------------------------------------------------------
/docs/cross-cutting-concerns.md:
--------------------------------------------------------------------------------
1 | # Cross cutting concerns
2 |
3 | ## Layouts
4 |
5 | If you have state that is shared between pages, simply put it in your layout.
6 | For example. In the generated `application.json.props`
7 |
8 | ```ruby
9 | path = request.format.json? ? param_to_dig_path(params[:props_at]) : nil
10 |
11 | json.data(dig: path) do
12 | json.temperature "HOT HOT HOT"
13 | yield json
14 | end
15 | ```
16 |
17 | In the above, every page that gets rendered will have `temperature` as part of
18 | the [page response].
19 |
20 | ## Partials
21 |
22 | We can also use partials to extract crosscutting concerns. For example, a shared header:
23 |
24 | ```treeview
25 | app/
26 | |-- controllers/
27 | |-- views/
28 | | |-- shared/
29 | | | |-- _header.json.props
30 | | |-- posts/
31 | | | |-- index.js
32 | | | |-- index.json.props
33 | | |-- comments/
34 | | | |-- index.js
35 | | | |-- index.json.props
36 | ```
37 |
38 | By [design](./redux-state-shape.md) this results in duplicate JSON nodes
39 | across our `pages` slice:
40 |
41 | ```json
42 | {
43 | pages: {
44 | "/posts": {
45 | data: {
46 | header: {
47 | email: "foo@foo.com"
48 | }
49 | }
50 | },
51 | "/comments": {
52 | data: {
53 | header: {
54 | email: "foo@foo.com"
55 | }
56 | }
57 | },
58 | }
59 | }
60 | ```
61 |
62 |
63 | ## Advanced functionality
64 |
65 | For most cases where you don't have to mutate your store, using layouts or
66 | partials would be good enough. Its a fine tradeoff for simplicity.
67 |
68 | Sometimes we have global concerns that we'd like to keep updated. This can be
69 | for across pages when navigating or if we'd like to perform client-side
70 | updates. For example, if we're showing a shopping cart quantity on the
71 | header, we want to keep that updated as we navigate back, and when updating
72 | line items locally.
73 |
74 | For this, Superglue has fragments and Redux slices.
75 |
76 | !!! hint
77 | You may not need to use fragments and Redux slices. For some apps, the only
78 | slices you'll ever need is the generated `flash.js` slice that comes with the
79 | install step.
80 |
81 | ## Fragments
82 |
83 | A fragment in Superglue is any props_template block with given name:
84 |
85 | ```
86 | json.body do
87 | json.cart(fragment: "shoppingCart"]) do
88 | end
89 | end
90 | ```
91 |
92 | Now whenever we encounter a fragment from a new `visit` or update a fragment using `remote`,
93 | Superglue will dispatch an `updateFragment` action.
94 |
95 |
96 | - [:octicons-arrow-right-24: See reference](reference/index.md#updatefragments)
97 | for `updateFragments`
98 |
99 |
100 | That's not a very useful thing by itself, but when combined with Redux toolkit
101 | [createSlice] and [useSelector], it offers a way to easily build global
102 | concerns.
103 |
104 | [useSelector]: https://react-redux.js.org/api/hooks#useselector
105 | [createSlice]: https://redux-toolkit.js.org/api/createSlice
106 |
107 |
108 | ## Slices
109 |
110 | Whenever a fragment is received or updated, a `UPDATE_FRAGMENTS` action is
111 | dispatched with the value. You can return that value as your state to
112 | keep your slice updated as the user navigates.
113 |
114 | For example:
115 |
116 | ```javascript
117 | import { createSlice } from '@reduxjs/toolkit'
118 | import { updateFragment } from '@thoughtbot/superglue'
119 |
120 | export const cartSlice = createSlice({
121 | name: 'cart',
122 | extraReducers: (builder) => {
123 | builder.addCase(updateFragments, (state, action) => {
124 | const { value, name } = action.payload;
125 |
126 | if (name === "cart") {
127 | return value
128 | } else {
129 | return state;
130 | }
131 | })
132 | }
133 | })
134 | ```
135 |
136 | Then somewhere in a component you can [useSelector]:
137 |
138 | ```
139 | import { useSelector } from 'react-redux'
140 |
141 | ...
142 |
143 | const cart = useSelector((state) => state.cart)
144 | ```
145 |
146 |
147 | And as this is just a normal Redux [slice], you can also add custom [reducers]
148 | to the mix for client-side updates.
149 |
150 | [useSelector]: https://redux-toolkit.js.org/tutorials/quick-start#use-redux-state-and-actions-in-react-components
151 | [slice]: https://redux-toolkit.js.org/api/createSlice
152 | [reducers]: https://redux-toolkit.js.org/api/createSlice#reducers
153 |
154 | ### initialState
155 |
156 | You can render your slice's initial state in the [slices] `key` of the page
157 | object, it'll be merged with the `initialState` passed to your `buildStore`
158 | function in your [application.js](./configuration.md#applicationjs)
159 |
160 | [slices]: ./page-response.md#slices
161 |
162 | ## Other actions
163 |
164 | Aside from `UPDATE_FRAGMENTS`, superglue comes with other actions that get
165 | dispatched during lifecycle events that you can make use of. The `flashSlice`
166 | that was generated with the installation is a good example of this.
167 |
168 | To higlight a few:
169 |
170 |
171 | `BEFORE_FETCH` - Action created before a before a fetch is called.
172 |
173 | ```
174 | {
175 | type: "@@superglue/BEFORE_FETCH",
176 | payload: [..array args that are passed to fetch]
177 | }
178 | ```
179 |
180 | `BEFORE_VISIT` - Same as above, but called only for a `visit` action.
181 |
182 | ```
183 | {
184 | type: "@@superglue/BEFORE_VISIT",
185 | payload: [..array args that are passed to fetch]
186 | }
187 | ```
188 |
189 | `BEFORE_REMOTE` - Same as above, but called only a `remote` action.
190 |
191 | ```
192 | {
193 | type: "@@superglue/BEFORE_REMOTE",
194 | payload: [..array args that are passed to fetch]
195 | }
196 | ```
197 |
198 | `SAVE_RESPONSE` - Whenever a [page response] is received.
199 |
200 | ```
201 | {
202 | type: "@@superglue/SAVE_RESPONSE",
203 | payload: {
204 | pageKey: "/posts",
205 | page: {...the page response},
206 | },
207 | }
208 | ```
209 |
210 | [page response]: ./page-response.md
211 | [extraReducers]: https://redux-toolkit.js.org/api/createSlice#extrareducers
212 |
213 |
--------------------------------------------------------------------------------
/docs/deferments.md:
--------------------------------------------------------------------------------
1 | Sometimes you may want to load parts of your page later, like a slow sidebar, a
2 | graph that takes extra time to load, or tab content that shouldn't appear
3 | immediately. These scenarios are perfect use cases for Deferments.
4 |
5 | Deferments are a low effort way to load content later, both automatically and
6 | manually. Better yet, most of the work takes place in Rails land in your views.
7 |
8 | ## `defer: :auto`
9 |
10 | This option make it easy to defer content in a single setting.
11 |
12 | === "views/posts/index.json.props"
13 |
14 | ``` ruby
15 | json.metrics(defer: [:auto, placeholder: {totalVisitors: 0}]) do
16 | sleep 10 # expensive operation
17 | json.totalVisitors 30
18 | end
19 | ```
20 |
21 | === "views/layouts/application.json.props"
22 |
23 | ``` ruby
24 | json.data do
25 | yield
26 | end
27 | ```
28 |
29 | And that's it!
30 |
31 | ### Behind the scenes
32 |
33 | When a user lands on a page Superglue will receive
34 |
35 | ```json
36 | {
37 | data: {
38 | metrics: {
39 | totalVisitors: 0
40 | }
41 | },
42 | defers:[
43 | {url: '/dashboard?props_at=data.metrics', type: "auto"}
44 | ],
45 | ...other
46 | }
47 | ```
48 |
49 | Your page components will receive `{metrics: {totalVisitors: 0}}` and render. Superglue will then
50 | make a remote request:
51 |
52 | ```
53 | remote("/dashboard?props_at=data.metrics")
54 | ```
55 |
56 | 10 seconds later the response succeeds with `{total_visitors: 30}`. Superglue
57 | then immutably grafts that payload into the `/dashboard` page at the path
58 | `data.metrics`. The page state would look like the following:
59 |
60 | ```
61 | {
62 | data: {
63 | metrics: {
64 | totalVisitors: 30
65 | }
66 | },
67 | defers:[...others],
68 | ...other
69 | }
70 | ```
71 |
72 | Your page component finally recieves the new props and rerenders. For more
73 | control, you may provide a `success_action` or `fail_action`, and Superglue
74 | will dispatch these actions when the promise resolves successfully or fails.
75 |
76 | ```ruby
77 | json.metrics(defer: [:auto, placeholder: {totalVisitors: 0}, success_action: "SUCCESS", fail_action: "FAIL"]) do
78 | sleep 10 # expensive operation
79 | json.totalVisitors 30
80 | end
81 | ```
82 |
83 | ## `defer: :manual`
84 |
85 | When you want control over when deferred content loads, e.g., tabbed content,
86 | use `defer: :manual` to stop the content from loading
87 |
88 | ```ruby
89 | json.metrics(defer: [:manual, placeholder: {totalVisitors: 0}]) do
90 | sleep 10 # expensive operation
91 | json.totalVisitors 30
92 | end
93 | ```
94 |
95 | and manually use `remote`
96 |
97 | ```
98 | remote("/dashboard?props_at=data.metrics")
99 | ```
100 |
101 |
102 |
--------------------------------------------------------------------------------
/docs/demo.md:
--------------------------------------------------------------------------------
1 | # Demo Application
2 |
3 | We have a non-trivial [demo] application built using Superglue and the original
4 | [Rails and StimulusJS] version built by [Sean Doyle] The intent is to help you
5 | compare and contrast both approaches and showcase how enjoyable and
6 | Rails-like Superglue/React/Redux can be.
7 |
8 | 
9 |
10 | We recommend going over the meticulously verbose commit history on [Sean's
11 | version] and comparing that with the [Superglue version].
12 |
13 | [demo]: https://github.com/thoughtbot/select-your-own-seat-superglue
14 | [Rails and StimulusJS]: https://github.com/seanpdoyle/select-your-own-seat
15 | [Sean Doyle]: https://github.com/seanpdoyle
16 | [Sean's version]: https://github.com/seanpdoyle/select-your-own-seat/commits/main
17 | [Superglue version]: https://github.com/thoughtbot/select-your-own-seat-superglue/commits/superglue
18 |
19 |
--------------------------------------------------------------------------------
/docs/digging.md:
--------------------------------------------------------------------------------
1 | # Digging
2 |
3 | Beyond full page navigation, Superglue can make selective updates to parts of
4 | the page without a full load through digging. You may recognize digging from
5 | earlier docs:
6 |
7 | ```
8 | /some_current_page?props_at=data.rightDrawer.dailySpecials
9 | ```
10 |
11 | By simply adding a `props_at` parameter to your requests, you can selectively
12 | fetch parts of the page without incurring the cost of loading unneeded content. This
13 | is great for functionality like modals, tabs, etc.
14 |
15 | ## The `props_at` param
16 |
17 | The `props_at` param is a keypath to the content in your PropsTemplate. As a simplified
18 | example, imagine this page with no layouts:
19 |
20 | ```ruby
21 | path = param_to_dig_path(params[:props_at])
22 | json.data(dig: path) do
23 | json.header do
24 | json.search do
25 | # Results is a leaf node
26 | json.results Post.search(params[:some_search_str])
27 | end
28 | end
29 |
30 | json.content do
31 | json.barChart do
32 | ...bar chart data
33 | end
34 |
35 | ...
36 | end
37 |
38 | ...
39 | end
40 | ```
41 |
42 | To fetch the `json.search` node, we would need to walk to `data` then `header`
43 | then `search`. Translating that to a url with a `props_at` param:
44 |
45 | ```
46 | /dashboard?props_at=data.header.search&some_search_str=haircuts
47 | ```
48 |
49 | Digging is normally combined with using [data-sg-remote] or [remote] to update
50 | content in async fashion.
51 |
52 | !!! info
53 | `props_at` can be used with `data-sg-visit`
54 |
55 | [data-sg-remote]: ./ujs.md#data-sg-remote
56 | [remote]: ./requests.md#remote
57 |
58 |
59 | ## Collections
60 | There are two ways to query collections. Looking at the following example:
61 |
62 | ```ruby
63 | path = param_to_dig_path(params[:props_at])
64 | json.data(dig: path) do
65 | json.posts do
66 | json.array! @posts do |post|
67 | json.details do
68 | json.title post.title
69 | end
70 | end
71 | end
72 | end
73 | ```
74 |
75 | ### Index-based selection
76 | You may use an index-based key to fetch an item in a list like so:
77 |
78 | ```js
79 | remote('/dashboard?props_at=data.posts.0.details')
80 | ```
81 |
82 | To enable this functionality, you are required to implement `member_at(index)`
83 | on the passed collection.
84 |
85 | ?> PropsTemplate includes a `Array` extension which delegates to `at`. If you've
86 | used the Superglue generators, it will be included in an initializer.
87 |
88 | While traversing by index works fine, it can lead the wrong post being updated
89 | if your Redux state has changed by the time the request comes back.
90 |
91 | ### Attribute-based selection
92 | Attribute-based keys for collections look like this:
93 |
94 | ```js
95 | remote('/dashboard?props_at=data.posts.some_id=1.details')
96 | ```
97 |
98 | Notice that we're now referencing the collection member by `some_id=1` instead
99 | of index. This will fetch the node from the backend and graft it correctly in
100 | Redux.
101 |
102 | To enable this, you are required to implement `member_by(attribute, value)` on
103 | the passed collection AND use the option `:key` in `json.array!`. For example:
104 |
105 | ```ruby
106 | path = param_to_dig_path(params[:props_at])
107 | json.data(dig: params[:props_at]) do
108 | json.posts do
109 | json.array! @posts, key: :some_id do |post|
110 | json.details do
111 | json.title post.title
112 | end
113 |
114 | # The following will be auto appended by the key: option
115 | # json.some_id post.some_id
116 | end
117 | end
118 | end
119 | ```
120 |
121 | ## Partials
122 |
123 | You can even query into partials.
124 |
125 | ```js
126 | remote('/dashboard?props_at=data.posts.some_id=1.details')
127 | ```
128 |
129 | ```ruby
130 | json.data(dig: params[:props_at]) do
131 | json.posts(partial: 'list_of_posts')do
132 | end
133 | end
134 | ```
135 |
136 | ```ruby
137 | # list_of_posts.json.props
138 | json.array! @posts , key: :some_id do |post|
139 | json.details do
140 | json.title post.title
141 | end
142 |
143 | # The following will be auto appended by the key: option
144 | # json.some_id post.some_id
145 | end
146 | ```
147 |
148 | !!! info
149 | When querying, Superglue will disable
150 | [caching](https://github.com/thoughtbot/props_template#caching) and
151 | [deferment](https://github.com/thoughtbot/props_template#deferment) until the
152 | target node is reached.
153 |
154 | With digging, many modern SPA functionality can be achieved by just a keypath and a
155 | few lines of code.
156 |
157 | [PropsTemplate]: https://github.com/thoughtbot/props_template
158 |
159 |
--------------------------------------------------------------------------------
/docs/images/demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thoughtbot/superglue/1dd67e4526e03004ad3efc6f367c9a0caa02942b/docs/images/demo.jpg
--------------------------------------------------------------------------------
/docs/images/haircuts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thoughtbot/superglue/1dd67e4526e03004ad3efc6f367c9a0caa02942b/docs/images/haircuts.png
--------------------------------------------------------------------------------
/docs/images/no_apis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thoughtbot/superglue/1dd67e4526e03004ad3efc6f367c9a0caa02942b/docs/images/no_apis.png
--------------------------------------------------------------------------------
/docs/images/props_template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thoughtbot/superglue/1dd67e4526e03004ad3efc6f367c9a0caa02942b/docs/images/props_template.png
--------------------------------------------------------------------------------
/docs/images/s-color.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/images/superglue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thoughtbot/superglue/1dd67e4526e03004ad3efc6f367c9a0caa02942b/docs/images/superglue.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Overview
2 |
3 | Superglue and friends [thoughtfully pairs Rails and React]. Its built with a
4 | laser focus on The Rails Way and aims to provide a simple developer experience
5 | on par with Hotwire, Stimulus, and Turbo. Confidently use Rails routes,
6 | controllers, views as you normally would in a multi-page application and
7 | integrate with React's vast ecosystem.
8 |
9 | ## Who is it for?
10 |
11 | Superglue is built from the ground up for
12 |
13 | - **The Rails developer**: For those of us who want to harness the full power
14 | of ALL of Rails --controllers, server-side routing, views, form helpers,
15 | and more — to create seamless, interactive React applications **without** the
16 | hassle of APIs and client side routing.
17 |
18 | - **Teams fighting complexity**: Its not easy pivoting from complexity.
19 | Superglue empowers teams to take small steps back without giving up the
20 | effort invested in React components.
21 |
22 | - **Startups moving fast**: Founders looking to hit the ground running by
23 | combining the speed of Rails development with React's vast ecosystem of
24 | prebuilt UI libraries.
25 |
26 | - **Javascript fatigue**: Anyone tired of JS complexity and just want to get
27 | work done.
28 |
29 |
30 | ## How does it work?
31 |
32 | ### It’s Rails
33 |
34 | Superglue leans on Rails' ability to respond to different mime types on the
35 | same route and divides the usual `foobar.html.erb` into three familiar
36 | templates.
37 |
38 | - `foobar.json.props` A presenter written in a jbuilder-like template that
39 | builds your page props.
40 | - `foobar.(jsx|tsx)` Your page component that receives the props from above.
41 | - `foobar.html.erb` Injects your page props into Redux when the browser loads
42 | it.
43 |
44 | Shape your `props` to roughly match your component structure. For example:
45 |
46 | ```ruby
47 | json.header do
48 | json.username @user.username
49 | json.linkToProfile url_for(@user)
50 | end
51 |
52 | json.rightDrawer do
53 | json.cart(partial: 'cart') do
54 | end
55 | json.dailySpecials(partial: 'specials') do
56 | end
57 | end
58 |
59 | json.body do
60 | json.productFilter do
61 | form_props(url: "/", method: "GET") do |f|
62 | f.select(:category, ["lifestyle", "programming", "spiritual"])
63 | f.submit
64 | end
65 | end
66 |
67 | json.products do
68 | json.array! @products do |product|
69 | json.title product.title
70 | json.urlToProduct url_for(product)
71 | end
72 | end
73 | end
74 |
75 | json.footer do
76 | json.copyrightYear "2023"
77 | end
78 | ```
79 |
80 | Familiar Rails conveniences include form_props (a fork of `form_with` made for React),
81 | flash messages integrated as a Redux [slice], and [Unobtrusive Javascript][UJS] helpers.
82 |
83 | ### It’s React
84 |
85 | But there are no APIs! The above is injected as a script tag in the DOM so everything
86 | loads in the initial request. Its added to your [Redux state] and passed to
87 | the page component in a hook, for example:
88 |
89 | ```js
90 | import React from 'react';
91 | import { useSelector } from 'react-redux';
92 | import { Drawer, Header, Footer, ProductList, ProductFilter } from './components';
93 | import { useContent } from '@thoughtbot/superglue'
94 |
95 | export default function FooBar() {
96 | const {
97 | header,
98 | products = [],
99 | productFilter,
100 | rightDrawer,
101 | footer
102 | } = useContent()
103 |
104 | const flash = useSelector((state) => state.flash);
105 |
106 | return (
107 | <>
108 |
{flash && flash.notice}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | >
119 | );
120 | }
121 | ```
122 |
123 | ### It’s Turbolinks
124 |
125 | Superglue drew inspiration fromthe original Turbolinks, but instead of sending
126 | your `foobar.html.erb` over the wire and swapping the ``, it sends
127 | `foobar.json.props` over the wire to your React and Redux app and swaps the
128 | page component.
129 |
130 | This behavior is opt-in. Superglue provides UJS helpers that you can use with
131 | your React components to SPA transition to the next page.
132 |
133 | ```jsx
134 | Next Page
135 | ```
136 |
137 | ### The return of UJS
138 |
139 | Superglue’s secret sauce is that your `foobar.json.props` is diggable; making
140 | any part of your page dynamic by using a query string. It’s a simpler approach
141 | to Turbo Frames and Turbo Stream.
142 |
143 | Need to reload a part of the page? Just add a query parameter and combine with
144 | the [UJS] helper attribute `data-sg-remote`:
145 |
146 | ```jsx
147 |
148 |
149 |
150 |
151 | Reload Daily Specials
152 |
153 |
154 | ```
155 |
156 | The above will traverse `foobar.json.props`, grab `dailySpecials` while
157 | skipping other nodes, and immutably graft it to your Redux store.
158 |
159 | This works well for [modals], chat, streaming, and [more]!
160 |
161 | [secret sauce]: digging.md
162 | [UJS]: ujs.md
163 |
164 | ### One-stop shop
165 |
166 | We know first hand how complex React can be, but we don't shy away from
167 | complexity. We want to make things better for everyone and to that end, we have
168 | a supporting cast of tooling under one shop to bring ease and consistancy to
169 | your team.
170 |
171 |
172 |
173 | - __Superglue JS__
174 |
175 | ---
176 |
177 | The javascript library thoughfully pairing Rails and React.
178 |
179 | [:octicons-arrow-right-24: SuperglueJs](https://github.com/thoughtbot/superglue)
180 |
181 |
182 | - __Superglue Rails__
183 |
184 | ---
185 |
186 | Integration helpers, and generators for installation and scaffolding.
187 |
188 | [:octicons-arrow-right-24: superglue_rails](https://github.com/thoughtbot/superglue/tree/main/superglue_rails)
189 |
190 | - __PropsTemplate__
191 |
192 | ---
193 |
194 | A very fast JSON builder. The [secret sauce] that give [UJS] superpowers.
195 |
196 | [:octicons-arrow-right-24: props_template](https://github.com/thoughtbot/props_template)
197 |
198 |
199 | - __Humid__
200 |
201 | ---
202 |
203 | Server Side Rendering using MiniRacer and V8 isolates.
204 |
205 | [:octicons-arrow-right-24: Humid](recipes/ssr.md)
206 |
207 | - __FormProps__
208 |
209 | ---
210 |
211 | A `form_with` FormBuilder that lets you use Rails forms with React.
212 |
213 | [:octicons-arrow-right-24: form_props](https://github.com/thoughtbot/form_props)
214 |
215 | - __CandyWrapper__
216 |
217 | ---
218 |
219 | Lightweight wrapper components around popular React UI libraries made to work with
220 | FormProps.
221 |
222 |
223 | [:octicons-arrow-right-24: candy_wrapper](https://github.com/thoughtbot/candy_wrapper)
224 |
225 |
226 |
227 |
228 | [Redux state]: ./redux-state-shape.md
229 | [modals]: recipes/modals.md
230 | [more]: recipes/
231 | [slice]: ./cross-cutting-concerns.md#slices
232 | [thoughtfully pairs Rails and React]: https://thoughtbot.com/blog/superglue-1-0-react-rails-a-new-era-of-thoughtfulness
233 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | !!! info "Prerequisites"
4 | To get started with Superglue, you'll need
5 |
6 | - A javascript bundler. We'll assume esbuild with js-bundling, but you can also use vite.
7 | - `yarn`
8 |
9 | Add the following to your Gemfile
10 |
11 | ```ruby
12 | # Gemfile
13 | gem 'superglue'
14 | ```
15 |
16 | Run bundle and the installation generator:
17 |
18 | ```terminal
19 | bundle
20 | rails g superglue:install
21 |
22 | ```
23 |
24 | !!! example ""
25 | If you prefer typescript
26 |
27 | ```terminal
28 | rails g superglue:install --typescript
29 | ```
30 |
31 | The above will generate the following files:
32 |
33 | ```terminal
34 | .
35 | └─ app/
36 | └─ javascript/
37 | ├─ slices/
38 | │ ├─ flash.js
39 | | └─ pages.js
40 | ├─ actions.js
41 | ├─ application.js
42 | ├─ application_visit.js
43 | ├─ page_to_page_mapping.js
44 | └─ store.js
45 | ```
46 |
47 |
48 | ## Redux Toolkit
49 |
50 | If you've ever encountered Redux then the files above may seem familiar to you.
51 | Superglue works as a complete and fully functional Redux Toolkit application.
52 | For the most part, all the functionality you would need resides in these files
53 | and you'll make minimum edits, but they are made available if you ever need
54 | greater control over state management.
55 |
56 | ## Configuration
57 |
58 | We recommend getting familiar with the following files:
59 |
60 | - `application_visit.js` - Add custom functionality to Superglue navigation, e.g, progress bars.
61 | - `page_to_page_mapping.js` - Pairs your `props` files with your page components.
62 | - `flash.js` - Seamlessly, integrates with the Rails flash.
63 |
64 | For more information, visit the [configuration] section.
65 |
66 | [configuration]: configuration.md
67 |
68 | ## Scaffold
69 |
70 | If you'd like to dive right in, you can start with a scaffold:
71 |
72 | ```terminal
73 | rails g superglue:scaffold post body:string
74 | ```
75 |
76 | !!! example ""
77 | If you prefer typescript
78 |
79 | ```terminal
80 | rails g superglue:scaffold post body:string --typescript
81 | ```
82 |
83 | or proceed with a [tutorial](./tutorial.md)
84 |
85 |
--------------------------------------------------------------------------------
/docs/navigation-context.md:
--------------------------------------------------------------------------------
1 | # NavigationContext
2 |
3 | In addition to `visit` and `remote`, the `NavigationContext` provides a few
4 | other methods and properties that are best decribed in the context of
5 | `navigateTo`.
6 |
7 | ```
8 | import { NavigationContext } from '@thoughtbot/superglue'
9 |
10 | const {
11 | navigateTo,
12 | visit,
13 | remote,
14 | pageKey,
15 | search
16 | } = useContext(NavigationContext)
17 | ```
18 |
19 |
20 | - [:octicons-arrow-right-24: See complete reference](reference/types.md#navigationcontextprops)
21 | for `NavigationContext`
22 |
23 |
24 | ## `navigateTo`
25 |
26 | Fundamentally, `visit` is responsible for `fetch`ing a page, [saving] it, and
27 | lastly use `navigateTo` to load the page, update the url, and swap the page
28 | component. The NavigationContext exposes `navigateTo` for you to use
29 | use independently. For example:
30 |
31 | ```javascript
32 | navigateTo('/posts')
33 | ```
34 |
35 | !!! Note
36 | The page must exist in the store, or `navigateTo` will throw a error. Use [copyPage]
37 | to prepopulate before navigating.
38 |
39 |
40 | `navigateTo` is especially useful for optimistic navigation like local facted
41 | search and works best when combined with `search` and `pageKey` from the same
42 | NavigationContext, and the [copyPage] action.
43 |
44 | In this example, we'll assume we're on pageKey "/posts":
45 |
46 | ```
47 | import { copyPage, NavigationContext } from '@thoughtbot/superglue'
48 | import { myAppDispatch } from '@myJavascript/store'
49 |
50 | // In your component somewhere
51 | const {
52 | navigateTo,
53 | pageKey,
54 | search
55 | } = useContext(NavigationContext)
56 |
57 | const nextPageKey = pageKey + "?active=true"
58 | dispatch(copyPage({from: pageKey, to: nextPageKey}))
59 |
60 | // On a click handler
61 | navigateTo(nextPageKey, { action: 'push'})
62 |
63 | // later after navigation.
64 | console.log(search) // would return {active: "true"}
65 | ```
66 |
67 | With the above, we're able to make use of the URL search param as a source of
68 | state. And by using `navigateTo`, we're able to filter local results while updating
69 | the URL.
70 |
71 |
72 | - [:octicons-arrow-right-24: See complete reference](reference/types.md#navigateto-1)
73 | for `navigateTo`
74 |
75 |
76 | [saving]: ./reference/index.md#saveandprocesspage
77 | [copyPage]: ./reference/index.md#copypage
78 |
--------------------------------------------------------------------------------
/docs/page-response.md:
--------------------------------------------------------------------------------
1 | # The `page` response
2 |
3 | Superglue expects your JSON responses to contain the following attributes. If you
4 | used Superglue's generators, this would be all set for you in
5 | `application.json.props`.
6 |
7 | ```
8 | {
9 | data: {
10 | ...
11 | },
12 | componentIdentifier,
13 | defers,
14 | assets,
15 | csrfToken,
16 | action,
17 | path,
18 | renderedAt,
19 | fragments,
20 | restoreStrategy,
21 | slices
22 | }
23 | ```
24 |
25 | ### `data`
26 | Your page's content. This can be accessed using the
27 |
28 | ### `componentIdentifier`
29 | A `string` to instruct Superglue which component to render. The generated
30 | `application.json.props` will set this to the `active_template_virtual_path`
31 | (added by [props_template]), but you can customize this to fit your needs.
32 |
33 | ```ruby
34 | # application.json.props
35 | json.componentIdentifier active_template_virtual_path
36 | ```
37 |
38 | You can control which `componentIdentifier` will render which component in the
39 | `page_to_page_mapping.js`.
40 |
41 |
42 | - [:octicons-arrow-right-24: See reference](configuration.md#page_to_page_mappingjs)
43 | for page_to_page_mapping.js
44 |
45 |
46 |
47 | ### `assets`
48 | An `array` of asset fingerprint `string`s. Used by Superglue to detect the need to
49 | refresh the browser due to new assets. You can control the refresh behavior in
50 | `application_visit.js`.
51 |
52 | ### `csrfToken`
53 | The authenticity token that Superglue will use for non-GET request made by using
54 | `visit` or `remote` thunks. This includes forms that have the `data-sg-visit`
55 | or `data-sg-remote` attribute.
56 |
57 | ### `action` and `path`
58 | Only included when `props_at` is part of the request parameters. `action` is always
59 | set to `graft` and `path` is the camelCase keypath to the requested node.
60 | Superglue uses these attributes to immutably graft a node from the server-side to
61 | the client-side.
62 |
63 | ### `renderedAt`
64 | An UNIX timestamp representing the time the response was rendered.
65 |
66 | ### `fragments`
67 | An `array` of [fragments](./cross-cutting-concerns.md#advanced-functionality). In
68 | `application.json.props` this is set to `json.fragments!`.
69 |
70 | ### `restoreStrategy`
71 | By specifying the restore strategy used (`fromCacheOnly`, `revisitOnly`, or
72 | `fromCacheAndRevisitInBackground`), you can control what superglue does when
73 | encountering the page again when pressing the back or forward browser navigation
74 | buttons.
75 |
76 | - `fromCacheAndRevisitInBackground` will transition to the cached page, then
77 | issue a visit in the background, redirecting and replacing history if needed.
78 | This is the option set in `application.json.props` when using the generators.
79 | - `revisitOnly` will always issue a visit request in the background before
80 | - `fromCacheOnly` will only restore the page from cache
81 | transitioning
82 |
83 | ### `slices`
84 | An object merged with the `initialState` when implementing `buildStore` inside
85 | of `application.js`. You can use this as the initial state for redux slices.
86 | Take advantage of the `SAVE_RESPONSE` to continually update your slice everytime
87 | superglue recieves a new page request.
88 |
89 | [props_template]: https://github.com/thoughtbot/props_template
90 |
91 |
--------------------------------------------------------------------------------
/docs/rails-utils.md:
--------------------------------------------------------------------------------
1 | # Rails utils
2 |
3 |
4 | ## Rendering defaults
5 |
6 | Superglue typically requires 3 templates.
7 |
8 | ```
9 | app/views/
10 | posts/
11 | index.html.erb # duplicated
12 | index.jsx
13 | index.json.props
14 | users/
15 | index.html.erb # duplicated
16 | index.jsx
17 | index.json.props
18 | ```
19 |
20 | Use `use_jsx_rendering_defaults` and `superglue_template` for cleaner
21 | directories.
22 |
23 | ```ruby
24 | class PostsController < ApplicationController
25 | before_action :use_jsx_rendering_defaults
26 | superglue_template "application/superglue" #defaults to application/superglue
27 | end
28 | ```
29 |
30 | !!! warning
31 | The `file`, `partial`, `body`, `plain`, `html`, `inline` will not work with
32 | `render` when using `before_action :use_jsx_rendering_defaults` callback. Make use of
33 | `:only` and `:except` to narrow down its usage.
34 |
35 | Which will allow you to deduplicate the files:
36 |
37 | ```
38 | app/views
39 | application/
40 | superglue.html.erb
41 | posts/
42 | index.jsx
43 | index.json.props
44 | users/
45 | index.jsx
46 | index.json.props
47 | ```
48 |
49 | and omit `props` files for cases when there is no content.
50 |
51 | ```
52 | app/views
53 | application/
54 | superglue.html.erb
55 | about/
56 | index.jsx
57 | ```
58 |
59 | ## `redirect_back_with_props_at`
60 |
61 | A helper to help retain the `props_at` parameter as part of the redirect `location`.
62 | This helper has the same method signature as Rails own `redirect_back`.
63 |
64 | ```ruby
65 | def create
66 | redirect_back_with_props_at fallback_url: '/'
67 | end
68 | ```
69 |
70 |
71 | ## Setting the content location
72 |
73 | You can override the URL Superglue uses to display on the address bar and
74 | store your response directly from the server using `content-location`. This
75 | is optional. For example:
76 |
77 | ```ruby
78 | def create
79 | @post = Post.new(post_params)
80 |
81 | if @post.save
82 | redirect_to @post, notice: 'Post was successfully created.'
83 | else
84 | response.set_header("content-location", new_post_path)
85 | render :new
86 | end
87 | end
88 | ```
89 |
90 |
91 |
--------------------------------------------------------------------------------
/docs/recipes/infinite-scroll.md:
--------------------------------------------------------------------------------
1 | # Infinite scroll
2 |
3 | In this recipe, we'll add infinite scroll to our application. Superglue doesn't
4 | have an infinite scroll component, but it has tools that make it easy to
5 | work with React's ecosystem.
6 |
7 | Lets begin by adding `react-infinite-scroll-hook`
8 |
9 | ```
10 | yarn add react-infinite-scroll-hook
11 | ```
12 |
13 | And continue off from our [pagination] recipe.
14 |
15 | !!! tip
16 | We'll use the `beforeSave` callback to modify the payload before superglue
17 | saves it to the store. This callback is an option for both `visit` and
18 | `remote` functions. See the
19 | [beforeSave reference](../reference/types.requests.md#beforesave-2) for more details.
20 |
21 | ```diff
22 | // app/views/posts/index.js
23 |
24 | import React from 'react'
25 | - import {useContent} from '@thoughtbot/superglue'
26 | + import {useContent, NavigationContext} from '@thoughtbot/superglue'
27 | import PostList from './PostList'
28 | import Header from './Header'
29 | + import useInfiniteScroll from 'react-infinite-scroll-hook';
30 |
31 | export default PostIndex = () => {
32 | const {
33 | posts,
34 | header,
35 | pathToNextPage,
36 | pathToPrevPage
37 | } = useContent()
38 |
39 | + const { remote, pageKey } = useContext(NavigationContext)
40 | + const { loading, setLoading } = useState(false)
41 | + const hasNextPage = !!pathToNextPage
42 | +
43 | + const beforeSave = (prevPage, receivedPage) => {
44 | + const prevPosts = prevPage.data.posts
45 | + const receivedPosts = receivedPage.data.posts
46 | + receivedPage.data.posts = prevPosts + receivedPosts
47 | +
48 | + return receivedPage
49 | + }
50 | +
51 | + const loadMore = () => {
52 | + setLoading(true)
53 | + remote(pathToNextPage, {pageKey, beforeSave})
54 | + .then(() => setLoading(false))
55 | + }
56 | +
57 | + const [sentryRef] = useInfiniteScroll({
58 | + loading,
59 | + hasNextPage,
60 | + onLoadMore: loadMore,
61 | + });
62 |
63 | return (
64 | <>
65 |
66 |
78 | - {pathToPrevPage && Prev Page}
79 | - {pathToNextPage && Next Page}
80 | >
81 | )
82 | }
83 |
84 | ```
85 |
86 | [pagination]: ./spa-pagination.md
87 |
--------------------------------------------------------------------------------
/docs/recipes/modals.md:
--------------------------------------------------------------------------------
1 | # Modals
2 |
3 | Modals are easy. Lets imagine a scenario where we have two urls:
4 |
5 | 1. `/posts`
6 | 2. `/posts/new`
7 |
8 | When a user visits `/posts/new` from `/posts`, we want a modal to appear
9 | overlaying the existing list of posts. The overlay should work if a user
10 | chooses instead to directly visit `/posts/new`.
11 |
12 | ## The setup
13 |
14 | Both urls render a list of posts. Lets set up the controller and the
15 | `page_to_page_mapping.js` the same way.
16 |
17 |
18 | === "`posts_controller.rb`"
19 | !!! info "Same template different action"
20 | Notice that we're rendering the `index` for the `new` action. While the
21 | content is the same, the `componentIdentifier` is different as that has
22 | been setup to use the controller and action name.
23 |
24 | ```ruby
25 | # app/controllers/posts_controller.rb
26 |
27 | def index
28 | @posts = Post.all
29 | end
30 |
31 | def new
32 | @posts = Post.all
33 | render :index
34 | end
35 | ```
36 |
37 |
38 | === "`page_to_page_mapping.js`"
39 | !!! info
40 | Similarly, we tie the `componentIdentifier` to the same page component.
41 |
42 | **Vite Users** This step can be entirely optional if you're using Vite. See
43 | the [recipe](./vite.md) for more information.
44 |
45 | ```js
46 | import PostIndex from '../views/posts/index'
47 |
48 | export const pageIdentifierToPageComponent = {
49 | 'posts/index': PostIndex,
50 | 'posts/new': PostIndex,
51 | };
52 | ```
53 |
54 |
55 | ## Add a link to `/posts/new`
56 |
57 | Imagine a list of posts, lets add a button somewhere on the index page to
58 | direct the user to `/posts/new`. As seen previously, both `/posts` and
59 | `/posts/new` render the same thing.
60 |
61 | === "`posts/index.json.props`"
62 | ```ruby
63 | # app/views/posts/index.json.props
64 |
65 | ...
66 |
67 | json.newPostPath new_post_path
68 | ```
69 |
70 | === "`posts/index.js`"
71 | ```js
72 | import { useContent } from '@thoughtbot/superglue'
73 |
74 | export default PostIndex = () => {
75 | const { newPostPath, ...rest } = useContent()
76 |
77 | return (
78 | ...
79 |
82 | New Post
83 |
84 | ...
85 | )
86 | }
87 | ```
88 |
89 | ## The modal
90 | The link appears and we're able to navigate to `/posts/new`, but
91 | `/posts/new` is missing a modal. Not surprising as both routes are
92 | rendering the same content.
93 |
94 | Lets add a modal.
95 |
96 | === "`posts/index.json.props`"
97 | !!! info
98 | For simplicity, we'll use a "Hello World" as the modal contents
99 | ```diff
100 | # app/views/posts/index.json
101 |
102 | ...
103 |
104 | json.newPostPath new_post_path
105 |
106 | + json.createPostModal do
107 | + json.greeting "Hello World"
108 | + end
109 |
110 | ```
111 |
112 | === "`index.js`"
113 | ```diff
114 | + import Modal from './Modal'
115 |
116 | export default PostIndex = ({
117 | newPostPath,
118 | createPostModal
119 | ...rest
120 | }) => {
121 |
122 | return (
123 | ...
124 |
127 | New Post
128 |
129 | +
130 | ...
131 | )
132 | }
133 | ```
134 |
135 | === "`Modal.js`"
136 |
137 | !!! info
138 | This is a simplified modal, in practice you'll use this with `