Skip to content

Commit

Permalink
Pass element dataset to request factory
Browse files Browse the repository at this point in the history
This enables `application_visit.js` to receive the dataset of elements enabled
with UJS, data-sg-visit and data-sg-remote. Devs would be able to add their own
custom functionality around the visit and remote requests. For example, adding
data attributes to control how a progress bar functions, overriding the suggested
action with a custom navigation option, etc..
  • Loading branch information
jho406 committed Dec 4, 2024
1 parent fcd4561 commit 18bbf46
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 78 deletions.
44 changes: 21 additions & 23 deletions docs/recipes/progress-bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,35 @@ yarn add request-stripe

And make the following edits to `application_visit.js`

```diff
````diff
import { visit, remote } from '@thoughtbot/superglue/action_creators'
+ import { requestStripe } from 'request-stripe';

export function buildVisitAndRemote(ref, store) {
const appRemote = (...args) => {
+ const done = requestStripe();
return store.dispatch(remote(...args))
const appRemote = (path, {dataset, options} = {}) => {
/**
* You can make use of `dataset` to add custom UJS options.
* If you are implementing a progress bar, you can selectively
* hide it for some links. For example:
*
* ```
* <a href="/posts?props_at=data.header" data-sg-remote data-sg-hide-progress>
* Click me
* </a>
* ```
*
* This would be available as `sgHideProgress` on the dataset
*/
+ const done = requestStripe()
return store.dispatch(remote(path, options))
+ .finally(() => done())
}

const appVisit = (...args) => {
+ const done = requestStripe();

// Do something before
// e.g, show loading state, you can access the current pageKey
// via store.getState().superglue.currentPageKey
let { action } = args

const appVisit = (path, {dataset, ...options} = {}) => {
+ const done = requestStripe()
return store
.dispatch(visit(...args))
.dispatch(visit(path, options))
.then((meta) => {
// The assets fingerprints changed, instead of transitioning
// just go to the URL directly to retrieve new assets
if (meta.needsRefresh) {
window.location = meta.url
return
Expand All @@ -44,13 +49,9 @@ export function buildVisitAndRemote(ref, store) {
action: meta.suggestedAction,
})

// always return meta
return meta
})
.finally(() => {
// Do something after
// e.g, hide loading state, you can access the changed pageKey
// via getState().superglue.currentPageKey
+ done()
})
.catch((err) => {
Expand All @@ -62,9 +63,6 @@ export function buildVisitAndRemote(ref, store) {
}

if (response.ok) {
// err gets thrown, but if the response is ok,
// it must be an html body that
// superglue can't parse, just go to the location
window.location = response.url
} else {
if (response.status >= 400 && response.status < 500) {
Expand All @@ -82,4 +80,4 @@ export function buildVisitAndRemote(ref, store) {

return { visit: appVisit, remote: appRemote }
}
```
````
17 changes: 14 additions & 3 deletions docs/ujs.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Superglue operates like a multipage application. In other to transition to the
next page without reloading you'll need to use UJS attributes `data-sg-remote`
or `data-sg-visit`.

## `data-sg-visit`
### `data-sg-visit`

Use `data-sg-visit` when you want to navigate to the next page and update the
address bar without reloading.
Expand All @@ -53,7 +53,7 @@ You can also use `data-sg-visit` on forms:
<form action='/some_url' data-sg-visit />
```

## `data-sg-remote`
### `data-sg-remote`

Use `data-sg-remote` when you want to update parts of the **current page** without
reloading the screen.
Expand All @@ -74,9 +74,20 @@ Combine this with props_template's [digging] to selectively load content.
You can also use `data-sg-remote` on forms.

```jsx
<form action="/posts" method="GET" data-sg-remote>
<form action="/posts" method="GET" data-sg-remote>
<input type="search" .... />
....
</form>
```

## Expanding UJS

The [dataset] of the element enabled with `data-sg-visit` or `data-sg-remote` is
passed to your [application_visit.js]. You can add your own options to control the
behavior of the UJS helpers. For example, if you want to selectively show a
[progress bar] on some links.


[dataset]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
[application_visit.js]: ./configuration.md
[progress bar]: ./recipes/progress-bar.md
21 changes: 14 additions & 7 deletions superglue/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { EnhancedStore, Tuple, StoreEnhancer } from '@reduxjs/toolkit'
import { ThunkDispatch } from '@reduxjs/toolkit'
import { ThunkAction } from '@reduxjs/toolkit'
import { ConnectedComponent } from 'react-redux'
import { Visit, Remote, VisitProps, RemoteProps } from './requests'
import {
Visit,
Remote,
VisitProps,
RemoteProps,
ApplicationVisit,
ApplicationRemote,
} from './requests'
import { History } from 'history'
import { rootReducer } from '../reducers'
import { NavigationProvider } from '../components/Navigation'
Expand Down Expand Up @@ -338,8 +345,8 @@ export type UJSHandlers = ({
store,
}: {
ujsAttributePrefix: string
visit: Visit
remote: Remote
visit: ApplicationVisit
remote: ApplicationRemote
store: SuperglueStore
}) => Handlers

Expand Down Expand Up @@ -428,8 +435,8 @@ export type NavigateTo = (
*/
export type NavigationContextProps = {
navigateTo: NavigateTo
visit: Visit
remote: Remote
visit: ApplicationVisit
remote: ApplicationRemote
pageKey: PageKey
}

Expand All @@ -444,8 +451,8 @@ export type NavigationContextProps = {
*/
export type NavigationProviderProps = {
history: History
visit: Visit
remote: Remote
visit: ApplicationVisit
remote: ApplicationRemote
mapping: Record<
ComponentIdentifier,
ConnectedComponent<React.ComponentType, PageOwnProps>
Expand Down
51 changes: 48 additions & 3 deletions superglue/lib/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ export interface Remote {
* store. Remote does not navigate, and it does not change the browser history.
* There can be multiple Remote requests running concurrently.
*
* This function is to be built, customized, and returned to superglue by the
* developer. This is usually generated as `application_visit.js` where you can
* make minimum edits to affect its global usage.
* This function is to be wrapped by a deverloper as a {@link ApplicationRemote}
* and returned to superglue. This is usually generated as
* `application_visit.js` where you can make minimum edits to affect its
* global usage.
*
* @param input The first argument to Fetch
* @param options The fetch RequestInit with additional options
Expand Down Expand Up @@ -117,3 +118,47 @@ export interface BeforeSave {
*/
(prevPage: VisitResponse, receivedPage: VisitResponse): VisitResponse
}

export interface ApplicationRemote extends Remote {
/**
* ApplicationRemote is the developer provided wrapper around {@link Remote}.
*
* It contains custom functionality, but is bound by the interface that
* Superglue uses to make a `remote` call. See {@link Remote} for more details.
*
* The only difference between the two interfaces is ApplicationRemote will also
* be passed a dataset as an option. This is because Superglue UJS uses
* ApplicationRemote and will pass the dataset of the HTML element where UJS is
* enabled on.
*/
(
input: string | PageKey,
options: RemoteProps & {
dataset?: {
[name: string]: string | undefined
}
}
): Promise<Meta>
}

export interface ApplicationVisit {
/**
* ApplicationVisit is the developer provided wrapper around {@link Remote}.
*
* It contains custom functionality, but is bound by the interface that
* Superglue uses to make a `visit` call. See {@link Remote} for more details.
*
* The only difference between the two interfaces is ApplicationVisit will also
* be passed a dataset as an option. This is because Superglue UJS uses
* ApplicationVisit and will pass the dataset of the HTML element where UJS is
* enabled on.
*/
(
input: string | PageKey,
options: VisitProps & {
dataset?: {
[name: string]: string | undefined
}
}
): Promise<Meta>
}
28 changes: 17 additions & 11 deletions superglue/lib/utils/ujs.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { withoutBusters, urlToPageKey } from './url'

Check failure on line 1 in superglue/lib/utils/ujs.ts

View workflow job for this annotation

GitHub Actions / Test superglue.js

'urlToPageKey' is defined but never used
import {
Visit,
Remote,
VisitProps,
RemoteProps,
Meta,
Handlers,
UJSHandlers,
SuperglueStore,
ApplicationVisit,
ApplicationRemote,
RemoteProps,
VisitProps,
} from '../types'

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

constructor({
Expand All @@ -23,8 +23,8 @@ export class HandlerBuilder {
store,
}: {
ujsAttributePrefix: string
visit: Visit
remote: Remote
visit: ApplicationVisit
remote: ApplicationRemote
store: SuperglueStore
}) {
this.attributePrefix = ujsAttributePrefix
Expand Down Expand Up @@ -114,15 +114,21 @@ export class HandlerBuilder {
visitOrRemote(
linkOrForm: HTMLAnchorElement | HTMLFormElement,
url: string,
opts: VisitProps | RemoteProps
opts: RemoteProps | VisitProps
): Promise<Meta> | undefined {
const dataset = { ...linkOrForm.dataset }

if (linkOrForm.getAttribute(this.attributePrefix + '-visit')) {
return this.visit(url, { ...opts })
return this.visit(url, { ...opts, dataset })
}

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

Expand Down
Loading

0 comments on commit 18bbf46

Please sign in to comment.