From 487fe5e33a87173a539f9ca8dba7a2bab3c1e64c Mon Sep 17 00:00:00 2001 From: harlan Date: Mon, 16 Sep 2024 04:00:54 +1000 Subject: [PATCH] fix(scripts,vue): prefer ref promises --- .../1.usage/2.composables/4.use-script.md | 25 ++++++++- .../vite-ssr-vue/src/pages/ref-trigger.vue | 55 +++++++++++++++++++ packages/vue/src/composables/useScript.ts | 38 ++++++++++++- test/vue/e2e/scripts.test.ts | 47 ++++++++++++++++ 4 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 examples/vite-ssr-vue/src/pages/ref-trigger.vue diff --git a/docs/content/1.usage/2.composables/4.use-script.md b/docs/content/1.usage/2.composables/4.use-script.md index a26b60ec..8c01b420 100644 --- a/docs/content/1.usage/2.composables/4.use-script.md +++ b/docs/content/1.usage/2.composables/4.use-script.md @@ -155,6 +155,13 @@ useScript('/script.js', { }) ``` +```ts [Ref (Vue)] +const shouldLoad = ref(false) +useScript('/script.js', { + trigger: shouldLoad +}) +``` + :: ### Waiting for Script Load @@ -362,11 +369,14 @@ useScript({ #### `trigger` -- Type: `'undefined' | 'manual' | 'server' | 'client' | Promise` +- Type: `'undefined' | 'manual' | 'server' | 'client' | Promise` +- Additional Vue Types: `Ref` 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', }, { @@ -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. diff --git a/examples/vite-ssr-vue/src/pages/ref-trigger.vue b/examples/vite-ssr-vue/src/pages/ref-trigger.vue new file mode 100644 index 00000000..8dea8ceb --- /dev/null +++ b/examples/vite-ssr-vue/src/pages/ref-trigger.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/vue/src/composables/useScript.ts b/packages/vue/src/composables/useScript.ts index b9e179eb..e7d440ed 100644 --- a/packages/vue/src/composables/useScript.ts +++ b/packages/vue/src/composables/useScript.ts @@ -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' @@ -21,6 +22,18 @@ export interface VueScriptInstance> exten } export type UseScriptInput = string | (MaybeComputedRefEntriesOnly> & { src: string }) +export interface UseScriptOptions = {}, U = {}> extends HeadEntryOptions, Pick, '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 +} export type UseScriptContext> = (Promise & VueScriptInstance) @@ -70,8 +83,26 @@ export function useScript = Record + options.trigger = new Promise((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 @@ -79,6 +110,7 @@ export function useScript = Record(script.status) diff --git a/test/vue/e2e/scripts.test.ts b/test/vue/e2e/scripts.test.ts index 48e9fc41..3a4e49f3 100644 --- a/test/vue/e2e/scripts.test.ts +++ b/test/vue/e2e/scripts.test.ts @@ -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((resolve) => { + setTimeout(() => { + resolve() + }, 25) + }) + + // both should be loaded + expect(status.value).toEqual('loading') + expect(status2.value).toEqual('loading') + }) })