From d530b28348ead2df82d7633c737a73c9e7d5ab49 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 18 Nov 2022 10:56:00 -0500 Subject: [PATCH 1/7] Give direct access to `input` and `value` fields on actions --- leptos_server/src/lib.rs | 36 +++++------------------------------ router/src/components/form.rs | 8 +++++--- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/leptos_server/src/lib.rs b/leptos_server/src/lib.rs index f6b5edf10..24263365b 100644 --- a/leptos_server/src/lib.rs +++ b/leptos_server/src/lib.rs @@ -347,8 +347,11 @@ where { /// How many times the action has successfully resolved. pub version: RwSignal, - input: RwSignal>, - value: RwSignal>, + /// The current argument that was dispatched to the `async` function. + /// `Some` while we are waiting for it to resolve, `None` if it has resolved. + pub input: RwSignal>, + /// The most recent return value of the `async` function. + pub value: RwSignal>, pending: RwSignal, url: Option, #[allow(clippy::complexity)] @@ -383,35 +386,6 @@ where self.pending.read_only() } - /// The argument that was dispatched to the `async` function, - /// only while we are waiting for it to resolve. - pub fn input(&self) -> ReadSignal> { - self.input.read_only() - } - - /// The argument that was dispatched to the `async` function. - /// - /// You probably don't need to call this unless you are implementing a form - /// or some other kind of wrapper for an action and need to set the input - /// based on its internal logic. - pub fn set_input(&self, value: I) { - self.input.set(Some(value)); - } - - /// The most recent return value of the `async` function. - pub fn value(&self) -> ReadSignal> { - self.value.read_only() - } - - /// Sets the most recent return value of the `async` function. - /// - /// You probably don't need to call this unless you are implementing a form - /// or some other kind of wrapper for an action and need to set the value - /// based on its internal logic. - pub fn set_value(&self, value: O) { - self.value.set(Some(value)); - } - /// The URL associated with the action (typically as part of a server function.) /// This enables integration with the `ActionForm` component in `leptos_router`. pub fn url(&self) -> Option<&str> { diff --git a/router/src/components/form.rs b/router/src/components/form.rs index bb61532b3..b54ad7bf1 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -258,6 +258,8 @@ where "" }.to_string(); let version = props.action.version; + let value = props.action.value; + let input = props.action.input; let on_form_data = { let action = props.action.clone(); @@ -267,7 +269,7 @@ where let data = data.to_string().as_string().unwrap_or_default(); let data = serde_urlencoded::from_str::(&data); match data { - Ok(data) => action.set_input(data), + Ok(data) => input.set(Some(data)), Err(e) => log::error!("{e}"), } }) @@ -291,9 +293,9 @@ where match O::from_json( &json.as_string().expect("couldn't get String from JsString"), ) { - Ok(res) => action.set_value(Ok(res)), + Ok(res) => value.set(Some(Ok(res))), Err(e) => { - action.set_value(Err(ServerFnError::Deserialization(e.to_string()))) + value.set(Some(Err(ServerFnError::Deserialization(e.to_string())))) } } } From 43524c013589d2de7db20fdc18373040e5f385b5 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 18 Nov 2022 11:48:08 -0500 Subject: [PATCH 2/7] Clean up docs on `counter-isomorphic` --- examples/counter-isomorphic/README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/counter-isomorphic/README.md b/examples/counter-isomorphic/README.md index b2be3b83d..65a8a58d7 100644 --- a/examples/counter-isomorphic/README.md +++ b/examples/counter-isomorphic/README.md @@ -3,16 +3,19 @@ This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result. ## Server Side Rendering With Hydration -To run it as a server side app with hydration, first you should run + +To run it as a server side app with hydration, first you should run + ```bash wasm-pack build --target=web --no-default-features --features=hydrate ``` + to generate the Webassembly to provide hydration features for the server. -Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration. +Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration. + ```bash cargo run --no-default-features --features=ssr ``` + > Note that if your hydration code changes, you will have to rerun the wasm-pack command above > This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time! - -If for some reason you want to run it as a fully client side app, that can be done with the instructions below. From 5c36f0963c183332cb6d9d61c818559723736613 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 18 Nov 2022 13:25:12 -0500 Subject: [PATCH 3/7] Initial version of todo app with sqlite --- examples/todo-app-sqlite/Cargo.toml | 47 +++++++ examples/todo-app-sqlite/LICENSE | 21 +++ examples/todo-app-sqlite/README.md | 21 +++ examples/todo-app-sqlite/Todos.db | Bin 0 -> 16384 bytes .../20221118172000_create_todo_table.sql | 6 + examples/todo-app-sqlite/src/lib.rs | 22 +++ examples/todo-app-sqlite/src/main.rs | 108 +++++++++++++++ examples/todo-app-sqlite/src/todo.rs | 131 ++++++++++++++++++ 8 files changed, 356 insertions(+) create mode 100644 examples/todo-app-sqlite/Cargo.toml create mode 100644 examples/todo-app-sqlite/LICENSE create mode 100644 examples/todo-app-sqlite/README.md create mode 100644 examples/todo-app-sqlite/Todos.db create mode 100644 examples/todo-app-sqlite/migrations/20221118172000_create_todo_table.sql create mode 100644 examples/todo-app-sqlite/src/lib.rs create mode 100644 examples/todo-app-sqlite/src/main.rs create mode 100644 examples/todo-app-sqlite/src/todo.rs diff --git a/examples/todo-app-sqlite/Cargo.toml b/examples/todo-app-sqlite/Cargo.toml new file mode 100644 index 000000000..1eff4d2b8 --- /dev/null +++ b/examples/todo-app-sqlite/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "todo-app-sqlite" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +actix-files = { version = "0.6", optional = true } +actix-web = { version = "4", optional = true, features = ["openssl", "macros"] } +anyhow = "1" +broadcaster = "1" +console_log = "0.2" +console_error_panic_hook = "0.1" +serde = { version = "1", features = ["derive"] } +futures = "0.3" +cfg-if = "1" +leptos = { path = "../../../leptos/leptos", default-features = false, features = [ + "serde", +] } +leptos_meta = { path = "../../../leptos/meta", default-features = false } +leptos_router = { path = "../../../leptos/router", default-features = false } +log = "0.4" +simple_logger = "2" +gloo = { git = "https://github.com/rustwasm/gloo" } +sqlx = { version = "0.6", features = [ + "runtime-tokio-rustls", + "sqlite", +], optional = true } + +[features] +default = ["csr"] +csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:actix-files", + "dep:actix-web", + "dep:sqlx", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] + +[package.metadata.cargo-all-features] +denylist = ["actix-files", "actix-web", "sqlx"] +skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]] diff --git a/examples/todo-app-sqlite/LICENSE b/examples/todo-app-sqlite/LICENSE new file mode 100644 index 000000000..77d5625cb --- /dev/null +++ b/examples/todo-app-sqlite/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Greg Johnston + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/todo-app-sqlite/README.md b/examples/todo-app-sqlite/README.md new file mode 100644 index 000000000..f319dc32f --- /dev/null +++ b/examples/todo-app-sqlite/README.md @@ -0,0 +1,21 @@ +# Leptos Counter Isomorphic Example + +This example demonstrates how to use a server functions and multi-actions to build a simple todo app. + +## Server Side Rendering With Hydration + +To run it as a server side app with hydration, first you should run + +```bash +wasm-pack build --target=web --no-default-features --features=hydrate +``` + +to generate the Webassembly to provide hydration features for the server. +Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration. + +```bash +cargo run --no-default-features --features=ssr +``` + +> Note that if your hydration code changes, you will have to rerun the wasm-pack command above +> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time! diff --git a/examples/todo-app-sqlite/Todos.db b/examples/todo-app-sqlite/Todos.db new file mode 100644 index 0000000000000000000000000000000000000000..6df3c241029e0d6f7012e5fb0e49b340ee4b56d3 GIT binary patch literal 16384 zcmeI&zi-n(6bJCL^E+y51Vbt$q!SBNA}vXgs5%gwU`m9O6vsi0kTAi%f)yujV+R82 zQp5ri69^<=f`0%pG183zCPoJ6(iI8B$T?{u$j}WW(D%t_=lt%R=TD}Ku6jYWeMVQ^ zPOIV5d9sfP0y#}7AtcIMn73hw{KKz(VVKzC8YQ!rvXhcaLQ^7>i_$f&c^{ z009U<00Izz00bZafj=m4XGV;r(rMvt#BW@6nBlEEJ)>n`>ok1ZZF}SWP(f2vS*z3YO8r`M2(I9$8IV;yxofc}Erj&JKG-NQ{>zXF>JesdmR7Eb2<_)su zb!IlbZj0vCN`5TIdd%zwRT;kBV#D%>#U*EQp=hd_7Kq)pSdT;TrL)oK`fk5(jAIy? z@jJotIUf9$n^7T_N~MI&W*^;mE!XRx#NT-Pw~~RWZ3V7_;H1iuqK#@F_w{YxVY_25 z%39%^tOc)bx~(;b`7C(SE=_V~k{2UNJ4E`*PY4J=00Izz00bZa0SG_<0uX=z1paM- zm>4Dp;<;RILI{&X@qG6dZP`vUSpN&s6T%M!1Rwwb2tWV=5P$##AOHafKmY=NUBD7z zk-7PV6WVW0KX`Y=?63yks2}VIp#41q+2h%4I+ICf7HDQ+F?(V$bF%W{*}aEbhc_l( zA6%V2<<_5Wf0K@EZGUTXm%g>Jw4_(ppkE3YT{dkNBa!Vd%l gAOHafKmY;|fB*y_009U<00RG&Kq3@L4E|R52_>P_egFUf literal 0 HcmV?d00001 diff --git a/examples/todo-app-sqlite/migrations/20221118172000_create_todo_table.sql b/examples/todo-app-sqlite/migrations/20221118172000_create_todo_table.sql new file mode 100644 index 000000000..5bc74dcf2 --- /dev/null +++ b/examples/todo-app-sqlite/migrations/20221118172000_create_todo_table.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS todos +( + id INTEGER NOT NULL PRIMARY KEY, + title VARCHAR, + completed BOOLEAN +); \ No newline at end of file diff --git a/examples/todo-app-sqlite/src/lib.rs b/examples/todo-app-sqlite/src/lib.rs new file mode 100644 index 000000000..a16d7caa7 --- /dev/null +++ b/examples/todo-app-sqlite/src/lib.rs @@ -0,0 +1,22 @@ +use cfg_if::cfg_if; +use leptos::*; +pub mod todo; + +// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong. +cfg_if! { + if #[cfg(feature = "hydrate")] { + use wasm_bindgen::prelude::wasm_bindgen; + use crate::todo::*; + + #[wasm_bindgen] + pub fn hydrate() { + console_error_panic_hook::set_once(); + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + + leptos::hydrate(body().unwrap(), |cx| { + view! { cx, } + }); + } + } +} diff --git a/examples/todo-app-sqlite/src/main.rs b/examples/todo-app-sqlite/src/main.rs new file mode 100644 index 000000000..de5fb02ae --- /dev/null +++ b/examples/todo-app-sqlite/src/main.rs @@ -0,0 +1,108 @@ +use cfg_if::cfg_if; +use leptos::*; +use leptos_router::*; +mod todo; + +// boilerplate to run in different modes +cfg_if! { + // server-only stuff + if #[cfg(feature = "ssr")] { + use actix_files::{Files}; + use actix_web::*; + use crate::todo::*; + + #[get("{tail:.*}")] + async fn render(req: HttpRequest) -> impl Responder { + let path = req.path(); + let path = "http://leptos".to_string() + path; + println!("path = {path}"); + + HttpResponse::Ok().content_type("text/html").body(format!( + r#" + + + + + Leptos Todos + + + {} + + + "#, + run_scope({ + move |cx| { + let integration = ServerIntegration { path: path.clone() }; + provide_context(cx, RouterIntegrationContext::new(integration)); + + view! { cx, } + } + }) + )) + } + + #[post("/api/{tail:.*}")] + async fn handle_server_fns( + req: HttpRequest, + params: web::Path, + body: web::Bytes, + ) -> impl Responder { + let path = params.into_inner(); + let accept_header = req + .headers() + .get("Accept") + .and_then(|value| value.to_str().ok()); + + if let Some(server_fn) = server_fn_by_path(path.as_str()) { + let body: &[u8] = &body; + match server_fn(&body).await { + Ok(serialized) => { + // if this is Accept: application/json then send a serialized JSON response + if let Some("application/json") = accept_header { + HttpResponse::Ok().body(serialized) + } + // otherwise, it's probably a
submit or something: redirect back to the referrer + else { + HttpResponse::SeeOther() + .insert_header(("Location", "/")) + .content_type("application/json") + .body(serialized) + } + } + Err(e) => { + eprintln!("server function error: {e:#?}"); + HttpResponse::InternalServerError().body(e.to_string()) + } + } + } else { + HttpResponse::BadRequest().body(format!("Could not find a server function at that route.")) + } + } + + #[actix_web::main] + async fn main() -> std::io::Result<()> { + let mut conn = db().await.expect("couldn't connect to DB"); + sqlx::migrate!() + .run(&mut conn) + .await + .expect("could not run SQLx migrations"); + + crate::todo::register_server_functions(); + + HttpServer::new(|| { + App::new() + .service(Files::new("/pkg", "./pkg")) + .service(handle_server_fns) + .service(render) + //.wrap(middleware::Compress::default()) + }) + .bind(("127.0.0.1", 8081))? + .run() + .await + } + } + + else { + // no client-only version is possible, because we're depending on the server + } +} diff --git a/examples/todo-app-sqlite/src/todo.rs b/examples/todo-app-sqlite/src/todo.rs new file mode 100644 index 000000000..a87b78a3e --- /dev/null +++ b/examples/todo-app-sqlite/src/todo.rs @@ -0,0 +1,131 @@ +use cfg_if::cfg_if; +use leptos::*; +use leptos_router::*; +use serde::{Deserialize, Serialize}; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use sqlx::{Connection, SqliteConnection}; + + pub async fn db() -> Result { + Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?) + } + + pub fn register_server_functions() { + GetTodos::register(); + AddTodo::register(); + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] + pub struct Todo { + id: u16, + title: String, + completed: bool, + } + } else { + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] + pub struct Todo { + id: u16, + title: String, + completed: bool, + } + } +} + +#[server(GetTodos, "/api")] +pub async fn get_todos() -> Result, ServerFnError> { + use futures::TryStreamExt; + + let mut conn = db().await?; + + let mut todos = Vec::new(); + let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn); + while let Some(row) = rows + .try_next() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + { + todos.push(row); + } + + Ok(todos) +} + +#[server(AddTodo, "/api")] +pub async fn add_todo(title: String) -> Result { + use futures::TryStreamExt; + + let mut conn = db().await?; + + match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)") + .bind(title) + .execute(&mut conn) + .await + { + Ok(row) => Ok(0), + Err(e) => Err(ServerFnError::ServerError(e.to_string())), + } +} + +#[component] +pub fn TodoApp(cx: Scope) -> Element { + view! { + cx, +
+ +
+

"My Tasks"

+
+
+ + + }/> + +
+
+
+ } +} + +#[component] +pub fn Todos(cx: Scope) -> Element { + let add_todo = create_server_multi_action::(cx); + let add_changed = add_todo.version; + + let todos = create_resource(cx, move || add_changed(), |_| get_todos()); + let todos_view = move || { + todos.read().map(|todos| match todos { + Err(e) => view! { cx,
"Server Error: " {e.to_string()}
}, + Ok(todos) => { + if todos.is_empty() { + view! { cx,

"No tasks were found."

} + } else { + let todos = todos + .into_iter() + .map(|todo| view! { cx,
  • {todo.title}
  • }) + .collect::>(); + view! { + cx, +
      {todos}
    + } + } + } + }) + }; + + view! { + cx, +
    + + + + + {todos_view} +
    + } +} From 412693c2c3059b72e277f637d5913eb4c3892a26 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 18 Nov 2022 13:25:46 -0500 Subject: [PATCH 4/7] `MultiAction`, `create_multi_action`, `create_server_multi_action`, and `MultiActionForm` --- Cargo.toml | 1 + leptos_server/src/action.rs | 271 +++++++++++++++++++++++++++++ leptos_server/src/lib.rs | 275 +---------------------------- leptos_server/src/multi_action.rs | 276 ++++++++++++++++++++++++++++++ router/Cargo.toml | 1 + router/src/components/form.rs | 255 +++++++++++++++++---------- 6 files changed, 721 insertions(+), 358 deletions(-) create mode 100644 leptos_server/src/action.rs create mode 100644 leptos_server/src/multi_action.rs diff --git a/Cargo.toml b/Cargo.toml index f98ca2ecd..15f8e1777 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "examples/parent-child", "examples/router", "examples/todomvc", + "examples/todo-app-sqlite", "examples/view-tests", # book diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs new file mode 100644 index 000000000..9f3cb4f05 --- /dev/null +++ b/leptos_server/src/action.rs @@ -0,0 +1,271 @@ +use crate::{ServerFn, ServerFnError}; +use leptos_reactive::{create_rw_signal, spawn_local, ReadSignal, RwSignal, Scope}; +use std::{future::Future, pin::Pin, rc::Rc}; + +/// An action synchronizes an imperative `async` call to the synchronous reactive system. +/// +/// If you’re trying to load data by running an `async` function reactively, you probably +/// want to use a [Resource](leptos_reactive::Resource) instead. If you’re trying to occasionally +/// run an `async` function in response to something like a user clicking a button, you're in the right place. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_action; +/// # run_scope(|cx| { +/// async fn send_new_todo_to_api(task: String) -> usize { +/// // do something... +/// // return a task id +/// 42 +/// } +/// let save_data = create_action(cx, |task: &String| { +/// // `task` is given as `&String` because its value is available in `input` +/// send_new_todo_to_api(task.clone()) +/// }); +/// +/// // the argument currently running +/// let input = save_data.input(); +/// // the most recent returned result +/// let result_of_call = save_data.value(); +/// // whether the call is pending +/// let pending = save_data.pending(); +/// // how many times the action has run +/// // useful for reactively updating something else in response to a `dispatch` and response +/// let version = save_data.version; +/// +/// // before we do anything +/// assert_eq!(input(), None); // no argument yet +/// assert_eq!(pending(), false); // isn't pending a response +/// assert_eq!(result_of_call(), None); // there's no "last value" +/// assert_eq!(version(), 0); +/// # if !cfg!(any(feature = "csr", feature = "hydrate")) { +/// // dispatch the action +/// save_data.dispatch("My todo".to_string()); +/// +/// // when we're making the call +/// // assert_eq!(input(), Some("My todo".to_string())); +/// // assert_eq!(pending(), true); // is pending +/// // assert_eq!(result_of_call(), None); // has not yet gotten a response +/// +/// // after call has resolved +/// assert_eq!(input(), None); // input clears out after resolved +/// assert_eq!(pending(), false); // no longer pending +/// assert_eq!(result_of_call(), Some(42)); +/// assert_eq!(version(), 1); +/// # } +/// # }); +/// ``` +/// +/// The input to the `async` function should always be a single value, +/// but it can be of any type. The argument is always passed by reference to the +/// function, because it is stored in [Action::input] as well. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_action; +/// # run_scope(|cx| { +/// // if there's a single argument, just use that +/// let action1 = create_action(cx, |input: &String| { +/// let input = input.clone(); +/// async move { todo!() } +/// }); +/// +/// // if there are no arguments, use the unit type `()` +/// let action2 = create_action(cx, |input: &()| async { todo!() }); +/// +/// // if there are multiple arguments, use a tuple +/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() }); +/// # }); +/// ``` +#[derive(Clone)] +pub struct Action +where + I: 'static, + O: 'static, +{ + /// How many times the action has successfully resolved. + pub version: RwSignal, + /// The current argument that was dispatched to the `async` function. + /// `Some` while we are waiting for it to resolve, `None` if it has resolved. + pub input: RwSignal>, + /// The most recent return value of the `async` function. + pub value: RwSignal>, + pending: RwSignal, + url: Option, + #[allow(clippy::complexity)] + action_fn: Rc Pin>>>, +} + +impl Action +where + I: 'static, + O: 'static, +{ + /// Calls the `async` function with a reference to the input type as its argument. + pub fn dispatch(&self, input: I) { + let fut = (self.action_fn)(&input); + self.input.set(Some(input)); + let input = self.input; + let version = self.version; + let pending = self.pending; + let value = self.value; + pending.set(true); + spawn_local(async move { + let new_value = fut.await; + input.set(None); + pending.set(false); + value.set(Some(new_value)); + version.update(|n| *n += 1); + }) + } + + /// Whether the action has been dispatched and is currently waiting for its future to be resolved. + pub fn pending(&self) -> ReadSignal { + self.pending.read_only() + } + + /// The URL associated with the action (typically as part of a server function.) + /// This enables integration with the `ActionForm` component in `leptos_router`. + pub fn url(&self) -> Option<&str> { + self.url.as_deref() + } + + /// Associates the URL of the given server function with this action. + /// This enables integration with the `ActionForm` component in `leptos_router`. + pub fn using_server_fn(mut self) -> Self { + let prefix = T::prefix(); + self.url = if prefix.is_empty() { + Some(T::url().to_string()) + } else { + Some(prefix.to_string() + "/" + T::url()) + }; + self + } +} + +/// Creates an [Action] to synchronize an imperative `async` call to the synchronous reactive system. +/// +/// If you’re trying to load data by running an `async` function reactively, you probably +/// want to use a [create_resource](leptos_reactive::create_resource) instead. If you’re trying +/// to occasionally run an `async` function in response to something like a user clicking a button, +/// you're in the right place. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_action; +/// # run_scope(|cx| { +/// async fn send_new_todo_to_api(task: String) -> usize { +/// // do something... +/// // return a task id +/// 42 +/// } +/// let save_data = create_action(cx, |task: &String| { +/// // `task` is given as `&String` because its value is available in `input` +/// send_new_todo_to_api(task.clone()) +/// }); +/// +/// // the argument currently running +/// let input = save_data.input(); +/// // the most recent returned result +/// let result_of_call = save_data.value(); +/// // whether the call is pending +/// let pending = save_data.pending(); +/// // how many times the action has run +/// // useful for reactively updating something else in response to a `dispatch` and response +/// let version = save_data.version; +/// +/// // before we do anything +/// assert_eq!(input(), None); // no argument yet +/// assert_eq!(pending(), false); // isn't pending a response +/// assert_eq!(result_of_call(), None); // there's no "last value" +/// assert_eq!(version(), 0); +/// # if !cfg!(any(feature = "csr", feature = "hydrate")) { +/// // dispatch the action +/// save_data.dispatch("My todo".to_string()); +/// +/// // when we're making the call +/// // assert_eq!(input(), Some("My todo".to_string())); +/// // assert_eq!(pending(), true); // is pending +/// // assert_eq!(result_of_call(), None); // has not yet gotten a response +/// +/// // after call has resolved +/// assert_eq!(input(), None); // input clears out after resolved +/// assert_eq!(pending(), false); // no longer pending +/// assert_eq!(result_of_call(), Some(42)); +/// assert_eq!(version(), 1); +/// # } +/// # }); +/// ``` +/// +/// The input to the `async` function should always be a single value, +/// but it can be of any type. The argument is always passed by reference to the +/// function, because it is stored in [Action::input] as well. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_action; +/// # run_scope(|cx| { +/// // if there's a single argument, just use that +/// let action1 = create_action(cx, |input: &String| { +/// let input = input.clone(); +/// async move { todo!() } +/// }); +/// +/// // if there are no arguments, use the unit type `()` +/// let action2 = create_action(cx, |input: &()| async { todo!() }); +/// +/// // if there are multiple arguments, use a tuple +/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() }); +/// # }); +/// ``` +pub fn create_action(cx: Scope, action_fn: F) -> Action +where + I: 'static, + O: 'static, + F: Fn(&I) -> Fu + 'static, + Fu: Future + 'static, +{ + let version = create_rw_signal(cx, 0); + let input = create_rw_signal(cx, None); + let value = create_rw_signal(cx, None); + let pending = create_rw_signal(cx, false); + let action_fn = Rc::new(move |input: &I| { + let fut = action_fn(input); + Box::pin(async move { fut.await }) as Pin>> + }); + + Action { + version, + url: None, + input, + value, + pending, + action_fn, + } +} + +/// Creates an [Action] that can be used to call a server function. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::{create_server_action, ServerFnError, ServerFn}; +/// # use leptos_macro::server; +/// +/// #[server(MyServerFn)] +/// async fn my_server_fn() -> Result<(), ServerFnError> { +/// todo!() +/// } +/// +/// # run_scope(|cx| { +/// let my_server_action = create_server_action::(cx); +/// # }); +/// ``` +pub fn create_server_action(cx: Scope) -> Action> +where + S: Clone + ServerFn, +{ + #[cfg(feature = "ssr")] + let c = |args: &S| S::call_fn(args.clone()); + #[cfg(not(feature = "ssr"))] + let c = |args: &S| S::call_fn_client(args.clone()); + create_action(cx, c).using_server_fn::() +} diff --git a/leptos_server/src/lib.rs b/leptos_server/src/lib.rs index 24263365b..924bd347d 100644 --- a/leptos_server/src/lib.rs +++ b/leptos_server/src/lib.rs @@ -62,9 +62,14 @@ pub use form_urlencoded; use leptos_reactive::*; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{future::Future, pin::Pin, rc::Rc}; +use std::{future::Future, pin::Pin}; use thiserror::Error; +mod action; +mod multi_action; +pub use action::*; +pub use multi_action::*; + #[cfg(any(feature = "ssr", doc))] use std::{ collections::HashMap, @@ -264,271 +269,3 @@ where T::from_json(&text).map_err(|e| ServerFnError::Deserialization(e.to_string())) } - -/// An action synchronizes an imperative `async` call to the synchronous reactive system. -/// -/// If you’re trying to load data by running an `async` function reactively, you probably -/// want to use a [Resource](leptos_reactive::Resource) instead. If you’re trying to occasionally -/// run an `async` function in response to something like a user clicking a button, you're in the right place. -/// -/// ```rust -/// # use leptos_reactive::run_scope; -/// # use leptos_server::create_action; -/// # run_scope(|cx| { -/// async fn send_new_todo_to_api(task: String) -> usize { -/// // do something... -/// // return a task id -/// 42 -/// } -/// let save_data = create_action(cx, |task: &String| { -/// // `task` is given as `&String` because its value is available in `input` -/// send_new_todo_to_api(task.clone()) -/// }); -/// -/// // the argument currently running -/// let input = save_data.input(); -/// // the most recent returned result -/// let result_of_call = save_data.value(); -/// // whether the call is pending -/// let pending = save_data.pending(); -/// // how many times the action has run -/// // useful for reactively updating something else in response to a `dispatch` and response -/// let version = save_data.version; -/// -/// // before we do anything -/// assert_eq!(input(), None); // no argument yet -/// assert_eq!(pending(), false); // isn't pending a response -/// assert_eq!(result_of_call(), None); // there's no "last value" -/// assert_eq!(version(), 0); -/// # if !cfg!(any(feature = "csr", feature = "hydrate")) { -/// // dispatch the action -/// save_data.dispatch("My todo".to_string()); -/// -/// // when we're making the call -/// // assert_eq!(input(), Some("My todo".to_string())); -/// // assert_eq!(pending(), true); // is pending -/// // assert_eq!(result_of_call(), None); // has not yet gotten a response -/// -/// // after call has resolved -/// assert_eq!(input(), None); // input clears out after resolved -/// assert_eq!(pending(), false); // no longer pending -/// assert_eq!(result_of_call(), Some(42)); -/// assert_eq!(version(), 1); -/// # } -/// # }); -/// ``` -/// -/// The input to the `async` function should always be a single value, -/// but it can be of any type. The argument is always passed by reference to the -/// function, because it is stored in [Action::input] as well. -/// -/// ```rust -/// # use leptos_reactive::run_scope; -/// # use leptos_server::create_action; -/// # run_scope(|cx| { -/// // if there's a single argument, just use that -/// let action1 = create_action(cx, |input: &String| { -/// let input = input.clone(); -/// async move { todo!() } -/// }); -/// -/// // if there are no arguments, use the unit type `()` -/// let action2 = create_action(cx, |input: &()| async { todo!() }); -/// -/// // if there are multiple arguments, use a tuple -/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() }); -/// # }); -/// ``` -#[derive(Clone)] -pub struct Action -where - I: 'static, - O: 'static, -{ - /// How many times the action has successfully resolved. - pub version: RwSignal, - /// The current argument that was dispatched to the `async` function. - /// `Some` while we are waiting for it to resolve, `None` if it has resolved. - pub input: RwSignal>, - /// The most recent return value of the `async` function. - pub value: RwSignal>, - pending: RwSignal, - url: Option, - #[allow(clippy::complexity)] - action_fn: Rc Pin>>>, -} - -impl Action -where - I: 'static, - O: 'static, -{ - /// Calls the server function a reference to the input type as its argument. - pub fn dispatch(&self, input: I) { - let fut = (self.action_fn)(&input); - self.input.set(Some(input)); - let input = self.input; - let version = self.version; - let pending = self.pending; - let value = self.value; - pending.set(true); - spawn_local(async move { - let new_value = fut.await; - input.set(None); - pending.set(false); - value.set(Some(new_value)); - version.update(|n| *n += 1); - }) - } - - /// Whether the action has been dispatched and is currently waiting for its future to be resolved. - pub fn pending(&self) -> ReadSignal { - self.pending.read_only() - } - - /// The URL associated with the action (typically as part of a server function.) - /// This enables integration with the `ActionForm` component in `leptos_router`. - pub fn url(&self) -> Option<&str> { - self.url.as_deref() - } - - /// Associates the URL of the given server function with this action. - /// This enables integration with the `ActionForm` component in `leptos_router`. - pub fn using_server_fn(mut self) -> Self { - let prefix = T::prefix(); - self.url = if prefix.is_empty() { - Some(T::url().to_string()) - } else { - Some(prefix.to_string() + "/" + T::url()) - }; - self - } -} - -/// Creates an [Action] to synchronize an imperative `async` call to the synchronous reactive system. -/// -/// If you’re trying to load data by running an `async` function reactively, you probably -/// want to use a [create_resource](leptos_reactive::create_resource) instead. If you’re trying -/// to occasionally run an `async` function in response to something like a user clicking a button, -/// you're in the right place. -/// -/// ```rust -/// # use leptos_reactive::run_scope; -/// # use leptos_server::create_action; -/// # run_scope(|cx| { -/// async fn send_new_todo_to_api(task: String) -> usize { -/// // do something... -/// // return a task id -/// 42 -/// } -/// let save_data = create_action(cx, |task: &String| { -/// // `task` is given as `&String` because its value is available in `input` -/// send_new_todo_to_api(task.clone()) -/// }); -/// -/// // the argument currently running -/// let input = save_data.input(); -/// // the most recent returned result -/// let result_of_call = save_data.value(); -/// // whether the call is pending -/// let pending = save_data.pending(); -/// // how many times the action has run -/// // useful for reactively updating something else in response to a `dispatch` and response -/// let version = save_data.version; -/// -/// // before we do anything -/// assert_eq!(input(), None); // no argument yet -/// assert_eq!(pending(), false); // isn't pending a response -/// assert_eq!(result_of_call(), None); // there's no "last value" -/// assert_eq!(version(), 0); -/// # if !cfg!(any(feature = "csr", feature = "hydrate")) { -/// // dispatch the action -/// save_data.dispatch("My todo".to_string()); -/// -/// // when we're making the call -/// // assert_eq!(input(), Some("My todo".to_string())); -/// // assert_eq!(pending(), true); // is pending -/// // assert_eq!(result_of_call(), None); // has not yet gotten a response -/// -/// // after call has resolved -/// assert_eq!(input(), None); // input clears out after resolved -/// assert_eq!(pending(), false); // no longer pending -/// assert_eq!(result_of_call(), Some(42)); -/// assert_eq!(version(), 1); -/// # } -/// # }); -/// ``` -/// -/// The input to the `async` function should always be a single value, -/// but it can be of any type. The argument is always passed by reference to the -/// function, because it is stored in [Action::input] as well. -/// -/// ```rust -/// # use leptos_reactive::run_scope; -/// # use leptos_server::create_action; -/// # run_scope(|cx| { -/// // if there's a single argument, just use that -/// let action1 = create_action(cx, |input: &String| { -/// let input = input.clone(); -/// async move { todo!() } -/// }); -/// -/// // if there are no arguments, use the unit type `()` -/// let action2 = create_action(cx, |input: &()| async { todo!() }); -/// -/// // if there are multiple arguments, use a tuple -/// let action3 = create_action(cx, |input: &(usize, String)| async { todo!() }); -/// # }); -/// ``` -pub fn create_action(cx: Scope, action_fn: F) -> Action -where - I: 'static, - O: 'static, - F: Fn(&I) -> Fu + 'static, - Fu: Future + 'static, -{ - let version = create_rw_signal(cx, 0); - let input = create_rw_signal(cx, None); - let value = create_rw_signal(cx, None); - let pending = create_rw_signal(cx, false); - let action_fn = Rc::new(move |input: &I| { - let fut = action_fn(input); - Box::pin(async move { fut.await }) as Pin>> - }); - - Action { - version, - url: None, - input, - value, - pending, - action_fn, - } -} - -/// Creates an [Action] that can be used to call a server function. -/// -/// ```rust -/// # use leptos_reactive::run_scope; -/// # use leptos_server::{create_server_action, ServerFnError, ServerFn}; -/// # use leptos_macro::server; -/// -/// #[server(MyServerFn)] -/// async fn my_server_fn() -> Result<(), ServerFnError> { -/// todo!() -/// } -/// -/// # run_scope(|cx| { -/// let my_server_action = create_server_action::(cx); -/// # }); -/// ``` -pub fn create_server_action(cx: Scope) -> Action> -where - S: Clone + ServerFn, -{ - #[cfg(feature = "ssr")] - let c = |args: &S| S::call_fn(args.clone()); - #[cfg(not(feature = "ssr"))] - let c = |args: &S| S::call_fn_client(args.clone()); - create_action(cx, c).using_server_fn::() -} diff --git a/leptos_server/src/multi_action.rs b/leptos_server/src/multi_action.rs new file mode 100644 index 000000000..5943bf6ed --- /dev/null +++ b/leptos_server/src/multi_action.rs @@ -0,0 +1,276 @@ +use crate::{ServerFn, ServerFnError}; +use leptos_reactive::{create_rw_signal, spawn_local, ReadSignal, RwSignal, Scope}; +use std::{future::Future, pin::Pin, rc::Rc}; + +/// An action that synchronizes multiple imperative `async` calls to the reactive system, +/// tracking the progress of each one. +/// +/// Where an [Action](crate::Action) fires a single call, a `MultiAction` allows you to +/// keep track of multiple in-flight actions. +/// +/// If you’re trying to load data by running an `async` function reactively, you probably +/// want to use a [Resource](leptos_reactive::Resource) instead. If you’re trying to occasionally +/// run an `async` function in response to something like a user adding a task to a todo list, +/// you’re in the right place. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_multi_action; +/// # run_scope(|cx| { +/// async fn send_new_todo_to_api(task: String) -> usize { +/// // do something... +/// // return a task id +/// 42 +/// } +/// let add_todo = create_multi_action(cx, |task: &String| { +/// // `task` is given as `&String` because its value is available in `input` +/// send_new_todo_to_api(task.clone()) +/// }); +/// +/// add_todo.dispatch("Buy milk".to_string()); +/// add_todo.dispatch("???".to_string()); +/// add_todo.dispatch("Profit!!!".to_string()); +/// # }); +/// ``` +/// +/// The input to the `async` function should always be a single value, +/// but it can be of any type. The argument is always passed by reference to the +/// function, because it is stored in [MultiAction::input] as well. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_multi_action; +/// # run_scope(|cx| { +/// // if there's a single argument, just use that +/// let action1 = create_multi_action(cx, |input: &String| { +/// let input = input.clone(); +/// async move { todo!() } +/// }); +/// +/// // if there are no arguments, use the unit type `()` +/// let action2 = create_multi_action(cx, |input: &()| async { todo!() }); +/// +/// // if there are multiple arguments, use a tuple +/// let action3 = create_multi_action(cx, |input: &(usize, String)| async { todo!() }); +/// # }); +/// ``` +#[derive(Clone)] +pub struct MultiAction +where + I: 'static, + O: 'static, +{ + cx: Scope, + /// How many times an action has successfully resolved. + pub version: RwSignal, + submissions: RwSignal>>, + url: Option, + #[allow(clippy::complexity)] + action_fn: Rc Pin>>>, +} + +/// An action that has been submitted by dispatching it to a [MultiAction](crate::MultiAction). +pub struct Submission +where + I: 'static, + O: 'static, +{ + /// The current argument that was dispatched to the `async` function. + /// `Some` while we are waiting for it to resolve, `None` if it has resolved. + pub input: RwSignal>, + /// The most recent return value of the `async` function. + pub value: RwSignal>, + pub(crate) pending: RwSignal, + /// Controls this submission has been canceled. + pub canceled: RwSignal, +} + +impl Clone for Submission { + fn clone(&self) -> Self { + Self { + input: self.input, + value: self.value, + pending: self.pending, + canceled: self.canceled, + } + } +} + +impl Copy for Submission {} + +impl Submission +where + I: 'static, + O: 'static, +{ + /// Whether this submission is currently waiting to resolve. + pub fn pending(&self) -> ReadSignal { + self.pending.read_only() + } + + /// Cancels the submission, preventing it from resolving. + pub fn cancel(&self) { + self.canceled.set(true); + } +} + +impl MultiAction +where + I: 'static, + O: 'static, +{ + /// Calls the `async` function with a reference to the input type as its argument. + pub fn dispatch(&self, input: I) { + let cx = self.cx; + let fut = (self.action_fn)(&input); + + let submission = Submission { + input: create_rw_signal(cx, Some(input)), + value: create_rw_signal(cx, None), + pending: create_rw_signal(cx, true), + canceled: create_rw_signal(cx, false), + }; + + self.submissions.update(|subs| subs.push(submission)); + + let canceled = submission.canceled; + let input = submission.input; + let pending = submission.pending; + let value = submission.value; + let version = self.version; + + spawn_local(async move { + let new_value = fut.await; + let canceled = cx.untrack(move || canceled.get()); + input.set(None); + pending.set(false); + if !canceled { + value.set(Some(new_value)); + } + version.update(|n| *n += 1); + }) + } + + /// The set of all submissions to this multi-action. + pub fn submissions(&self) -> ReadSignal>> { + self.submissions.read_only() + } + + /// The URL associated with the action (typically as part of a server function.) + /// This enables integration with the `MultiActionForm` component in `leptos_router`. + pub fn url(&self) -> Option<&str> { + self.url.as_deref() + } + + /// Associates the URL of the given server function with this action. + /// This enables integration with the `MultiActionForm` component in `leptos_router`. + pub fn using_server_fn(mut self) -> Self { + let prefix = T::prefix(); + self.url = if prefix.is_empty() { + Some(T::url().to_string()) + } else { + Some(prefix.to_string() + "/" + T::url()) + }; + self + } +} + +/// Creates an [MultiAction] to synchronize an imperative `async` call to the synchronous reactive system. +/// +/// If you’re trying to load data by running an `async` function reactively, you probably +/// want to use a [create_resource](leptos_reactive::create_resource) instead. If you’re trying +/// to occasionally run an `async` function in response to something like a user clicking a button, +/// you're in the right place. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_multi_action; +/// # run_scope(|cx| { +/// async fn send_new_todo_to_api(task: String) -> usize { +/// // do something... +/// // return a task id +/// 42 +/// } +/// let add_todo = create_multi_action(cx, |task: &String| { +/// // `task` is given as `&String` because its value is available in `input` +/// send_new_todo_to_api(task.clone()) +/// }); +/// +/// add_todo.dispatch("Buy milk".to_string()); +/// add_todo.dispatch("???".to_string()); +/// add_todo.dispatch("Profit!!!".to_string()); +/// +/// assert_eq!(add_todo.submissions().get().len(), 3); +/// # }); +/// ``` +/// +/// The input to the `async` function should always be a single value, +/// but it can be of any type. The argument is always passed by reference to the +/// function, because it is stored in [MultiAction::input] as well. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::create_multi_action; +/// # run_scope(|cx| { +/// // if there's a single argument, just use that +/// let action1 = create_multi_action(cx, |input: &String| { +/// let input = input.clone(); +/// async move { todo!() } +/// }); +/// +/// // if there are no arguments, use the unit type `()` +/// let action2 = create_multi_action(cx, |input: &()| async { todo!() }); +/// +/// // if there are multiple arguments, use a tuple +/// let action3 = create_multi_action(cx, |input: &(usize, String)| async { todo!() }); +/// # }); +/// ``` +pub fn create_multi_action(cx: Scope, action_fn: F) -> MultiAction +where + I: 'static, + O: 'static, + F: Fn(&I) -> Fu + 'static, + Fu: Future + 'static, +{ + let version = create_rw_signal(cx, 0); + let submissions = create_rw_signal(cx, Vec::new()); + let action_fn = Rc::new(move |input: &I| { + let fut = action_fn(input); + Box::pin(async move { fut.await }) as Pin>> + }); + + MultiAction { + cx, + version, + submissions, + url: None, + action_fn, + } +} + +/// Creates an [MultiAction] that can be used to call a server function. +/// +/// ```rust +/// # use leptos_reactive::run_scope; +/// # use leptos_server::{create_server_multi_action, ServerFnError, ServerFn}; +/// # use leptos_macro::server; +/// +/// #[server(MyServerFn)] +/// async fn my_server_fn() -> Result<(), ServerFnError> { +/// todo!() +/// } +/// +/// # run_scope(|cx| { +/// let my_server_multi_action = create_server_multi_action::(cx); +/// # }); +/// ``` +pub fn create_server_multi_action(cx: Scope) -> MultiAction> +where + S: Clone + ServerFn, +{ + #[cfg(feature = "ssr")] + let c = |args: &S| S::call_fn(args.clone()); + #[cfg(not(feature = "ssr"))] + let c = |args: &S| S::call_fn_client(args.clone()); + create_multi_action(cx, c).using_server_fn::() +} diff --git a/router/Cargo.toml b/router/Cargo.toml index 5d7e716f1..2058742bc 100644 --- a/router/Cargo.toml +++ b/router/Cargo.toml @@ -23,6 +23,7 @@ urlencoding = "2" thiserror = "1" typed-builder = "0.10" serde_urlencoded = "0.7" +serde = "1" js-sys = { version = "0.3" } wasm-bindgen = { version = "0.2" } wasm-bindgen-futures = { version = "0.4" } diff --git a/router/src/components/form.rs b/router/src/components/form.rs index b54ad7bf1..524dc8c42 100644 --- a/router/src/components/form.rs +++ b/router/src/components/form.rs @@ -69,93 +69,9 @@ where return; } ev.prevent_default(); - let submitter = ev.unchecked_ref::().submitter(); let navigate = use_navigate(cx); - let (form, method, action, enctype) = match &submitter { - Some(el) => { - if let Some(form) = el.dyn_ref::() { - ( - form.clone(), - form.get_attribute("method") - .unwrap_or_else(|| "get".to_string()) - .to_lowercase(), - form.get_attribute("action") - .unwrap_or_else(|| "".to_string()) - .to_lowercase(), - form.get_attribute("enctype") - .unwrap_or_else(|| "application/x-www-form-urlencoded".to_string()) - .to_lowercase(), - ) - } else if let Some(input) = el.dyn_ref::() { - let form = ev - .target() - .unwrap() - .unchecked_into::(); - ( - form.clone(), - input.get_attribute("method").unwrap_or_else(|| { - form.get_attribute("method") - .unwrap_or_else(|| "get".to_string()) - .to_lowercase() - }), - input.get_attribute("action").unwrap_or_else(|| { - form.get_attribute("action") - .unwrap_or_else(|| "".to_string()) - .to_lowercase() - }), - input.get_attribute("enctype").unwrap_or_else(|| { - form.get_attribute("enctype") - .unwrap_or_else(|| "application/x-www-form-urlencoded".to_string()) - .to_lowercase() - }), - ) - } else if let Some(button) = el.dyn_ref::() { - let form = ev - .target() - .unwrap() - .unchecked_into::(); - ( - form.clone(), - button.get_attribute("method").unwrap_or_else(|| { - form.get_attribute("method") - .unwrap_or_else(|| "get".to_string()) - .to_lowercase() - }), - button.get_attribute("action").unwrap_or_else(|| { - form.get_attribute("action") - .unwrap_or_else(|| "".to_string()) - .to_lowercase() - }), - button.get_attribute("enctype").unwrap_or_else(|| { - form.get_attribute("enctype") - .unwrap_or_else(|| "application/x-www-form-urlencoded".to_string()) - .to_lowercase() - }), - ) - } else { - leptos_dom::debug_warn!(" cannot be submitted from a tag other than , , or