Expose Script component from next/script (#1355)

This commit is contained in:
Masen Furer 2023-07-18 18:57:50 -07:00 committed by GitHub
parent 3cbee575fe
commit bfec196d84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 6 deletions

View File

@ -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

View File

@ -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(),
),
)

View File

@ -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

View File

@ -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

View File

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

View 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)

View 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"]
)