Skip to content

Commit bcf859e

Browse files
committed
make scripts behave more like normal elements
1 parent 0c1edb5 commit bcf859e

File tree

3 files changed

+122
-19
lines changed

3 files changed

+122
-19
lines changed

src/client/packages/idom-client-react/src/components.js

+26-5
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function Element({ model }) {
3535
return null;
3636
}
3737
} else if (model.tagName == "script") {
38-
return html`<${ScriptElement} script=${model.children[0]} />`;
38+
return html`<${ScriptElement} model=${model} />`;
3939
} else if (model.importSource) {
4040
return html`<${ImportedElement} model=${model} />`;
4141
} else {
@@ -58,10 +58,31 @@ function StandardElement({ model }) {
5858
);
5959
}
6060

61-
function ScriptElement({ script }) {
62-
const el = React.useRef();
63-
React.useEffect(eval(script), [script]);
64-
return null;
61+
function ScriptElement({ model }) {
62+
const ref = React.useRef();
63+
React.useEffect(() => {
64+
if (model?.children?.length > 1) {
65+
console.error("Too many children for 'script' element.");
66+
}
67+
68+
let scriptContent = model?.children?.[0];
69+
70+
let scriptElement;
71+
if (model.attributes) {
72+
scriptElement = document.createElement("script");
73+
for (const [k, v] of Object.entries(model.attributes)) {
74+
scriptElement.setAttribute(k, v);
75+
}
76+
scriptElement.appendChild(document.createTextNode(scriptContent));
77+
ref.current.appendChild(scriptElement);
78+
} else {
79+
let scriptResult = eval(scriptContent);
80+
if (typeof scriptResult == "function") {
81+
return scriptResult();
82+
}
83+
}
84+
}, [model.key]);
85+
return html`<div ref=${ref} />`;
6586
}
6687

6788
function ImportedElement({ model }) {

src/idom/html.py

+42-10
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,12 @@
150150
- :func:`template`
151151
"""
152152

153+
from __future__ import annotations
154+
155+
from typing import Any, Mapping
156+
153157
from .core.proto import VdomDict
154-
from .core.vdom import make_vdom_constructor
158+
from .core.vdom import coalesce_attributes_and_children, make_vdom_constructor
155159

156160

157161
# Dcument metadata
@@ -250,18 +254,46 @@
250254
noscript = make_vdom_constructor("noscript")
251255

252256

253-
def script(content: str) -> VdomDict:
257+
def script(
258+
*attributes_and_children: Mapping[str, Any] | str,
259+
key: str | int | None = None,
260+
) -> VdomDict:
254261
"""Create a new `<{script}> <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script>`__ element.
255262
256-
Parameters:
257-
content:
258-
The text of the script should evaluate to a function. This function will be
259-
called when the script is initially created or when the content of the
260-
script changes. The function may optionally return a teardown function that
261-
is called when the script element is removed from the tree, or when the
262-
script content changes.
263+
This behaves slightly differently than a normal script element in that it may be run
264+
multiple times if its key changes (depending on specific browser behaviors). If no
265+
key is given, the key is inferred to be the content of the script or, lastly its
266+
'src' attribute if that is given.
267+
268+
If no attributes are given, the content of the script may evaluate to a function.
269+
This function will be called when the script is initially created or when the
270+
content of the script changes. The function may itself optionally return a teardown
271+
function that is called when the script element is removed from the tree, or when
272+
the script content changes.
263273
"""
264-
return {"tagName": "script", "children": [content]}
274+
model: VdomDict = {"tagName": "script"}
275+
276+
attributes, children = coalesce_attributes_and_children(attributes_and_children)
277+
278+
if children:
279+
if len(children) > 1:
280+
raise ValueError("'script' nodes may have, at most, one child.")
281+
elif not isinstance(children[0], str):
282+
raise ValueError("The child of a 'script' must be a string.")
283+
else:
284+
model["children"] = children
285+
if key is None:
286+
key = children[0]
287+
288+
if attributes:
289+
model["attributes"] = attributes
290+
if key is None and not children and "src" in attributes:
291+
key = attributes["src"]
292+
293+
if key is not None:
294+
model["key"] = key
295+
296+
return model
265297

266298

267299
# Demarcating edits

tests/test_html.py

+54-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from idom import component, html, use_state
1+
import pytest
2+
3+
from idom import component, config, html, use_state
24
from idom.utils import Ref
35

46

@@ -7,8 +9,8 @@ def use_toggle():
79
return state, lambda: set_state(not state)
810

911

10-
def use_counter():
11-
state, set_state = use_state(1)
12+
def use_counter(initial_value):
13+
state, set_state = use_state(initial_value)
1214
return state, lambda: set_state(state + 1)
1315

1416

@@ -61,7 +63,7 @@ def test_script_re_run_on_content_change(driver, driver_wait, display):
6163

6264
@component
6365
def HasScript():
64-
count, incr_count.current = use_counter()
66+
count, incr_count.current = use_counter(1)
6567
return html.div(
6668
html.div({"id": "mount-count", "data-value": 0}),
6769
html.div({"id": "unmount-count", "data-value": 0}),
@@ -92,3 +94,51 @@ def HasScript():
9294

9395
driver_wait.until(lambda d: mount_count.get_attribute("data-value") == "3")
9496
driver_wait.until(lambda d: unmount_count.get_attribute("data-value") == "2")
97+
98+
99+
def test_script_from_src(driver, driver_wait, display):
100+
incr_src_id = Ref()
101+
file_name_template = "__some_js_script_{src_id}__.js"
102+
103+
@component
104+
def HasScript():
105+
src_id, incr_src_id.current = use_counter(0)
106+
if src_id == 0:
107+
# on initial display we haven't added the file yet.
108+
return html.div()
109+
else:
110+
return html.div(
111+
html.div({"id": "run-count", "data-value": 0}),
112+
html.script(
113+
{"src": f"/modules/{file_name_template.format(src_id=src_id)}"}
114+
),
115+
)
116+
117+
display(HasScript)
118+
119+
for i in range(1, 4):
120+
script_file = config.IDOM_WED_MODULES_DIR.current / file_name_template.format(
121+
src_id=i
122+
)
123+
script_file.write_text(
124+
f"""
125+
let runCountEl = document.getElementById("run-count");
126+
runCountEl.setAttribute("data-value", {i});
127+
"""
128+
)
129+
130+
incr_src_id.current()
131+
132+
run_count = driver.find_element("id", "run-count")
133+
134+
driver_wait.until(lambda d: (run_count.get_attribute("data-value") == "1"))
135+
136+
137+
def test_script_may_only_have_one_child():
138+
with pytest.raises(ValueError, match="'script' nodes may have, at most, one child"):
139+
html.script("one child", "two child")
140+
141+
142+
def test_child_of_script_must_be_string():
143+
with pytest.raises(ValueError, match="The child of a 'script' must be a string"):
144+
html.script(1)

0 commit comments

Comments
 (0)