diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index 261d948c0..bb4ea5e7f 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -30,6 +30,7 @@ Unreleased - :pull:`1281` - Added ``reactpy.Vdom`` primitive interface for creating VDOM dictionaries. - :pull:`1281` - Added type hints to ``reactpy.html`` attributes. - :pull:`1285` - Added support for nested components in web modules +- :pull:`1289` - Added support for inline JavaScript as event handlers or other attributes that expect a callable via ``reactpy.types.InlineJavaScript`` **Changed** diff --git a/src/js/packages/@reactpy/client/src/types.ts b/src/js/packages/@reactpy/client/src/types.ts index 3c0330a07..148a3486c 100644 --- a/src/js/packages/@reactpy/client/src/types.ts +++ b/src/js/packages/@reactpy/client/src/types.ts @@ -53,6 +53,7 @@ export type ReactPyVdom = { children?: (ReactPyVdom | string)[]; error?: string; eventHandlers?: { [key: string]: ReactPyVdomEventHandler }; + inlineJavaScript?: { [key: string]: string }; importSource?: ReactPyVdomImportSource; }; diff --git a/src/js/packages/@reactpy/client/src/vdom.tsx b/src/js/packages/@reactpy/client/src/vdom.tsx index cae706787..4bd882ff4 100644 --- a/src/js/packages/@reactpy/client/src/vdom.tsx +++ b/src/js/packages/@reactpy/client/src/vdom.tsx @@ -189,6 +189,12 @@ export function createAttributes( createEventHandler(client, name, handler), ), ), + ...Object.fromEntries( + Object.entries(model.inlineJavaScript || {}).map( + ([name, inlineJavaScript]) => + createInlineJavaScript(name, inlineJavaScript), + ), + ), }), ); } @@ -198,23 +204,51 @@ function createEventHandler( name: string, { target, preventDefault, stopPropagation }: ReactPyVdomEventHandler, ): [string, () => void] { - return [ - name, - function (...args: any[]) { - const data = Array.from(args).map((value) => { - if (!(typeof value === "object" && value.nativeEvent)) { - return value; - } - const event = value as React.SyntheticEvent; - if (preventDefault) { - event.preventDefault(); - } - if (stopPropagation) { - event.stopPropagation(); - } - return serializeEvent(event.nativeEvent); - }); - client.sendMessage({ type: "layout-event", data, target }); - }, - ]; + const eventHandler = function (...args: any[]) { + const data = Array.from(args).map((value) => { + if (!(typeof value === "object" && value.nativeEvent)) { + return value; + } + const event = value as React.SyntheticEvent; + if (preventDefault) { + event.preventDefault(); + } + if (stopPropagation) { + event.stopPropagation(); + } + return serializeEvent(event.nativeEvent); + }); + client.sendMessage({ type: "layout-event", data, target }); + }; + eventHandler.isHandler = true; + return [name, eventHandler]; +} + +function createInlineJavaScript( + name: string, + inlineJavaScript: string, +): [string, () => void] { + /* Function that will execute the string-like InlineJavaScript + via eval in the most appropriate way */ + const wrappedExecutable = function (...args: any[]) { + function handleExecution(...args: any[]) { + const evalResult = eval(inlineJavaScript); + if (typeof evalResult == "function") { + return evalResult(...args); + } + } + if (args.length > 0 && args[0] instanceof Event) { + /* If being triggered by an event, set the event's current + target to "this". This ensures that inline + javascript statements such as the following work: + html.button({"onclick": 'this.value = "Clicked!"'}, "Click Me")*/ + return handleExecution.call(args[0].currentTarget, ...args); + } else { + /* If not being triggered by an event, do not set "this" and + just call normally */ + return handleExecution(...args); + } + }; + wrappedExecutable.isHandler = false; + return [name, wrappedExecutable]; } diff --git a/src/reactpy/core/layout.py b/src/reactpy/core/layout.py index a32f97083..a81ecc6d7 100644 --- a/src/reactpy/core/layout.py +++ b/src/reactpy/core/layout.py @@ -262,6 +262,10 @@ def _render_model_attributes( attrs = raw_model["attributes"].copy() new_state.model.current["attributes"] = attrs + if "inlineJavaScript" in raw_model: + inline_javascript = raw_model["inlineJavaScript"].copy() + new_state.model.current["inlineJavaScript"] = inline_javascript + if old_state is None: self._render_model_event_handlers_without_old_state( new_state, handlers_by_event diff --git a/src/reactpy/core/vdom.py b/src/reactpy/core/vdom.py index 7ecddcf0e..8d70af53d 100644 --- a/src/reactpy/core/vdom.py +++ b/src/reactpy/core/vdom.py @@ -2,6 +2,7 @@ from __future__ import annotations import json +import re from collections.abc import Mapping, Sequence from typing import ( Any, @@ -23,12 +24,16 @@ EventHandlerDict, EventHandlerType, ImportSourceDict, + InlineJavaScript, + InlineJavaScriptDict, VdomAttributes, VdomChildren, VdomDict, VdomJson, ) +EVENT_ATTRIBUTE_PATTERN = re.compile(r"^on[A-Z]\w+") + VDOM_JSON_SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema", "$ref": "#/definitions/element", @@ -42,6 +47,7 @@ "children": {"$ref": "#/definitions/elementChildren"}, "attributes": {"type": "object"}, "eventHandlers": {"$ref": "#/definitions/elementEventHandlers"}, + "inlineJavaScript": {"$ref": "#/definitions/elementInlineJavaScripts"}, "importSource": {"$ref": "#/definitions/importSource"}, }, # The 'tagName' is required because its presence is a useful indicator of @@ -71,6 +77,12 @@ }, "required": ["target"], }, + "elementInlineJavaScripts": { + "type": "object", + "patternProperties": { + ".*": "str", + }, + }, "importSource": { "type": "object", "properties": { @@ -160,7 +172,9 @@ def __call__( """The entry point for the VDOM API, for example reactpy.html().""" attributes, children = separate_attributes_and_children(attributes_and_children) key = attributes.get("key", None) - attributes, event_handlers = separate_attributes_and_event_handlers(attributes) + attributes, event_handlers, inline_javascript = ( + separate_attributes_handlers_and_inline_javascript(attributes) + ) if REACTPY_CHECK_JSON_ATTRS.current: json.dumps(attributes) @@ -180,6 +194,9 @@ def __call__( **({"children": children} if children else {}), **({"attributes": attributes} if attributes else {}), **({"eventHandlers": event_handlers} if event_handlers else {}), + **( + {"inlineJavaScript": inline_javascript} if inline_javascript else {} + ), **({"importSource": self.import_source} if self.import_source else {}), } @@ -212,26 +229,26 @@ def separate_attributes_and_children( return _attributes, _children -def separate_attributes_and_event_handlers( +def separate_attributes_handlers_and_inline_javascript( attributes: Mapping[str, Any], -) -> tuple[VdomAttributes, EventHandlerDict]: +) -> tuple[VdomAttributes, EventHandlerDict, InlineJavaScriptDict]: _attributes: VdomAttributes = {} _event_handlers: dict[str, EventHandlerType] = {} + _inline_javascript: dict[str, InlineJavaScript] = {} for k, v in attributes.items(): - handler: EventHandlerType - if callable(v): - handler = EventHandler(to_event_handler_function(v)) + _event_handlers[k] = EventHandler(to_event_handler_function(v)) elif isinstance(v, EventHandler): - handler = v + _event_handlers[k] = v + elif EVENT_ATTRIBUTE_PATTERN.match(k) and isinstance(v, str): + _inline_javascript[k] = InlineJavaScript(v) + elif isinstance(v, InlineJavaScript): + _inline_javascript[k] = v else: _attributes[k] = v - continue - - _event_handlers[k] = handler - return _attributes, _event_handlers + return _attributes, _event_handlers, _inline_javascript def _flatten_children(children: Sequence[Any]) -> list[Any]: diff --git a/src/reactpy/transforms.py b/src/reactpy/transforms.py index cdac48c7e..072653c95 100644 --- a/src/reactpy/transforms.py +++ b/src/reactpy/transforms.py @@ -6,6 +6,16 @@ from reactpy.types import VdomAttributes, VdomDict +def attributes_to_reactjs(attributes: VdomAttributes): + """Convert HTML attribute names to their ReactJS equivalents.""" + attrs = cast(VdomAttributes, attributes.items()) + attrs = cast( + VdomAttributes, + {REACT_PROP_SUBSTITUTIONS.get(k, k): v for k, v in attrs}, + ) + return attrs + + class RequiredTransforms: """Performs any necessary transformations related to `string_to_reactpy` to automatically prevent issues with React's rendering engine. @@ -36,16 +46,6 @@ def normalize_style_attributes(self, vdom: dict[str, Any]) -> None: ) } - @staticmethod - def html_props_to_reactjs(vdom: VdomDict) -> None: - """Convert HTML prop names to their ReactJS equivalents.""" - if "attributes" in vdom: - items = cast(VdomAttributes, vdom["attributes"].items()) - vdom["attributes"] = cast( - VdomAttributes, - {REACT_PROP_SUBSTITUTIONS.get(k, k): v for k, v in items}, - ) - @staticmethod def textarea_children_to_prop(vdom: VdomDict) -> None: """Transformation that converts the text content of a