mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
2 Commits
api-routes
...
979
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8b6dc440 | ||
|
|
af561abdf8 |
@@ -1,5 +1,3 @@
|
||||
extend = [{ path = "../cargo-make/common.toml" }]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use broadcaster::BroadcastChannel;
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
use broadcaster::BroadcastChannel;
|
||||
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
}
|
||||
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
|
||||
#[server(GetServerCount, "/api")]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
@@ -29,10 +29,7 @@ pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
}
|
||||
|
||||
#[server(AdjustServerCount, "/api")]
|
||||
pub async fn adjust_server_count(
|
||||
delta: i32,
|
||||
msg: String,
|
||||
) -> Result<i32, ServerFnError> {
|
||||
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
|
||||
let new = COUNT.load(Ordering::Relaxed) + delta;
|
||||
COUNT.store(new, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&new).await;
|
||||
@@ -49,49 +46,36 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! { cx,
|
||||
view! {
|
||||
cx,
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>
|
||||
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
|
||||
</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<A href="">"Simple"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="form">"Form-Based"</A>
|
||||
</li>
|
||||
<li>
|
||||
<A href="multi">"Multi-User"</A>
|
||||
</li>
|
||||
<li><A href="">"Simple"</A></li>
|
||||
<li><A href="form">"Form-Based"</A></li>
|
||||
<li><A href="multi">"Multi-User"</A></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
view=|cx| {
|
||||
view! { cx, <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|cx| {
|
||||
view! { cx, <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|cx| {
|
||||
view! { cx, <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Counter/>
|
||||
}/>
|
||||
<Route path="form" view=|cx| view! {
|
||||
cx,
|
||||
<FormCounter/>
|
||||
}/>
|
||||
<Route path="multi" view=|cx| view! {
|
||||
cx,
|
||||
<MultiuserCounter/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -109,47 +93,33 @@ pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || {
|
||||
(
|
||||
dec.version().get(),
|
||||
inc.version().get(),
|
||||
clear.version().get(),
|
||||
)
|
||||
},
|
||||
move || (dec.version().get(), inc.version().get(), clear.version().get()),
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
let value = move || {
|
||||
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
|
||||
let error_msg = move || {
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|count| count.unwrap_or(0))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let error_msg = move || {
|
||||
counter.read(cx).and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
.map(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
})
|
||||
.flatten()
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<h2>"Simple Counter"</h2>
|
||||
<p>
|
||||
"This counter sets the value on the server and automatically reloads the new value."
|
||||
</p>
|
||||
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
{move || {
|
||||
error_msg()
|
||||
.map(|msg| {
|
||||
view! { cx, <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -172,15 +142,19 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
);
|
||||
let value = move || {
|
||||
log::debug!("FormCounter looking for value");
|
||||
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
|
||||
counter
|
||||
.read(cx)
|
||||
.map(|n| n.ok())
|
||||
.flatten()
|
||||
.map(|n| n)
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<h2>"Form Counter"</h2>
|
||||
<p>
|
||||
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
|
||||
</p>
|
||||
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
|
||||
<div>
|
||||
// calling a server function is the same as POSTing to its API URL
|
||||
// so we can just do that with a form and button
|
||||
@@ -211,32 +185,26 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
let dec =
|
||||
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut source =
|
||||
gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
source
|
||||
.subscribe("message")
|
||||
.unwrap()
|
||||
.map(|value| match value {
|
||||
Ok(value) => value
|
||||
.1
|
||||
.data()
|
||||
.as_string()
|
||||
.expect("expected string value"),
|
||||
source.subscribe("message").unwrap().map(|value| {
|
||||
match value {
|
||||
Ok(value) => {
|
||||
value.1.data().as_string().expect("expected string value")
|
||||
},
|
||||
Err(_) => "0".to_string(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
on_cleanup(cx, move || source.close());
|
||||
@@ -244,20 +212,18 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
|
||||
let (multiplayer_value, _) =
|
||||
create_signal(cx, None::<i32>);
|
||||
|
||||
view! { cx,
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<h2>"Multi-User Counter"</h2>
|
||||
<p>
|
||||
"This one uses server-sent events (SSE) to live-update when other users make changes."
|
||||
</p>
|
||||
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>
|
||||
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
|
||||
</span>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod counters;
|
||||
|
||||
// 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 leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::counters::*;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use leptos::*;
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
|
||||
@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", features = ["macros"] }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
|
||||
anyhow = "1.0.68"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "1.0.0"
|
||||
@@ -19,7 +19,7 @@ cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_actix = { path = "../../integrations/actix" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
@@ -36,8 +36,10 @@ default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
@@ -107,9 +107,6 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
<Api path="bananas" route=web::get().to(|req: HttpRequest| async move {
|
||||
req.path()
|
||||
})
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
||||
@@ -16,7 +16,7 @@ use actix_web::{
|
||||
use futures::{Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use leptos::{
|
||||
leptos_dom::{Transparent, ssr::render_to_stream_with_prefix_undisposed_with_context},
|
||||
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
|
||||
leptos_server::{server_fn_by_path, Payload},
|
||||
server_fn::Encoding,
|
||||
*,
|
||||
@@ -922,7 +922,7 @@ pub fn generate_route_list_with_exclusions<IV>(
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
let (mut routes, mut api_routes) = leptos_router::generate_route_list_inner(app_fn);
|
||||
let mut routes = leptos_router::generate_route_list_inner(app_fn);
|
||||
|
||||
// Empty strings screw with Actix pathing, they need to be "/"
|
||||
routes = routes
|
||||
@@ -1113,14 +1113,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines an API route, which mounts the given route handler at this path.
|
||||
#[component(transparent)]
|
||||
pub fn Api<P>(cx: leptos::Scope, path: P, route: actix_web::Route) -> impl IntoView
|
||||
where P: Into<String>
|
||||
{
|
||||
Transparent::new(ApiRouteListing::new(path.into(), route))
|
||||
}
|
||||
|
||||
/// A helper to make it easier to use Axum extractors in server functions. This takes
|
||||
/// a handler function as its argument. The handler follows similar rules to an Actix
|
||||
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
|
||||
|
||||
@@ -240,3 +240,24 @@ pub fn component_props_builder<P: Props>(
|
||||
) -> <P as Props>::Builder {
|
||||
<P as Props>::builder()
|
||||
}
|
||||
|
||||
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
|
||||
compile_error!(
|
||||
"You have both `csr` and `ssr` enabled as features, which may cause \
|
||||
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
|
||||
default on `leptos`, and can be disabled by adding `default-features = \
|
||||
false` to your `leptos` dependency."
|
||||
);
|
||||
|
||||
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
|
||||
compile_error!(
|
||||
"You have both `hydrate` and `ssr` enabled as features, which may cause \
|
||||
issues like <Suspense/>` failing to work silently."
|
||||
);
|
||||
|
||||
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
|
||||
compile_error!(
|
||||
"You have both `hydrate` and `csr` enabled as features, which may cause \
|
||||
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
|
||||
adding `default-features = false` to your `leptos` dependency."
|
||||
);
|
||||
|
||||
@@ -904,40 +904,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Optionally adds an event listener to this element.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// #[component]
|
||||
/// pub fn Input(
|
||||
/// cx: Scope,
|
||||
/// #[prop(optional)] value: Option<RwSignal<String>>,
|
||||
/// ) -> impl IntoView {
|
||||
/// view! { cx, <input/> }
|
||||
/// // only add event if `value` is `Some(signal)`
|
||||
/// .optional_event(
|
||||
/// ev::input,
|
||||
/// value.map(|value| move |ev| value.set(event_target_value(&ev))),
|
||||
/// )
|
||||
/// }
|
||||
/// #
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn optional_event<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
#[allow(unused_mut)] // used for tracing in debug
|
||||
mut event_handler: Option<impl FnMut(E::EventType) + 'static>,
|
||||
) -> Self {
|
||||
if let Some(event_handler) = event_handler {
|
||||
self.on(event, event_handler)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a child to this element.
|
||||
#[track_caller]
|
||||
pub fn child(self, child: impl IntoView) -> Self {
|
||||
|
||||
@@ -834,15 +834,6 @@ where
|
||||
F: FnOnce(Scope) -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_warn(
|
||||
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
|
||||
features, which may cause issues like <Suspense/>` failing to work \
|
||||
silently. `csr` is enabled by default on `leptos`, and can be \
|
||||
disabled by adding `default-features = false` to your `leptos` \
|
||||
dependency.",
|
||||
);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
mount_to(crate::document().body().expect("body element to exist"), f)
|
||||
|
||||
@@ -271,15 +271,6 @@ impl View {
|
||||
instrument(level = "info", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently. `csr` is enabled by default on \
|
||||
`leptos`, and can be disabled by adding `default-features = \
|
||||
false` to your `leptos` dependency.\n",
|
||||
);
|
||||
|
||||
self.render_to_string_helper(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,15 +55,6 @@ pub fn render_to_stream_in_order_with_prefix(
|
||||
view: impl FnOnce(Scope) -> View + 'static,
|
||||
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently. `csr` is enabled by default on `leptos`, \
|
||||
and can be disabled by adding `default-features = false` to your \
|
||||
`leptos` dependency.\n",
|
||||
);
|
||||
|
||||
let (stream, runtime, _) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view,
|
||||
|
||||
@@ -7,10 +7,9 @@ use itertools::Itertools;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::Parse, parse_quote, spanned::Spanned,
|
||||
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
|
||||
ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path,
|
||||
PathArguments, ReturnType, Stmt, Type, TypePath, Visibility,
|
||||
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
|
||||
FnArg, GenericArgument, ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat,
|
||||
PatIdent, Path, PathArguments, ReturnType, Type, TypePath, Visibility,
|
||||
};
|
||||
|
||||
pub struct Model {
|
||||
@@ -130,25 +129,6 @@ impl ToTokens for Model {
|
||||
|
||||
let mut body = body.to_owned();
|
||||
|
||||
// check for components that end ;
|
||||
if !is_transparent {
|
||||
let ends_semi =
|
||||
body.block.stmts.iter().last().and_then(|stmt| match stmt {
|
||||
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(semi) = ends_semi {
|
||||
proc_macro_error::emit_error!(
|
||||
semi.span(),
|
||||
"A component that ends with a `view!` macro followed by a \
|
||||
semicolon will return (), an empty view. This is usually \
|
||||
an accident, not intentional, so we prevent it. If you’d \
|
||||
like to return (), you can do it it explicitly by \
|
||||
returning () as the last item from the component."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
body.sig.ident = format_ident!("__{}", body.sig.ident);
|
||||
#[allow(clippy::redundant_clone)] // false positive
|
||||
let body_name = body.sig.ident.clone();
|
||||
|
||||
@@ -816,7 +816,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
/// form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
||||
/// `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
||||
/// - **The `Scope` comes from the server.** Optionally, the first argument of a server function
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
//! form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
|
||||
//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
|
||||
//! - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
|
||||
@@ -21,7 +21,7 @@ regex = { version = "1", optional = true }
|
||||
url = { version = "2", optional = true }
|
||||
percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
serde_qs = "0.12"
|
||||
serde_html_form = "0.2"
|
||||
serde = "1"
|
||||
tracing = "0.1"
|
||||
js-sys = { version = "0.3" }
|
||||
|
||||
@@ -537,12 +537,14 @@ where
|
||||
Self: Sized + serde::de::DeserializeOwned,
|
||||
{
|
||||
/// Tries to deserialize the data, given only the `submit` event.
|
||||
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error>;
|
||||
fn from_event(
|
||||
ev: &web_sys::Event,
|
||||
) -> Result<Self, serde_html_form::de::Error>;
|
||||
|
||||
/// Tries to deserialize the data, given the actual form data.
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_qs::Error>;
|
||||
) -> Result<Self, serde_html_form::de::Error>;
|
||||
}
|
||||
|
||||
impl<T> FromFormData for T
|
||||
@@ -553,7 +555,9 @@ where
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error> {
|
||||
fn from_event(
|
||||
ev: &web_sys::Event,
|
||||
) -> Result<Self, serde_html_form::de::Error> {
|
||||
let (form, _, _, _) = extract_form_attributes(ev);
|
||||
|
||||
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
|
||||
@@ -566,11 +570,11 @@ where
|
||||
)]
|
||||
fn from_form_data(
|
||||
form_data: &web_sys::FormData,
|
||||
) -> Result<Self, serde_qs::Error> {
|
||||
) -> Result<Self, serde_html_form::de::Error> {
|
||||
let data =
|
||||
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
|
||||
.unwrap_throw();
|
||||
let data = data.to_string().as_string().unwrap_or_default();
|
||||
serde_qs::from_str::<Self>(&data)
|
||||
serde_html_form::from_str::<Self>(&data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::{
|
||||
RouteDefinition, RouteMatch,
|
||||
},
|
||||
use_is_back_navigation, RouteContext, RouterContext,
|
||||
ApiRouteListing
|
||||
};
|
||||
use leptos::{leptos_dom::HydrationCtx, *};
|
||||
use std::{
|
||||
@@ -44,7 +43,7 @@ pub fn Routes(
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
|
||||
Branches::with(&base, |branches| {
|
||||
*context.ui.borrow_mut() = branches.to_vec();
|
||||
*context.0.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +118,7 @@ pub fn AnimatedRoutes(
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
|
||||
Branches::with(&base, |branches| {
|
||||
*context.ui.borrow_mut() = branches.to_vec()
|
||||
*context.0.borrow_mut() = branches.to_vec()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,12 +224,8 @@ pub fn AnimatedRoutes(
|
||||
|
||||
pub(crate) struct Branches;
|
||||
|
||||
type AppRoutes = (Vec<Branch>, Vec<ApiRouteListing>);
|
||||
|
||||
thread_local! {
|
||||
// map is indexed by base
|
||||
// this allows multiple apps per server
|
||||
static BRANCHES: RefCell<HashMap<String, AppRoutes>> = Default::default();
|
||||
static BRANCHES: RefCell<HashMap<String, Vec<Branch>>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Branches {
|
||||
@@ -238,30 +233,29 @@ impl Branches {
|
||||
BRANCHES.with(|branches| {
|
||||
let mut current = branches.borrow_mut();
|
||||
if !current.contains_key(base) {
|
||||
let mut branches = (Vec::new(), Vec::new());
|
||||
let mut route_defs = Vec::new();
|
||||
let mut api_routes = Vec::new();
|
||||
for child in children
|
||||
let mut branches = Vec::new();
|
||||
let children = children
|
||||
.as_children()
|
||||
.iter() {
|
||||
let transparent = child.as_transparent();
|
||||
if let Some(def) = transparent.and_then(|t| t.downcast_ref::<RouteDefinition>()) {
|
||||
route_defs.push(def.clone());
|
||||
} else if let Some(def) = transparent.and_then(|t| t.downcast_ref::<ApiRouteListing>()) {
|
||||
api_routes.push(def.clone());
|
||||
} else {
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
let def = child
|
||||
.as_transparent()
|
||||
.and_then(|t| t.downcast_ref::<RouteDefinition>());
|
||||
if def.is_none() {
|
||||
warn!(
|
||||
"[NOTE] The <Routes/> component should \
|
||||
include *only* <Route/> or <ProtectedRoute/> or <ApiRoute/> \
|
||||
include *only* <Route/>or <ProtectedRoute/> \
|
||||
components, or some \
|
||||
#[component(transparent)] that returns a \
|
||||
RouteDefinition."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
def
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
create_branches(
|
||||
&route_defs,
|
||||
&children,
|
||||
base,
|
||||
&mut Vec::new(),
|
||||
&mut branches,
|
||||
@@ -278,18 +272,7 @@ impl Branches {
|
||||
"Branches::initialize() should be called before \
|
||||
Branches::with()",
|
||||
);
|
||||
cb(&branches.0)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_api_routes<T>(base: &str, cb: impl FnOnce(&[ApiRouteListing]) -> T) -> T {
|
||||
BRANCHES.with(|branches| {
|
||||
let branches = branches.borrow();
|
||||
let branches = branches.get(base).expect(
|
||||
"Branches::initialize() should be called before \
|
||||
Branches::with()",
|
||||
);
|
||||
cb(&branches.1)
|
||||
cb(branches)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -498,7 +481,7 @@ fn create_branches(
|
||||
route_defs: &[RouteDefinition],
|
||||
base: &str,
|
||||
stack: &mut Vec<RouteData>,
|
||||
branches: &mut (Vec<Branch>, Vec<ApiRouteListing>),
|
||||
branches: &mut Vec<Branch>,
|
||||
) {
|
||||
for def in route_defs {
|
||||
let routes = create_routes(def, base);
|
||||
@@ -506,8 +489,8 @@ fn create_branches(
|
||||
stack.push(route.clone());
|
||||
|
||||
if def.children.is_empty() {
|
||||
let branch = create_branch(stack, branches.0.len());
|
||||
branches.0.push(branch);
|
||||
let branch = create_branch(stack, branches.len());
|
||||
branches.push(branch);
|
||||
} else {
|
||||
create_branches(&def.children, &route.pattern, stack, branches);
|
||||
}
|
||||
@@ -517,7 +500,7 @@ fn create_branches(
|
||||
}
|
||||
|
||||
if stack.is_empty() {
|
||||
branches.0.sort_by_key(|branch| Reverse(branch.score));
|
||||
branches.sort_by_key(|branch| Reverse(branch.score));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,24 +2,14 @@ use crate::{
|
||||
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
|
||||
};
|
||||
use leptos::*;
|
||||
use std::{any::Any, cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
|
||||
use std::{cell::RefCell, collections::HashSet, rc::Rc};
|
||||
|
||||
/// Context to contain all possible routes.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct PossibleBranchContext {
|
||||
pub(crate) ui: Rc<RefCell<Vec<Branch>>>,
|
||||
pub(crate) api: Rc<RefCell<Vec<ApiRouteListing>>>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A route that this application can serve.
|
||||
pub enum PossibleRouteListing {
|
||||
View(RouteListing),
|
||||
Api(ApiRouteListing)
|
||||
}
|
||||
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
/// Route listing for a component-based view.
|
||||
/// A route that this application can serve.
|
||||
pub struct RouteListing {
|
||||
path: String,
|
||||
mode: SsrMode,
|
||||
@@ -56,69 +46,12 @@ impl RouteListing {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Route listing for an API route.
|
||||
pub struct ApiRouteListing {
|
||||
path: String,
|
||||
methods: Option<HashSet<Method>>,
|
||||
// this will be downcast by the implementation
|
||||
handler: Arc<dyn Any>
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ApiRouteListing {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ApiRouteListing").field("path", &self.path).field("methods", &self.methods).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiRouteListing {
|
||||
/// Create an API route listing from its parts.
|
||||
pub fn new<T: 'static>(
|
||||
path: impl ToString,
|
||||
handler: T
|
||||
) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
methods: None,
|
||||
handler: Arc::new(handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an API route listing from its parts.
|
||||
pub fn new_with_methods<T: 'static>(
|
||||
path: impl ToString,
|
||||
methods: impl IntoIterator<Item = Method>,
|
||||
handler: T
|
||||
) -> Self {
|
||||
Self {
|
||||
path: path.to_string(),
|
||||
methods: Some(methods.into_iter().collect()),
|
||||
handler: Arc::new(handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// The path this route handles.
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// The HTTP request methods this path can handle.
|
||||
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
|
||||
self.methods.iter().flatten().copied()
|
||||
}
|
||||
|
||||
/// The handler for a request at this route
|
||||
pub fn handler<T: 'static>(&self) -> Option<&T> {
|
||||
self.handler.downcast_ref::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
|
||||
/// format. Odds are you want `generate_route_list()` from either the actix, axum, or viz integrations if you want
|
||||
/// to work with their router
|
||||
pub fn generate_route_list_inner<IV>(
|
||||
app_fn: impl FnOnce(Scope) -> IV + 'static,
|
||||
) -> (Vec<RouteListing>, Vec<ApiRouteListing>)
|
||||
) -> Vec<RouteListing>
|
||||
where
|
||||
IV: IntoView + 'static,
|
||||
{
|
||||
@@ -136,8 +69,8 @@ where
|
||||
_ = app_fn(cx).into_view(cx);
|
||||
leptos::suppress_resource_load(false);
|
||||
|
||||
let ui_branches = branches.ui.borrow();
|
||||
let ui = ui_branches
|
||||
let branches = branches.0.borrow();
|
||||
branches
|
||||
.iter()
|
||||
.flat_map(|branch| {
|
||||
let mode = branch
|
||||
@@ -160,12 +93,6 @@ where
|
||||
methods: methods.clone(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let api_branches = branches.api.borrow();
|
||||
let api = api_branches
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
(ui, api)
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ readme = "../README.md"
|
||||
[dependencies]
|
||||
server_fn_macro_default = { workspace = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_qs = "0.12"
|
||||
serde_html_form = "0.2"
|
||||
thiserror = "1"
|
||||
serde_json = "1"
|
||||
quote = "1"
|
||||
|
||||
@@ -49,7 +49,7 @@ use syn::__private::ToTokens;
|
||||
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
|
||||
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
|
||||
/// They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
/// form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
//! This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
//! need to deserialize the result to return it to the client.
|
||||
//! - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
//! form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
|
||||
//! form data using [`serde_html_form`](https://docs.rs/serde_html_form/latest/serde_html_form/) or as `application/cbor`
|
||||
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
|
||||
// used by the macro
|
||||
@@ -308,7 +308,7 @@ where
|
||||
// decode the args
|
||||
let value = match Self::encoding() {
|
||||
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => {
|
||||
serde_qs::from_bytes(data).map_err(|e| {
|
||||
serde_html_form::from_bytes(data).map_err(|e| {
|
||||
ServerFnError::Deserialization(e.to_string())
|
||||
})
|
||||
}
|
||||
@@ -408,7 +408,7 @@ where
|
||||
}
|
||||
let args_encoded = match &enc {
|
||||
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => Payload::Url(
|
||||
serde_qs::to_string(&args)
|
||||
serde_html_form::to_string(&args)
|
||||
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
|
||||
),
|
||||
Encoding::Cbor => {
|
||||
|
||||
Reference in New Issue
Block a user