Compare commits

..

17 Commits

Author SHA1 Message Date
Greg Johnston
0821de8a3a invert stable and nightly features 2023-05-05 17:27:49 -04:00
Greg Johnston
a5f6e0bac4 docs: document that <ActionForm/> only works with form-encoded server functions (closes #977) (#1005) 2023-05-05 13:37:53 -04:00
Douglas Parsons
2c9de79576 docs: Reduce firmness of overlapping signals warnings (#1004)
Following [discord
question](https://discord.com/channels/1031524867910148188/1049869221636620300/1104043773194928163)
2023-05-05 11:28:36 -04:00
agilarity
63dd00a050 fix: lint issues in todomvc example (#1001)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:28:14 -04:00
agilarity
99823a3d4f fix: lint issues in todo_app_sqlite_viz example (#1000)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:38 -04:00
agilarity
c0bdd464f6 fix: lint issues in todo_app_sqlite_axum example (#999)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:27 -04:00
agilarity
7e7377f4f7 fix: lint issues in todo_app_sqlite example (#998)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:27:11 -04:00
agilarity
15448765dd fix: lint issues in session_auth_axum example (#997)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:26:44 -04:00
agilarity
f0f1c3144b fix: lint issues in router example (#996)
* build: add common tasks

* fix: resolve check-style issues
2023-05-05 11:26:24 -04:00
agilarity
630da4212d fix: lint issues in login_with_token_csr_only example (#995)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:26:09 -04:00
agilarity
38bc24bb9e fix: lint issues in hackernews_axum example (#992)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:25:24 -04:00
agilarity
012285337b fix: lint issues in hackernews example (#991)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:25:13 -04:00
agilarity
3ba4f62cef fix: lint issues in fetch example (#989)
* build: add common tasks

* test: resolve check-style issues
2023-05-05 11:24:28 -04:00
agilarity
b4996769c1 fix: lint issues in errors_axum example (#988) 2023-05-05 11:23:59 -04:00
Greg Johnston
9a6b1f53da fix: lint issues in counters example
fix: lint issues in `counters` example
2023-05-05 11:23:38 -04:00
Greg Johnston
ef45828ca7 fix: don't assume OutOfOrder and GET for / 2023-05-05 10:20:36 -04:00
Joseph Cruz
5ab799bbf8 test: resolve check-style issues 2023-05-03 12:34:03 -04:00
86 changed files with 492 additions and 525 deletions

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
use crate::{
error_template::{ErrorTemplate, ErrorTemplateProps},
errors::AppError,
};
use crate::{error_template::ErrorTemplate, errors::AppError};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
@@ -54,7 +51,8 @@ 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,5 +1,4 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -8,6 +7,7 @@ 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,7 +37,8 @@ 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();
@@ -51,7 +52,11 @@ 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,3 +1,5 @@
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,3 +1,5 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -4,10 +4,7 @@ use leptos_meta::*;
use leptos_router::*;
mod api;
mod routes;
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
use routes::{nav::*, stories::*, story::*, 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,AppProps};
use hackernews::{App};
use leptos_actix::{LeptosRoutes, generate_route_list};
#[get("/style.css")]
@@ -46,7 +46,7 @@ cfg_if! {
}
} else {
fn main() {
use hackernews::{App, AppProps};
use hackernews::{App};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -1,8 +1,7 @@
use crate::api;
use leptos::*;
use leptos_router::*;
use crate::api;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
@@ -37,8 +36,10 @@ 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,11 +13,20 @@ 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>
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
<li inner_html={user.about} class="about"></li>
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>

View File

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

View File

@@ -1,7 +1,4 @@
use leptos::{
signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
View,
};
use leptos::{view, Errors, For, 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,10 +7,7 @@ pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
use routes::{nav::*, stories::*, story::*, users::*};
#[component]
pub fn App(cx: Scope) -> impl IntoView {

View File

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

View File

@@ -1,8 +1,7 @@
use crate::api;
use leptos::*;
use leptos_router::*;
use crate::api;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
@@ -37,8 +36,10 @@ 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,11 +13,20 @@ 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,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
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,54 +20,56 @@ 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);
}
// 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();
<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);
}
_=> {
let val = event_target_value(&ev);
set_password.update(|p|*p = val);
on:change=move |ev| {
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_password.update(|p|*p = val);
}
/>
<button
prop:disabled = move || button_is_disabled.get()
on:click = move |_| dispatch_action()
>
{ action_label }
</button>
</form>
/>
<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>
}
}

View File

@@ -1,8 +1,7 @@
use crate::Page;
use leptos::*;
use leptos_router::*;
use crate::Page;
#[component]
pub fn NavBar<F>(
cx: Scope,
@@ -13,20 +12,27 @@ 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,9 +1,8 @@
use api_boundary::*;
use gloo_storage::{LocalStorage, Storage};
use leptos::*;
use leptos_router::*;
use api_boundary::*;
mod api;
mod components;
mod pages;
@@ -86,45 +85,51 @@ 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,9 +1,8 @@
use leptos::*;
use client::*;
use leptos::*;
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,15 +6,19 @@ 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,13 +1,11 @@
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
@@ -53,14 +51,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,13 +1,11 @@
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 {
@@ -52,26 +50,24 @@ 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

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

View File

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

View File

@@ -1,5 +1,4 @@
use std::{env, sync::Arc};
use api_boundary as json;
use axum::{
extract::{State, TypedHeader},
headers::{authorization::Bearer, Authorization},
@@ -8,10 +7,9 @@ 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;
@@ -25,7 +23,10 @@ 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,3 +1,5 @@
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,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,9 +1,7 @@
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")] {
@@ -168,7 +166,9 @@ 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,7 +13,9 @@ 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,5 +1,4 @@
use crate::auth::*;
use crate::error_template::{ErrorTemplate, ErrorTemplateProps};
use crate::{auth::*, error_template::ErrorTemplate};
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
@@ -73,7 +72,8 @@ 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,11 +111,13 @@ 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())),
@@ -304,7 +306,10 @@ 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>
@@ -330,7 +335,10 @@ pub fn Login(cx: Scope, action: Action<Login, Result<(), ServerFnError>>) -> imp
}
#[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>
@@ -362,7 +370,10 @@ pub fn Signup(cx: Scope, action: Action<Signup, Result<(), ServerFnError>>) -> i
}
#[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

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

View File

@@ -1,3 +1,5 @@
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.clone();
let addr = conf.leptos_options.site_addr;
// 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> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
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())),
}
}
@@ -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>
@@ -133,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,3 +1,5 @@
extend = { path = "../cargo-make/common.toml" }
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,5 +1,4 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -8,6 +7,7 @@ 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")] {
if #[cfg(feature = "ssr")] {
use leptos::*;
use axum::{
routing::{post, get},
extract::{Extension, Path},

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod error_template;
pub mod errors;
pub mod fallback;
@@ -8,6 +7,7 @@ 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,9 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
if #[cfg(feature = "ssr")] {
use leptos::*;
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, ErrorTemplateProps};
use crate::error_template::ErrorTemplate;
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;

View File

@@ -1,3 +1,5 @@
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("hashchange", move |_| {
window_event_listener_untyped("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 || {
input.focus();
let _ = input.focus();
});
}
});
@@ -348,19 +348,14 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, 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,33 +1,27 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
//! The DOM implementation for `leptos`.
@@ -1125,7 +1125,7 @@ viewable_primitive![
];
cfg_if! {
if #[cfg(not(feature = "stable"))] {
if #[cfg(feature = "nightly")] {
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(not(feature = "stable"))] {
if #[cfg(feature = "nightly")] {
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
type Output = Option<HtmlElement<T>>;

View File

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

View File

@@ -1,4 +1,4 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![cfg_attr(feature = "nightly", 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, not(feature = "stable")))] {
if #[cfg(all(debug_assertions, feature = "nightly"))] {
Some(leptos_hot_reload::span_to_stable_id(
site.source_file().path(),
site.into()

View File

@@ -68,15 +68,16 @@ hydrate = [
"dep:web-sys",
]
ssr = ["dep:tokio"]
stable = []
nightly = []
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(not(feature = "stable"), feature(fn_traits))]
#![cfg_attr(not(feature = "stable"), feature(unboxed_closures))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", 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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
impl<T: Clone> FnOnce<()> for $ty<T> {
type Output = T;
@@ -27,7 +27,7 @@ macro_rules! impl_get_fn_traits {
}
}
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
impl<T> FnOnce<(T,)> for $ty<T> {
type Output = ();
@@ -65,7 +65,7 @@ macro_rules! impl_set_fn_traits {
}
}
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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);
///
/// // ❌ don't try to call the getter within the setter
/// // ❌ you could call the getter within the setter
/// // set_count(count() + 1);
///
/// // ✅ instead, use .update() to mutate the value in place
/// // ✅ however it's more efficient to use .update() and 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);
///
/// // ❌ don't try to call the getter within the setter
/// // ❌ you could call the getter within the setter
/// // set_count(count() + 1);
///
/// // ✅ instead, use .update() to mutate the value in place
/// // ✅ however it's more efficient to use .update() and 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);
///
/// // ❌ don't try to call the getter within the setter
/// // ❌ you could call the getter within the setter
/// // set_count(count() + 1);
///
/// // ✅ instead, use .update() to mutate the value in place
/// // ✅ however it's more efficient to use .update() and 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);
///
/// // ❌ don't try to call the getter within the setter
/// // ❌ you can call the getter within the setter
/// // count.set(count.get() + 1);
///
/// // ✅ instead, use .update() to mutate the value in place
/// // ✅ however, it's more efficient to use .update() and 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);
///
/// // ❌ don't try to call the getter within the setter
/// // ❌ you can call the getter within the setter
/// // count.set(count.get() + 1);
///
/// // ✅ instead, use .update() to mutate the value in place
/// // ✅ however, it's more efficient to use .update() and 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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
impl FnOnce<()> for Trigger {
type Output = ();
@@ -235,7 +235,7 @@ impl FnOnce<()> for Trigger {
}
}
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
impl Fn<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
use leptos_reactive::{
create_isomorphic_effect, create_runtime, create_scope, create_signal,
signal_prelude::*, SignalGetUntracked, SignalSetUntracked,
};
#[cfg(not(feature = "stable"))]
#[cfg(feature = "nightly")]
#[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(not(feature = "stable"))]
#[cfg(feature = "nightly")]
#[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"]
stable = ["leptos_reactive/stable", "server_fn/stable"]
nightly = ["leptos_reactive/nightly", "server_fn/nightly"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View File

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

View File

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

View File

@@ -206,6 +206,12 @@ 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,)
@@ -264,55 +270,61 @@ where
let resp = resp.clone().expect("couldn't get Response");
let status = resp.status();
spawn_local(async move {
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(),
);
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:?}");
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)));
}
}
error.try_set(Some(Box::new(
ServerFnError::Request(
e.as_string().unwrap_or_default(),
),
)));
}
}
}
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);
});

View File

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

View File

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

View File

@@ -140,7 +140,7 @@ where
}
cfg_if::cfg_if! {
if #[cfg(not(feature = "stable"))] {
if #[cfg(feature = "nightly")] {
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(not(feature = "stable"), feature(auto_traits))]
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
#![cfg_attr(feature = "nightly", feature(auto_traits))]
#![cfg_attr(feature = "nightly", feature(negative_impls))]
#![cfg_attr(feature = "nightly", feature(type_name_of_val))]
mod animation;
mod components;

View File

@@ -34,4 +34,4 @@ default = ["default-tls"]
default-tls = ["reqwest/default-tls"]
rustls = ["reqwest/rustls-tls"]
ssr = []
stable = ["server_fn_macro_default/stable"]
nightly = ["server_fn_macro_default/nightly"]

View File

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

View File

@@ -1,4 +1,4 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
#![cfg_attr(feature = "nightly", 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)]

View File

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

View File

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