Skip to content

Commit

Permalink
actually use server functions in ActionForm
Browse files Browse the repository at this point in the history
  • Loading branch information
gbj committed Jan 5, 2024
1 parent 36d66e2 commit a5ce625
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 33 deletions.
Binary file modified examples/todo_app_sqlite/Todos.db
Binary file not shown.
5 changes: 5 additions & 0 deletions leptos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ experimental-islands = [
"dep:serde",
"dep:serde_json",
]
trace-component-props = [
"leptos_dom/trace-component-props",
"leptos_macro/trace-component-props"
]

[package.metadata.cargo-all-features]
denylist = [
Expand All @@ -83,6 +87,7 @@ denylist = [
"rustls",
"default-tls",
"wasm-bindgen",
"trace-component-props"
]
skip_feature_sets = [
[
Expand Down
145 changes: 118 additions & 27 deletions router/src/components/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ use crate::{
use leptos::{
html::form,
logging::*,
server_fn::{error::ServerFnErrorSerde, ServerFn},
server_fn::{
client::Client,
codec::{Encoding, PostUrl},
request::ClientReq,
ServerFn,
},
*,
};
use serde::{de::DeserializeOwned, Serialize};
use std::{error::Error, fmt::Debug, rc::Rc};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::RequestRedirect;
use web_sys::{
FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement,
RequestRedirect, SubmitEvent,
};

type OnFormData = Rc<dyn Fn(&web_sys::FormData)>;
type OnResponse = Rc<dyn Fn(&web_sys::Response)>;
Expand Down Expand Up @@ -416,11 +424,14 @@ fn current_window_origin() -> String {
tracing::instrument(level = "trace", skip_all,)
)]
#[component]
pub fn ActionForm<I, O, Enc>(
pub fn ActionForm<ServFn>(
/// The action from which to build the form. This should include a URL, which can be generated
/// by default using [`create_server_action`](l:eptos_server::create_server_action) or added
/// manually using [`using_server_fn`](leptos_server::Action::using_server_fn).
action: Action<I, Result<O, ServerFnError<I::Error>>>,
action: Action<
ServFn,
Result<ServFn::Output, ServerFnError<ServFn::Error>>,
>,
/// Sets the `class` attribute on the underlying `<form>` tag, making it easier to style.
#[prop(optional, into)]
class: Option<AttributeValue>,
Expand All @@ -440,11 +451,15 @@ pub fn ActionForm<I, O, Enc>(
children: Children,
) -> impl IntoView
where
I: Clone + DeserializeOwned + ServerFn<InputEncoding = Enc> + 'static,
O: Clone + Serialize + DeserializeOwned + 'static,
ServerFnError<I::Error>: Debug + Clone, // Enc: FormDataEncoding,
I::Error: Debug + 'static,
ServFn:
Clone + DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
ServerFnError<ServFn::Error>: Debug + Clone,
ServFn::Error: Debug + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
ServFn::Error,
>>::FormData: From<FormData>,
{
let has_router = has_router();
let action_url = if let Some(url) = action.url() {
url
} else {
Expand All @@ -458,7 +473,81 @@ where
let value = action.value();
let input = action.input();

let on_error = Rc::new(move |e: &gloo_net::Error| {
let class = class.map(|bx| bx.into_attribute_boxed());

let on_submit = {
let action_url = action_url.clone();
move |ev: SubmitEvent| {
if ev.default_prevented() {
return;
}

ev.prevent_default();

let navigate = has_router.then(use_navigate);
let navigate_options = NavigateOptions {
scroll: !noscroll,
..Default::default()
};

let form =
form_from_event(&ev).expect("couldn't find form submitter");
let form_data = FormData::new_with_form(&form).unwrap();
let req = <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
ServFn::Error,
>>::try_new_post_form_data(
&action_url,
ServFn::OutputEncoding::CONTENT_TYPE,
ServFn::InputEncoding::CONTENT_TYPE,
form_data.into(),
);
match req {
Ok(req) => {
spawn_local(async move {
// TODO check order of setting things here, and use batch as needed
// TODO set version?
match <ServFn as ServerFn>::run_on_client_with_req(req)
.await
{
Ok(res) => {
batch(move || {
version.update(|n| *n += 1);
value.try_set(Some(Ok(res)));
});
}
Err(err) => {
batch(move || {
value.set(Some(Err(err.clone())));
if let Some(error) = error {
error.set(Some(Box::new(
ServerFnErrorErr::from(err),
)));
}
});
}
}
});
}
Err(_) => todo!(),
}
}
};

let mut action_form = form()
.attr("action", action_url)
.attr("method", "post")
.attr("class", class)
.on(ev::submit, on_submit)
.child(children());
if let Some(node_ref) = node_ref {
action_form = action_form.node_ref(node_ref)
};
for (attr_name, attr_value) in attributes {
action_form = action_form.attr(attr_name, attr_value);
}
action_form

/* let on_error = Rc::new(move |e: &gloo_net::Error| {
batch(move || {
action.set_pending(false);
let e = ServerFnError::Request(e.to_string());
Expand Down Expand Up @@ -556,24 +645,7 @@ where
action.set_pending(false);
});
});
});
let class = class.map(|bx| bx.into_attribute_boxed());

let mut props = FormProps::builder()
.action(action_url)
.version(version)
.on_form_data(on_form_data)
.on_response(on_response)
.on_error(on_error)
.method("post")
.class(class)
.noscroll(noscroll)
.children(children)
.build();
props.error = error;
props.node_ref = node_ref;
props.attributes = attributes;
Form(props)
});*/
}

/// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML
Expand Down Expand Up @@ -657,6 +729,25 @@ where
}
form
}

fn form_from_event(ev: &SubmitEvent) -> Option<HtmlFormElement> {
let submitter = ev.unchecked_ref::<SubmitEvent>().submitter();
match &submitter {
Some(el) => {
if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
Some(form.clone())
} else if el.is_instance_of::<HtmlInputElement>()
|| el.is_instance_of::<HtmlButtonElement>()
{
Some(ev.target().unwrap().unchecked_into())
} else {
None
}
}
None => ev.target().map(|form| form.unchecked_into()),
}
}

#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
Expand Down
18 changes: 13 additions & 5 deletions server_fn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ where
// create and send request on client
let req =
self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?;
Self::run_on_client_with_req(req).await
}
}

fn run_on_client_with_req(
req: <Self::Client as Client<Self::Error>>::Request,
) -> impl Future<Output = Result<Self::Output, ServerFnError<Self::Error>>> + Send
{
async move {
let res = Self::Client::send(req).await?;

let status = res.status();
Expand All @@ -122,7 +131,6 @@ where
if (300..=399).contains(&status) {
redirect::call_redirect_hook(&location);
}

res
}
}
Expand Down Expand Up @@ -288,11 +296,11 @@ pub mod axum {
path: &str,
) -> Option<BoxedService<Request<Body>, Response<Body>>> {
REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| {
//let middleware = (server_fn.middleware)();
let middleware = (server_fn.middleware)();
let mut service = BoxedService::new(server_fn.clone());
//for middleware in middleware {
//service = middleware.layer(service);
//}
for middleware in middleware {
service = middleware.layer(service);
}
service
})
}
Expand Down
28 changes: 27 additions & 1 deletion server_fn/src/request/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bytes::Bytes;
pub use gloo_net::http::Request;
use js_sys::Uint8Array;
use send_wrapper::SendWrapper;
use web_sys::FormData;
use web_sys::{FormData, UrlSearchParams};

#[derive(Debug)]
pub struct BrowserRequest(pub(crate) SendWrapper<Request>);
Expand Down Expand Up @@ -89,4 +89,30 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
}

fn try_new_post_form_data(
path: &str,
accepts: &str,
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
let form_data = body.0.take();
let url_params =
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
.map_err(|e| {
ServerFnError::Serialization(e.as_string().unwrap_or_else(
|| {
"Could not serialize FormData to URLSearchParams"
.to_string()
},
))
})?;
Ok(Self(SendWrapper::new(
Request::post(path)
.header("Content-Type", content_type)
.header("Accept", accepts)
.body(url_params)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
}
}
7 changes: 7 additions & 0 deletions server_fn/src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ where
body: Bytes,
) -> Result<Self, ServerFnError<CustErr>>;

fn try_new_post_form_data(
path: &str,
accepts: &str,
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>>;

fn try_new_multipart(
path: &str,
accepts: &str,
Expand Down
15 changes: 15 additions & 0 deletions server_fn/src/request/reqwest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,19 @@ impl<CustErr> ClientReq<CustErr> for Request {
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))
}

fn try_new_post_form_data(
path: &str,
accepts: &str,
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
/*CLIENT
.post(path)
.header(ACCEPT, accepts)
.multipart(body)
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))*/
todo!()
}
}

0 comments on commit a5ce625

Please sign in to comment.