Преглед изворни кода

Expose Script component from next/script (#1355)

Masen Furer пре 1 година
родитељ
комит
bfec196d84

+ 1 - 0
reflex/__init__.py

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

+ 2 - 2
reflex/compiler/utils.py

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

+ 2 - 2
reflex/components/__init__.py

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

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

@@ -1,7 +1,8 @@
 """Base components."""
 """Base components."""
 
 
 from .body import Body
 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 .head import Head
 from .link import RawLink, ScriptTag
 from .link import RawLink, ScriptTag
 from .meta import Description, Image, Meta, Title
 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"
     tag = "Main"
 
 
 
 
-class Script(NextDocumentLib):
+class NextScript(NextDocumentLib):
     """The document main scripts."""
     """The document main scripts."""
 
 
     tag = "NextScript"
     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"]
+    )