Compare commits

..

2 Commits

Author SHA1 Message Date
Greg Johnston
8f8b6dc440 cargo fmt 2023-05-03 08:49:12 -04:00
Greg Johnston
af561abdf8 fix: suppress warning when loading local resource not under <Suspense/> in hydrate mode 2023-05-03 08:48:20 -04:00
93 changed files with 508 additions and 666 deletions

View File

@@ -41,7 +41,7 @@ ssr = [
"leptos_meta/ssr",
"leptos_router/ssr",
]
nightly = ["leptos/nightly", "leptos_router/nightly"]
stable = ["leptos/stable", "leptos_router/stable"]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]

View File

@@ -1,5 +1,3 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

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

View File

@@ -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::*;

View File

@@ -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::*;

View File

@@ -1,4 +1,4 @@
use leptos::{For, *};
use leptos::{For, ForProps, *};
const MANY_COUNTERS: usize = 1000;

View File

@@ -1,5 +1,3 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,4 +1,7 @@
use crate::{error_template::ErrorTemplate, errors::AppError};
use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::AppError,
};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
@@ -51,8 +54,7 @@ pub fn App(cx: Scope) -> impl IntoView {
#[component]
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
let generate_internal_error =
create_server_action::<CauseInternalServerError>(cx);
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
view! { cx,
<p>

View File

@@ -1,4 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -7,7 +8,6 @@ pub mod landing;
// 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::landing::*;

View File

@@ -37,8 +37,7 @@ async fn custom_handler(
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Debug)
.expect("couldn't initialize logging");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
crate::landing::register_server_functions();
@@ -52,11 +51,7 @@ async fn main() {
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(
leptos_options.clone(),
routes,
|cx| view! { cx, <App/> },
)
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));

View File

@@ -1,5 +1,3 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -14,7 +14,7 @@ pub enum FetchError {
#[error("Error loading data from serving.")]
Request,
#[error("Error deserializaing cat data from request.")]
Json,
Json
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, FetchError> {

View File

@@ -1,5 +1,3 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -4,7 +4,10 @@ use leptos_meta::*;
use leptos_router::*;
mod api;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {

View File

@@ -7,7 +7,7 @@ cfg_if! {
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use hackernews::{App};
use hackernews::{App,AppProps};
use leptos_actix::{LeptosRoutes, generate_route_list};
#[get("/style.css")]
@@ -46,7 +46,7 @@ cfg_if! {
}
} else {
fn main() {
use hackernews::{App};
use hackernews::{App, AppProps};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -1,7 +1,8 @@
use crate::api;
use leptos::*;
use leptos_router::*;
use crate::api;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
@@ -36,10 +37,8 @@ pub fn Stories(cx: Scope) -> impl IntoView {
);
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link = move |cx| {
pending()
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
};
let hide_more_link =
move |cx| pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,

View File

@@ -13,20 +13,11 @@ pub fn Story(cx: Scope) -> impl IntoView {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(
cx,
&api::story(&format!("item/{id}")),
)
.await
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
}
},
);
let meta_description = move || {
story
.read(cx)
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>

View File

@@ -31,7 +31,7 @@ pub fn User(cx: Scope) -> impl IntoView {
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
<li inner_html={user.about} class="about"></li>
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>

View File

@@ -1,5 +1,3 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,4 +1,7 @@
use leptos::{view, Errors, For, IntoView, RwSignal, Scope, View};
use leptos::{
signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
View,
};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them

View File

@@ -7,7 +7,10 @@ pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::{nav::*, stories::*, story::*, users::*};
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {

View File

@@ -1,4 +1,4 @@
use leptos::{component, view, IntoView, Scope};
use leptos::{component, Scope, IntoView, view};
use leptos_router::*;
#[component]

View File

@@ -1,7 +1,8 @@
use crate::api;
use leptos::*;
use leptos_router::*;
use crate::api;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
@@ -36,10 +37,8 @@ pub fn Stories(cx: Scope) -> impl IntoView {
);
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link = move || {
pending()
|| stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28
};
let hide_more_link =
move || pending() || stories.read(cx).unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,

View File

@@ -13,20 +13,11 @@ pub fn Story(cx: Scope) -> impl IntoView {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(
cx,
&api::story(&format!("item/{id}")),
)
.await
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
}
},
);
let meta_description = move || {
story
.read(cx)
.and_then(|story| story.map(|story| story.title))
.unwrap_or_else(|| "Loading story...".to_string())
};
let meta_description = move || story.read(cx).and_then(|story| story.map(|story| story.title)).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<>

View File

@@ -1,5 +1,3 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1 +0,0 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -1 +0,0 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -1,8 +1,9 @@
use api_boundary::*;
use gloo_net::http::{Request, Response};
use serde::de::DeserializeOwned;
use thiserror::Error;
use api_boundary::*;
#[derive(Clone, Copy)]
pub struct UnauthorizedApi {
url: &'static str,

View File

@@ -20,56 +20,54 @@ pub fn CredentialsForm(
});
view! { cx,
<form on:submit=|ev| ev.prevent_default()>
<p>{title}</p>
{move || {
error
.get()
.map(|err| {
view! { cx, <p style="color:red;">{err}</p> }
})
}}
<input
type="email"
required
placeholder="Email address"
prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_email.update(|v| *v = val);
<form on:submit=|ev|ev.prevent_default()>
<p>{ title }</p>
{move || error.get().map(|err| view!{ cx,
<p style ="color:red;" >{ err }</p>
})}
<input
type = "email"
required
placeholder = "Email address"
prop:disabled = move || disabled.get()
on:keyup = move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_email.update(|v|*v = val);
}
// The `change` event fires when the browser fills the form automatically,
on:change = move |ev| {
let val = event_target_value(&ev);
set_email.update(|v|*v = val);
}
/>
<input
type = "password"
required
placeholder = "Password"
prop:disabled = move || disabled.get()
on:keyup = move |ev: ev::KeyboardEvent| {
match &*ev.key() {
"Enter" => {
dispatch_action();
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_email.update(|v| *v = val);
_=> {
let val = event_target_value(&ev);
set_password.update(|p|*p = val);
}
/>
<input
type="password"
required
placeholder="Password"
prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| {
match &*ev.key() {
"Enter" => {
dispatch_action();
}
_ => {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
}
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action()
>
{action_label}
</button>
</form>
}
}
// The `change` event fires when the browser fills the form automatically,
on:change = move |ev| {
let val = event_target_value(&ev);
set_password.update(|p|*p = val);
}
/>
<button
prop:disabled = move || button_is_disabled.get()
on:click = move |_| dispatch_action()
>
{ action_label }
</button>
</form>
}
}

View File

@@ -1,7 +1,8 @@
use crate::Page;
use leptos::*;
use leptos_router::*;
use crate::Page;
#[component]
pub fn NavBar<F>(
cx: Scope,
@@ -12,27 +13,20 @@ where
F: Fn() + 'static + Clone,
{
view! { cx,
<nav>
<Show
when=move || logged_in.get()
fallback=|cx| {
view! { cx,
<A href=Page::Login.path()>"Login"</A>
" | "
<A href=Page::Register.path()>"Register"</A>
}
}
>
<a
href="#"
on:click={
let on_logout = on_logout.clone();
move |_| on_logout()
}
>
"Logout"
</a>
</Show>
</nav>
<nav>
<Show
when = move || logged_in.get()
fallback = |cx| view! { cx,
<A href=Page::Login.path() >"Login"</A>
" | "
<A href=Page::Register.path() >"Register"</A>
}
>
<a href="#" on:click={
let on_logout = on_logout.clone();
move |_| on_logout()
}>"Logout"</a>
</Show>
</nav>
}
}

View File

@@ -1,8 +1,9 @@
use api_boundary::*;
use gloo_storage::{LocalStorage, Storage};
use leptos::*;
use leptos_router::*;
use api_boundary::*;
mod api;
mod components;
mod pages;
@@ -85,51 +86,45 @@ pub fn App(cx: Scope) -> impl IntoView {
.expect("LocalStorage::set");
}
None => {
log::debug!(
"API is no longer authorized: delete token from \
LocalStorage"
);
log::debug!("API is no longer authorized: delete token from LocalStorage");
LocalStorage::delete(API_TOKEN_STORAGE_KEY);
}
}
});
view! { cx,
<Router>
<NavBar logged_in on_logout/>
<main>
<Routes>
<Route
path=Page::Home.path()
view=move |cx| {
view! { cx, <Home user_info=user_info.into()/> }
}
/>
<Route
path=Page::Login.path()
view=move |cx| {
view! { cx,
<Login
api=unauthorized_api
on_success=move |api| {
log::info!("Successfully logged in");
authorized_api.update(|v| *v = Some(api));
let navigate = use_navigate(cx);
navigate(Page::Home.path(), Default::default()).expect("Home route");
fetch_user_info.dispatch(());
}
/>
}
}
/>
<Route
path=Page::Register.path()
view=move |cx| {
view! { cx, <Register api=unauthorized_api/> }
}
/>
</Routes>
</main>
</Router>
<Router>
<NavBar logged_in on_logout />
<main>
<Routes>
<Route
path=Page::Home.path()
view=move |cx| view! { cx,
<Home user_info = user_info.into() />
}
/>
<Route
path=Page::Login.path()
view=move |cx| view! { cx,
<Login
api = unauthorized_api
on_success = move |api| {
log::info!("Successfully logged in");
authorized_api.update(|v| *v = Some(api));
let navigate = use_navigate(cx);
navigate(Page::Home.path(), Default::default()).expect("Home route");
fetch_user_info.dispatch(());
} />
}
/>
<Route
path=Page::Register.path()
view=move |cx| view! { cx,
<Register api = unauthorized_api />
}
/>
</Routes>
</main>
</Router>
}
}

View File

@@ -1,8 +1,9 @@
use client::*;
use leptos::*;
use client::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <App/> })
mount_to_body(|cx| view! { cx, <App /> })
}

View File

@@ -6,19 +6,15 @@ use leptos_router::*;
#[component]
pub fn Home(cx: Scope, user_info: Signal<Option<UserInfo>>) -> impl IntoView {
view! { cx,
<h2>"Leptos Login example"</h2>
{move || match user_info.get() {
Some(info) => {
view! { cx, <p>"You are logged in with " {info.email} "."</p> }
.into_view(cx)
}
None => {
view! { cx,
<p>"You are not logged in."</p>
<A href=Page::Login.path()>"Login now."</A>
}
.into_view(cx)
}
}}
<h2>"Leptos Login example"</h2>
{move || match user_info.get() {
Some(info) => view!{ cx,
<p>"You are logged in with "{ info.email }"."</p>
}.into_view(cx),
None => view!{ cx,
<p>"You are not logged in."</p>
<A href=Page::Login.path() >"Login now."</A>
}.into_view(cx)
}}
}
}

View File

@@ -1,11 +1,13 @@
use leptos::*;
use leptos_router::*;
use api_boundary::*;
use crate::{
api::{self, AuthorizedApi, UnauthorizedApi},
components::credentials::*,
Page,
};
use api_boundary::*;
use leptos::*;
use leptos_router::*;
#[component]
pub fn Login<F>(cx: Scope, api: UnauthorizedApi, on_success: F) -> impl IntoView
@@ -51,14 +53,14 @@ where
let disabled = Signal::derive(cx, move || wait_for_response.get());
view! { cx,
<CredentialsForm
title="Please login to your account"
action_label="Login"
action=login_action
error=login_error.into()
disabled
/>
<p>"Don't have an account?"</p>
<A href=Page::Register.path()>"Register"</A>
<CredentialsForm
title = "Please login to your account"
action_label = "Login"
action = login_action
error = login_error.into()
disabled
/>
<p>"Don't have an account?"</p>
<A href=Page::Register.path()>"Register"</A>
}
}

View File

@@ -1,11 +1,13 @@
use leptos::*;
use leptos_router::*;
use api_boundary::*;
use crate::{
api::{self, UnauthorizedApi},
components::credentials::*,
Page,
};
use api_boundary::*;
use leptos::*;
use leptos_router::*;
#[component]
pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
@@ -50,24 +52,26 @@ pub fn Register(cx: Scope, api: UnauthorizedApi) -> impl IntoView {
let disabled = Signal::derive(cx, move || wait_for_response.get());
view! { cx,
<Show
when=move || register_response.get().is_some()
fallback=move |_| {
view! { cx,
<CredentialsForm
title="Please enter the desired credentials"
action_label="Register"
action=register_action
error=register_error.into()
disabled
/>
<p>"Your already have an account?"</p>
<A href=Page::Login.path()>"Login"</A>
}
}
>
<p>"You have successfully registered."</p>
<p>"You can now " <A href=Page::Login.path()>"login"</A> " with your new account."</p>
</Show>
<Show
when = move || register_response.get().is_some()
fallback = move |_| view!{ cx,
<CredentialsForm
title = "Please enter the desired credentials"
action_label = "Register"
action = register_action
error = register_error.into()
disabled
/>
<p>"Your already have an account?"</p>
<A href=Page::Login.path()>"Login"</A>
}
>
<p>"You have successfully registered."</p>
<p>
"You can now "
<A href=Page::Login.path()>"login"</A>
" with your new account."
</p>
</Show>
}
}

View File

@@ -1 +0,0 @@
extend = { path = "../../cargo-make/common.toml" }

View File

@@ -2,7 +2,8 @@ use crate::{application::*, Error};
use api_boundary as json;
use axum::{
http::StatusCode,
response::{IntoResponse, Json, Response},
response::Json,
response::{IntoResponse, Response},
};
use thiserror::Error;

View File

@@ -1,4 +1,5 @@
use api_boundary as json;
use std::{env, sync::Arc};
use axum::{
extract::{State, TypedHeader},
headers::{authorization::Bearer, Authorization},
@@ -7,9 +8,10 @@ use axum::{
routing::{get, post},
Router,
};
use std::{env, sync::Arc};
use tower_http::cors::{Any, CorsLayer};
use api_boundary as json;
mod adapters;
mod application;
@@ -23,10 +25,7 @@ async fn main() -> anyhow::Result<()> {
env::set_var("RUST_LOG", "debug");
}
env::VarError::NotUnicode(_) => {
return Err(anyhow::anyhow!(
"The value of 'RUST_LOG' does not contain valid unicode \
data."
));
return Err(anyhow::anyhow!("The value of 'RUST_LOG' does not contain valid unicode data."));
}
}
}

View File

@@ -1,5 +1,3 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -107,7 +107,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
{move || view! { cx, <ul>{contacts}</ul>}}
</Suspense>
<AnimatedOutlet
<AnimatedOutlet
class="outlet"
outro="fadeOut"
intro="fadeIn"

View File

@@ -1,5 +1,3 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,7 +1,9 @@
use std::collections::HashSet;
use cfg_if::cfg_if;
use leptos::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
cfg_if! {
if #[cfg(feature = "ssr")] {
@@ -166,9 +168,7 @@ pub async fn login(
.ok_or("User does not exist.")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
match verify(password, &user.password)
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
match verify(password, &user.password).map_err(|e| ServerFnError::ServerError(e.to_string()))? {
true => {
auth.login_user(user.id);
auth.remember_user(remember.is_some());

View File

@@ -13,9 +13,7 @@ impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -1,4 +1,5 @@
use crate::{auth::*, error_template::ErrorTemplate};
use crate::auth::*;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
@@ -72,8 +73,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let pool = pool(cx)?;
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
let mut rows = sqlx::query_as::<_, SqlTodo>("SELECT * FROM todos").fetch(&pool);
while let Some(row) = rows
.try_next()
@@ -111,13 +111,11 @@ pub async fn add_todo(cx: Scope, title: String) -> Result<(), ServerFnError> {
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
match sqlx::query(
"INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)",
)
.bind(title)
.bind(id)
.execute(&pool)
.await
match sqlx::query("INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false)")
.bind(title)
.bind(id)
.execute(&pool)
.await
{
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
@@ -306,10 +304,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Login(
cx: Scope,
action: Action<Login, Result<(), ServerFnError>>,
) -> impl IntoView {
pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> impl IntoView {
view! {
cx,
<ActionForm action=action>
@@ -335,10 +330,7 @@ pub fn Login(
}
#[component]
pub fn Signup(
cx: Scope,
action: Action<Signup, Result<(), ServerFnError>>,
) -> impl IntoView {
pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> impl IntoView {
view! {
cx,
<ActionForm action=action>
@@ -370,10 +362,7 @@ pub fn Signup(
}
#[component]
pub fn Logout(
cx: Scope,
action: Action<Logout, Result<(), ServerFnError>>,
) -> impl IntoView {
pub fn Logout(cx: Scope, action: Action<Logout, Result<(), ServerFnError>>) -> impl IntoView {
view! {
cx,
<div id="loginbox">

View File

@@ -1,5 +1,3 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,10 +1,10 @@
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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;

View File

@@ -29,7 +29,7 @@ cfg_if! {
// Setting this to None means we'll be using cargo-leptos and its env vars.
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
let addr = conf.leptos_options.site_addr.clone();
// Generate the list of routes in your Leptos App
let routes = generate_route_list(|cx| view! { cx, <TodoApp/> });
@@ -43,7 +43,7 @@ cfg_if! {
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <TodoApp/> })
.service(Files::new("/", site_root))
.service(Files::new("/", &site_root))
//.wrap(middleware::Compress::default())
})
.bind(addr)?

View File

@@ -9,7 +9,7 @@ cfg_if! {
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn register_server_functions() {
@@ -37,18 +37,18 @@ cfg_if! {
#[server(GetTodos, "/api")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
let req = use_context::<actix_web::HttpRequest>(cx);
if let Some(req) = req {
println!("req.path = {:#?}", req.path());
let req =
use_context::<actix_web::HttpRequest>(cx);
if let Some(req) = req{
println!("req.path = {:#?}", req.path());
}
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);
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
@@ -73,7 +73,7 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
.execute(&mut conn)
.await
{
Ok(_row) => Ok(()),
Ok(row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
@@ -130,7 +130,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
cx,
<div>
<MultiActionForm
// we can handle client-side validation in the on:submit event
// we can handle client-side validation in the on:submit event
// leptos_router implements a `FromFormData` trait that lets you
// parse deserializable types from form data and check them
on:submit=move |ev| {

View File

@@ -1,5 +1,3 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,4 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -7,7 +8,6 @@ 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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;

View File

@@ -1,8 +1,8 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos::*;
if #[cfg(feature = "ssr")] {
use axum::{
routing::{post, get},
extract::{Extension, Path},

View File

@@ -1,4 +1,4 @@
use crate::error_template::ErrorTemplate;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;

View File

@@ -1,5 +1,3 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,4 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -7,7 +8,6 @@ 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 leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;

View File

@@ -1,9 +1,8 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos::*;
if #[cfg(feature = "ssr")] {
use crate::fallback::file_and_error_handler;
use crate::todo::*;
use leptos_viz::{generate_route_list, LeptosRoutes};

View File

@@ -1,4 +1,4 @@
use crate::error_template::ErrorTemplate;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;

View File

@@ -1,5 +1,3 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -136,7 +136,7 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
// Handle the three filter modes: All, Active, and Completed
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener_untyped("hashchange", move |_| {
window_event_listener("hashchange", move |_| {
let new_mode =
location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
@@ -202,15 +202,15 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
}
});
// focus the main input on load
// focus the main input on load
create_effect(cx, move |_| {
if let Some(input) = input_ref.get() {
// We use request_animation_frame here because the NodeRef
// is filled when the element is created, but before it's mounted
// We use request_animation_frame here because the NodeRef
// is filled when the element is created, but before it's mounted
// to the DOM. Calling .focus() before it's mounted does nothing.
// So inside, we wait a tick for the browser to mount it, then .focus()
request_animation_frame(move || {
let _ = input.focus();
input.focus();
});
}
});
@@ -348,14 +348,19 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Active,
Completed,
#[default]
All,
}
impl Default for Mode {
fn default() -> Self {
Mode::All
}
}
pub fn route(hash: &str) -> Mode {
match hash {
"/active" => Mode::Active,

View File

@@ -1,27 +1,33 @@
use crate::Todo;
use leptos::{signal_prelude::*, Scope};
use serde::{Deserialize, Serialize};
use leptos::{
signal_prelude::*,
Scope,
};
use serde::{
Deserialize,
Serialize,
};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: Uuid,
pub title: String,
pub completed: bool,
pub id: Uuid,
pub title: String,
pub completed: bool,
}
impl TodoSerialized {
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
}
impl From<&Todo> for TodoSerialized {
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: todo.completed.get(),
}
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: todo.completed.get(),
}
}
}

View File

@@ -1068,9 +1068,9 @@ where
let path = listing.path();
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.mode(),
listing.methods(),
"/",
Default::default(),
[leptos_router::Method::Get],
)
} else {
listing

View File

@@ -992,9 +992,9 @@ where
let path = listing.path();
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.mode(),
listing.methods(),
"/",
Default::default(),
[leptos_router::Method::Get],
)
} else {
listing

View File

@@ -23,7 +23,7 @@ server_fn = { workspace = true, default-features = false }
leptos = { path = ".", default-features = false }
[features]
default = ["serde", "nightly"]
default = ["csr", "serde"]
csr = [
"leptos_dom/web",
"leptos_macro/csr",
@@ -44,11 +44,11 @@ ssr = [
"leptos_reactive/ssr",
"leptos_server/ssr",
]
nightly = [
"leptos_dom/nightly",
"leptos_macro/nightly",
"leptos_reactive/nightly",
"leptos_server/nightly",
stable = [
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]
@@ -57,10 +57,8 @@ rkyv = ["leptos_reactive/rkyv"]
tracing = ["leptos_macro/tracing"]
[package.metadata.cargo-all-features]
denylist = ["tracing"]
denylist = ["stable", "tracing"]
skip_feature_sets = [
[
],
[
"csr",
"ssr",

View File

@@ -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."
);

View File

@@ -157,7 +157,7 @@ features = [
default = []
web = ["leptos_reactive/csr"]
ssr = ["leptos_reactive/ssr"]
nightly = ["leptos_reactive/nightly"]
stable = ["leptos_reactive/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

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

View File

@@ -1,7 +1,7 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
//! The DOM implementation for `leptos`.
@@ -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)
@@ -1125,7 +1116,7 @@ viewable_primitive![
];
cfg_if! {
if #[cfg(feature = "nightly")] {
if #[cfg(not(feature = "stable"))] {
viewable_primitive! {
std::backtrace::Backtrace
}

View File

@@ -160,7 +160,7 @@ impl<T: ElementDescriptor> Clone for NodeRef<T> {
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
if #[cfg(not(feature = "stable"))] {
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<HtmlElement<T>>;

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ default = ["ssr"]
csr = []
hydrate = []
ssr = []
nightly = ["server_fn_macro/nightly"]
stable = ["server_fn_macro/stable"]
tracing = []
[package.metadata.cargo-all-features]

View File

@@ -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 youd \
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();

View File

@@ -1,4 +1,4 @@
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![forbid(unsafe_code)]
#[macro_use]
@@ -353,7 +353,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
fn normalized_call_site(site: proc_macro::Span) -> Option<String> {
cfg_if::cfg_if! {
if #[cfg(all(debug_assertions, feature = "nightly"))] {
if #[cfg(all(debug_assertions, not(feature = "stable")))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.into()
@@ -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

View File

@@ -68,16 +68,15 @@ hydrate = [
"dep:web-sys",
]
ssr = ["dep:tokio"]
nightly = []
stable = []
serde = []
serde-lite = ["dep:serde-lite"]
miniserde = ["dep:miniserde"]
rkyv = ["dep:rkyv", "dep:bytecheck"]
[package.metadata.cargo-all-features]
denylist = ["stable"]
skip_feature_sets = [
[
],
[
"csr",
"ssr",

View File

@@ -1,7 +1,7 @@
#![deny(missing_docs)]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
//! The reactive system for the [Leptos](https://docs.rs/leptos/latest/leptos/) Web framework.
//!

View File

@@ -17,7 +17,7 @@ use thiserror::Error;
macro_rules! impl_get_fn_traits {
($($ty:ident $(($method_name:ident))?),*) => {
$(
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T: Clone> FnOnce<()> for $ty<T> {
type Output = T;
@@ -27,7 +27,7 @@ macro_rules! impl_get_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T: Clone> FnMut<()> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
@@ -35,7 +35,7 @@ macro_rules! impl_get_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T: Clone> Fn<()> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
@@ -55,7 +55,7 @@ macro_rules! impl_get_fn_traits {
macro_rules! impl_set_fn_traits {
($($ty:ident $($method_name:ident)?),*) => {
$(
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<(T,)> for $ty<T> {
type Output = ();
@@ -65,7 +65,7 @@ macro_rules! impl_set_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T> FnMut<(T,)> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, args: (T,)) -> Self::Output {
@@ -73,7 +73,7 @@ macro_rules! impl_set_fn_traits {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl<T> Fn<(T,)> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {
@@ -315,10 +315,10 @@ pub trait SignalDispose {
/// set_count(1);
/// assert_eq!(count(), 1);
///
/// // ❌ you could call the getter within the setter
/// // ❌ don't try to call the getter within the setter
/// // set_count(count() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// // ✅ instead, use .update() to mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count(), 2);
///
@@ -472,10 +472,10 @@ pub fn create_signal_from_stream<T>(
/// set_count(1);
/// assert_eq!(count(), 1);
///
/// // ❌ you could call the getter within the setter
/// // ❌ don't try to call the getter within the setter
/// // set_count(count() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// // ✅ instead, use .update() to mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count(), 2);
///
@@ -845,10 +845,10 @@ impl<T> Copy for ReadSignal<T> {}
/// set_count(1);
/// assert_eq!(count(), 1);
///
/// // ❌ you could call the getter within the setter
/// // ❌ don't try to call the getter within the setter
/// // set_count(count() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// // ✅ instead, use .update() to mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count(), 2);
/// # }).dispose();
@@ -1103,10 +1103,10 @@ impl<T> Copy for WriteSignal<T> {}
/// count.set(1);
/// assert_eq!(count(), 1);
///
/// // ❌ you can call the getter within the setter
/// // ❌ don't try to call the getter within the setter
/// // count.set(count.get() + 1);
///
/// // ✅ however, it's more efficient to use .update() and mutate the value in place
/// // ✅ instead, use .update() to mutate the value in place
/// count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count(), 2);
/// # }).dispose();
@@ -1163,10 +1163,10 @@ pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
/// count.set(1);
/// assert_eq!(count(), 1);
///
/// // ❌ you can call the getter within the setter
/// // ❌ don't try to call the getter within the setter
/// // count.set(count.get() + 1);
///
/// // ✅ however, it's more efficient to use .update() and mutate the value in place
/// // ✅ instead, use .update() to mutate the value in place
/// count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count(), 2);
/// # }).dispose();

View File

@@ -225,7 +225,7 @@ impl SignalSet<()> for Trigger {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl FnOnce<()> for Trigger {
type Output = ();
@@ -235,7 +235,7 @@ impl FnOnce<()> for Trigger {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl FnMut<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
@@ -243,7 +243,7 @@ impl FnMut<()> for Trigger {
}
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
impl Fn<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {

View File

@@ -1,10 +1,10 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_memo, create_runtime, create_rw_signal,
create_scope, create_signal, SignalSet,
};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn effect_runs() {
use std::{cell::RefCell, rc::Rc};
@@ -32,7 +32,7 @@ fn effect_runs() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn effect_tracks_memo() {
use std::{cell::RefCell, rc::Rc};
@@ -62,7 +62,7 @@ fn effect_tracks_memo() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn untrack_mutes_effect() {
use std::{cell::RefCell, rc::Rc};
@@ -92,7 +92,7 @@ fn untrack_mutes_effect() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn batching_actually_batches() {
use std::{cell::Cell, rc::Rc};

View File

@@ -1,9 +1,9 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_memo, create_runtime, create_scope, create_signal,
};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn basic_memo() {
create_scope(create_runtime(), |cx| {
@@ -13,7 +13,7 @@ fn basic_memo() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn memo_with_computed_value() {
create_scope(create_runtime(), |cx| {
@@ -29,7 +29,7 @@ fn memo_with_computed_value() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn nested_memos() {
create_scope(create_runtime(), |cx| {
@@ -51,7 +51,7 @@ fn nested_memos() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn memo_runs_only_when_inputs_change() {
use std::{cell::Cell, rc::Rc};
@@ -94,7 +94,7 @@ fn memo_runs_only_when_inputs_change() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn diamond_problem() {
use std::{cell::Cell, rc::Rc};
@@ -131,7 +131,7 @@ fn diamond_problem() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn dynamic_dependencies() {
use leptos_reactive::create_isomorphic_effect;

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{create_runtime, create_scope, create_signal};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn basic_signal() {
create_scope(create_runtime(), |cx| {
@@ -13,7 +13,7 @@ fn basic_signal() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn derived_signals() {
create_scope(create_runtime(), |cx| {

View File

@@ -1,10 +1,10 @@
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
use leptos_reactive::{
create_isomorphic_effect, create_runtime, create_scope, create_signal,
signal_prelude::*, SignalGetUntracked, SignalSetUntracked,
};
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn untracked_set_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};
@@ -36,7 +36,7 @@ fn untracked_set_doesnt_trigger_effect() {
.dispose()
}
#[cfg(feature = "nightly")]
#[cfg(not(feature = "stable"))]
#[test]
fn untracked_get_doesnt_trigger_effect() {
use std::{cell::RefCell, rc::Rc};

View File

@@ -26,7 +26,7 @@ default-tls = ["server_fn/default-tls"]
hydrate = ["leptos_reactive/hydrate"]
rustls = ["server_fn/rustls"]
ssr = ["leptos_reactive/ssr", "server_fn/ssr"]
nightly = ["leptos_reactive/nightly", "server_fn/nightly"]
stable = ["leptos_reactive/stable", "server_fn/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

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

View File

@@ -22,7 +22,7 @@ default = []
csr = ["leptos/csr", "leptos/tracing"]
hydrate = ["leptos/hydrate", "leptos/tracing"]
ssr = ["leptos/ssr", "leptos/tracing"]
nightly = ["leptos/nightly", "leptos/tracing"]
stable = ["leptos/stable", "leptos/tracing"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

@@ -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" }
@@ -59,7 +59,7 @@ default = []
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "dep:cached", "dep:lru", "dep:url", "dep:regex"]
nightly = ["leptos/nightly"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature

View File

@@ -206,12 +206,6 @@ where
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
/// progressively enhanced to use client-side routing.
///
/// ## Encoding
/// **Note:** `<ActionForm/>` only works with server functions that use the
/// default `Url` encoding or the `GetJSON` encoding, not with `CBOR` or other
/// encoding schemes. This is to ensure that `<ActionForm/>` works correctly
/// both before and after WASM has loaded.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
@@ -270,61 +264,55 @@ where
let resp = resp.clone().expect("couldn't get Response");
let status = resp.status();
spawn_local(async move {
let redirected = resp.redirected();
if !redirected {
let body = JsFuture::from(
resp.text().expect("couldn't get .text() from Response"),
)
.await;
match body {
Ok(json) => {
// 500 just returns text of error, not JSON
if status == 500 {
let err = ServerFnError::ServerError(
json.as_string().unwrap_or_default(),
);
if let Some(error) = error {
error.try_set(Some(Box::new(err.clone())));
}
value.try_set(Some(Err(err)));
} else {
match O::de(
&json.as_string().expect(
"couldn't get String from JsString",
),
) {
Ok(res) => {
value.try_set(Some(Ok(res)));
if let Some(error) = error {
error.try_set(None);
}
}
Err(e) => {
value.try_set(Some(Err(
ServerFnError::Deserialization(
e.to_string(),
),
)));
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
}
}
}
}
}
Err(e) => {
error!("{e:?}");
let body = JsFuture::from(
resp.text().expect("couldn't get .text() from Response"),
)
.await;
match body {
Ok(json) => {
// 500 just returns text of error, not JSON
if status == 500 {
let err = ServerFnError::ServerError(
json.as_string().unwrap_or_default(),
);
if let Some(error) = error {
error.try_set(Some(Box::new(
ServerFnError::Request(
e.as_string().unwrap_or_default(),
),
)));
error.try_set(Some(Box::new(err.clone())));
}
value.try_set(Some(Err(err)));
} else {
match O::de(
&json
.as_string()
.expect("couldn't get String from JsString"),
) {
Ok(res) => {
value.try_set(Some(Ok(res)));
if let Some(error) = error {
error.try_set(None);
}
}
Err(e) => {
value.try_set(Some(Err(
ServerFnError::Deserialization(
e.to_string(),
),
)));
if let Some(error) = error {
error.try_set(Some(Box::new(e)));
}
}
}
}
};
}
}
Err(e) => {
error!("{e:?}");
if let Some(error) = error {
error.try_set(Some(Box::new(ServerFnError::Request(
e.as_string().unwrap_or_default(),
))));
}
}
};
input.try_set(None);
action.set_pending(false);
});
@@ -549,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
@@ -565,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();
@@ -578,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)
}
}

View File

@@ -140,7 +140,7 @@ where
}
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
if #[cfg(not(feature = "stable"))] {
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}

View File

@@ -183,9 +183,9 @@
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
mod animation;
mod components;

View File

@@ -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"
@@ -34,4 +34,4 @@ default = ["default-tls"]
default-tls = ["reqwest/default-tls"]
rustls = ["reqwest/rustls-tls"]
ssr = []
nightly = ["server_fn_macro_default/nightly"]
stable = ["server_fn_macro_default/stable"]

View File

@@ -19,4 +19,4 @@ server_fn = { version = "0.2" }
serde = "1"
[features]
nightly = ["server_fn_macro/nightly"]
stable = ["server_fn_macro/stable"]

View File

@@ -1,4 +1,4 @@
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
#![forbid(unsafe_code)]
@@ -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 {

View File

@@ -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 => {

View File

@@ -18,4 +18,4 @@ xxhash-rust = { version = "0.8.6", features = ["const_xxh64"] }
const_format = "0.2.30"
[features]
nightly = []
stable = []

View File

@@ -1,4 +1,4 @@
#![cfg_attr(feature = "nightly", feature(proc_macro_span))]
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
//! Implementation of the server_fn macro.