diff --git a/src/client/packages/idom-client-react/src/components.js b/src/client/packages/idom-client-react/src/components.js
index 5caed180e..465f94801 100644
--- a/src/client/packages/idom-client-react/src/components.js
+++ b/src/client/packages/idom-client-react/src/components.js
@@ -35,7 +35,7 @@ export function Element({ model }) {
return null;
}
} else if (model.tagName == "script") {
- return html`<${ScriptElement} script=${model.children[0]} />`;
+ return html`<${ScriptElement} model=${model} />`;
} else if (model.importSource) {
return html`<${ImportedElement} model=${model} />`;
} else {
@@ -58,10 +58,31 @@ function StandardElement({ model }) {
);
}
-function ScriptElement({ script }) {
- const el = React.useRef();
- React.useEffect(eval(script), [script]);
- return null;
+function ScriptElement({ model }) {
+ const ref = React.useRef();
+ React.useEffect(() => {
+ if (model?.children?.length > 1) {
+ console.error("Too many children for 'script' element.");
+ }
+
+ let scriptContent = model?.children?.[0];
+
+ let scriptElement;
+ if (model.attributes) {
+ scriptElement = document.createElement("script");
+ for (const [k, v] of Object.entries(model.attributes)) {
+ scriptElement.setAttribute(k, v);
+ }
+ scriptElement.appendChild(document.createTextNode(scriptContent));
+ ref.current.appendChild(scriptElement);
+ } else {
+ let scriptResult = eval(scriptContent);
+ if (typeof scriptResult == "function") {
+ return scriptResult();
+ }
+ }
+ }, [model.key]);
+ return html`
`;
}
function ImportedElement({ model }) {
diff --git a/src/idom/html.py b/src/idom/html.py
index 64536595e..dd7ac24af 100644
--- a/src/idom/html.py
+++ b/src/idom/html.py
@@ -150,8 +150,12 @@
- :func:`template`
"""
+from __future__ import annotations
+
+from typing import Any, Mapping
+
from .core.proto import VdomDict
-from .core.vdom import make_vdom_constructor
+from .core.vdom import coalesce_attributes_and_children, make_vdom_constructor
# Dcument metadata
@@ -250,18 +254,46 @@
noscript = make_vdom_constructor("noscript")
-def script(content: str) -> VdomDict:
+def script(
+ *attributes_and_children: Mapping[str, Any] | str,
+ key: str | int | None = None,
+) -> VdomDict:
"""Create a new `<{script}> `__ element.
- Parameters:
- content:
- The text of the script should evaluate to a function. This function will be
- called when the script is initially created or when the content of the
- script changes. The function may optionally return a teardown function that
- is called when the script element is removed from the tree, or when the
- script content changes.
+ This behaves slightly differently than a normal script element in that it may be run
+ multiple times if its key changes (depending on specific browser behaviors). If no
+ key is given, the key is inferred to be the content of the script or, lastly its
+ 'src' attribute if that is given.
+
+ If no attributes are given, the content of the script may evaluate to a function.
+ This function will be called when the script is initially created or when the
+ content of the script changes. The function may itself optionally return a teardown
+ function that is called when the script element is removed from the tree, or when
+ the script content changes.
"""
- return {"tagName": "script", "children": [content]}
+ model: VdomDict = {"tagName": "script"}
+
+ attributes, children = coalesce_attributes_and_children(attributes_and_children)
+
+ if children:
+ if len(children) > 1:
+ raise ValueError("'script' nodes may have, at most, one child.")
+ elif not isinstance(children[0], str):
+ raise ValueError("The child of a 'script' must be a string.")
+ else:
+ model["children"] = children
+ if key is None:
+ key = children[0]
+
+ if attributes:
+ model["attributes"] = attributes
+ if key is None and not children and "src" in attributes:
+ key = attributes["src"]
+
+ if key is not None:
+ model["key"] = key
+
+ return model
# Demarcating edits
diff --git a/tests/test_html.py b/tests/test_html.py
index d62e7a954..fbc888627 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -1,4 +1,6 @@
-from idom import component, html, use_state
+import pytest
+
+from idom import component, config, html, use_state
from idom.utils import Ref
@@ -7,8 +9,8 @@ def use_toggle():
return state, lambda: set_state(not state)
-def use_counter():
- state, set_state = use_state(1)
+def use_counter(initial_value):
+ state, set_state = use_state(initial_value)
return state, lambda: set_state(state + 1)
@@ -61,7 +63,7 @@ def test_script_re_run_on_content_change(driver, driver_wait, display):
@component
def HasScript():
- count, incr_count.current = use_counter()
+ count, incr_count.current = use_counter(1)
return html.div(
html.div({"id": "mount-count", "data-value": 0}),
html.div({"id": "unmount-count", "data-value": 0}),
@@ -92,3 +94,51 @@ def HasScript():
driver_wait.until(lambda d: mount_count.get_attribute("data-value") == "3")
driver_wait.until(lambda d: unmount_count.get_attribute("data-value") == "2")
+
+
+def test_script_from_src(driver, driver_wait, display):
+ incr_src_id = Ref()
+ file_name_template = "__some_js_script_{src_id}__.js"
+
+ @component
+ def HasScript():
+ src_id, incr_src_id.current = use_counter(0)
+ if src_id == 0:
+ # on initial display we haven't added the file yet.
+ return html.div()
+ else:
+ return html.div(
+ html.div({"id": "run-count", "data-value": 0}),
+ html.script(
+ {"src": f"/modules/{file_name_template.format(src_id=src_id)}"}
+ ),
+ )
+
+ display(HasScript)
+
+ for i in range(1, 4):
+ script_file = config.IDOM_WED_MODULES_DIR.current / file_name_template.format(
+ src_id=i
+ )
+ script_file.write_text(
+ f"""
+ let runCountEl = document.getElementById("run-count");
+ runCountEl.setAttribute("data-value", {i});
+ """
+ )
+
+ incr_src_id.current()
+
+ run_count = driver.find_element("id", "run-count")
+
+ driver_wait.until(lambda d: (run_count.get_attribute("data-value") == "1"))
+
+
+def test_script_may_only_have_one_child():
+ with pytest.raises(ValueError, match="'script' nodes may have, at most, one child"):
+ html.script("one child", "two child")
+
+
+def test_child_of_script_must_be_string():
+ with pytest.raises(ValueError, match="The child of a 'script' must be a string"):
+ html.script(1)