diff --git a/requirements/pkg-extras.txt b/requirements/pkg-extras.txt index 7d7a59bdb..54aab2c12 100644 --- a/requirements/pkg-extras.txt +++ b/requirements/pkg-extras.txt @@ -3,8 +3,9 @@ starlette >=0.13.6 uvicorn[standard] >=0.19.0 # extra=sanic -sanic >=21 +sanic >=21,<22.12 sanic-cors +uvicorn[standard] >=0.19.0 # extra=fastapi fastapi >=0.63.0 diff --git a/requirements/test-env.txt b/requirements/test-env.txt index 7e5ab1955..6522ebd17 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -9,3 +9,6 @@ playwright # I'm not quite sure why this needs to be installed for tests with Sanic to pass sanic-testing + +# required to test uploading form data with starlette +python-multipart diff --git a/src/client/packages/idom-client-react/src/components.js b/src/client/packages/idom-client-react/src/components.js index 7ca471c6e..9412ede1f 100644 --- a/src/client/packages/idom-client-react/src/components.js +++ b/src/client/packages/idom-client-react/src/components.js @@ -29,6 +29,14 @@ export function Layout({ saveUpdateHook, sendEvent, loadImportSource }) { `; } +const ELEMENT_WRAPPER_COMPONENTS = { + form: FormElement, + input: UserInputElement, + script: ScriptElement, + select: UserInputElement, + textarea: UserInputElement, +}; + export function Element({ model }) { if (model.error !== undefined) { if (model.error) { @@ -36,15 +44,16 @@ export function Element({ model }) { } else { return null; } - } else if (model.tagName == "script") { - return html`<${ScriptElement} model=${model} />`; - } else if (["input", "select", "textarea"].includes(model.tagName)) { - return html`<${UserInputElement} model=${model} />`; - } else if (model.importSource) { - return html`<${ImportedElement} model=${model} />`; + } + + let Component; + if (model.importSource) { + Component = ImportedElement; } else { - return html`<${StandardElement} model=${model} />`; + Component = ELEMENT_WRAPPER_COMPONENTS[model.tagName] || StandardElement; } + + return html`<${Component} model=${model} />`; } function StandardElement({ model }) { @@ -155,6 +164,41 @@ function ScriptElement({ model }) { return html`
`; } +function FormElement({ model }) { + const layoutContext = React.useContext(LayoutContext); + const props = createElementAttributes(model, layoutContext.sendEvent); + + if (props.target && props.target !== "_self") { + return html`<${StandardElement} model=${model} />`; + } + + const oldOnSubmit = props.onSubmit || (() => {}); + async function onSubmit(event) { + event.preventDefault(); + if (!model?.eventHandlers?.onSubmit?.preventDefault) { + await fetch(props.action || window.location.href, { + method: props.method || "POST", + body: new FormData(event.target), + }); + } + if (oldOnSubmit) { + oldOnSubmit(event); + } + } + + // Use createElement here to avoid warning about variable numbers of children not + // having keys. Warning about this must now be the responsibility of the server + // providing the models instead of the client rendering them. + return React.createElement( + model.tagName, + { ...props, onSubmit }, + ...createElementChildren( + model, + (model) => html`<${Element} key=${model.key} model=${model} />` + ) + ); +} + function ImportedElement({ model }) { const layoutContext = React.useContext(LayoutContext); diff --git a/src/idom/backend/_common.py b/src/idom/backend/_common.py index 90e2dea5b..85f3d29af 100644 --- a/src/idom/backend/_common.py +++ b/src/idom/backend/_common.py @@ -32,6 +32,8 @@ async def serve_development_asgi( started: asyncio.Event | None, ) -> None: """Run a development server for starlette""" + started = started or asyncio.Event() + server = UvicornServer( UvicornConfig( app, @@ -42,15 +44,11 @@ async def serve_development_asgi( ) ) - coros: list[Awaitable[Any]] = [server.serve()] - - if started: - coros.append(_check_if_started(server, started)) - try: - await asyncio.gather(*coros) + await asyncio.gather(server.serve(), _check_if_started(server, started)) finally: - await asyncio.wait_for(server.shutdown(), timeout=3) + if started.is_set(): + await asyncio.wait_for(server.shutdown(), timeout=3) async def _check_if_started(server: UvicornServer, started: asyncio.Event) -> None: diff --git a/src/idom/backend/sanic.py b/src/idom/backend/sanic.py index fda9d214f..d6f79e2bc 100644 --- a/src/idom/backend/sanic.py +++ b/src/idom/backend/sanic.py @@ -122,8 +122,16 @@ async def single_page_app_files( ) -> response.HTTPResponse: return response.html(index_html) - spa_blueprint.add_route(single_page_app_files, "/") - spa_blueprint.add_route(single_page_app_files, "/<_:path>") + spa_blueprint.add_route( + single_page_app_files, + "/", + name="single_page_app_files_root", + ) + spa_blueprint.add_route( + single_page_app_files, + "/<_:path>", + name="single_page_app_files_path", + ) async def asset_files( request: request.Request, @@ -188,8 +196,16 @@ async def model_stream( recv, ) - api_blueprint.add_websocket_route(model_stream, f"/{STREAM_PATH.name}") - api_blueprint.add_websocket_route(model_stream, f"/{STREAM_PATH.name}/