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 d1a008dc..b1cc26e9 100644 --- a/docs/content/1.usage/2.composables/4.use-script.md +++ b/docs/content/1.usage/2.composables/4.use-script.md @@ -50,7 +50,7 @@ use the API effectively. The `useScript` composable aims to solve these issues and more with the goal of making third-party scripts a breeze to use. ```ts -const { gtag } = useScript('https://www.google-analytics.com/analytics.js', { +const googleAnalytics = useScript('https://www.google-analytics.com/analytics.js', { beforeInit() { // Google Analytics Setup window.dataLayer = window.dataLayer || [] @@ -62,6 +62,7 @@ const { gtag } = useScript('https://www.google-analytics.com/analytic return { gtag: window.gtag } } }) +const { gtag } = googleAnalytics.proxy // fully typed, usable in SSR and when lazy loaded gtag('event', 'page_view', { page_title: 'Home', @@ -76,52 +77,6 @@ interface GoogleTag { ## Usage -### Your First Script - -The simplest usage of the `useScript` composable is to load a script and use the API it provides. To do -so you need a URL to the script and a function that resolves the API `use()`. - -```ts -const instance = useScript('https://example.com/my-awesome-script.js', { - // The `use` function will only called client-side, it's used to resolve the API - use() { - return window.myAwesomeScript - } -}) -``` - -Done, but accessing the API should provide type safety. To do so, you can use the `useScript` composable with a generic type. - -```ts -interface MyAwesomeScript { - myFunction: ((arg: string) => void) -} -const { myFunction } = useScript('https://example.com/my-awesome-script.js', { - use() { - return window.myAwesomeScript - } -}) - -// fully typed, usable in SSR and when lazy loaded -myFunction('hello') -``` - -Because `useScript` returns a Proxy API, you can call the script functions before it's loaded. This will noop for SSR and be stubbable. - -This also lets you load in the script lazily while still being able to use the API immediately. - -```ts -const { myFunction } = useScript('https://example.com/my-awesome-script.js', { - trigger: 'manual', - use() { - return window.myAwesomeScript - } -}) - -// only client-side it will be called when the script is finished loaded -myFunction('hello') -``` - ### `referrerpolicy` and `crossorigin` The `useScript` composable is optimized for end user privacy and security. @@ -169,7 +124,7 @@ The `trigger` option is used to control when the script is loaded by the browser It can be one of the following: - `undefined` | `client`: Script tag will be inserted as the `useScript` is hydrated on the client side. The script will be usable once the network request is complete. -- `manual`: Load the script manually using the `$script.load` method. Only runs on the client. +- `manual`: Load the script manually using the `load()` function. Only runs on the client. - `Promise`: Load the script when the promise resolves. This allows you to load the script after a certain time or event, for example on the `requestIdleCallback` hook. Only runs on the client. - `Function`: Load the script when the function is called. Only runs on the client. @@ -180,15 +135,15 @@ When you're using a `trigger` that isn't `server`, the script will not exist wit ::code-group ```ts [Manual] -const { $script } = useScript('https://example.com/script.js', { +const { load } = useScript('https://example.com/script.js', { trigger: 'manual' }) // ... -$script.load() +load() ``` ```ts [Promise] -const { $script } = useScript('https://example.com/script.js', { +useScript('https://example.com/script.js', { trigger: new Promise((resolve) => { setTimeout(resolve, 10000) // load after 10 seconds }) @@ -196,7 +151,7 @@ const { $script } = useScript('https://example.com/script.js', { ``` ```ts [Idle] -const { $script } = useScript('https://example.com/script.js', { +useScript('https://example.com/script.js', { trigger: typeof window !== 'undefined' ? window.requestIdleCallback : 'manual' }) ``` @@ -205,106 +160,123 @@ 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. +Sometimes you'll want to directly use the script instead of relying on the proxy. For this you can use the script instance as a Promise. + +Using the script instance as a promise is preferable over calling `.load()` when you don't want to accidentally bypass +the configuration trigger. ```ts -const { $script } = useScript('https://example.com/my-awesome-script.js', { +const myScript = useScript('https://example.com/my-awesome-script.js', { use() { return window.myAwesomeScript }, }) // Note: Do not do this if you have a `manual` trigger -$script.then((myAwesomeScript) => { +myScript.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. +When you have a manual trigger awaiting the promise will never resolve unless you `load()` the script. ```ts -const { $script } = useScript('https://example.com/my-awesome-script.js', { - use() { - return window.myAwesomeScript - }, +const myScript = useScript('/script.js', { trigger: 'manual' }) +myScript.then(() => { + // will never fire unless you call myScript.load() +}) +``` + +### Removing a Script -// Warning: Will never resolve! -await $script +When you're done with a script, you can remove it from the document using the `remove()` function. -// Make sure you call load if you're going to await with a manual trigger -$script.load() +```ts +const myScript = useScript('/script.js') + +myScript.remove() ``` -### Removing a Script +The `remove()` function will return a boolean indicating if the script was removed in the case where the +script has already been removed. -When you're done with a script, you can remove it from the document using the `$script.remove()` method. +### Script Loading Errors + +Sometimes scripts just won't load, this can be due to network issues, browser extensions blocking the script +or many other reasons. + +As the script instance is a native promise, you can use the `.catch()` function. ```ts -const { $script } = useScript('https://example.com/my-awesome-script.js') +const myScript = useScript('/script.js') + .catch((err) => { + console.error('Failed to load script', err) + }) +``` + +Otherwise, you always check the status of the script using `status`. -$script.remove() +::code-block + +```ts [Vanilla] +const myScript = useScript('/script.js') +myScript.status // 'awaitingLoad' | 'loading' | 'loaded' | 'error' +``` + +```ts [Vue] +const myScript = useScript('/script.js') +myScript.status // Ref<'awaitingLoad' | 'loading' | 'loaded' | 'error'> ``` -The `remove()` function will return a boolean indicating if the script was removed. +:: -### Handling Script Loading Failure +### Proxy API -Sometimes scripts just won't load, this can be due to network issues, the script being blocked, etc. +A `proxy` object is accessible on the script instance which provides a consistent interface for calling script functions +regardless of the script being loaded. -To handle this, you can catch exceptions thrown from `$script`. +This can be useful in instances where you don't care when the function is called, just that it is when the script is loaded. ```ts -const { $script } = useScript('https://example.com/my-awesome-script.js', { - use() { - return window.myAwesomeScript - }, +const myScript = useScript<{ event: ((arg: string) => boolean) }>('/analytics.js', { + use() { return window.myAwesomeScript } }) +// send an event if or when the script is loaded +myScript.proxy.event('foo') // Promise +```` -$script.catch((err) => { - console.error('Failed to load script', err) -}) -``` +Using the proxy API will noop in SSR, is stubbable and is future-proofed to support loading scripts through web workers. -Otherwise, you always check the status of the script using `$script.status`. +The proxy accessors are converted to async functions, meaning if you need the return of the data you can await the function. ```ts -const { $script } = useScript('https://example.com/my-awesome-script.js', { - use() { - return window.myAwesomeScript - }, +const myScript = useScript< { myFunction: ((arg: string) => boolean) }>('/script.js', { + use() { return window.myAwesomeScript } }) -$script.status // 'awaitingLoad' | 'loading' | 'loaded' | 'error' -``` +// myFunction becomes (arg: string) => Promise +myScript.proxy.myFunction('hello').then((val) => { + // val is boolean +}) +```` + +### Stubbing -### SSR Stubbing +In cases where you're using the Proxy API, you may want to replace some of the functionality. -In cases where you want to use the script API on the server, you can use the `stub` option. This lets -you call your script functions and handle them in a way that makes sense for your server. +For example, in a server context, we probably want to polyfill some returns so our scrits remains functional. -For example, we can stub the `gtag` function to send events on the server to Google Analytics. Meaning -you have a single API to use for both server and client to achieve the same result. +In the case of Google Analytics, we may stub the `dataLayer` so that it can be used as an array on the server. ```ts -const { gtag } = useScript('https://www.google-analytics.com/analytics.js', { +const googleAnalytics = useScript('https://www.google-analytics.com/analytics.js', { use() { - return { gtag: window.gtag } + return { dataLayer: window.dataLayer } + }, + stub({ fn }) { + return fn === 'dataLayer' && typeof window === 'undefined' ? [] : undefined }, - stub() { - if (process.server) { - return (fn: 'event', opt: string, opt2: { [key: string]: string }) => { - // send fetch to ga - return fetch('https://www.google-analytics.com/analytics.js', { - method: 'POST', - body: JSON.stringify({ event: opt, ...op2 }) - }) - } - } - } }) ``` @@ -346,14 +318,15 @@ useScript({ A function that resolves the scripts API. This is only called client-side. ```ts -const { trackPageview } = useScript({ +const fathom = useScript({ // fathom analytics src: 'https://cdn.usefathom.com/script.js', }, { use: () => window.fathom }) -// just works -trackPageview({ url: 'https://example.com' }) +fathom.then((api) => { + // api is equal to window.fathom +}) ``` #### `trigger` @@ -380,7 +353,7 @@ This is particularly useful when the API you want to use is a primitive and you pushing to `dataLayer` when using Google Tag Manager. ```ts -const { sendEvent, doSomething } = useScript({ +const myScript = useScript({ src: 'https://example.com/script.js', }, { use: () => window.myScript, @@ -390,6 +363,7 @@ const { sendEvent, doSomething } = useScript({ return (opt: string) => fetch('https://api.example.com/event', { method: 'POST', body: opt }) } }) +const { sendEvent, doSomething } = myScript.proxy // on server, will send a fetch to https://api.example.com/event // on client it falls back to the real API sendEvent('event') @@ -398,44 +372,65 @@ sendEvent('event') doSomething() ``` -### Return Value +## Script Instance API -The `useScript` composable returns a Proxy API that you can use to interact with the script. +The `useScript` composable returns the script instance that you can use to interact with the script. -Any requests to the API will be proxied to the real script when it's loaded. +### id -#### $script +The unique ID of the script instance. -The `$script` property is a special property that gives you access to the underlying script instance. +### status + +The status of the script. Can be one of the following: `'awaitingLoad' | 'loading' | 'loaded' | 'error'` -It is a Promise and the script API in one. This means you can await the script to load and use the API directly. +In Vue, this is a `Ref`. + +### load() + +Trigger the script to load. This is useful when using the `manual` loading strategy. ```ts -const { $script } = useScript({ - // ... +const { load } = useScript('/script.js', { + trigger: 'manual' }) - -$script - .then() // script is loaded - .catch() // script failed to load +// ... +load() ``` -- `status` +### remove() -The status of the script. Can be one of the following: `'awaitingLoad' | 'loading' | 'loaded' | 'error'` +Remove the script from the document. -- `load` +#### proxy -Trigger the script to load. This is useful when using the `manual` loading strategy. +The proxy API for calling the script functions. ```ts -const { $script } = useScript({ - // ... -}, { - trigger: 'manual' +const myScript = useScript('/script.js', { + use() { return window.myScript } }) -// ... -$script.load() +myScript.proxy.myFunction('hello') +``` + +#### instance + +Internal value providing the `use()` function, this will be the result. This is passed when resolving the script using `then()` or `load()`. + +```ts +const myScript = useScript('/script.js', { + use() { return window.myScript } +}) +myScript.instance // window.myScript +``` + +### entry + +The internal head entry for the script. This is useful for debugging and tracking the script. + +```ts +const myScript = useScript('/script.js') +myScript.entry // ReturnType ``` ## Examples diff --git a/examples/vite-ssr-vue/src/pages/fathom.vue b/examples/vite-ssr-vue/src/pages/fathom.vue index be9cce45..6d7fa799 100644 --- a/examples/vite-ssr-vue/src/pages/fathom.vue +++ b/examples/vite-ssr-vue/src/pages/fathom.vue @@ -1,4 +1,5 @@