From 50947b28802f13aec3ddc5173c45c715ac62b5ba Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Sat, 14 Dec 2024 21:46:56 +0530 Subject: [PATCH 1/2] styled --- app/components/CodeEditor.tsx | 2 +- app/renderer.tsx | 7 ++++++ package-lock.json | 4 ++-- py/gooey_gui/components/common.py | 39 +++++++++++++++++++++++++++---- py/gooey_gui/core/__init__.py | 9 ++++++- py/gooey_gui/core/renderer.py | 14 +++++++++++ 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/app/components/CodeEditor.tsx b/app/components/CodeEditor.tsx index 18704aa..22c282a 100644 --- a/app/components/CodeEditor.tsx +++ b/app/components/CodeEditor.tsx @@ -73,7 +73,7 @@ export default function CodeEditor({ }; return ( -
+
); case "nav-tab-content": @@ -526,10 +527,12 @@ export function RenderedChildren({ children, onChange, state, + className, }: { children: Array; onChange: OnChange; state: Record; + className?: string; }) { let elements = children.map((node, idx) => { let key; @@ -538,6 +541,10 @@ export function RenderedChildren({ } else { key = `idx:${idx}`; } + let prevClassName = node.props.className || ""; + if (className && !prevClassName.includes(className)) { + node.props.className = `${className} ${prevClassName}`; + } return ( str: if value is None: return BLANK_OPTION @@ -54,17 +66,17 @@ def nav_tab_content(): return _node("nav-tab-content") -def div(**props) -> core.NestingCtx: - return tag("div", **props) +def div(*, style: StyleProp = None, **props) -> core.NestingCtx: + return tag("div", style=style, **props) def link(*, to: str, **props) -> core.NestingCtx: return _node("Link", to=to, **props) -def tag(tag_name: str, **props) -> core.NestingCtx: +def tag(tag_name: str, *, style: StyleProp = None, **props) -> core.NestingCtx: props["__reactjsxelement"] = tag_name - return _node("tag", **props) + return _node("tag", style=style, **props) def html(body: str, **props): @@ -115,11 +127,30 @@ def markdown( def _node(nodename: str, **props): + if style := props.get("style"): + selector = style.pop("selector", None) + if selector: + identifier = "." + core.md5_values(selector) + for s, rules in selector.items(): + _node("css-in-js", selector=s.replace("&", identifier), rules=rules) + props["className"] = " ".join( + filter(None, (props.get("className"), identifier)) + ) + # media = style.pop("@media", None) node = core.RenderTreeNode(name=nodename, props=props) node.mount() return core.NestingCtx(node) +def styled(css: str) -> core.RenderTreeNode: + css = dedent(css).strip() + className = "gui-" + core.md5_values(css) + selector = "." + className + css = css.replace("&", selector) + core.add_styles(className, css) + return _node("", className=className) + + def text(body: str, **props): core.RenderTreeNode( name="pre", diff --git a/py/gooey_gui/core/__init__.py b/py/gooey_gui/core/__init__.py index 602aa15..4191848 100644 --- a/py/gooey_gui/core/__init__.py +++ b/py/gooey_gui/core/__init__.py @@ -16,7 +16,14 @@ realtime_clear_subs, md5_values, ) -from .renderer import RenderTreeNode, NestingCtx, renderer, route, current_root_ctx +from .renderer import ( + RenderTreeNode, + NestingCtx, + renderer, + route, + current_root_ctx, + add_styles, +) from .state import ( get_session_state, set_session_state, diff --git a/py/gooey_gui/core/renderer.py b/py/gooey_gui/core/renderer.py index 4157f20..50a16b4 100644 --- a/py/gooey_gui/core/renderer.py +++ b/py/gooey_gui/core/renderer.py @@ -23,6 +23,10 @@ def current_root_ctx() -> "NestingCtx": return threadlocal.root_ctx +def add_styles(className: str, css: str): + threadlocal.styles[className] = css + + class RenderTreeNode(BaseModel): name: str props: ReactHTMLProps = {} @@ -108,17 +112,27 @@ def renderer( set_query_params(query_params or {}) realtime_clear_subs() threadlocal.use_state_count = 0 + threadlocal.styles = {} while True: try: root = RenderTreeNode(name="root") threadlocal.root_ctx = NestingCtx(root) with threadlocal.root_ctx: + styles_node = RenderTreeNode( + name="tag", + props=dict(__reactjsxelement="style"), + ).mount() try: ret = render() except StopException: ret = None except RedirectException as e: return RedirectResponse(e.url, status_code=e.status_code) + if threadlocal.styles: + styles_node.props["dangerouslySetInnerHTML"] = { + "__html": "\n".join(threadlocal.styles.values()) + } + print(get_subscriptions()) if isinstance(ret, Response): return ret return JSONResponse( From 96c82c1dcc98650e0668f72105f08c09310aa710 Mon Sep 17 00:00:00 2001 From: Dev Aggarwal Date: Tue, 17 Dec 2024 14:46:51 +0530 Subject: [PATCH 2/2] load inline js event handlers for inputs --- app/components/CodeEditor.tsx | 14 ++++---------- app/components/GooeySelect.tsx | 1 + app/gooeyInput.tsx | 23 +++++++++++++++++++++++ app/jsonFormInput.tsx | 5 +++++ app/renderer.tsx | 1 + 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/components/CodeEditor.tsx b/app/components/CodeEditor.tsx index 22c282a..417a64b 100644 --- a/app/components/CodeEditor.tsx +++ b/app/components/CodeEditor.tsx @@ -41,19 +41,13 @@ export default function CodeEditor({ onChange: OnChange; state: Record; }) { - const { - name, - defaultValue, - height, - label, - help, - tooltipPlacement, - ...restProps - } = props; + const { name, defaultValue, height, label, help, tooltipPlacement, ...args } = + props; const [inputRef, value, setValue] = useGooeyStringInput({ state, name, defaultValue, + args, }); const handleChange = (val: string) => { setValue(val); @@ -98,7 +92,7 @@ export default function CodeEditor({ value={value} onChange={handleChange} extensions={[javascript(), lintGutter(), jsLinter(lintOptions)]} - {...restProps} + {...args} />
); diff --git a/app/components/GooeySelect.tsx b/app/components/GooeySelect.tsx index 376cc4a..b8d6998 100644 --- a/app/components/GooeySelect.tsx +++ b/app/components/GooeySelect.tsx @@ -25,6 +25,7 @@ export default function GooeySelect({ defaultValue, name, onChange, + args, }); // if the state value is changed by the server code, then update the value diff --git a/app/gooeyInput.tsx b/app/gooeyInput.tsx index b1be7e8..ffe6c1c 100644 --- a/app/gooeyInput.tsx +++ b/app/gooeyInput.tsx @@ -37,6 +37,7 @@ export function GooeyTextarea({ state, name, defaultValue, + args, }); return (
@@ -74,6 +75,7 @@ export function GooeyInput({ state, name, defaultValue, + args, }); return (
@@ -100,10 +102,12 @@ export function useGooeyStringInput< state, name, defaultValue, + args, }: { state: Record; name: string; defaultValue: string; + args?: Record; }): [ inputRef: React.RefObject, value: string, @@ -117,6 +121,8 @@ export function useGooeyStringInput< // but to avoid reloading the page on every change with onChange (gets very annoying when typing), we need to use this extra state variable with a useEffect const [value, setValue] = useState(state[name] || defaultValue); + loadEventHandlers(value, setValue, args); + useEffect(() => { const element = inputRef.current; if ( @@ -234,3 +240,20 @@ export function useGooeyCheckedInput({ return inputRef; } + +export function loadEventHandlers( + value: T, + setValue: (value: T) => void, + args?: Record +) { + if (!args) return; + for (const [attr, body] of Object.entries(args)) { + if (!attr.startsWith("on")) continue; + args[attr] = (event: React.SyntheticEvent) => { + // new Function(...args, body) + const fn = new Function("event", "value", "setValue", body); + // fn.call(thisArg, ...args) + return fn.call(event.currentTarget, event.nativeEvent, value, setValue); + }; + } +} diff --git a/app/jsonFormInput.tsx b/app/jsonFormInput.tsx index 97b1dce..8466cb5 100644 --- a/app/jsonFormInput.tsx +++ b/app/jsonFormInput.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { loadEventHandlers } from "./gooeyInput"; export function useJsonFormInput({ defaultValue, @@ -6,16 +7,20 @@ export function useJsonFormInput({ onChange, encode = JSON.stringify, decode = JSON.parse, + args, }: { defaultValue: T; name: string; onChange?: () => void; encode?: (value: T) => string; decode?: (value: string) => T; + args?: Record; }): [React.FunctionComponent, T, (value: T) => void] { const [value, setValue] = useState(defaultValue); const ref = useRef(null); + loadEventHandlers(value, setValue, args); + useEffect(() => { if (!ref.current) return; if (!ref.current.value) { diff --git a/app/renderer.tsx b/app/renderer.tsx index 352a4e2..aabf9a7 100644 --- a/app/renderer.tsx +++ b/app/renderer.tsx @@ -628,6 +628,7 @@ export function GuiExpander({ defaultValue: open, name, onChange, + args: props, }); return (