Expose Script component from next/script (#1355)
This commit is contained in:
parent
3cbee575fe
commit
bfec196d84
@ -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
|
||||
|
@ -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(),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -28,7 +28,7 @@ class Main(NextDocumentLib):
|
||||
tag = "Main"
|
||||
|
||||
|
||||
class Script(NextDocumentLib):
|
||||
class NextScript(NextDocumentLib):
|
||||
"""The document main scripts."""
|
||||
|
||||
tag = "NextScript"
|
||||
|
83
reflex/components/base/script.py
Normal file
83
reflex/components/base/script.py
Normal file
@ -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
tests/components/base/test_script.py
Normal file
69
tests/components/base/test_script.py
Normal file
@ -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"]
|
||||
)
|
Loading…
Reference in New Issue
Block a user