diff --git a/src/client/packages/idom-client-react/src/components.js b/src/client/packages/idom-client-react/src/components.js index 9383e1f5f..5caed180e 100644 --- a/src/client/packages/idom-client-react/src/components.js +++ b/src/client/packages/idom-client-react/src/components.js @@ -34,6 +34,8 @@ export function Element({ model }) { } else { return null; } + } else if (model.tagName == "script") { + return html`<${ScriptElement} script=${model.children[0]} />`; } else if (model.importSource) { return html`<${ImportedElement} model=${model} />`; } else { @@ -56,6 +58,12 @@ function StandardElement({ model }) { ); } +function ScriptElement({ script }) { + const el = React.useRef(); + React.useEffect(eval(script), [script]); + return null; +} + function ImportedElement({ model }) { const layoutContext = React.useContext(LayoutContext); diff --git a/src/idom/html.py b/src/idom/html.py index 2dc9a22e6..64536595e 100644 --- a/src/idom/html.py +++ b/src/idom/html.py @@ -150,6 +150,7 @@ - :func:`template` """ +from .core.proto import VdomDict from .core.vdom import make_vdom_constructor @@ -247,7 +248,21 @@ # Scripting canvas = make_vdom_constructor("canvas") noscript = make_vdom_constructor("noscript") -script = make_vdom_constructor("script") + + +def script(content: str) -> 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. + """ + return {"tagName": "script", "children": [content]} + # Demarcating edits del_ = make_vdom_constructor("del") diff --git a/tests/test_html.py b/tests/test_html.py new file mode 100644 index 000000000..d62e7a954 --- /dev/null +++ b/tests/test_html.py @@ -0,0 +1,94 @@ +from idom import component, html, use_state +from idom.utils import Ref + + +def use_toggle(): + state, set_state = use_state(True) + return state, lambda: set_state(not state) + + +def use_counter(): + state, set_state = use_state(1) + return state, lambda: set_state(state + 1) + + +def test_script_mount_unmount(driver, driver_wait, display): + toggle_is_mounted = Ref() + + @component + def Root(): + is_mounted, toggle_is_mounted.current = use_toggle() + if is_mounted: + el = HasScript() + else: + el = html.div() + + return html.div( + html.div({"id": "mount-state", "data-value": False}), + el, + ) + + @component + def HasScript(): + return html.script( + """() => { + const mapping = {"false": false, "true": true}; + const mountStateEl = document.getElementById("mount-state"); + mountStateEl.setAttribute( + "data-value", !mapping[mountStateEl.getAttribute("data-value")]); + return () => mountStateEl.setAttribute( + "data-value", !mapping[mountStateEl.getAttribute("data-value")]); + }""" + ) + + display(Root) + + mount_state = driver.find_element("id", "mount-state") + + driver_wait.until(lambda d: mount_state.get_attribute("data-value") == "true") + + toggle_is_mounted.current() + + driver_wait.until(lambda d: mount_state.get_attribute("data-value") == "false") + + toggle_is_mounted.current() + + driver_wait.until(lambda d: mount_state.get_attribute("data-value") == "true") + + +def test_script_re_run_on_content_change(driver, driver_wait, display): + incr_count = Ref() + + @component + def HasScript(): + count, incr_count.current = use_counter() + return html.div( + html.div({"id": "mount-count", "data-value": 0}), + html.div({"id": "unmount-count", "data-value": 0}), + html.script( + f"""() => {{ + const mountCountEl = document.getElementById("mount-count"); + const unmountCountEl = document.getElementById("unmount-count"); + mountCountEl.setAttribute("data-value", {count}); + return () => unmountCountEl.setAttribute("data-value", {count});; + }}""" + ), + ) + + display(HasScript) + + mount_count = driver.find_element("id", "mount-count") + unmount_count = driver.find_element("id", "unmount-count") + + driver_wait.until(lambda d: mount_count.get_attribute("data-value") == "1") + driver_wait.until(lambda d: unmount_count.get_attribute("data-value") == "0") + + incr_count.current() + + driver_wait.until(lambda d: mount_count.get_attribute("data-value") == "2") + driver_wait.until(lambda d: unmount_count.get_attribute("data-value") == "1") + + incr_count.current() + + driver_wait.until(lambda d: mount_count.get_attribute("data-value") == "3") + driver_wait.until(lambda d: unmount_count.get_attribute("data-value") == "2")