Skip to content

Commit f7de016

Browse files
committed
feat: adds handling of null container value
1 parent 4be0781 commit f7de016

File tree

3 files changed

+63
-54
lines changed

3 files changed

+63
-54
lines changed

README.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,17 @@ await loadScript("https://example.com/library.js", {
2828

2929
## 🧠 API
3030

31-
### `loadScript(src, options?, target?)`
31+
### `loadScript(src, options?, container?)`
3232

3333
Loads an external script dynamically and returns a `Promise<HTMLScriptElement>`.
3434

3535
#### Parameters:
3636

37-
| Name | Type | Description |
38-
| --------- | ------------------- | --------------------------------------------- |
39-
| `src` | `string` | Script URL (required) |
40-
| `options` | `LoadScriptOptions` | `loadScript` options (e.g. `async`, `type`) |
41-
| `target` | `HTMLElement` | Target element to append to (default: `head`) |
42-
43-
---
37+
| Name | Type | Description |
38+
| ----------- | ------------------- | ----------------------------------------------------------------- |
39+
| `src` | `string` | Script URL (required) |
40+
| `options` | `LoadScriptOptions` | `loadScript` options (e.g. `async`, `type`) |
41+
| `container` | `HTMLElement` | HTML element to append `<script />` to (default: `document.head`) |
4442

4543
## ✅ Features
4644

@@ -72,11 +70,10 @@ const WidgetLoader = () => {
7270
};
7371
```
7472

75-
---
76-
7773
## 📝 Notes
7874

7975
- Scripts are cached by `src` unless `innerHTML` or `textContent` is used
76+
- A nil (`undefined`/`null`) container value will append the script to `document.head`.
8077
- Cleanup is not automatic — script elements remain in the DOM
8178

8279
---

src/__tests__/loadScript.test.tsx

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,54 @@ describe("loadScript", () => {
1414
await expect(loadScript("")).rejects.toThrow('No "src" provided to loadScript');
1515
});
1616

17+
test("assigns all direct script properties correctly", async () => {
18+
const props = {
19+
async: false,
20+
defer: true,
21+
fetchPriority: "low",
22+
noModule: true,
23+
id: "test-script",
24+
type: "text/javascript",
25+
crossOrigin: "anonymous",
26+
referrerPolicy: "origin",
27+
integrity: "sha384-abc123",
28+
nonce: "xyz123",
29+
textContent: "Hello, world!",
30+
} as LoadScriptOptions;
31+
32+
const promise = loadScript(SCRIPT_SRC, props);
33+
34+
const [script] = document.querySelectorAll(`script[src="${SCRIPT_SRC}"]`);
35+
expect(script).toBeDefined();
36+
37+
script?.dispatchEvent(new Event("load"));
38+
const resolved = await promise;
39+
40+
expect(resolved.async).toBe(false);
41+
expect(resolved.id).toBe("test-script");
42+
expect(resolved.defer).toBe(true);
43+
expect(resolved.fetchPriority).toBe("low");
44+
expect(resolved.noModule).toBe(true);
45+
expect(resolved.type).toBe("text/javascript");
46+
expect(resolved.crossOrigin).toBe("anonymous");
47+
expect(resolved.referrerPolicy).toBe("origin");
48+
expect(resolved.integrity).toBe("sha384-abc123");
49+
expect(resolved.nonce).toBe("xyz123");
50+
expect(resolved.textContent).toBe("Hello, world!");
51+
});
52+
53+
test("applies additional script attributes via setAttribute", async () => {
54+
const promise = loadScript(SCRIPT_SRC, { "data-id": "custom-script-id" });
55+
56+
const [script] = document.querySelectorAll(`script[src="${SCRIPT_SRC}"]`);
57+
expect(script).toBeDefined();
58+
59+
script?.dispatchEvent(new Event("load"));
60+
const resolved = await promise;
61+
62+
expect(resolved.getAttribute("data-id")).toBe("custom-script-id");
63+
});
64+
1765
test("resolves immediately if script already exists in DOM", async () => {
1866
const script = document.createElement("script");
1967
script.src = SCRIPT_SRC;
@@ -80,52 +128,16 @@ describe("loadScript", () => {
80128
expect(resolved2).not.toBe(resolved1);
81129
});
82130

83-
test("assigns all direct script properties correctly", async () => {
84-
const props = {
85-
async: false,
86-
defer: true,
87-
fetchPriority: "low",
88-
noModule: true,
89-
id: "test-script",
90-
type: "text/javascript",
91-
crossOrigin: "anonymous",
92-
referrerPolicy: "origin",
93-
integrity: "sha384-abc123",
94-
nonce: "xyz123",
95-
textContent: "Hello, world!",
96-
} as LoadScriptOptions;
97-
98-
const promise = loadScript(SCRIPT_SRC, props);
99-
100-
const [script] = document.querySelectorAll(`script[src="${SCRIPT_SRC}"]`);
101-
expect(script).toBeDefined();
102-
103-
script?.dispatchEvent(new Event("load"));
104-
const resolved = await promise;
105-
106-
expect(resolved.async).toBe(false);
107-
expect(resolved.id).toBe("test-script");
108-
expect(resolved.defer).toBe(true);
109-
expect(resolved.fetchPriority).toBe("low");
110-
expect(resolved.noModule).toBe(true);
111-
expect(resolved.type).toBe("text/javascript");
112-
expect(resolved.crossOrigin).toBe("anonymous");
113-
expect(resolved.referrerPolicy).toBe("origin");
114-
expect(resolved.integrity).toBe("sha384-abc123");
115-
expect(resolved.nonce).toBe("xyz123");
116-
expect(resolved.textContent).toBe("Hello, world!");
117-
});
118-
119-
test("applies additional script attributes via setAttribute", async () => {
120-
const promise = loadScript(SCRIPT_SRC, { "data-id": "custom-script-id" });
131+
test("falls back to document.head when container is null", async () => {
132+
const promise = loadScript(SCRIPT_SRC, { async: true }, null);
121133

122-
const [script] = document.querySelectorAll(`script[src="${SCRIPT_SRC}"]`);
134+
const script = document.querySelector(`head script[src="${SCRIPT_SRC}"]`);
123135
expect(script).toBeDefined();
124136

125137
script?.dispatchEvent(new Event("load"));
126138
const resolved = await promise;
127139

128-
expect(resolved.getAttribute("data-id")).toBe("custom-script-id");
140+
expect(document.head.contains(resolved)).toBe(true);
129141
});
130142

131143
test("resolves when script loads", async () => {

src/loadScript.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,18 @@ export const __resetScriptCache = () => {
7676
* @example
7777
* ```ts
7878
* const script = await loadScript("https://example.com/library.js", { type: "module" }, document.head);
79-
* console.log("Script loaded:", script.src);
8079
* ```
8180
*
8281
* @param src - The script URL to load.
8382
* @param options - additional options for the script (e.g. async, type, textContent).
84-
* @param target - Optional DOM element to append the script to (defaults to `document.head`).
83+
* @param container - Optional DOM element to append the script to (defaults to `document.head`).
8584
* @returns A Promise that resolves to the script element.
8685
*/
8786

8887
export const loadScript = async (
8988
src: string,
9089
options: LoadScriptOptions = {},
91-
target: HTMLElement = document.head,
90+
container: HTMLElement | null = document.head,
9291
): Promise<HTMLScriptElement> => {
9392
const {
9493
id,
@@ -139,7 +138,8 @@ export const loadScript = async (
139138
const promise = new Promise<HTMLScriptElement>((resolve, reject) => {
140139
const script = document.createElement("script");
141140

142-
target.appendChild(script);
141+
const _container = container ?? document.head;
142+
_container.appendChild(script);
143143

144144
script.src = src;
145145
script.type = type;

0 commit comments

Comments
 (0)