Skip to content

Commit

Permalink
refactor(script)!: $script promisable, remove waitForLoad
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Mar 23, 2024
1 parent 1b1532e commit 4646d57
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 66 deletions.
257 changes: 233 additions & 24 deletions docs/content/1.usage/2.composables/4.use-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,72 @@ const { $script } = useScript('https://example.com/script.js', {

::

### Waiting for Script Load

Sometimes you'll want to directly use the script instead of relying on the proxy. For this you can use the `$script` object as a Promise.

```ts
const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
use() {
return window.myAwesomeScript
},
})
// Note: Do not do this if you have a `manual` trigger
$scipt.then((myAwesomeScript) => {
// accesses the script directly, proxy is not used
myAwesomeScript.myFunction('hello')
})
// OR - will block rendering until script is available
const myAwesomeScript = await $script
myAwesomeScript.myFunction('hello')
```

When you have a manual trigger awaiting the promise will never resolve unless you `load()` the $script.

```ts
const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
use() {
return window.myAwesomeScript
},
trigger: 'manual'
})

// Warning: Will never resolve!
await $script

// Make sure you call load if you're going to await with a manual trigger
$script.load()
```

### Handling Script Loading Failure

Sometimes scripts just won't load, this can be due to network issues, the script being blocked, etc.

To handle this, you can catch exceptions thrown from `$script`.

```ts
const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
use() {
return window.myAwesomeScript
},
})

$script.catch((err) => {
console.error('Failed to load script', err)
})
```

Otherwise, you always check the status of the script using `$script.status`.

```ts
const { $script } = useScript<MyAwesomeScript>('https://example.com/my-awesome-script.js', {
use() {
return window.myAwesomeScript
},
})
$script.status // 'awaitingLoad' | 'loading' | 'loaded' | 'error'
```

### SSR Stubbing

In cases where you want to use the script API on the server, you can use the `stub` option. This lets
Expand Down Expand Up @@ -181,13 +247,11 @@ const { gtag } = useScript<GoogleTag>('https://www.google-analytics.com/analytic

## API

### Arguments

```ts
useScript<API>(scriptOptions, options)
```

### scriptOptions
### Argument: Script Options

The script options, this is the same as the `script` option for `useHead`. For example `src`, `async`, etc.

Expand All @@ -206,15 +270,19 @@ useScript({
useScript('https://www.google-analytics.com/analytics.js')
```

### options
### Argument: Use Script Options

- `skipEarlyConnections`
#### `skipEarlyConnections`

- Type: `boolean`

Used to skip early connections such as `dns-prefetch` and `preconnect`.

Useful when you're loading a script from your own domain.

- `use`
#### `use`

- Type: `() => API`

A function that resolves the scripts API. This is only called client-side.

Expand All @@ -229,7 +297,9 @@ const { trackPageview } = useScript<FathomApi>({
trackPageview({ url: 'https://example.com' })
```

- `trigger`
#### `trigger`

- Type: `'idle' | 'manual' | Promise<void>`

An optional loading strategy to use. `idle` or `manual`. Defaults to `undefined`.

Expand All @@ -242,7 +312,7 @@ useScript({
})
```

- `stub`
#### `stub`

A more advanced function used to stub out the logic of the API. This will be called on the server and client.

Expand All @@ -265,10 +335,15 @@ sendEvent('event')
doSomething()
```

### $script
### Return Value

The `useScript` composable returns a Proxy API that you can use to interact with the script.

The return value is an object with the API provided by the script. It also contains a special `$script` property
that gives you access to the underlying script instance.
Any requests to the API will be proxied to the real script when it's loaded.

#### $script

The `$script` property is a special property that gives you access to the underlying script instance.

```ts
const { $script } = useScript({
Expand Down Expand Up @@ -315,30 +390,164 @@ $script.waitForLoad().then(() => {

## Examples

### CloudFlare Analytics

::code-group

```ts [Unhead]
import { useScript } from 'unhead'

interface CloudflareAnalyticsApi {
__cfBeacon: {
load: 'single'
spa: boolean
token: string
}
__cfRl?: unknown
}

declare global {
interface Window extends CloudflareAnalyticsApi {}
}

export function useCloudflareAnalytics() {
return useScript<CloudflareAnalyticsApi>({
'src': 'https://static.cloudflareinsights.com/beacon.min.js',
'data-cf-beacon': JSON.stringify({ token: 'my-token', spa: true }),
'trigger': 'idle',
}, {
use() {
return { __cfBeacon: window.__cfBeacon, __cfRl: window.__cfRl }
},
})
}
```

```ts [Vue]
import { useScript } from '@unhead/vue'

interface CloudflareAnalyticsApi {
__cfBeacon: {
load: 'single'
spa: boolean
token: string
}
__cfRl?: unknown
}

declare global {
interface Window extends CloudflareAnalyticsApi {}
}

export function useCloudflareAnalytics() {
return useScript<CloudflareAnalyticsApi>({
'src': 'https://static.cloudflareinsights.com/beacon.min.js',
'data-cf-beacon': JSON.stringify({ token: 'my-token', spa: true }),
'trigger': 'idle',
}, {
use() {
return { __cfBeacon: window.__cfBeacon, __cfRl: window.__cfRl }
},
})
}
```

::

### Fathom Analytics

::code-group

```ts [Unhead]
import { useScript } from 'unhead'

interface FathomAnalyticsApi {
trackPageview: (ctx?: { url: string, referrer?: string }) => void
trackGoal: (eventName: string, value?: { _value: number }) => void
}

declare global {
interface Window { fathom: FathomAnalyticsApi }
}

export function useFathomAnalytics() {
return useScript<FathomAnalyticsApi>({
'src': 'https://cdn.usefathom.com/script.js',
'data-site': 'my-site',
// See https://usefathom.com/docs/script/script-advanced
}, {
use: () => window.fathom,
})
}
```

```ts [Vue]
import { useScript } from '@unhead/vue'

interface FathomAnalyticsApi {
trackPageview: (ctx?: { url: string, referrer?: string }) => void
trackGoal: (eventName: string, value?: { _value: number }) => void
}

declare global {
interface Window { fathom: FathomAnalyticsApi }
}

export function useFathomAnalytics() {
return useScript<FathomAnalyticsApi>({
'src': 'https://cdn.usefathom.com/script.js',
'data-site': 'my-site',
// See https://usefathom.com/docs/script/script-advanced
}, {
use: () => window.fathom,
})
}
```

::

### Google Analytics

::code-group

```ts [Unhead]
import { useScript } from 'unhead'

const { gtag } = useScript({
src: 'https://www.google-analytics.com/analytics.js',
}, {
use: () => ({ gtag: window.gtag })
})
interface GoogleAnalyticsApi {
gtag: ((fn: 'event', opt: string, opt2: { [key: string]: string }) => void)
}

declare global {
interface Window extends GoogleAnalyticsApi {}
}

export function useGoogleAnalytics() {
return useScript<GoogleAnalyticsApi>({
src: 'https://www.google-analytics.com/analytics.js',
}, {
use: () => ({ gtag: window.gtag })
})
}
```

```vue [Vue]
<script lang="ts" setup>
```ts [Vue]
import { useScript } from '@unhead/vue'

const { gtag } = useScript({
src: 'https://www.google-analytics.com/analytics.js',
}, {
use: () => ({ gtag: window.gtag })
})
</script>
interface GoogleAnalyticsApi {
gtag: ((fn: 'event', opt: string, opt2: { [key: string]: string }) => void)
}

declare global {
interface Window extends GoogleAnalyticsApi {}
}

export function useGoogleAnalytics() {
return useScript<GoogleAnalyticsApi>({
src: 'https://www.google-analytics.com/analytics.js',
}, {
use: () => ({ gtag: window.gtag })
})
}
```

::
7 changes: 4 additions & 3 deletions packages/schema/src/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ export type UseScriptStatus = 'awaitingLoad' | 'loading' | 'loaded' | 'error' |
export type UseScriptInput = string | (Omit<Script, 'src'> & { src: string })
export type UseScriptResolvedInput = Omit<Script, 'src'> & { src: string }

export interface ScriptInstance<T> {
export type ScriptInstance<T> = {
id: string
entry?: ActiveHeadEntry<any>
loaded: boolean
status: UseScriptStatus
loadPromise: Promise<T>
entry?: ActiveHeadEntry<any>
load: () => Promise<T>
waitForLoad: () => Promise<T>
remove: () => boolean
}
} & Promise<T>

export interface UseScriptOptions<T> extends Omit<HeadEntryOptions, 'transform'> {
/**
Expand Down
Loading

0 comments on commit 4646d57

Please sign in to comment.