-
-# Superglue
-
-Use classic Rails to build rich React Redux applications with **NO APIs** and
-**NO client-side routing**.
-
-[![Test superglue_js](https://github.com/thoughtbot/superglue/actions/workflows/build_js.yml/badge.svg)](https://github.com/thoughtbot/superglue/actions/workflows/build_js.yml)
-[![Test superglue_rails](https://github.com/thoughtbot/superglue/actions/workflows/build_rails.yml/badge.svg)](https://github.com/thoughtbot/superglue/actions/workflows/build_rails.yml)
-
-Superglue makes React and Redux as productive as Hotwire, Turbo and Stimulus.
-Its inspired by Turbolinks and designed to feel like a natural extension of
-Rails. Enjoy the benefits of Redux state management and React components
-without giving up the productivity of Rails form helpers, UJS, tag helpers,
-flash, cookie auth, and more.
-
-## Caution
-
-This project is in its early phases of development. Its interface, behavior,
-and name are likely to change drastically before a major version release.
-
-### No APIs
-
-Instead of APIs, Superglue leans on Rail's ability to respond to different
-[mime types](https://apidock.com/rails/ActionController/MimeResponds/InstanceMethods/respond_to)
-on the same route. In a Superglue application, if you direct your browser to
-`/dashboard.html`, you would see the HTML version of the content, and if you
-went to `/dashboard.json` you would see the JSON version of the exact same
-content down to the footer.
-
-The end result would be something like this:
-
-![No Apis](https://thoughtbot.github.io/superglue/images/no_apis.png)
-
-### Powered by Classic Rails
-Superglue leans on Rails. Features like the flash, cookie auth, and URL
-helpers continue to be useful. Here's a look at the directory structure of a
-typical Rails application with Superglue.
-
-```treeview
-app/
-|-- controllers/
-|-- views/
-| |-- dashboard/
-| | |-- index.js # 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
-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.
-
-![No Apis](https://thoughtbot.github.io/superglue/images/props_template.png)
-
-### All together now!
-Superglue comes with batteries that bring all the above concepts together to make
-building popular SPA features easy, painless, and productive.
-
-#### SPA Navigation
-A popular ask of SPAs is page-to-page navigation without reloading. This is
-easily done with Superglue's own UJS attributes inspired by Turbolinks:
-
-```jsx
-
-```
-
-The above will request for `/posts` with an `accept` of `application/json`, and
-when the client receives the response, swap out the current component for the
-component the response asks for, and `pushState` on history.
-
-
-#### Easy Partial updates
-Some features rely on updating some parts of the existing page. Imagine
-implementing type-ahead search. In traditional applications, you may need a new
-controller, routes, a discussion over versioning, JSON serializer, plenty of
-new JS code, etc.
-
-![haircuts](https://thoughtbot.github.io/superglue/images/haircuts.png)
-
-With Superglue, this can be done with a simple `onChange`
-
-```js
-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
-letting React re-render.
-
-For more on what you can do, check out our documentation.
-
-#### Server-Side Rendering
-Server-Side Rendering is supported via [Humid](https://github.com/thoughtbot/humid).
-See the [documentation for server-side rendering][ssr docs].
-
- [ssr docs]: ./recipes/server-side-rendering.md
-
-## Documentation
-
-Documentation is hosted on [Github pages](https://thoughtbot.github.io/superglue).
-
-## Contributing
-
-Thank you, [contributors]!
-
- [contributors]: https://github.com/thoughtbot/superglue/graphs/contributors
-
-## Special Thanks
-
-Thanks to [jbuilder](https://github.com/rails/jbuilder),
-[scour](https://github.com/rstacruz/scour),
-[turbolinks3](https://github.com/turbolinks/turbolinks-classic),
-[turbograft](https://github.com/Shopify/turbograft/),
-[turbostreamer](https://github.com/malomalo/turbostreamer)
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
deleted file mode 100644
index 713c579b..00000000
--- a/docs/_sidebar.md
+++ /dev/null
@@ -1,35 +0,0 @@
-- [Introduction](README.md)
-
-- Getting Started
- - [Installation](installation.md)
- - [Tutorial](tutorial.md)
-
-- [Demo](demo.md)
-
-- Concepts
-
- - [Philosophy](concepts.md)
- - [The Redux state shape](redux-state-shape.md)
- - [The page response](page-response.md)
-
-- Features
-
- - [Navigation](navigation.md)
- - [Digging](traversal-guide.md)
- - [Fragments and Slices](fragments-and-slices.md)
-
-- API
- - [React Redux](react-redux.md)
- - [Rails](rails.md)
- - [Utility](utility.md)
-
-- Recipes
- - [Modals](recipes/modals.md)
- - [Server-Side Rendering](recipes/server-side-rendering.md)
- - [Loading content later](recipes/load-content-later.md)
- - [Loading a tab onClick](recipes/load-tab-onclick.md)
- - [Updating shopping cart](recipes/update-shopping-cart.md)
- - [Turbolinks navigation behavior](recipes/turbolinks.md)
- - [Usage with Kaminari](recipes/kaminari.md)
- - [Custom reducers](recipes/custom-reducers.md)
-
diff --git a/docs/concepts.md b/docs/concepts.md
deleted file mode 100644
index 159efe31..00000000
--- a/docs/concepts.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# Philosophy
-
-## Lean on Rails
-
-Move as much logic as you can into Rails. For example, instead of doing
-this:
-
-```ruby
- json.firstName @user.first_name
-```
-
-```js
-const Header = ({firstName}) => (
)
-```
-
-## Shape with presentation
-
-As an end user, its easy to look at any regular web page and understand all the
-actions that you can take. Its the strength of HTML and of HATEOAS. Superglue is
-inspired by this and encourages you to shape your JSON in the same way.
-
-Shape your pages roughly how you'll organize your components. Some may critic
-the addition of presentation, but there is intentionally only one consumer of
-your shape, your page components.
-
-?> **What about forms?** You can use [form_props] to build the right JSON for your
-React components.
-
- [form_props]: https://github.com/thoughtbot/form_props
-
-
-## HTML Thinking
-
-Just because you can create a link without a `href` in React doesn't mean you
-should. Ultimately, everything renders as HTML. That's why Superglue doesn't
-give links the ability to make non-get requests. Instead it encourages you to
-create forms that look like links, the same approach Rails uses. This has the
-added benefit of easily building for [Server Side Rendering].
-
- [Server Side Rendering]: ./recipes/server-side-rendering.md
-
-## Embrace Unobtrusive Javascript (UJS)
-
-You may have noticed that we've been using `data-sg-remote` or `data-sg-visit`
-in the examples.
-
-```jsx
-
-```
-
-Superglue embraces Unobtrusive Javascript. Any link or form with a `data-sg`
-attribute receives superpowers inspired by Rails data attributes.
-
-For more advanced use cases, an action creator is [passed] to all your connected
-page components.
-
- [passed]: ./navigation.md
-
-
-## You can dig it!
-
-```
-/dashboard?props_at=data.greet
-```
-
-A single keypath will dig your template for content and place it to where it
-belongs in your Redux state. That's incredibly productive and can be used for a
-variety of rich applications. See the the [digging docs] or recipes section.
-
-## Its still just Rails
-
-We might using React components, but most of what gives Superglue superpowers is that
-its driven by all the conveniences of old fashion and boring Rails.
-
-```treeview
-app/
-|-- controllers/
-|-- views/
-| |-- posts/
-| | |-- index.js
-| | |-- index.json.props
-| | |-- index.html.erb
-```
-
-[props_template]: https://github.com/thoughtbot/props_template
-[digging docs]: ./traversal-guide.md
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 00000000..697d4fe6
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,120 @@
+## `application_visit.js`
+
+!!! hint
+ If you want a progress bar, this is likely 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
+[data-sg-visit] and [data-sg-remote] UJS attributes.
+
+This file is meant for you to customize. For example, you'll likely
+want to add a [progress bar], control how visits work, or flash
+when the internet is down.
+
+[remote]: requests.md#remote
+[visit]: requests.md#visit
+[data-sg-remote]: ujs.md#data-sg-remote
+[data-sg-visit]: ujs.md#data-sg-visit
+[progress bar]: recipes/progress-bar.md
+
+
+## `page_to_page_mapping.js`
+
+!!! hint
+ Stop by the [tutorial] to get an idea of how to work with this file.
+
+This file exports a mapping between a `componentIdentifier` to an imported page
+component. This gets used in your `application.js` so that superglue knows
+which component to render with which identifier.
+
+For example:
+
+```js
+const pageIdentifierToPageComponent = {
+ 'posts/edit': PostsEdit,
+ 'posts/new': PostsNew,
+ 'posts/show': PostsShow,
+ 'posts/index': PostsIndex,
+}
+```
+
+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`
+
+!!! hint
+ Normally you wouldn't need to configure this class as it'll be generated
+ for you.
+
+Your `Application` component inherits from Superglue's [ApplicationBase]
+abstract class and is the entry point for your Superglue app. It overrides
+the methods [buildStore], [visitAndRemote], and [mapping], to perform
+setup of redux, UJS, and other functionality.
+
+
+```js
+import { ApplicationBase } from '@thoughtbot/superglue'
+
+export default class Application extends ApplicationBase {
+ ...
+}
+```
+
+
+
+ - [:octicons-arrow-right-24: See complete reference](reference/index.md#abstract-applicationbase)
+ for `ApplicationBase`
+
+
+## `flash.js`
+
+The installation generator will add a `flash.js` slice to `app/javascript/slices`
+and will work with the Rails `flash`. You can modify this however you like, out of the box:
+
+ - When using `data-sg-visit`, all data in the flash slice will be cleared before the request.
+ - When using `data-sg-visit` or `data-sg-remote`, the recieved flash
+ will be merged with the current flash. You can change this behavior
+ by modifying the flash slice.
+
+
+!!! hint
+ If you're curious how this works, in your layout, `application.json.props`,
+ the flash is serialized using `flash.to_h`
+
+
+To use in your page components, simply use a selector.
+
+```jsx
+import { useSelector } from 'react-redux'
+
+...
+
+const flash = useSelector((state) => state.flash)
+```
+
+then use the flash as you would normally in a controller
+
+```ruby
+def create
+ flash[:success] = "Post was saved!"
+end
+```
+
+[ApplicationBase]: reference/index.md#abstract-applicationbase
+[buildStore]: reference/index.md#buildstore
+[visitAndRemote]: requests.md
+[mapping]: reference/index.md#mapping
+[installation]: installation.md
+
diff --git a/docs/deferments.md b/docs/deferments.md
new file mode 100644
index 00000000..cdb7df20
--- /dev/null
+++ b/docs/deferments.md
@@ -0,0 +1,101 @@
+Sometimes you may want to load parts of your page later, like a slow sidebar, a
+graph that takes extra time to load, or tab content that shouldn't appear
+immediately. These scenarios are perfect use cases for Deferments.
+
+Deferments are a low effort way to load content later, both automatically and
+manually. Better yet, most of the work takes place in Rails land in your views.
+
+## `defer: :auto`
+
+This option make it easy to defer content in a single setting.
+
+=== "views/posts/index.json.props"
+
+ ``` ruby
+ json.metrics(defer: [:auto, placeholder: {totalVisitors: 0}]) do
+ sleep 10 # expensive operation
+ json.totalVisitors 30
+ end
+ ```
+
+=== "views/layouts/application.json.props"
+
+ ``` ruby
+ json.data do
+ yield
+ end
+ ```
+
+And that's it!
+
+### Behind the scenes
+
+When a user lands on a page Superglue will receive
+
+```json
+{
+ data: {
+ metrics: {
+ totalVisitors: 0
+ }
+ },
+ defers:[
+ {url: '/dashboard?props_at=data.metrics', type: "auto"}
+ ],
+ ...other
+}
+```
+
+Your page components will receive `{metrics: {totalVisitors: 0}}` and render. Superglue will then
+make a remote request:
+
+```
+remote("/dashboard?props_at=data.metrics")
+```
+
+10 seconds later the response succeeds with `{total_visitors: 30}`. Superglue
+then immutably grafts that payload into the `/dashboard` page at the path
+`data.metrics`. The page state would look like the following:
+
+```
+{
+ data: {
+ metrics: {
+ totalVisitors: 30
+ }
+ },
+ defers:[...others],
+ ...other
+}
+```
+
+Your page component finally recieves the new props and rerenders. For more
+control, you may provide a `success_action` or `fail_action`, and Superglue
+will dispatch these actions when the promise resolves successfully or fails.
+
+```ruby
+json.metrics(defer: [:auto, placeholder: {totalVisitors: 0}, success_action: "SUCCESS", fail_action: "FAIL"]) do
+ sleep 10 # expensive operation
+ json.totalVisitors 30
+end
+```
+
+## `defer: :manual`
+
+When you want control over when deferred content loads, e.g., tabbed content,
+use `defer: :manual` to stop the content from loading
+
+```ruby
+json.metrics(defer: [:manual, placeholder: {totalVisitors: 0}]) do
+ sleep 10 # expensive operation
+ json.totalVisitors 30
+end
+```
+
+and manually use `remote`
+
+```
+remote("/dashboard?props_at=data.metrics")
+```
+
+
diff --git a/docs/demo.md b/docs/demo.md
index 50fc840f..2c0449fe 100644
--- a/docs/demo.md
+++ b/docs/demo.md
@@ -15,3 +15,4 @@ version] and comparing that with the [Superglue version].
[Sean Doyle]: https://github.com/seanpdoyle
[Sean's version]: https://github.com/seanpdoyle/select-your-own-seat/commits/main
[Superglue version]: https://github.com/thoughtbot/select-your-own-seat-superglue/commits/superglue
+
diff --git a/docs/traversal-guide.md b/docs/digging.md
similarity index 64%
rename from docs/traversal-guide.md
rename to docs/digging.md
index f2380896..3b950288 100644
--- a/docs/traversal-guide.md
+++ b/docs/digging.md
@@ -1,20 +1,21 @@
# Digging
-Superglue's thunks work hand-in-hand with [PropsTemplate] to query your JSON
-template for nodes. This guide helps you understand how the tools work with
-each other.
+Beyond full page navigation, Superglue can make selective updates to parts of
+the page without a full load through digging. You may recognize digging from
+earlier docs:
-## The props_at param
-The `props_at` param is a keypath to nodes in your tree and is used almost
-exclusively with the `remote` thunk. On the PropsTemplate side, we pass that
-param over to an internal node in order to walk your templates.
+```
+/some_current_page?props_at=data.rightDrawer.dailySpecials
+```
-?> `props_at` can be used with `data-sg-visit`, but only combined with
-`data-sg-placeholder` to [build placeholders].
+By simply adding a `props_at` parameter to your requests, you can selectively
+fetch parts of the page without incurring the cost of loading unneeded content. This
+is great for functionality like modals, tabs, etc.
-[build placeholders]: ./navigation.md#placeholders
+## The `props_at` param
-For example, with a template below.
+The `props_at` param is a keypath to the content in your PropsTemplate. As a simplified
+example, imagine this page with no layouts:
```ruby
path = param_to_search_path(params[:props_at])
@@ -39,11 +40,23 @@ end
```
To fetch the `json.search` node, we would need to walk to `data` then `header`
-then `search`. Translating that to a remote call with a `props_at` param:
+then `search`. Translating that to a url with a `props_at` param:
-```js
-remote('/dashboard?props_at=data.header.search&some_search_str=haircuts')
```
+/dashboard?props_at=data.header.search&some_search_str=haircuts
+```
+
+Digging is normally combined with using [data-sg-remote] or [remote] to update
+content in async fashion.
+
+!!! info
+ `props_at` can be used with `data-sg-visit`, but only combined with
+ [data-sg-placeholder].
+
+[data-sg-placeholder]: ./ujs.md#data-sg-placeholder
+[data-sg-remote]: ./ujs.md#data-sg-remote
+[remote]: ./requests.md#remote
+
## Collections
There are two ways to query collections. Looking at the following example:
@@ -134,13 +147,14 @@ json.array! @posts , key: :some_id do |post|
end
```
-!> When querying, Superglue will disable
-[caching](https://github.com/thoughtbot/props_template#caching) and
-[deferment](https://github.com/thoughtbot/props_template#deferment) until the
-target node is reached.
+!!! info
+ When querying, Superglue will disable
+ [caching](https://github.com/thoughtbot/props_template#caching) and
+ [deferment](https://github.com/thoughtbot/props_template#deferment) until the
+ target node is reached.
-That's the basics of traversing with Superglue. Many modern SPA functionality
-can be achieved by just a few lines of code. For examples, see our recipes
-section.
+With digging, many modern SPA functionality can be achieved by just a keypath and a
+few lines of code.
[PropsTemplate]: https://github.com/thoughtbot/props_template
+
diff --git a/docs/fragments-and-slices.md b/docs/fragments-and-slices.md
index 79767d1f..89491259 100644
--- a/docs/fragments-and-slices.md
+++ b/docs/fragments-and-slices.md
@@ -1,5 +1,8 @@
# Fragments and Slices
+!!! warning
+ This functionality is experimental
+
When building pages, we commonly use partials to extract crosscutting concerns.
For example, a shared header:
@@ -43,7 +46,7 @@ across our `pages` slice:
}
```
-Superglue fragments, and Redux slices are two recommended ways for updating cross-cutting
+Superglue fragments, and Redux slices are two ways for updating cross-cutting
concerns.
# Fragments
@@ -81,8 +84,9 @@ fragments: [
]
```
-?> Fragments used in nodes that are [deferred](./navigation.md#deferments) do
-not show up inside the metadata until the deferred nodes are loaded.
+!!! info
+ Fragments used in nodes that are [deferred](./deferments.md) do
+ not show up inside the metadata until the deferred nodes are loaded.
## Automatic updates
@@ -109,7 +113,7 @@ that can make this process easier.
### InitialState
You can render your slice's initial state in the [slices] `key` of the page
object, it'll be merged with the `initialState` passed to your `buildStore`
-function in your [application.js](./react-redux.md#applicationbase)
+function in your [application.js](./configuration.md#applicationjs)
### extraReducers
@@ -201,3 +205,4 @@ export const cartSlice = createSlice({
[page response]: ./page-response.md
[slices]: ./page-response.md#slices
[extraReducers]: https://redux-toolkit.js.org/api/createSlice#extrareducers
+
diff --git a/docs/functions-passed.md b/docs/functions-passed.md
new file mode 100644
index 00000000..e8a5d102
--- /dev/null
+++ b/docs/functions-passed.md
@@ -0,0 +1,59 @@
+All page components receives the following functions.
+
+## visit and remote
+
+These are the methods described in [advanced requests].
+
+[advanced requests]: ../requests.md
+
+## navigateTo
+
+Superglue comes with a basic `Nav` component that manages swapping of
+the different page components. The component comes with a `navigateTo` method
+that is passed to all your page components which you can use to perform a
+full-page navigation using your cached state.
+
+```javascript
+navigateTo('/posts', {ownProps:{something: true}})
+```
+
+
+ - [:octicons-arrow-right-24: See complete reference](components.Nav.md#navigateto)
+ for `navigateTo`
+
+
+## saveAndProcessPage
+
+Save and process a rendered view from PropsTemplate. This is the primitive
+function that [visit and remote] calls when it receives a [page]. If you are
+able to render a [page] outside the normal request response cycle, e.g,
+websocket, you can use this function to save the payload.
+
+[page]: ../page-response.md
+
+| Arguments | Type | Notes |
+| :--- | :--- | :--- |
+| pageKey | `String` | The key that Superglue uses to store the response |
+| page | `String` | A rendered PropsTemplate|
+
+### copyPage
+
+Copies an existing page in the store, and sets it to a different `pageKey`.
+Useful for optimistic updates on the next page before you navigate.
+
+```js
+copyPage({
+ from: '/current_page',
+ to '/next_page'
+})
+```
+
+| Arguments | Type | Notes |
+| :--- | :--- | :--- |
+| {from} | `String` | The key of the page you want to copy from.
+| {to} | `String` | The key of the page you want to copy to.
+
+
+[digging guide]: ./digging.md
+[PropsTemplate]: https://github.com/thoughtbot/props_template#partial-fragments
+[visit and remote]: ../requests.md
diff --git a/docs/index.html b/docs/index.html
deleted file mode 100644
index c4e8da6c..00000000
--- a/docs/index.html
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
- Document
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..76e93be0
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,195 @@
+# Overview
+
+Superglue is a library that thoughtfully pairs Rails and React. Its built with
+The Rails Way in mind and aims to provide a simple developer experience that is
+on par with Hotwire, Stimulus, and Turbo. Confidently use Rails routes,
+controllers, views as you normally would in a multi-page application and
+integrate with React's vast ecosystem.
+
+
+!!! warning
+ Superglue is in active development as it approaches 1.0. While its
+ interface and behavior are stablizing, changes are expected.
+
+## Who is it for?
+
+Superglue is built from the ground up for Rails developers who
+
+- Have a complex React integration and want to move back to to a simple rails
+ workflow without giving up components already built.
+- Want to use normal controllers, server side routing, views, helpers, etc. to
+ develop interactive React applications
+- Want to hit the ground running with React and easily access all the components
+ that the React eco-system offers.
+- Don't want to write another custom React integration
+- Don't want to build APIs.
+- Are tired of JS complexity and just want to get work done!
+
+## 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
+built a supporting cast of tooling under one shop to bring ease and consistancy
+to your team.
+
+
+
+- __Superglue Rails__
+
+ ---
+
+ Integrates Superglue with Rails, and generates a new
+ app.
+
+
+- __PropsTemplate__
+
+ ---
+
+ A very fast JSON builder. The secret sauce that enabled much more than just Rails/React integration
+
+ [:octicons-arrow-right-24: props_template](#)
+
+- __Humid__
+
+ ---
+
+ Server Side Rendering using MiniRacer and V8 isolates.
+
+ [:octicons-arrow-right-24: Humid](#)
+
+- __Form Props__
+
+ ---
+
+ Just use Rails forms with React. Combine it with React
+ components.
+
+ [:octicons-arrow-right-24: form_props](#)
+
+
+
+## How does it work?
+
+### It’s Rails
+
+Superglue leans on Rails' ability to respond to different mime types on the
+same route and divides the usual `foobar.html.erb` into three familiar
+templates.
+
+- `foobar.json.props` A presenter written in a jbuilder-like template that
+ builds your page props.
+- `foobar.js` Your page component that receives the props from above.
+- `foobar.html.erb` Injects your page props into Redux when the browser loads
+ it.
+
+Shape your `props` to roughly how your components are presented. For example:
+
+```ruby
+json.header do
+ json.username @user.username
+ json.linkToProfile url_for(@user)
+end
+
+json.rightDrawer do
+ json.cart(partial: 'cart') do
+ end
+ json.dailySpecials(partial: 'specials') do
+ end
+end
+
+json.body do
+ json.productFilter do
+ form_props(url: "/", method: "GET") do |f|
+ f.select(:category, ["lifestyle", "programming", "spiritual"])
+ f.submit
+ end
+ end
+
+ json.products do
+ json.array! @products do |product|
+ json.title product.title
+ json.urlToProduct url_for(product)
+ end
+ end
+end
+
+json.footer do
+ json.copyrightYear "2023"
+end
+```
+
+Familiar Rails conveniences include [form props], a fork of `form_with` made for
+React; the [flash] is 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
+`foobar.js` as props, for example:
+
+```js
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { Drawer, Header, Footer, ProductList, ProductFilter } from './components';
+
+export default function FooBar({ header, products = [], productFilter, rightDrawer, footer }) {
+ const flash = useSelector((state) => state.flash);
+
+ return (
+ <>
+
{flash && flash.notice}
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+### It’s Turbolinks and UJS
+
+At heart, Superglue is a fork of [Turbolinks 3], but instead of sending your
+`foobar.html.erb` over the wire and swapping the ``, it sends
+`foobar.json.props` over the wire to your React and Redux app and swaps the
+page component.
+
+This behavior is opt-in. Superglue provides UJS helpers that you can use with
+your React components to SPA transition to the next page.
+
+```jsx
+ Next Page
+```
+
+### It’s more!
+
+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.
+
+Need to reload a part of the page? Just add a query parameter and combine with
+the [UJS helper] attribute `data-sg-remote`:
+
+```jsx
+
+
+
+
+ Reload Daily Specials
+
+
+```
+
+The above will traverse `foobar.json.props`, grab `dailySpecials` while
+skipping other nodes, and immutably graft it to your Redux store.
+
+This works well for [modals], chat, streaming, and [more]!
+
+
diff --git a/docs/installation.md b/docs/installation.md
index c3a27caa..c05ae82b 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -1,9 +1,8 @@
-# Getting started
-
-## Installation
-
-Ensure you are using esbuild (enabled with JSX in `.js`)
+# Installation
+!!! info "Prerequisite"
+ To get started with Superglue, the only prerequisite is to be setup with a javascript
+ bundler. We'll assume esbuild with js-bundling, but you can also use vite.
Add the following to your Gemfile
@@ -19,32 +18,49 @@ bundle
rails superglue:install:web
```
-## Contents
-The above will also generate a redux toolkit starter that's
-configured to work with Superglue. You'll find:
+The above will generate the following files:
+
+```terminal
+.
+└─ app/
+ └─ javascript/
+ ├─ slices/
+ │ ├─ flash.js
+ | └─ pages.js
+ ├─ actions.js
+ ├─ application.js
+ ├─ application_visit.js
+ ├─ page_to_page_mapping.js
+ └─ store.js
+```
+
+## Redux toolkit
+
+If you've ever encountered Redux then the files above may seem familiar to you.
+Superglue works as a complete and fully functional Redux toolkit application.
+For the most part, all the functionality you would need resides in these files
+and you'll make minimum edits, but they are made available if you ever need
+greater control over state management.
+
+## Configuration
-- A [slice for the flash] that works with the Rails flash
-- A pages slice that can be used for [custom reducers]
-- And `application_visit.js` that can be used to add before and
- after behavior for [visit and remote]
-- A `store.js` that puts the above together
-- And a [pre-configured entry point] in
- `app/javascript/packs/application.js`
+We recommend getting familiar with the following files:
-For more information, visit the [react redux] section.
+- `application_visit.js` - Add custom functionality to Superglue navigation, e.g, progress bars.
+- `page_to_page_mapping.js` - Pairs your `props` files with your page components.
+- `flash.js` - Seamlessly, integrates with the Rails flash.
- [preconfigured entry point]: https://github.com/thoughtbot/Superglue/blob/main/superglue_rails/lib/install/templates/web/application.js
- [slice for the flash]: rails.md#rails-flash
- [visit and remote]: navigation.md#visit-and-remote
- [custom reducers]: ./recipes/custom-reducers.md
- [react redux]: react-redux.md
+For more information, visit the [configuration] section.
-### Scaffold
+[configuration]: configuration.md
-If you'd like to dive right in, you can work with a scaffold.
+## Scaffold
+
+If you'd like to dive right in, you can start with a scaffold:
```terminal
rails generate scaffold post body:string --force --no-template-engine --superglue
```
or proceed with a [tutorial](./tutorial.md)
+
diff --git a/docs/navigation.md b/docs/navigation.md
deleted file mode 100644
index 0f340d16..00000000
--- a/docs/navigation.md
+++ /dev/null
@@ -1,171 +0,0 @@
-# Navigation
-
-Navigation is [inspired by turbolinks](./concepts.md#inspired-by-turbolinks).
-
-## Visit and Remote
-
-Superglue comes with two thunks that wrap around fetch:
-
-1. `visit` is used for page-to-page navigation, there can be only [one visit] at
-a time.
-2. `remote` is used with urls that contain the `props_at` param for partial page
-updates.
-
-When configuring your application in `application.js`, your page components
-would all receive a `visit` and `remote` function that will dispatch when
-called.
-
-## `application_visit.js`
-
-Out of the box, the `visit` thunk is bare, it doesn't navigate on success or
-specify any behavior on error. We have to enhance it with sane defaults for the
-web then inject it into your application to override the thunk.
-
-If you've used the generators, this is done for you in `application_visit.js`
-and the resulting `visit` is injected in `application.js`.
-
-You can add customizations to `visit` or `remote` in `application_visit.js`.
-
-## Single page navigation using `visit`
-
-Single page navigation must be explicitly enabled using a [data attribute]
-
-```jsx
-
-```
-
-or manually called using the `visit` thunk somewhere in your component:
-
-```javascript
- this.props.visit("/posts", {...options})
- .then(...) #add navigateTo
-```
-
-Options passed to `visit` are also passed to `fetch`. Additionally, there are
-two features that enable low effort interactivity.
-
-### placeholders
-
-The idea of placeholders is to optimistically copy the current page state over
-to the next page's state before the request. This is handy if the next page
-looks almost identical to the current page. Use cases include:
-
-1. Modals
-2. Providing content for manual deferments
-
-Example:
-
-```jsx
-
-```
-
-or
-
-```javascript
- this.props
- .visit("/posts/new?props_at=data.body.modal", { placeholderKey: "/new"})
- .then(...) #add navigateTo
-```
-
-### `beforeSave`
-
-You can provide a callback that will modify the page before it gets saved to
-the Redux store. Very handy for chat applications that need to merge the
-current page's messages with the next one.
-
-Example:
-
-```javascript
- const beforeSave = (prevPage, nextPage) => {
- nextPage.data.messages = [
- prevPage.data.messages,
- ... nextPage.data.messages
- ]
-
- return nextPage
- }
-
- this.props.visit("/posts", {beforeSave}).then(...) #add navigateTo
-```
-
-## Partial page updates with `remote`
-
-`remote` combined with the `props_at` parameter can update any part of the Redux
-store in the background. Most of the time, you would be using this thunk to
-update the current page the user is seeing. Like `visit`, you can
-provide a `beforeSave` callback to modify content before it gets saved to the
-store.
-
-```javascript
- const beforeSave = (prevPage, nextPage) => {
- nextPage.data.messages = [
- prevPage.data.messages,
- ... nextPage.data.messages
- ]
-
- return nextPage
- }
-
-
- this.props.remote("/posts?props_at=data.header", {beforeSave})
-```
-
-You may also [specify](./react-redux.md#remote) a `pageKey` param to tell
-Superglue where to store the results. If you're using the thunk through a
-connected component, this will be set to the key of the current page for you.
-
-# Deferments
-
-Deferments are a low effort way to load content in async fashion, both
-automatically and manually.
-
-## `auto`
-
-```ruby
- json.metrics(defer: [:auto, placeholder: {totalVisitors: 0}]) do
- sleep 10 # expensive operation
- json.totalVisitors 30
- end
-```
-
-When visiting the above, PropsTemplate will render with
-
-```
-{
- metrics: {
- total_visitors: 0
- }
-}
-```
-
-Then make a `remote("/dashboard?props_at=data.metrics")` call and 10 seconds later,
-`{total_visitors: 30}` will be immutably grafted into the same position on the
-Redux store and React will rerender. For more control, you may provide a
-`success_action` or `fail_action`, and Superglue will dispatch these actions when
-the promise resolves successfully or fails.
-
-```ruby
-json.metrics(defer: [:auto, placeholder: {totalVisitors: 0}, success_action: "SUCCESS", fail_action: "FAIL"]) do
- sleep 10 # expensive operation
- json.totalVisitors 30
-end
-```
-
-## `manual`
-Using `manual` with deferment means that a `remote` call will not
-take place, it is up to you to fetch the node using `remote` yourself.
-
-```ruby
-json.metrics(defer: [:manual, placeholder: {totalVisitors: 0}]) do
- sleep 10 # expensive operation
- json.totalVisitors 30
-end
-```
-
-[one visit]: ./react-redux.md#visit
-[data attribute]: ./react-redux.md#data-sg-visit
-
diff --git a/docs/page-response.md b/docs/page-response.md
index f4fd9885..d3056230 100644
--- a/docs/page-response.md
+++ b/docs/page-response.md
@@ -1,4 +1,5 @@
-## The `page` response
+# The `page` response
+
Superglue expects your JSON responses to contain the following attributes. If you
used Superglue's generators, this would be all set for you in
`application.json.props`.
@@ -25,7 +26,7 @@ used Superglue's generators, this would be all set for you in
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](./react-redux.md#navigateto)
+merged when [navigating](reference/functions-passed.md#navigateto)
### `componentIdentifier`
A `string` to instruct Superglue which component to render. The generated
@@ -40,25 +41,11 @@ json.componentIdentifier active_template_virtual_path
You can control which `componentIdentifier` will render which component in the
`page_to_page_mapping.js`.
-```js
-const pageIdentifierToPageComponent = {
- 'posts/edit': PostsEdit,
- 'posts/new': PostsNew,
- 'posts/show': PostsShow,
- 'posts/index': PostsIndex,
-}
-```
-
-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`.
+
+ - [:octicons-arrow-right-24: See reference](configuration.md#page_to_page_mappingjs)
+ for page_to_page_mapping.js
+
-```js
-const pageIdentifierToPageComponent = {
- 'posts/index': PostsIndex,
- 'posts/show': PostsIndex,
-}
-```
### `assets`
An `array` of asset fingerprint `string`s. Used by Superglue to detect the need to
@@ -88,6 +75,7 @@ By specifying the restore strategy used (`fromCacheOnly`, `revisitOnly`, or
`fromCacheAndRevisitInBackground`), you can control what superglue does when
encountering the page again when pressing the back or forward browser navigation
buttons.
+
- `fromCacheAndRevisitInBackground` will transition to the cached page, then
issue a visit in the background, redirecting and replacing history if needed.
This is the option set in `application.json.props` when using the generators.
@@ -102,3 +90,4 @@ Take advantage of the `SAVE_RESPONSE` to continually update your slice everytime
superglue recieves a new page request.
[props_template]: https://github.com/thoughtbot/props_template
+
diff --git a/docs/rails.md b/docs/rails-utils.md
similarity index 52%
rename from docs/rails.md
rename to docs/rails-utils.md
index afcb15a2..344ce19a 100644
--- a/docs/rails.md
+++ b/docs/rails-utils.md
@@ -1,32 +1,4 @@
-# Rails
-
-## Rails Flash
-
-The installation generator will add a `flash.js` slice to `app/javascript/slices`
-and will work with the Rails `flash`.
-
-To use in your page components, simply use a selector.
-
-```jsx
-import { useSelector } from 'react-redux'
-
-...
-
-const flash = useSelector((state) => state.flash)
-```
-
-then use the flash as you would normally in a controller
-
-```ruby
-def create
- flash[:success] = "Post was saved!"
-end
-```
-
-?> When using `data-sg-visit`, all flash in slice will be cleared before the request.
-
-?> When using `data-sg-visit` or `data-sg-remote`, the recieved flash will be merged with the current flash.
-
+# Rails utils
## `redirect_back_with_props_at`
@@ -58,3 +30,4 @@ def create
end
```
+
diff --git a/docs/react-redux.md b/docs/react-redux.md
deleted file mode 100644
index 6c3fb4ef..00000000
--- a/docs/react-redux.md
+++ /dev/null
@@ -1,253 +0,0 @@
-# React Redux
-
-## ApplicationBase
-
-```js
-import { ApplicationBase } from '@thoughtbot/superglue'
-
-export default class Application extends ApplicationBase {
- ...
-}
-```
-
-Your `Application` component entry point inherits from this component. It
-performs setup of redux, UJS, and other functionality when rendered. This would
-be created for you if you used Superglue's generators. Components that inherits
-from `ApplicationBase` will not work without implementing the following methods:
-
-- `mapping` override this and return a [mapping](https://github.com/thoughtbot/Superglue/blob/main/superglue_rails/lib/install/templates/web/application.js)
-between your `prop` templates to the page component.
-- `visitAndRemote` override this and return an object with `visit` and `remote`.
-If you used the generators, a customizable one has been created for you in
-`application_visit.js`
-- `buildStore` override this and return a configured store. You'll be passed an
-`initialState` and 2 reducers, `superglue` and `pages`. The `initialState` will
-contain the initial state of `pages` and the merged `slices` object from the pages
-[payload](./page-response.md#slices)
-
-## Nav
-
-A nav component for your application. It is used by the render method in
-`ApplicationBase`.
-
-```javascript
-import Nav from '@thoughtbot/superglue/components/Nav'
-...
-
-
-
-```
-
-### navigateTo
-
-Use to `navigateTo` to perform a full-page navigation using your cached state.
-
-```javascript
-this.props.navigateTo('/posts', {ownProps:{something: true}})
-```
-
-If there is an existing page in your store `navigateTo` will restore the props,
-render the correct component, and return `true`. Otherwise, it will return
-`false`. This is [useful](./recipes/turbolinks.md) if you want
-to restore an existing page before making a call to `visit` or `remote`.
-
-| Parameter | Notes |
-| :--- | :--- |
-| pageKey | Use your rails `foo_path` helpers. This is the location where your props are stored in superglue. |
-| options | Additional options, see below. |
-
-| Options | Notes |
-| :--- | :--- |
-| ownProps | Any additional props to be passed to the next page component. |
-
-## Action Creators
-
-### visit
-
-Makes an ajax call to a page, and sets the response to the `pages` store. Use
-`visit` when you want full page-to-page transitions on the user's last click.
-There can only ever be one visit at a time. If you happen to call `visit` while
-another visit is taking place, it will abort the previous one.
-
-?> `visit` is used for full-page transitions and will strip the `props_at` query string
-from your pathQuery parameters that target a specific node. The exception to this
-rule is if you use a `props_at` query string with a `placeholderKey` option. This is
-allowed because `props_at` would have a page to graft onto.
-
-
-```javascript
-visit(pathQuery).then(({rsp, page, pageKey, screen, needsRefresh}) => {})
-
-visit(pathQuery, {...fetchRequestOptions}).then(({rsp, page, pageKey, screen, needsRefresh}) => {})
-
-visit(pathQuery, {...fetchRequestOptions}, pageKey).then(({rsp, page, pageKey, screen, needsRefresh}) => {})
-
-visit(pathQuery, {...fetchRequestOptions}, pageKey).catch(({message, fetchArgs, url, pageKey}) => {})
-```
-
-| Arguments | Type | Notes |
-| :--- | :--- | :--- |
-| pathQuery | `String` | The path and query of the url you want to fetch from. The path will be prefixed with a `BASE_URL` that you configure. |
-| fetchRequestOptionsAndMore | `Object` | Any fetch request options plus extras. Note that superglue will override the following headers: `accept`, `x-requested-with`, `x-superglue-request`, `x-csrf-token`, and `x-http-method-override`. |
-
-| fetchRequestOptionsAndMore | Type | Notes
-| :--- | :--- | :--- |
-| placeholderKey | `String` | When passing a url that has a `props_at` param, you can provide a `placeholderKey`, which superglue will use to copy the state over to the new url before making a request. If you do not provide this param, Superglue will remove any `props_at` param from the url.
-| | | Other options are passed on to `fetch`|
-
-| Callback options | Type | Notes |
-| :--- | :--- | :--- |
-| needsRefresh | `Boolean` | If the new request has new JS assets to get - i.e., the last fingerprint is different from the new fingerprint, then it will return true. |
-| componentIdentifier | `String` | The screen that your react application should render next. |
-| page | `Object` | The full parsed page response from your `foobar.json.props` template. |
-| pageKey | `String` | The key that Superglue uses to store the response |
-| suggestedAction | `String` | `push` or `replace`, to be used to `navigateTo`|
-| redirected | `Boolean` | `true` if the response was the result of a redirect, `false` otherwise|
-| rsp | `Object` | The raw response object |
-
-| Additional `.catch` error attributes\* | Type | Notes |
-| :--- | :--- | :--- |
-| fetchArgs | `Array` | The arguments passed to `fetch`, as tuple `[url, {req}]`. You can use this to implement your own retry logic. |
-| url | `String` | The full url, passed to `fetch`. |
-| pageKey | `String` | Location in the Superglue store where `page` is stored |
-
-### `data-sg-visit`
-
-A UJS equivalent of `visit` is available. For example:
-
-```javascript
-
-```
-
-!> You are not able to specify the HTTP method used in a UJS link. This is
-intentional. If you want to create a link that can support different HTML
-methods, create a UJS form that look like a link using
-[form_props](https://github.com/thoughtbot/form_props)
-
-or if you're using a form
-
-```javascript
-
-```
-
-`data-sg-visit` also has the companion attribute, `data-sg-placeholder` which
-lets you specify a [placeholder](react-redux.md#visit) for the page while the
-visit is being made.
-
-
-### remote
-
-Remote makes an ajax call and saves the response to the `pages` store in async
-fashion. Use this if you want to [update parts](react-redux.md#traversing-nodes)
-of the current page or preload other pages.
-
-?> Unlike `visit`, `remote` will not strip any `props_at` url parameters.
-
-```javascript
-remote(pathQuery, {...fetchRequestOptionsAndMore}, pageKey).then(({rsp, page, screen, needsRefresh}) => {})
-
-remote(pathQuery, {...fetchRequestOptionsAndMore}, pageKey).catch(({message, fetchArgs, url, pageKey}) => {})
-```
-
-Shares the same arguments as `visit` with a few differences:
-
-* `suggestedAction` is not available as an option passed to your then-able function.
-* `placeholder` is not available
-* You can override where the response is saved with a `pageKey` options
-
-| fetchRequestOptionsAndMore options | Type | Notes |
-| :--- | :--- | :--- |
-| pageKey | `String` | Where the response should be saved. By default, it is the current url.
-| | | Other options are passed on to `fetch`|
-
-### `data-sg-remote`
-
-A UJS equivalent of remote is available. Use this if you want to update parts
-of the current page, or another page in the Redux store without updating
-`window.history` for example:
-
-```javascript
-
-```
-
-### saveAndProcessPage
-
-Save and process a rendered view from PropsTemplate and fetch any deferments.
-
-| Arguments | Type | Notes |
-| :--- | :--- | :--- |
-| pageKey | `String` | The key that Superglue uses to store the response |
-| page | `String` | A rendered PropsTemplate|
-
-### copyPage
-
-Copies an existing page in the store, and sets it to a different `pageKey`.
-Useful for optimistic updates on the next page before you navigate.
-
-```js
-this.props.copyPage({
- from: '/current_page',
- to '/next_page'
-})
-```
-
-| Arguments | Type | Notes |
-| :--- | :--- | :--- |
-| {from} | `String` | The key of the page you want to copy from.
-| {to} | `String` | The key of the page you want to copy to.
-
-
-## Searching for nodes
-
-Superglue can search your content tree for a specific node. This is done by adding
-a `props_at=keypath.to.node` in your URL param, then passing the params in your
-`application.json.props`. PropsTemplate will ignore blocks that are not in the
-keypath, disable deferment and caching, and return the node. Superglue will then
-immutably set that node back onto its tree on the client-side. Fragments will
-also automatically be updated where needed. See our [digging docs]
-for more examples.
-
-For example:
-
-```javascript
-this.props.remote('/?props_at=header.shoppingCart')
-```
-
-and in your `application.json.props`
-
-```ruby
-path = param_to_search_path(params[:props_at])
-
-json.data(search: path) do
- yield json
-end
-
-...
-
-```
-
-## Updating Fragments
-
-A fragment is a lightweight alternative to redux slices that allows you to update
-cross cutting concerns by marking a node as shared across all pages. They can
-only be enabled as an option on partials using [PropsTemplate]
-
-For example:
-
-```ruby
-json.header partial: ['header', fragment: true] do
-end
-```
-
-This metadata can then be used by your reducers to make updates that span [across pages]
-
-[across pages]: ./fragments-and-slices.md#fragments
-[digging guide]: ./traversal-guide.md
-[PropsTemplate]: https://github.com/thoughtbot/props_template#partial-fragments
diff --git a/docs/recipes/custom-reducers.md b/docs/recipes/custom-reducers.md
deleted file mode 100644
index 28e26391..00000000
--- a/docs/recipes/custom-reducers.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Custom reducers
-
-Superglue will also generate a `pageSlice` for customized functionality. You
-can respond to Superglue [actions] For example, when you want to clear out
-form errors before visiting another page.
-
-```js
-import { createSlice } from '@reduxjs/toolkit'
-import { saveResponse, beforeVisit } from '../actions'
-
-export const pagesSlice = createSlice({
- name: 'pages',
- extraReducers: (builder) => {
- builder.addCase(beforeVisit, (state, action) => {
- const {currentPageKey} = action.payload
-
- const currentPage = draft[currentPageKey]
- delete currentPage.error
- })
- }
-})
-```
-
-[actions]: ../fragments-and-slices.md
diff --git a/docs/recipes/kaminari.md b/docs/recipes/kaminari.md
deleted file mode 100644
index 990d2e5b..00000000
--- a/docs/recipes/kaminari.md
+++ /dev/null
@@ -1,289 +0,0 @@
-# Usage with Kaminari
-
-Pagination without reload is easy to add.
-
-
-
-
-
-#### Starting point
-
-
-Lets pretend that we're already able to see a list of posts.
-
-
-
-
-
-#### **`posts_controller.rb`**
-
-```ruby
-# app/controllers/posts_controller.rb
-
-def index
- @posts = Post.all
-end
-```
-
-#### **`index.json.props`**
-
-```ruby
-# app/views/posts/index.json.props
-
-json.rightNav do
- ...
-end
-
-json.posts do
- json.list do
- json.array! @posts do |post|
- json.id post.id
- json.body post.body
- json.editPostPath edit_post_path(post)
- end
- end
-end
-```
-
-#### **`index.js`**
-
-```js
-# app/views/posts/index.js
-
-import React from 'react'
-import PostList from './PostList'
-import RightNav from './RightNav'
-
-export default PostIndex = ({
- posts,
- rightNav
-}) => {
- return (
- <>
-
-
- >
- )
-}
-
-```
-
-
-
-
-
-
-#### Add gems
-
-
-Lets also add Kaminari.
-
-
-
-
-
-#### **`terminal`**
-
-```terminal
-bundle install kaminari
-```
-
-
-
-
-
-#### Add pagination
-
-
-
-The changes here aren't indifferent from using Kaminari with
-`.erb`. We're using `path_to_next_page` and `path_to_prev_page`
-which come with Kaminari.
-
-?> Some [helpers] like `paginate` output HTML instead of
-JSON, but we can still use more primitives methods.
-
- [helpers]: https://github.com/kaminari/kaminari#the-paginate-helper-method
-
-
-
-
-
-#### **`posts_controller.rb`**
-
-```diff
-# app/controllers/posts_controller.rb
-
-def index
- @posts = Post.all
-+ .page(params[:page_num])
-+ .per(10)
-+ .order(created_at: :desc)
-end
-```
-
-#### **`index.json.props`**
-
-```diff
-# app/views/posts/index.json.props
-
-json.rightNav do
- ...
-end
-
-json.posts do
- json.list do
- json.array! @posts do |post|
- json.id post.id
- json.body post.body
- json.editPostPath edit_post_path(post)
- end
- end
-+
-+ json.pathToNextPage path_to_next_page(@posts)
-+ json.pathToPrevPage path_to_prev_page(@posts)
-end
-```
-
-#### **`index.js`**
-
-```diff
-# app/views/posts/index.js
-import React from 'react'
-import PostList from './PostList'
-import RightNav from './RightNav'
-
-export default PostIndex = ({
- posts,
- rightNav
-+ pathToNextPage,
-+ pathToPrevPage
-}) => {
- return (
- <>
-
-+
-+ Next Page
-+
-+
-+ Prev Page
-+
- >
- )
-}
-
-```
-
-
-
-
-
-#### Smooth navigation
-
-
-
-The above adds pagination, but each click on **Next Page** adds
-a new page load.
-
-Lets navigate without a reload. In this example, we're using `data-sg-remote`,
-which would set the current page's state to the response without changing the URL.
-
-?> `data-sg-visit` adds a new page to the store and changes the URL, but can't make use
-of `props_at` unless a `data-sg-placeholder` is present. `data-sg-remote` can add a new page,
-make use of the `props_at` parameter, but does not change the URL.
-
-
-
-
-
-#### **`index.js`**
-
-```diff
-# app/views/posts/index.js
-import React from 'react'
-import PostList from './PostList'
-import RightNav from './RightNav'
-
-export default PostIndex = ({
- posts,
- rightNav,
- pathToNextPage,
- pathToPrevPage
-}) => {
- return (
- <>
-
-
- Next Page
-
-
- Prev Page
-
- >
- )
-}
-
-```
-
-
-
-
-
-#### Optimize!
-
-
-Lets skip `data.rightNav` when navigating and dig for `data.posts`. For the
-user, only the posts lists change, but the rightNav stays the same.
-
-?> In effect, this achieves the same functionality as [Turbo Frames], but
-Superglue leans more on Unobtrusive Javascript and a simple `props_at` for
-better ergonomics.
-
-[Turbo Frames]: https://turbo.hotwired.dev/handbook/frames
-
-
-
-
-
-#### **`index.json.props`**
-
-Recall how [digging] for content works. We'll add a `props_at` that digs for
-the `json.posts` while skipping other content on that page.
-
- [digging]: ../tutorial.md#digging-for-content
-
-```diff
-# app/views/posts/index.json.props
-
-json.rightNav do
- ...
-end
-
-json.posts do
- json.list do
- json.array! @posts do |post|
- json.id post.id
- json.body post.body
- json.editPostPath edit_post_path(post)
- end
- end
-
-- json.pathToNextPage path_to_next_page(@posts)
-+ json.pathToNextPage path_to_next_page(@posts, props_at: 'data.posts')
-- json.pathToPrevPage path_to_prev_page(@posts)
-+ json.pathToPrevPage path_to_prev_page(@posts, props_at: 'data.posts')
-end
-```
-
-
-
diff --git a/docs/recipes/load-content-later.md b/docs/recipes/load-content-later.md
deleted file mode 100644
index 78595f0c..00000000
--- a/docs/recipes/load-content-later.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# Loading content later
-
-When parts of your page become slow, e.g, a metrics table that is expensive to
-render:
-
-```ruby
-# /dashboard.json.props
-json.header do
- ...
-end
-
-json.metrics do
- sleep 10 # expensive operation
- json.totalVisitors 30
-end
-```
-
-A common approach is to load content in async fashion by building out another
-set of routes, controllers, tests, with more work on the frontend to manage
-state, call fetch, etc.
-
-With Superglue, we can turn content async with a single setting.
-
-```ruby
-json.metrics(defer: :auto) do
- sleep 10 # expensive operation
- json.totalVisitors 30
-end
-```
-
-With `defer: :auto`, PropsTemplate will render `order.json.props` as usual, but
-without `json.metrics`, then when the content is received by the client, Superglue
-will automatically make an `remote` request for anything that was skipped:
-
-```javascript
-remote('/dashboard?props_at=data.metrics')
-```
-
-It is up to you to handle the case when `metrics` starts out empty. For example:
-
-```javascript
-//...in your component
-
- render() {
- return (
-
- )
- }
-```
-
-Alternatively, you can use a placeholder like so:
-
-```ruby
-json.metrics(defer: [:auto, placeholder: {totalVisitors: 0}]) do
- sleep 10 # expensive operation
- json.totalVisitors 30
-end
-```
-
diff --git a/docs/recipes/load-tab-onclick.md b/docs/recipes/load-tab-onclick.md
deleted file mode 100644
index 2b497a6e..00000000
--- a/docs/recipes/load-tab-onclick.md
+++ /dev/null
@@ -1,36 +0,0 @@
-## Loading tab content `onClick`
-
-Some features require loading content `onCLick`. For example, when a user clicks
-on an inactive tab to load its content async.
-
-```ruby
-# /posts.json.props
-
-json.posts do
- json.all do
- end
-
- json.pending(defer: :manual) do
- end
-end
-```
-
-```javascript
-//...in your component
- render() {
- return (
-
-
-
- ....
- )
- }
-```
-
-`defer: :manual` will instruct PropsTemplate to render the page without that
-node. You need to manually request the missing content using
-[template querying][dig] like the example above.
-
-
-[dig]: ../traversal-guide.md
diff --git a/docs/recipes/modals.md b/docs/recipes/modals.md
index 5241e7d6..43449135 100644
--- a/docs/recipes/modals.md
+++ b/docs/recipes/modals.md
@@ -9,243 +9,194 @@ When a user visits `/posts/new` from `/posts`, we want a modal to appear
overlaying the existing list of posts. The overlay should work if a user
chooses instead to directly visit `/posts/new`.
-
-
-
-
-#### The setup
-
+## The setup
Arriving at both urls results in a seeing list of posts. Lets set up the
controller and the `page_to_page_mapping.js` the same way.
-
-
-
-
-#### **`posts_controller.rb`**
-```ruby
-# app/controllers/posts_controller.rb
+=== "`posts_controller.rb`"
+ !!! info "Same template different action"
+ Notice that we're rendering the `index` for the `new` action. While the
+ content is the same, the `componentIdentifier` is different as that has
+ been setup to use the controller and action name.
-def index
- @posts = Post.all
-end
-
-def new
- @posts = Post.all
- render :index
-end
-```
+ ```ruby
+ # app/controllers/posts_controller.rb
-?> Notice that we're rendering the `index` for the `new` action. While the
-content is the same, the `componentIdentifier` is different as that has
-been setup to use the controller and action name.
+ def index
+ @posts = Post.all
+ end
-#### **`page_to_page_mapping.js`**
+ def new
+ @posts = Post.all
+ render :index
+ end
+ ```
-```js
-import PostIndex from '../views/posts/index'
-export const pageIdentifierToPageComponent = {
- 'posts/index': PostIndex,
- 'posts/new': PostIndex,
-};
-```
+=== "`page_to_page_mapping.js`"
+ !!! info
+ Similarly, we tie the `componentIdentifier` to the same page component.
+ ```js
+ import PostIndex from '../views/posts/index'
-?> Similarly, we tie the `componentIdentifier` to the same page component.
+ export const pageIdentifierToPageComponent = {
+ 'posts/index': PostIndex,
+ 'posts/new': PostIndex,
+ };
+ ```
-
-
-#### Add a link to `/posts/new`
-
+## Add a link to `/posts/new`
Imagine a list of posts, lets add a button somewhere on the index page to
direct the user to `/posts/new`. As seen previously, both `/posts` and
`/posts/new` render the same thing.
-
-
-
-
-#### **`posts/index.json`**
-
-```ruby
-# app/views/posts/index.json
-
-...
+=== "`posts/index.json.props`"
+ ```ruby
+ # app/views/posts/index.json.props
-json.newPostPath new_post_path
-```
-
-#### **`posts/index.js`**
-
-
-```js
-export default PostIndex = ({
- newPostPath,
- ...rest
-}) => {
-
- return (
- ...
-
- New Post
-
...
- )
-}
-```
-
-?> Similarly, we tie the `componentIdentifier` to the same page component.
-
-
-
-
-
-#### The modal
-
+ json.newPostPath new_post_path
+ ```
+
+=== "`posts/index.js`"
+ ```js
+ export default PostIndex = ({
+ newPostPath,
+ ...rest
+ }) => {
+
+ return (
+ ...
+
+ New Post
+
+ ...
+ )
+ }
+ ```
+
+## The modal
Now the link appears and we're able to navigate to `/posts/new`, but
`/posts/new` is missing a modal. Not surprising as both routes are
rendering the same content.
Lets add a modal.
-
-
-
-
-#### **`index.json`**
-
-?> For simplicity, we'll use a "Hello World" as the modal contents
-
-```diff
-# app/views/posts/index.json
-
-...
-
-json.newPostPath new_post_path
+=== "`posts/index.json.props`"
+ !!! info
+ For simplicity, we'll use a "Hello World" as the modal contents
+ ```diff
+ # app/views/posts/index.json
-+ json.createPostModal do
-+ json.greeting "Hello World"
-+ end
-
-```
-
-#### **`index.js`**
-
-```diff
-+ import Modal from './Modal'
-
-export default PostIndex = ({
- newPostPath,
- createPostModal
- ...rest
-}) => {
-
- return (
...
-
- New Post
-
-+
- ...
- )
-}
-```
-
-#### **`Modal.js`**
-
-?> This is a simplified modal, in practice you'll use this with `