Skip to content

Commit

Permalink
fix(scripts,vue): prefer ref promises
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Sep 15, 2024
1 parent 59640c1 commit 487fe5e
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 5 deletions.
25 changes: 23 additions & 2 deletions docs/content/1.usage/2.composables/4.use-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ useScript('/script.js', {
})
```

```ts [Ref (Vue)]
const shouldLoad = ref(false)
useScript('/script.js', {
trigger: shouldLoad
})
```

::

### Waiting for Script Load
Expand Down Expand Up @@ -362,11 +369,14 @@ useScript({

#### `trigger`

- Type: `'undefined' | 'manual' | 'server' | 'client' | Promise<void>`
- Type: `'undefined' | 'manual' | 'server' | 'client' | Promise<void>`
- Additional Vue Types: `Ref<boolean>`

A strategy to use for when the script should be loaded. Defaults to `client`.

```ts
::code-group

```ts [Promise]
useScript({
src: 'https://example.com/script.js',
}, {
Expand All @@ -376,6 +386,17 @@ useScript({
})
```

```ts [Vue - Ref]
const shouldLoad = ref(false)
useScript({
src: 'https://example.com/script.js',
}, {
trigger: shouldLoad
})
```

::

When `server` is set as the trigger, the script will be injected into the SSR HTML response, allowing for quicker
loading of the script.

Expand Down
55 changes: 55 additions & 0 deletions examples/vite-ssr-vue/src/pages/ref-trigger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script setup>
import { ref, onUnmounted } from 'vue'
import { useScript } from '@unhead/vue'
const loadScript = ref(false)
const { status, $script, onLoaded } = useScript({
key: 'stripe',
src: 'https://js.stripe.com/v3/',
onload() {
console.log('script onload input')
},
onerror() {
console.log('script error')
},
}, {
trigger: loadScript,
})
onLoaded(() => {
console.log('on loaded callback')
})
$script.then(() => {
console.log('script promise callback')
})
onUnmounted(() => {
$script.load()
})
useHead({
title: () => $script.status.value,
})
</script>

<template>
<div>
<h1>ref trigger</h1>
<button @click="loadScript = true">
load script
</button>
<div>
ref: {{ loadScript }}
</div>
<div>
script status: {{ status }}
</div>
</div>
</template>

<style scoped>
h1,
a {
color: green;
}
</style>
38 changes: 35 additions & 3 deletions packages/vue/src/composables/useScript.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useScript as _useScript } from 'unhead'
import { getCurrentInstance, onMounted, onScopeDispose, ref } from 'vue'
import { getCurrentInstance, isRef, onMounted, onScopeDispose, ref, watch } from 'vue'
import type {
AsAsyncFunctionValues,
UseScriptInput as BaseUseScriptInput,
UseScriptOptions as BaseUseScriptOptions,
DataKeys,
HeadEntryOptions,
SchemaAugmentations,
ScriptBase,
ScriptInstance,
UseFunctionType,
UseScriptOptions,
UseScriptResolvedInput,
UseScriptStatus,
} from '@unhead/schema'
Expand All @@ -21,6 +22,18 @@ export interface VueScriptInstance<T extends Record<symbol | string, any>> exten
}

export type UseScriptInput = string | (MaybeComputedRefEntriesOnly<Omit<ScriptBase & DataKeys & SchemaAugmentations['script'], 'src'>> & { src: string })
export interface UseScriptOptions<T extends Record<symbol | string, any> = {}, U = {}> extends HeadEntryOptions, Pick<BaseUseScriptOptions<T, U>, 'use' | 'stub' | 'eventContext' | 'beforeInit'> {
/**
* The trigger to load the script:
* - `undefined` | `client` - (Default) Load the script on the client when this js is loaded.
* - `manual` - Load the script manually by calling `$script.load()`, exists only on the client.
* - `Promise` - Load the script when the promise resolves, exists only on the client.
* - `Function` - Register a callback function to load the script, exists only on the client.
* - `server` - Have the script injected on the server.
* - `ref` - Load the script when the ref is true.
*/
trigger?: BaseUseScriptOptions['trigger'] | Ref<boolean>
}

export type UseScriptContext<T extends Record<symbol | string, any>> =
(Promise<T> & VueScriptInstance<T>)
Expand Down Expand Up @@ -70,15 +83,34 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
options.head = head
const scope = getCurrentInstance()
options.eventContext = scope
if (scope && typeof options.trigger === 'undefined')
if (scope && typeof options.trigger === 'undefined') {
options.trigger = onMounted
}
else if (isRef(options.trigger)) {
const refTrigger = options.trigger as Ref<boolean>
options.trigger = new Promise<boolean>((resolve) => {
const off = watch(refTrigger, (val) => {
if (val) {
off()
resolve(true)
}
}, {
immediate: true,
})
onScopeDispose(() => {
off()
resolve(false)
}, true)
})
}
// we may be re-using an existing script
// sync the status, need to register before useScript
// @ts-expect-error untyped
head._scriptStatusWatcher = head._scriptStatusWatcher || head.hooks.hook('script:updated', ({ script: s }) => {
// @ts-expect-error untyped
s._statusRef.value = s.status
})
// @ts-expect-error untyped
const script = _useScript(input as BaseUseScriptInput, options)
// @ts-expect-error untyped
script._statusRef = script._statusRef || ref<UseScriptStatus>(script.status)
Expand Down
47 changes: 47 additions & 0 deletions test/vue/e2e/scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,51 @@ describe('unhead vue e2e scripts', () => {
expect(status.value).toEqual('loading')
expect(status2.value).toEqual('loading')
})

it('ref trigger', async () => {
const dom = useDom()
const head = createHead({
document: dom.window.document,
})

const isTrigger1Active = ref(false)
const isTrigger2Active = ref(false)

const { status } = useScript({
src: '//duplicate.script',
}, {
// leaving the page will stop the trigger from activating
trigger: isTrigger1Active,
head,
})

const { status: status2, _triggerPromises } = useScript({
src: '//duplicate.script',
}, {
// leaving the page will stop the trigger from activating
trigger: isTrigger2Active,
head,
})

// two promises pending
expect(_triggerPromises).toMatchInlineSnapshot(`
[
Promise {},
Promise {},
]
`)

// trigger using the first promise
isTrigger1Active.value = true
// wait next tick
await new Promise<void>((resolve) => {
setTimeout(() => {
resolve()
}, 25)
})

// both should be loaded
expect(status.value).toEqual('loading')
expect(status2.value).toEqual('loading')
})
})

0 comments on commit 487fe5e

Please sign in to comment.