mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
Use serde_urlencoded
for server functions (making it easier to use normal text inputs for forms)
This commit is contained in:
parent
f8de0fff81
commit
22eaa92355
6 changed files with 48 additions and 45 deletions
|
@ -7,7 +7,14 @@ edition = "2021"
|
|||
actix-files = "0.6"
|
||||
actix-web = { version = "4" }
|
||||
futures = "0.3"
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["ssr", "serde"] }
|
||||
leptos_router = { path = "../../../router", default-features = false, features = ["ssr"] }
|
||||
counter-isomorphic = { path = "../counter", default-features = false, features = ["ssr"] }
|
||||
leptos = { path = "../../../leptos", default-features = false, features = [
|
||||
"ssr",
|
||||
"serde",
|
||||
] }
|
||||
leptos_router = { path = "../../../router", default-features = false, features = [
|
||||
"ssr",
|
||||
] }
|
||||
counter-isomorphic = { path = "../counter", default-features = false, features = [
|
||||
"ssr",
|
||||
] }
|
||||
lazy_static = "1"
|
||||
|
|
|
@ -9,7 +9,7 @@ use syn::{
|
|||
};
|
||||
|
||||
pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Result<TokenStream2> {
|
||||
let ServerFnName { struct_name, comma, prefix } = syn::parse::<ServerFnName>(args)?;
|
||||
let ServerFnName { struct_name, prefix, .. } = syn::parse::<ServerFnName>(args)?;
|
||||
let prefix = prefix.unwrap_or_else(|| Literal::string(""));
|
||||
|
||||
let body = syn::parse::<ServerFnBody>(s.into())?;
|
||||
|
@ -107,7 +107,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
};
|
||||
|
||||
Ok(quote::quote! {
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
|
||||
pub struct #struct_name {
|
||||
#(#fields),*
|
||||
}
|
||||
|
@ -115,23 +115,14 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
impl ServerFn for #struct_name {
|
||||
type Output = #output_ty;
|
||||
|
||||
fn prefix() -> &'static str {
|
||||
#prefix
|
||||
}
|
||||
|
||||
fn url() -> &'static str {
|
||||
#url
|
||||
}
|
||||
|
||||
fn as_form_data(&self) -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
#(#as_form_data_fields),*
|
||||
]
|
||||
}
|
||||
|
||||
fn from_form_data(data: &[u8]) -> Result<Self, ServerFnError> {
|
||||
let data = ::leptos::form_urlencoded::parse(data).collect::<Vec<_>>();
|
||||
Ok(Self {
|
||||
#(#from_form_data_fields),*
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn call_fn(self) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Output, ::leptos::ServerFnError>> + Send>> {
|
||||
let #struct_name { #(#field_names),* } = self;
|
||||
|
@ -151,7 +142,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
|
|||
}
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
#vis async fn #fn_name(#(#fn_args_2),*) #output_arrow #return_ty {
|
||||
let prefix = #prefix.to_string();
|
||||
let prefix = #struct_name::prefix().to_string();
|
||||
let url = prefix + "/" + #struct_name::url();
|
||||
::leptos::call_server_fn(&url, #struct_name { #(#field_names_5),* }).await
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ form_urlencoded = "1"
|
|||
gloo-net = "0.2"
|
||||
lazy_static = "1"
|
||||
linear-map = "1"
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_urlencoded = "0.7"
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
pub use form_urlencoded;
|
||||
use leptos_reactive::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::{future::Future, pin::Pin, rc::Rc};
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -145,20 +145,17 @@ pub fn server_fn_by_path(path: &str) -> Option<Arc<ServerFnTraitObj>> {
|
|||
/// Technically, the trait is implemented on a type that describes the server function's arguments.
|
||||
pub trait ServerFn
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
Self: Serialize + DeserializeOwned + Sized + 'static,
|
||||
{
|
||||
/// The return type of the function.
|
||||
type Output: Serializable;
|
||||
|
||||
/// URL prefix that should be prepended by the client to the generated URL.
|
||||
fn prefix() -> &'static str;
|
||||
|
||||
/// The path at which the server function can be reached on the server.
|
||||
fn url() -> &'static str;
|
||||
|
||||
/// A set of `(input_name, input_value)` pairs used to serialize the arguments to the server function.
|
||||
fn as_form_data(&self) -> Vec<(&'static str, String)>;
|
||||
|
||||
/// Deserializes the arguments to the server function from form data.
|
||||
fn from_form_data(data: &[u8]) -> Result<Self, ServerFnError>;
|
||||
|
||||
/// Runs the function on the server.
|
||||
#[cfg(any(feature = "ssr", doc))]
|
||||
fn call_fn(self) -> Pin<Box<dyn Future<Output = Result<Self::Output, ServerFnError>> + Send>>;
|
||||
|
@ -174,7 +171,8 @@ where
|
|||
// takes a String -> returns its async value
|
||||
let run_server_fn = Arc::new(|data: &[u8]| {
|
||||
// decode the args
|
||||
let value = Self::from_form_data(data);
|
||||
let value = serde_urlencoded::from_bytes::<Self>(data)
|
||||
.map_err(|e| ServerFnError::Deserialization(e.to_string()));
|
||||
Box::pin(async move {
|
||||
let value = match value {
|
||||
Ok(v) => v,
|
||||
|
@ -244,20 +242,13 @@ where
|
|||
{
|
||||
use leptos_dom::*;
|
||||
|
||||
let args_form_data = web_sys::FormData::new().expect_throw("could not create FormData");
|
||||
for (field_name, value) in args.as_form_data().into_iter() {
|
||||
args_form_data
|
||||
.append_with_str(field_name, &value)
|
||||
.expect_throw("could not append form field");
|
||||
}
|
||||
let args_form_data = web_sys::UrlSearchParams::new_with_str_sequence_sequence(&args_form_data)
|
||||
.expect_throw("could not URL encode FormData");
|
||||
let args_form_data = args_form_data.to_string().as_string().unwrap_or_default();
|
||||
let args_form_data = serde_urlencoded::to_string(&args)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
|
||||
|
||||
let resp = gloo_net::http::Request::post(url)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Accept", "application/json")
|
||||
.body(args_form_data.to_string())
|
||||
.body(args_form_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::Request(e.to_string()))?;
|
||||
|
@ -361,7 +352,7 @@ where
|
|||
input: RwSignal<Option<I>>,
|
||||
value: RwSignal<Option<O>>,
|
||||
pending: RwSignal<bool>,
|
||||
url: Option<&'static str>,
|
||||
url: Option<String>,
|
||||
#[allow(clippy::complexity)]
|
||||
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
|
||||
}
|
||||
|
@ -408,13 +399,18 @@ where
|
|||
/// 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
|
||||
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<T: ServerFn>(mut self) -> Self {
|
||||
self.url = Some(T::url());
|
||||
let prefix = T::prefix();
|
||||
self.url = if prefix.is_empty() {
|
||||
Some(T::url().to_string())
|
||||
} else {
|
||||
Some(prefix.to_string() + "/" + T::url())
|
||||
};
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -237,11 +237,11 @@ where
|
|||
O: 'static,
|
||||
{
|
||||
let action = if let Some(url) = props.action.url() {
|
||||
format!("/{url}")
|
||||
url
|
||||
} else {
|
||||
debug_warn!("<ActionForm/> action needs a URL. Either use create_server_action() or Action::using_server_fn().");
|
||||
"".to_string()
|
||||
};
|
||||
""
|
||||
}.to_string();
|
||||
let version = props.action.version;
|
||||
|
||||
Form(
|
||||
|
|
|
@ -62,7 +62,14 @@ where
|
|||
pub fn use_resolved_path(cx: Scope, path: impl Fn() -> String + 'static) -> Memo<Option<String>> {
|
||||
let route = use_route(cx);
|
||||
|
||||
create_memo(cx, move |_| route.resolve_path(&path()).map(String::from))
|
||||
create_memo(cx, move |_| {
|
||||
let path = path();
|
||||
if path.starts_with("/") {
|
||||
Some(path)
|
||||
} else {
|
||||
route.resolve_path(&path).map(String::from)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a function that can be used to navigate to a new route.
|
||||
|
|
Loading…
Reference in a new issue