Skip to content

Commit

Permalink
Default remote to a calculated key
Browse files Browse the repository at this point in the history
This makes remote derive the target page key just like visit instead of
defaulting to the current key. It enables a far nicer dev ex when using
remote outside of ujs.

However when using data-sg-remote, we default to the current pagekey just
like before. Its better dev ex when using remote in UJS.
  • Loading branch information
jho406 committed Dec 2, 2024
1 parent 8b437c2 commit d6a1424
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 123 deletions.
24 changes: 13 additions & 11 deletions docs/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ as you want.
Its possible to modify the remote payload before it saves
to the store. See the [beforeSave](reference/types.requests.md#remoteprops) callback.

By default, `remote` saves or updates the response to the current page that the
user is seeing. At glance it looks like this:
At glance it looks like this:

```mermaid
sequenceDiagram
Expand All @@ -68,18 +67,17 @@ sequenceDiagram
Superglue -->> Server: Re-request with format JSON `/posts/new.json`
activate Server
Server -->> Superglue: `/posts/new.json` response
Superglue -->> Superglue: Save response or update current page
Superglue -->> Superglue: Save response
Superglue -->> Browser: User on current page sees update
deactivate Server
deactivate Superglue
end
```

If you provide a `pageKey` you can also target a different page in your store
not visible to the user. Unlike `visit`, `remote` will not derive the target
page key from the response. As long as the componentIdentifier from the
response and target page is the same, `remote` will save and process the response
to the provided `pageKey`.
By default, `remote` derives a `pagekey` from the response to save the page.
You can override this behavior and expliclity pass a `pageKey` option to target
a different page in the store. If the user is not viewing the target page, they
will not see an update.

!!! warning
The componentIdentifier from the page response **MUST** match the target page, otherwise
Expand Down Expand Up @@ -116,6 +114,10 @@ sequenceDiagram

## Differences from UJS

Superglue UJS selectively exposes options of `visit` and `remote` as data
attribute and is architected for forms and links. The `visit` and `remote`
thunks are functions that return promises, allowing for greater flexibility.
The `visit` and `remote` thunks are functions that return promises, allowing
for greater flexibility. Superglue UJS selectively exposes options of `visit`
and `remote` for easy dev exp when using with forms and links.

!!! hint
Unlike `remote`, `data-sg-remote` does not derive the `pageKey`. Instead it
saves or grafts all page responses to current page.
13 changes: 10 additions & 3 deletions docs/ujs.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,15 @@ tabs, notifications, etc.

## `data-sg-remote`

Use `data-sg-remote` when you want to update parts of the current page without
reloading the screen. You'd can use this with props_template's [digging]
to selectively load content.
Use `data-sg-remote` when you want to update parts of the **current page** without
reloading the screen.

<div class="grid cards" markdown>
- [:octicons-arrow-right-24: See differences](requests.md#differences-from-ujs)
from `remote`
</div>

Combine this with props_template's [digging] to selectively load content.

```jsx
<a href='/posts?page_num=2&props_at=data.body.postsList' data-sg-remote/>
Expand All @@ -91,3 +97,4 @@ You can also use `data-sg-remote` on forms.
....
</form>
```

123 changes: 70 additions & 53 deletions superglue/lib/action_creators/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
Dispatch,
RemoteCreator,
VisitCreator,
SuggestedAction,
} from '../types'

function handleFetchErr(
Expand Down Expand Up @@ -82,24 +83,20 @@ export const remote: RemoteCreator = (
method = 'GET',
headers,
body,
pageKey: rawPageKey,
pageKey: targetPageKey,
force = false,
beforeSave = (prevPage: Page, receivedPage: PageResponse) => receivedPage,
} = {}
) => {
path = withoutBusters(path)
rawPageKey = rawPageKey && urlToPageKey(rawPageKey)
targetPageKey = targetPageKey && urlToPageKey(targetPageKey)

return (dispatch, getState) => {
const fetchArgs = argsForFetch(getState, path, {
method,
headers,
body,
})
if (rawPageKey === undefined) {
rawPageKey = getState().superglue.currentPageKey
}
const pageKey = rawPageKey
const currentPageKey = getState().superglue.currentPageKey

dispatch(beforeRemote({ currentPageKey, fetchArgs }))
Expand All @@ -110,17 +107,19 @@ export const remote: RemoteCreator = (
.then(({ rsp, json }) => {
const { superglue, pages = {} } = getState()

let pageKey
if (targetPageKey === undefined) {
const isGet = fetchArgs[1].method === 'GET'
pageKey = calculatePageKey(rsp, isGet, currentPageKey)
} else {
pageKey = targetPageKey
}

const meta = buildMeta(pageKey, json, superglue, rsp, fetchArgs)
const willReplaceCurrent = pageKey == currentPageKey
const existingId = pages[currentPageKey]?.componentIdentifier
const existingId = pages[pageKey]?.componentIdentifier
const receivedId = json.componentIdentifier

if (
willReplaceCurrent &&
!!existingId &&
existingId != receivedId &&
!force
) {
if (!!existingId && existingId != receivedId && !force) {
throw new MismatchedComponentError(
existingId,
receivedId,
Expand Down Expand Up @@ -153,18 +152,13 @@ export const visit: VisitCreator = (
} = {}
) => {
path = withoutBusters(path)
let pageKey = urlToPageKey(path)

return (dispatch, getState) => {
placeholderKey = placeholderKey && urlToPageKey(placeholderKey)
const hasPlaceholder = !!(
placeholderKey && getState().pages[placeholderKey]
)

if (placeholderKey && hasPlaceholder) {
dispatch(copyPage({ from: placeholderKey, to: pageKey }))
}

if (placeholderKey && !hasPlaceholder) {
console.warn(
`Could not find placeholder with key ${placeholderKey} in state. The props_at param will be ignored`
Expand Down Expand Up @@ -199,47 +193,70 @@ export const visit: VisitCreator = (
.then(parseResponse)
.then(({ rsp, json }) => {
const { superglue, pages = {} } = getState()

const meta = buildMeta(pageKey, json, superglue, rsp, fetchArgs)

const isGet = fetchArgs[1].method === 'GET'

meta.suggestedAction = 'push'

if (!rsp.redirected && !isGet) {
meta.suggestedAction = 'replace'
}
pageKey = urlToPageKey(rsp.url)

const isSamePage = pageKey == currentPageKey

if (isSamePage) {
meta.suggestedAction = 'none'
const pageKey = calculatePageKey(rsp, isGet, currentPageKey)
if (placeholderKey && hasPlaceholder) {
dispatch(copyPage({ from: placeholderKey, to: pageKey }))
}

if (revisit && isGet) {
if (rsp.redirected) {
meta.suggestedAction = 'replace'
} else {
meta.suggestedAction = 'none'
}
}

if (!isGet && !rsp.redirected) {
pageKey = currentPageKey
}
const meta = buildMeta(pageKey, json, superglue, rsp, fetchArgs)

const contentLocation = rsp.headers.get('content-location')
if (contentLocation) {
pageKey = urlToPageKey(contentLocation)
}
meta.suggestedAction = calculateNavAction(
meta,
rsp,
isGet,
pageKey,
currentPageKey,
revisit
)

const page = beforeSave(pages[pageKey], json)
return dispatch(saveAndProcessPage(pageKey, page)).then(() => {
meta.pageKey = pageKey
return meta
})
return dispatch(saveAndProcessPage(pageKey, page)).then(() => meta)
})
.catch((e) => handleFetchErr(e, fetchArgs, dispatch))
}
}

function calculateNavAction(
meta: Meta,
rsp: Response,
isGet: boolean,
pageKey: string,
currentPageKey: string,
revisit: boolean
) {
let suggestedAction: SuggestedAction = 'push'
if (!rsp.redirected && !isGet) {
suggestedAction = 'replace'
}
const isSamePage = pageKey == currentPageKey
if (isSamePage) {
suggestedAction = 'none'
}
if (revisit && isGet) {
if (rsp.redirected) {
suggestedAction = 'replace'
} else {
suggestedAction = 'none'
}
}

return suggestedAction
}

function calculatePageKey(
rsp: Response,
isGet: boolean,
currentPageKey: string
) {
let pageKey = urlToPageKey(rsp.url)
if (!isGet && !rsp.redirected) {
pageKey = currentPageKey
}

const contentLocation = rsp.headers.get('content-location')
if (contentLocation) {
pageKey = urlToPageKey(contentLocation)
}
return pageKey
}
10 changes: 9 additions & 1 deletion superglue/lib/utils/ujs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,29 @@ import {
Meta,
Handlers,
UJSHandlers,
SuperglueStore,
} from '../types'

export class HandlerBuilder {
public attributePrefix: string
public visit: Visit
public remote: Remote
private store: SuperglueStore

constructor({
ujsAttributePrefix,
visit,
remote,
store,
}: {
ujsAttributePrefix: string
visit: Visit
remote: Remote
store: SuperglueStore
}) {
this.attributePrefix = ujsAttributePrefix
this.isUJS = this.isUJS.bind(this)
this.store = store

this.handleSubmit = this.handleSubmit.bind(this)
this.handleClick = this.handleClick.bind(this)
Expand Down Expand Up @@ -123,7 +128,8 @@ export class HandlerBuilder {
}

if (linkOrForm.getAttribute(this.attributePrefix + '-remote')) {
return this.remote(url, opts)
const { currentPageKey } = this.store.getState().superglue
return this.remote(url, { ...opts, pageKey: currentPageKey })
}
}

Expand All @@ -139,11 +145,13 @@ export const ujsHandlers: UJSHandlers = ({
ujsAttributePrefix,
visit,
remote,
store,
}) => {
const builder = new HandlerBuilder({
visit,
remote,
ujsAttributePrefix,
store,
})

return builder.handlers()
Expand Down
38 changes: 32 additions & 6 deletions superglue/spec/lib/action_creators.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -878,8 +878,34 @@ describe('action creators', () => {
expect(allSuperglueActions(store)).toEqual(expectedActions)
})
})

it('defaults to the response url as the pageKey on GET requests', () => {
const store = buildStore({
superglue: {
currentPageKey: '/current_url',
csrfToken: 'token',
},
})

fetchMock.mock('/foobar?format=json', {
body: successfulBody(),
headers: {
'content-type': 'application/json',
},
})

return store
.dispatch(remote('/foobar', { method: 'GET' }))
.then((meta) => {
expect(meta).toEqual(
expect.objectContaining({
pageKey: '/foobar',
})
)
})
})

it('defaults to the currentPageKey as the pageKey', () => {
it('defaults to the currentPageKey as the pageKey when a non GET renders', () => {
const store = buildStore({
superglue: {
currentPageKey: '/current_url',
Expand All @@ -905,7 +931,7 @@ describe('action creators', () => {
})
})

it('uses the pageKey option to override the currentPageKey as the preferred pageKey', () => {
it('uses the pageKey option to explicitly specify where to store the response', () => {
const store = buildStore({
superglue: {
currentPageKey: '/url_to_be_overridden',
Expand Down Expand Up @@ -1463,10 +1489,6 @@ describe('action creators', () => {
)

const expectedActions = [
{
type: '@@superglue/COPY_PAGE',
payload: { from: '/current', to: '/details' },
},
{
type: '@@superglue/BEFORE_VISIT',
payload: expect.any(Object),
Expand All @@ -1475,6 +1497,10 @@ describe('action creators', () => {
type: '@@superglue/BEFORE_FETCH',
payload: expect.any(Object),
},
{
type: '@@superglue/COPY_PAGE',
payload: { from: '/current', to: '/details' },
},
{
type: '@@superglue/HANDLE_GRAFT',
payload: expect.any(Object),
Expand Down
Loading

0 comments on commit d6a1424

Please sign in to comment.