Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/reference/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ the current page.
| <a id="search"></a> `search` | `Record`\<`string`, `string` \| `undefined`\> | The query string object of the current url. | [types/index.ts:358](https://github.com/thoughtbot/superglue/blob/38ea0f22384ca668ffa28ead0054dc09a0f39366/superglue/lib/types/index.ts#L358) |
| <a id="csrftoken-3"></a> `csrfToken?` | `string` | The Rails csrfToken that you can use for forms. | [types/index.ts:360](https://github.com/thoughtbot/superglue/blob/38ea0f22384ca668ffa28ead0054dc09a0f39366/superglue/lib/types/index.ts#L360) |
| <a id="assets-3"></a> `assets` | `string`[] | The tracked asset digests. | [types/index.ts:362](https://github.com/thoughtbot/superglue/blob/38ea0f22384ca668ffa28ead0054dc09a0f39366/superglue/lib/types/index.ts#L362) |
| <a id="isvisiting"></a> `isVisiting` | `boolean` | Flag indicating if a visit is currently in flight. | [types/index.ts:364](https://github.com/thoughtbot/superglue/blob/38ea0f22384ca668ffa28ead0054dc09a0f39366/superglue/lib/types/index.ts#L364) |

***

Expand Down
26 changes: 26 additions & 0 deletions docs/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,32 @@ sequenceDiagram
`application_visit.js`. If you need a global customization, e.g, progress
bars, you can add them there.

## Tracking visit state

The `isVisiting` boolean is available in [SuperglueState](./reference/types.md#supergluestate)
and indicates whether a visit is currently in flight. This is useful for showing loading
indicators, progress bars, or disabling UI elements while navigation is happening.

```jsx
import { useSuperglue } from '@thoughtbot/superglue'

export function App() {
const { isVisiting } = useSuperglue()

return (
<div>
{isVisiting && <ProgressBar />}
<Outlet />
</div>
)
}
```

!!! note
`isVisiting` only tracks explicit `visit` calls. It does not track `remote` requests,
which can be many and concurrent. If you need to track loading state for multiple
concurrent requests, consider using Redux to track them independently.


## The `beforeSave` callback

Expand Down
4 changes: 4 additions & 0 deletions superglue/lib/action_creators/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
copyPage,
superglueError,
receiveResponse,
visitEnd,
} from '../actions'
import { saveAndProcessPage, preparePageForSave } from './page'
import {
Expand Down Expand Up @@ -278,6 +279,9 @@ to the same page.
return dispatch(saveAndProcessPage(pageKey, page)).then(() => visitMeta)
})
.catch((e) => handleFetchErr(e, fetchArgs, dispatch))
.finally(() => {
dispatch(visitEnd({ fetchArgs }))
})
}
}

Expand Down
19 changes: 19 additions & 0 deletions superglue/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ export const beforeVisit = createAction<{
fetchArgs: FetchArgs
}>('@@superglue/BEFORE_VISIT')

/**
* A redux action called after a `visit` completes. This fires regardless of
* success, error, redirect, or abort. You can hook into this event in your redux
* slices like this:
*
* ```
* import { visitEnd } from '@thoughtbot/superglue'
*
* export const exampleSlice = createSlice({
* name: 'Example',
* initialState: {},
* extraReducers: (builder) => {
* builder.addCase(visitEnd, (state, action) => {
* ```
*/
export const visitEnd = createAction<{
fetchArgs: FetchArgs
}>('@@superglue/VISIT_END')

/**
* A redux action called before `remote` takes place. You can hook into this event
* in your redux slices like this:
Expand Down
11 changes: 11 additions & 0 deletions superglue/lib/reducers/superglueReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import {
setCSRFToken,
setActivePage,
resetStore,
beforeVisit,
visitEnd,
} from '../actions'
import { SuperglueState } from '../types'

const initialSuperglueState: SuperglueState = {
currentPageKey: '',
search: {},
assets: [],
isVisiting: false,
}

export function superglueReducer(
Expand Down Expand Up @@ -58,5 +61,13 @@ export function superglueReducer(
return { ...state, csrfToken, assets }
}

if (beforeVisit.match(action)) {
return { ...state, isVisiting: true }
}

if (visitEnd.match(action)) {
return { ...state, isVisiting: false }
}

return state
}
2 changes: 2 additions & 0 deletions superglue/lib/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface SuperglueState {
csrfToken?: string
/** The tracked asset digests.*/
assets: string[]
/** Flag that says if a visit is currently in flight */
isVisiting: boolean
}

/**
Expand Down
63 changes: 63 additions & 0 deletions superglue/spec/lib/action_creators.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,10 @@ describe('action creators', () => {
type: '@@superglue/HANDLE_GRAFT',
payload: expect.any(Object),
},
{
type: '@@superglue/VISIT_END',
payload: expect.any(Object),
},
]

store
Expand Down Expand Up @@ -1835,6 +1839,10 @@ describe('action creators', () => {
type: '@@superglue/HANDLE_GRAFT',
payload: expect.any(Object),
},
{
type: '@@superglue/VISIT_END',
payload: expect.any(Object),
},
]

store
Expand Down Expand Up @@ -1911,5 +1919,60 @@ describe('action creators', () => {
)
})
})

it('sets isVisiting to true on beforeVisit and false on visitEnd', () => {
const initialState = {
pages: {},
superglue: {
assets: [],
isVisiting: false,
},
}

const store = buildStore(initialState)

fetchMock.mock(
'https://example.com/first?format=json',
rsp.visitSuccess()
)

expect(store.getState().superglue.isVisiting).toEqual(false)

return store
.dispatch(visit('/first'))
.then(() => {
expect(store.getState().superglue.isVisiting).toEqual(false)
})
})

it('dispatches beforeVisit and visitEnd actions', () => {
const initialState = {
pages: {},
superglue: {
assets: [],
isVisiting: false,
},
}

const store = buildStore(initialState)

fetchMock.mock(
'https://example.com/first?format=json',
rsp.visitSuccess()
)

return store.dispatch(visit('/first')).then(() => {
const actions = allSuperglueActions(store)
const beforeVisitAction = actions.find(
(a) => a.type === '@@superglue/BEFORE_VISIT'
)
const visitEndAction = actions.find(
(a) => a.type === '@@superglue/VISIT_END'
)

expect(beforeVisitAction).toBeDefined()
expect(visitEndAction).toBeDefined()
})
})
})
})
37 changes: 37 additions & 0 deletions superglue/spec/lib/reducers.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,43 @@ describe('reducers', () => {
})
})
})

describe('SUPERGLUE_BEFORE_VISIT', () => {
it('sets isVisiting to true', () => {
const prevState = { isVisiting: false, currentPageKey: '/foo' }
const action = {
type: '@@superglue/BEFORE_VISIT',
payload: {
currentPageKey: '/foo',
fetchArgs: [{}, {}],
},
}
const nextState = superglueReducer(prevState, action)

expect(nextState).toEqual({
isVisiting: true,
currentPageKey: '/foo',
})
})
})

describe('SUPERGLUE_VISIT_END', () => {
it('sets isVisiting to false', () => {
const prevState = { isVisiting: true, currentPageKey: '/foo' }
const action = {
type: '@@superglue/VISIT_END',
payload: {
fetchArgs: [{}, {}],
},
}
const nextState = superglueReducer(prevState, action)

expect(nextState).toEqual({
isVisiting: false,
currentPageKey: '/foo',
})
})
})
})

describe('page reducer', () => {
Expand Down