Use serde_urlencoded for server functions (making it easier to use normal text inputs for forms)

This commit is contained in:
Greg Johnston 2022-11-14 08:18:01 -05:00
parent f8de0fff81
commit 22eaa92355
6 changed files with 48 additions and 45 deletions

View file

@ -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"

View file

@ -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
}

View file

@ -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]

View file

@ -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
}
}

View file

@ -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(

View file

@ -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.