diff --git a/examples/ssr_modes_axum/src/app.rs b/examples/ssr_modes_axum/src/app.rs index 33faa0d57..be343bb6f 100644 --- a/examples/ssr_modes_axum/src/app.rs +++ b/examples/ssr_modes_axum/src/app.rs @@ -1,17 +1,9 @@ -use lazy_static::lazy_static; use leptos::prelude::*; -use leptos_meta::MetaTags; -use leptos_meta::*; +use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; use leptos_router::{ - components::{FlatRoutes, ProtectedRoute, Route, Router}, - hooks::use_params, - params::Params, - ParamSegment, SsrMode, StaticSegment, + components::{ProtectedParentRoute, Route, Router, Routes, A}, + StaticSegment, }; -use serde::{Deserialize, Serialize}; -#[cfg(feature = "ssr")] -use std::sync::atomic::{AtomicBool, Ordering}; -use thiserror::Error; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -31,90 +23,34 @@ pub fn shell(options: LeptosOptions) -> impl IntoView { } } -#[cfg(feature = "ssr")] -static IS_ADMIN: AtomicBool = AtomicBool::new(true); - -#[server] -pub async fn is_admin() -> Result { - Ok(IS_ADMIN.load(Ordering::Relaxed)) -} - -#[server] -pub async fn set_is_admin(is_admin: bool) -> Result<(), ServerFnError> { - IS_ADMIN.store(is_admin, Ordering::Relaxed); - Ok(()) -} - #[component] pub fn App() -> impl IntoView { - // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); - let fallback = || view! { "Page not found." }.into_view(); - let toggle_admin = ServerAction::::new(); - let is_admin = - Resource::new(move || toggle_admin.version().get(), |_| is_admin()); + + let x = untrack(|| "foo"); view! { - - - <Meta name="color-scheme" content="dark light"/> <Router> <nav> - <a href="/">"Home"</a> - <a href="/admin">"Admin"</a> - <Transition> - <ActionForm action=toggle_admin> - <input - type="hidden" - name="is_admin" - value=move || { - (!is_admin.get().and_then(|n| n.ok()).unwrap_or_default()) - .to_string() - } - /> - - <button> - {move || { - if is_admin.get().and_then(Result::ok).unwrap_or_default() { - "Log Out" - } else { - "Log In" - } - }} - - </button> - </ActionForm> - </Transition> + <A href="/">"Home"</A> + " | " + <A href="/dashboard">"Dashboard"</A> + " | " + <A href="/profile">"Profile"</A> </nav> <main> - <FlatRoutes fallback> - // We’ll load the home page with out-of-order streaming and <Suspense/> + <Routes fallback=|| "Page not found.".into_view()> <Route path=StaticSegment("") view=HomePage/> - - // We'll load the posts with async rendering, so they can set - // the title and metadata *after* loading the data - <Route - path=(StaticSegment("post"), ParamSegment("id")) - view=Post - ssr=SsrMode::Async - /> - <Route - path=(StaticSegment("post_in_order"), ParamSegment("id")) - view=Post - ssr=SsrMode::InOrder - /> - <Route - path=(StaticSegment("post_partially_blocked"), ParamSegment("id")) - view=Post - /> - <ProtectedRoute - path=StaticSegment("admin") - view=Admin - ssr=SsrMode::Async - condition=move || is_admin.get().map(|n| n.unwrap_or(false)) - redirect_path=|| "/" - /> - </FlatRoutes> + <ProtectedParentRoute + path=StaticSegment("") + view=|| view! { <div>"Protected Content"</div> } + condition=move || Some(false) + redirect_path=|| "/".to_string() + > + <Route path=StaticSegment("dashboard") view=DashboardPage/> + <Route path=StaticSegment("profile") view=ProfilePage/> + </ProtectedParentRoute> + </Routes> </main> </Router> } @@ -122,209 +58,24 @@ pub fn App() -> impl IntoView { #[component] fn HomePage() -> impl IntoView { - // load the posts - let posts = Resource::new(|| (), |_| list_post_metadata()); - let posts = move || { - posts - .get() - .map(|n| n.unwrap_or_default()) - .unwrap_or_default() - }; - - let posts2 = Resource::new(|| (), |_| list_post_metadata()); - let posts2 = Resource::new( - || (), - move |_| async move { posts2.await.as_ref().map(Vec::len).unwrap_or(0) }, - ); - view! { - <h1>"My Great Blog"</h1> - <Suspense fallback=move || view! { <p>"Loading posts..."</p> }> - <p>"number of posts: " {Suspend::new(async move { posts2.await })}</p> - </Suspense> - <Suspense fallback=move || view! { <p>"Loading posts..."</p> }> - <ul> - <For each=posts key=|post| post.id let:post> - <li> - <a href=format!("/post/{}", post.id)>{post.title.clone()}</a> - "|" - <a href=format!( - "/post_in_order/{}", - post.id, - )>{post.title.clone()} "(in order)"</a> - "|" - <a href=format!( - "/post_partially_blocked/{}", - post.id, - )>{post.title} "(partially blocked)"</a> - </li> - </For> - </ul> - </Suspense> - } -} - -#[derive(Params, Copy, Clone, Debug, PartialEq, Eq)] -pub struct PostParams { - id: Option<usize>, -} - -#[component] -fn Post() -> impl IntoView { - let query = use_params::<PostParams>(); - let id = move || { - query.with(|q| { - q.as_ref() - .map(|q| q.id.unwrap_or_default()) - .map_err(|_| PostError::InvalidId) - }) - }; - let post_resource = Resource::new_blocking(id, |id| async move { - match id { - Err(e) => Err(e), - Ok(id) => get_post(id) - .await - .map(|data| data.ok_or(PostError::PostNotFound)) - .map_err(|_| PostError::ServerError), - } - }); - let comments_resource = Resource::new(id, |id| async move { - match id { - Err(e) => Err(e), - Ok(id) => { - get_comments(id).await.map_err(|_| PostError::ServerError) - } - } - }); - - let post_view = Suspend::new(async move { - match post_resource.await { - Ok(Ok(post)) => { - Ok(view! { - <h1>{post.title.clone()}</h1> - <p>{post.content.clone()}</p> - - // since we're using async rendering for this page, - // this metadata should be included in the actual HTML <head> - // when it's first served - <Title text=post.title/> - <Meta name="description" content=post.content/> - }) - } - _ => Err(PostError::ServerError), - } - }); - let comments_view = Suspend::new(async move { - match comments_resource.await { - Ok(comments) => Ok(view! { - <h1>"Comments"</h1> - <ul> - {comments - .into_iter() - .map(|comment| view! { <li>{comment}</li> }) - .collect_view()} - - </ul> - }), - _ => Err(PostError::ServerError), - } - }); - - view! { - <em>"The world's best content."</em> - <Suspense fallback=move || view! { <p>"Loading post..."</p> }> - <ErrorBoundary fallback=|errors| { - view! { - <div class="error"> - <h1>"Something went wrong."</h1> - <ul> - {move || { - errors - .get() - .into_iter() - .map(|(_, error)| view! { <li>{error.to_string()}</li> }) - .collect::<Vec<_>>() - }} - - </ul> - </div> - } - }>{post_view}</ErrorBoundary> - </Suspense> - <Suspense fallback=move || view! { <p>"Loading comments..."</p> }>{comments_view}</Suspense> + <h1>"Welcome to the Home Page"</h1> + <p>"This page is accessible to everyone."</p> } } #[component] -pub fn Admin() -> impl IntoView { - view! { <p>"You can only see this page if you're logged in."</p> } +fn DashboardPage() -> impl IntoView { + view! { + <h1>"Dashboard"</h1> + <p>"This is a protected page. You should only see this if you're authenticated."</p> + } } -// Dummy API -lazy_static! { - static ref POSTS: Vec<Post> = vec![ - Post { - id: 0, - title: "My first post".to_string(), - content: "This is my first post".to_string(), - }, - Post { - id: 1, - title: "My second post".to_string(), - content: "This is my second post".to_string(), - }, - Post { - id: 2, - title: "My third post".to_string(), - content: "This is my third post".to_string(), - }, - ]; -} - -#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PostError { - #[error("Invalid post ID.")] - InvalidId, - #[error("Post not found.")] - PostNotFound, - #[error("Server error.")] - ServerError, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct Post { - id: usize, - title: String, - content: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct PostMetadata { - id: usize, - title: String, -} - -#[server] -pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - Ok(POSTS - .iter() - .map(|data| PostMetadata { - id: data.id, - title: data.title.clone(), - }) - .collect()) -} - -#[server] -pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - Ok(POSTS.iter().find(|post| post.id == id).cloned()) -} - -#[server] -pub async fn get_comments(id: usize) -> Result<Vec<String>, ServerFnError> { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - _ = id; - Ok(vec!["Some comment".into(), "Some other comment".into()]) +#[component] +fn ProfilePage() -> impl IntoView { + view! { + <h1>"Profile"</h1> + <p>"This is another protected page. You should only see this if you're authenticated."</p> + } } diff --git a/leptos/src/lib.rs b/leptos/src/lib.rs index 9ff269bb7..7bb7e6bed 100644 --- a/leptos/src/lib.rs +++ b/leptos/src/lib.rs @@ -168,7 +168,7 @@ pub mod prelude { pub use leptos_server::*; pub use oco_ref::*; pub use reactive_graph::{ - actions::*, computed::*, effect::*, owner::*, signal::*, + actions::*, computed::*, effect::*, owner::*, signal::*, untrack, wrappers::read::*, }; pub use server_fn::{self, ServerFnError}; diff --git a/router/src/components.rs b/router/src/components.rs index 2b198aab2..538efd750 100644 --- a/router/src/components.rs +++ b/router/src/components.rs @@ -443,6 +443,7 @@ pub fn Redirect<P>( ) where P: core::fmt::Display + 'static, { + leptos::logging::log!("running Redirect component"); // TODO resolve relative path let path = path.to_string();