Skip to content

Commit c12cbc9

Browse files
committed
feat: exports loadScript for imperative style loading
1 parent 6ea87e2 commit c12cbc9

File tree

9 files changed

+541
-76
lines changed

9 files changed

+541
-76
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Daniel Sanchez
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
# solid-create-script
88

9-
Solid utility hook to dynamically load an external script.
9+
Utility function to dynamically load external scripts in both declarative and imperative styles within SolidJS.
1010

11-
### Installation
11+
## Installation
1212

1313
```bash
1414
npm install solid-js solid-create-script
@@ -17,40 +17,108 @@ yarn add solid-js solid-create-script
1717
bun add solid-js solid-create-script
1818
```
1919

20-
### Usage
20+
## Summary
21+
22+
This library exports two APIs: `createScript` and `loadScript`.
23+
24+
```ts
25+
import { createScript, loadScript } from "solid-create-script";
26+
```
27+
28+
## API Breakdown
29+
30+
### `createScript` (Reactive API)
31+
32+
Utility hook built on top of `createResource` that loads a script and returns a `Resource<HTMLScriptElement>`.
33+
34+
The interface signature is as follows:
2135

22-
```tsx
23-
const script = createScript("https://some-library.js");
24-
const script = createScript("https://some-library.js", { async: true, ...scriptAttributes });
25-
const script = createScript("https://some-library.js", { defer: true, ...scriptAttributes });
36+
```ts
37+
export const createScript = (src: string, attributes?: Readonly<ScriptAttributes>)
2638
```
2739

28-
Under the hood `createScript` makes use of the `createResource` Solid API when loading the desired script at the specified `src`. As such, the result of `createScript` is a `Resource<Event>` object that allows you to inspect when the script has finished loading or returned an error via `script.loading` and `script.error` respectively.
40+
The three arguments:
2941

30-
When using `createScript`, here are some points to be aware of:
42+
- `src`: the script source to download
43+
- `attributes`: any `<script>` specific attributes to apply
3144

32-
- The created `<script>` tag will be appeneded to `<head>`.
33-
- The created `<script>` tag will not be removed from the DOM when a component that uses this hook unmounts. (i.e. we do not make use of `onCleanup` to remove the `<script>` from `<head>`).
34-
- The hook will ensure no duplicate `<script>` tags referencing the same `src` will be created. Moreover, when multiple components reference the same `src`, they will all point to the same shared resource ensuring consistency within the reactive graph.
45+
It is useful for:
3546

36-
### Full Example
47+
- Conditionally rendering based on script load status
48+
- Tracking `loading`, `error`, and ready states
3749

38-
```tsx
39-
import { Switch, Match } from "solid-js";
40-
import { createScript } from "solid-create-script";
50+
### Usage
51+
52+
```ts
53+
import { Switch, Match } from "solid-js"
54+
import { createScript } from "solid-create-script"
4155

42-
function App() {
43-
const script = createScript("https://some-library.js");
56+
const CustomComponent = () => {
57+
const script = createScript("https://example.com/library.js", { async: true });
4458

4559
return (
46-
<Switch fallback={<ScriptProvider>...</ScriptProvider>}>
60+
<Switch>
4761
<Match when={script.loading}>Loading Script...</Match>
48-
<Match when={script.error}>Failed to load script: {script.error.message}</Match>
62+
<Match when={script.error}>Failed to load</Match>
63+
<Match when={script()}>Script is ready!</Match>
4964
</Switch>
50-
);
65+
)
5166
}
67+
5268
```
5369

54-
### Feedback
70+
> ⚠️ The `<script>` that is downloaded with `createScript` will be appended to `<head>`. This API currently does not support specifying a mount target for the `<script>` tag.
71+
72+
---
73+
74+
### `loadScript` (Imperative API)
75+
76+
Utility function that returns a `Promise<HTMLScriptElement>`.
77+
78+
The interface signature is as follows:
79+
80+
```ts
81+
export const loadScript = (src: string, attributes?: Readonly<ScriptAttributes>, target?: HTMLElement)
82+
```
83+
84+
The three arguments:
85+
86+
- `src`: the script source to download
87+
- `attributes`: any `<script>` specific attributes to apply
88+
- `target`: the DOM target to append the `<script>` tag to.
89+
90+
Useful for:
91+
92+
- Full control over script placement
93+
- Timing-sensitive insertions
94+
95+
```ts
96+
import { Switch, Match, onMount } from "solid-js"
97+
import { loadScript } from "solid-create-script"
98+
99+
const CustomComponent = () => {
100+
let containerRef!: HTMLElement;
101+
102+
onMount(async () => {
103+
const script = await loadScript(
104+
"https://example.com/widget.js",
105+
{ type: "text/javascript" },
106+
containerRef,
107+
);
108+
});
109+
110+
return (
111+
<div ref={containerRef} />
112+
)
113+
}
114+
```
115+
116+
## Notes
117+
118+
- Scripts are automatically cached to prevent duplication.
119+
- The script is not removed on cleanup/unmount.
120+
- `createScript` uses `createResource` internally to manage async state.
121+
122+
## Feedback
55123

56-
Feel free to post any issues or suggestions to help improve this utility hook.
124+
Feel free to post issues or suggestions to help improve this library.

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"name": "solid-create-script",
66
"devDependencies": {
77
"@changesets/cli": "^2.29.3",
8+
"@dschz/try-catch": "^1.0.2",
89
"@solidjs/testing-library": "^0.8.10",
910
"@testing-library/jest-dom": "^6.5.0",
1011
"@types/bun": "^1.1.10",
@@ -135,6 +136,8 @@
135136

136137
"@csstools/css-tokenizer": ["@csstools/[email protected]", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="],
137138

139+
"@dschz/try-catch": ["@dschz/[email protected]", "", {}, "sha512-4w5kW3PmV6ig37VqOAxwTIDotjuXzhb5Zlpt6Hb/xNVWHtBcYHQ1qvJD5EERupGIGqhXJtM5kY7zfnIbsMbRaQ=="],
140+
138141
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
139142

140143
"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@
3939
"build": "tsup",
4040
"build:watch": "tsup --watch",
4141
"dev": "vite",
42-
"changeset": "changeset",
43-
"update-pkg-version": "changeset version",
44-
"release": "changeset publish",
4542
"format": "prettier . --check",
4643
"format:fix": "prettier . --write",
4744
"lint": "eslint . --ext .ts,.tsx",
4845
"lint:fix": "eslint . --ext .ts,.tsx --fix",
46+
"pkg:changeset": "changeset",
47+
"pkg:version": "changeset version",
48+
"pkg:publish": "changeset publish",
4949
"serve": "vite preview",
5050
"start": "vite",
5151
"test": "vitest run",
@@ -54,6 +54,7 @@
5454
},
5555
"devDependencies": {
5656
"@changesets/cli": "^2.29.3",
57+
"@dschz/try-catch": "^1.0.2",
5758
"@solidjs/testing-library": "^0.8.10",
5859
"@testing-library/jest-dom": "^6.5.0",
5960
"@types/bun": "^1.1.10",

playground/App.tsx

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,79 @@
1-
import { createScript } from "../src";
1+
import { tryCatch } from "@dschz/try-catch";
2+
import { createSignal, Match, onMount, Switch } from "solid-js";
3+
4+
import { createScript, loadScript } from "../src";
5+
6+
declare global {
7+
interface Window {
8+
google: unknown;
9+
}
10+
}
211

312
export const App = () => {
4-
const script = createScript("https://cdn.plaid.com/link/v2/stable/link-initialize.js", {
5-
defer: true,
13+
const googleScript = createScript("https://www.gstatic.com/charts/loader.js", {
14+
async: true,
15+
type: "text/javascript",
16+
onLoad: () => console.log("Google Charts loaded"),
17+
onError: (e) => console.error("Google Charts failed", e),
18+
});
19+
20+
const [plausibleStatus, setPlausibleStatus] = createSignal<"idle" | "loaded" | "error">("idle");
21+
22+
onMount(async () => {
23+
const [e] = await tryCatch(
24+
loadScript("https://plausible.io/js/script.js", {
25+
async: true,
26+
defer: true,
27+
"data-domain": "example.com",
28+
onLoad: () => {
29+
console.log("Plausible loaded");
30+
setPlausibleStatus("loaded");
31+
},
32+
onError: (e) => {
33+
console.error("Plausible failed", e);
34+
setPlausibleStatus("error");
35+
},
36+
}),
37+
);
38+
39+
if (e) {
40+
setPlausibleStatus("error");
41+
return;
42+
}
43+
44+
console.log("Plausible loaded");
45+
setPlausibleStatus("loaded");
646
});
747

848
return (
9-
<div>
10-
<div>Playground: solid-create-script</div>
11-
<div>Script Loading: {script.loading.toString()}</div>
49+
<div style={{ padding: "2rem", "font-family": "sans-serif" }}>
50+
<h1>solid-create-script Demo</h1>
51+
52+
<section style={{ "margin-top": "1.5rem" }}>
53+
<h2>createScript (Google Charts)</h2>
54+
<Switch>
55+
<Match when={googleScript.loading}>Loading Google Charts…</Match>
56+
<Match when={googleScript.error}>❌ Failed to load Google Charts</Match>
57+
<Match when={googleScript()}>
58+
✅ Script loaded. `google` is{" "}
59+
{typeof window.google !== "undefined" ? "defined" : "undefined"}.
60+
</Match>
61+
</Switch>
62+
</section>
63+
64+
<section style={{ "margin-top": "1.5rem" }}>
65+
<h2>loadScript (Plausible)</h2>
66+
<p>
67+
Status:{" "}
68+
{
69+
{
70+
idle: "⏳ loading...",
71+
loaded: "✅ Plausible loaded",
72+
error: "❌ Failed to load Plausible",
73+
}[plausibleStatus()]
74+
}
75+
</p>
76+
</section>
1277
</div>
1378
);
1479
};

0 commit comments

Comments
 (0)