Bläddra i källkod

Expose Script component from next/script (#1355)

Masen Furer 1 år sedan
förälder
incheckning
bfec196d84

+ 1 - 0
reflex/__init__.py

@@ -12,6 +12,7 @@ from .app import UploadFile as UploadFile
 from .base import Base as Base
 from .compiler.utils import get_asset_path
 from .components import *
+from .components.base.script import client_side
 from .components.component import custom_component as memo
 from .components.graphing.victory import data as data
 from .config import Config as Config

+ 2 - 2
reflex/compiler/utils.py

@@ -14,8 +14,8 @@ from reflex.components.base import (
     Image,
     Main,
     Meta,
+    NextScript,
     RawLink,
-    Script,
     Title,
 )
 from reflex.components.component import Component, ComponentStyle, CustomComponent
@@ -186,7 +186,7 @@ def create_document_root(stylesheets: List[str]) -> Component:
         Body.create(
             ColorModeScript.create(),
             Main.create(),
-            Script.create(),
+            NextScript.create(),
         ),
     )
 

+ 2 - 2
reflex/components/__init__.py

@@ -1,7 +1,7 @@
 """Import all the components."""
 from __future__ import annotations
 
-from .base import ScriptTag
+from .base import Script
 from .component import Component
 from .datadisplay import *
 from .disclosure import *
@@ -238,7 +238,7 @@ highlight = Highlight.create
 markdown = Markdown.create
 span = Span.create
 text = Text.create
-script = ScriptTag.create
+script = Script.create
 aspect_ratio = AspectRatio.create
 kbd = KeyboardKey.create
 color_mode_button = ColorModeButton.create

+ 2 - 1
reflex/components/base/__init__.py

@@ -1,7 +1,8 @@
 """Base components."""
 
 from .body import Body
-from .document import ColorModeScript, DocumentHead, Html, Main, Script
+from .document import ColorModeScript, DocumentHead, Html, Main, NextScript
 from .head import Head
 from .link import RawLink, ScriptTag
 from .meta import Description, Image, Meta, Title
+from .script import Script

+ 1 - 1
reflex/components/base/document.py

@@ -28,7 +28,7 @@ class Main(NextDocumentLib):
     tag = "Main"
 
 
-class Script(NextDocumentLib):
+class NextScript(NextDocumentLib):
     """The document main scripts."""
 
     tag = "NextScript"

+ 83 - 0
reflex/components/base/script.py

@@ -0,0 +1,83 @@
+"""Next.js script wrappers and inline script functionality.
+
+https://nextjs.org/docs/app/api-reference/components/script
+"""
+from typing import Set
+
+from reflex.components.component import Component
+from reflex.event import EventChain
+from reflex.vars import BaseVar, Var
+
+
+class Script(Component):
+    """Next.js script component.
+
+    Note that this component differs from reflex.components.base.document.NextScript
+    in that it is intended for use with custom and user-defined scripts.
+
+    It also differs from reflex.components.base.link.ScriptTag, which is the plain
+    HTML <script> tag which does not work when rendering a component.
+    """
+
+    library = "next/script"
+    tag = "Script"
+    is_default = True
+
+    # Required unless inline script is used
+    src: Var[str]
+
+    # When the script will execute: afterInteractive | beforeInteractive | lazyOnload
+    strategy: Var[str] = "afterInteractive"  # type: ignore
+
+    @classmethod
+    def create(cls, *children, **props) -> Component:
+        """Create an inline or user-defined script.
+
+        If a string is provided as the first child, it will be rendered as an inline script
+        otherwise the `src` prop must be provided.
+
+        The following event triggers are provided:
+
+        on_load: Execute code after the script has finished loading.
+        on_ready: Execute code after the script has finished loading and every
+            time the component is mounted.
+        on_error: Execute code if the script fails to load.
+
+        Args:
+            *children: The children of the component.
+            **props: The props of the component.
+
+        Returns:
+            The component.
+
+        Raises:
+            ValueError: when neither children nor `src` are specified.
+        """
+        if not children and not props.get("src"):
+            raise ValueError("Must provide inline script or `src` prop.")
+        return super().create(*children, **props)
+
+    def get_triggers(self) -> Set[str]:
+        """Get the event triggers for the component.
+
+        Returns:
+            The event triggers.
+        """
+        return super().get_triggers() | {"on_load", "on_ready", "on_error"}
+
+
+def client_side(javascript_code) -> Var[EventChain]:
+    """Create an event handler that executes arbitrary javascript code.
+
+    The provided code will have access to `args`, which come from the event itself.
+    The code may call functions or reference variables defined in a previously
+    included rx.script function.
+
+    Args:
+        javascript_code: The code to execute.
+
+    Returns:
+        An EventChain, passable to any component, that will execute the client side javascript
+        when triggered.
+    """
+    return BaseVar(name=f"...args => {{{javascript_code}}}", type_=EventChain)

+ 69 - 0
tests/components/base/test_script.py

@@ -0,0 +1,69 @@
+"""Test that Script from next/script renders correctly."""
+import pytest
+
+from reflex.components.base.script import Script
+from reflex.state import State
+
+
+def test_script_inline():
+    """Test inline scripts are rendered as children."""
+    component = Script.create("let x = 42")
+    render_dict = component.render()
+    assert render_dict["name"] == "Script"
+    assert not render_dict["contents"]
+    assert len(render_dict["children"]) == 1
+    assert render_dict["children"][0]["contents"] == "{`let x = 42`}"
+
+
+def test_script_src():
+    """Test src prop is rendered without children."""
+    component = Script.create(src="foo.js")
+    render_dict = component.render()
+    assert render_dict["name"] == "Script"
+    assert not render_dict["contents"]
+    assert not render_dict["children"]
+    assert 'src="foo.js"' in render_dict["props"]
+
+
+def test_script_neither():
+    """Specifying neither children nor src is a ValueError."""
+    with pytest.raises(ValueError):
+        Script.create()
+
+
+class EvState(State):
+    """State for testing event handlers."""
+
+    def on_ready(self):
+        """Empty event handler."""
+        pass
+
+    def on_load(self):
+        """Empty event handler."""
+        pass
+
+    def on_error(self):
+        """Empty event handler."""
+        pass
+
+
+def test_script_event_handler():
+    """Test event handlers are rendered as expected."""
+    component = Script.create(
+        src="foo.js",
+        on_ready=EvState.on_ready,
+        on_load=EvState.on_load,
+        on_error=EvState.on_error,
+    )
+    render_dict = component.render()
+    assert (
+        'onReady={_e => Event([E("ev_state.on_ready", {})], _e)}'
+        in render_dict["props"]
+    )
+    assert (
+        'onLoad={_e => Event([E("ev_state.on_load", {})], _e)}' in render_dict["props"]
+    )
+    assert (
+        'onError={_e => Event([E("ev_state.on_error", {})], _e)}'
+        in render_dict["props"]
+    )