Skip to content

make scripts behave more like normal html script element #632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/client/packages/idom-client-react/src/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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`<div ref=${ref} />`;
}

function ImportedElement({ model }) {
Expand Down
52 changes: 42 additions & 10 deletions src/idom/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}> <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/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
Expand Down
58 changes: 54 additions & 4 deletions tests/test_html.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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)


Expand Down Expand Up @@ -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}),
Expand Down Expand Up @@ -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)