From e86d2735027f33bdcbd4496477746ce0515be55a Mon Sep 17 00:00:00 2001 From: luoxiao Date: Tue, 30 Jul 2024 14:26:59 +0800 Subject: [PATCH] feat: support SSR --- demo/src/components/site_header.rs | 27 ++--- examples/ssr_axum/Cargo.toml | 3 +- examples/ssr_axum/src/lib.rs | 1 + thaw/src/anchor/mod.rs | 171 ++++++++++++++++------------- thaw_utils/src/class_list.rs | 20 +++- thaw_utils/src/dom/mount_style.rs | 18 +-- 6 files changed, 135 insertions(+), 105 deletions(-) diff --git a/demo/src/components/site_header.rs b/demo/src/components/site_header.rs index f0f4eec..94c37af 100644 --- a/demo/src/components/site_header.rs +++ b/demo/src/components/site_header.rs @@ -1,8 +1,5 @@ use super::switch_version::SwitchVersion; -use leptos::{ - ev::{self, MouseEvent}, - prelude::*, -}; +use leptos::{ev::MouseEvent, prelude::*}; use leptos_meta::Style; use leptos_router::hooks::use_navigate; // use leptos_use::{storage::use_local_storage, utils::FromToStringCodec}; @@ -77,16 +74,20 @@ pub fn SiteHeader() -> impl IntoView { } }; let auto_complete_ref = ComponentRef::::new(); - let handle = window_event_listener(ev::keydown, move |event| { - let key = event.key(); - if key == *"/" { - if let Some(auto_complete_ref) = auto_complete_ref.get_untracked() { - event.prevent_default(); - auto_complete_ref.focus(); + #[cfg(any(feature = "csr", feature = "hydrate"))] + { + use leptos::ev; + let handle = window_event_listener(ev::keydown, move |event| { + let key = event.key(); + if key == *"/" { + if let Some(auto_complete_ref) = auto_complete_ref.get_untracked() { + event.prevent_default(); + auto_complete_ref.focus(); + } } - } - }); - on_cleanup(move || handle.remove()); + }); + on_cleanup(move || handle.remove()); + } // let menu_value = use_menu_value(change_theme); view! { diff --git a/examples/ssr_axum/Cargo.toml b/examples/ssr_axum/Cargo.toml index 9e221a4..a9b0493 100644 --- a/examples/ssr_axum/Cargo.toml +++ b/examples/ssr_axum/Cargo.toml @@ -20,7 +20,8 @@ wasm-bindgen = "=0.2.92" thiserror = "1" tracing = { version = "0.1", optional = true } http = "1" - +console_log = "1" +log = "0.4" demo = { path = "../../demo", default-features = false } [features] diff --git a/examples/ssr_axum/src/lib.rs b/examples/ssr_axum/src/lib.rs index 151489d..b26f571 100644 --- a/examples/ssr_axum/src/lib.rs +++ b/examples/ssr_axum/src/lib.rs @@ -4,6 +4,7 @@ pub mod app; #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() { use crate::app::*; + let _ = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); leptos::mount::hydrate_body(App); } diff --git a/thaw/src/anchor/mod.rs b/thaw/src/anchor/mod.rs index 80e031c..4bffb93 100644 --- a/thaw/src/anchor/mod.rs +++ b/thaw/src/anchor/mod.rs @@ -2,17 +2,17 @@ mod anchor_link; pub use anchor_link::AnchorLink; -use leptos::{context::Provider, ev, html, prelude::*}; -use std::cmp::Ordering; -use thaw_utils::{add_event_listener_with_bool, class_list, mount_style, throttle}; +use leptos::{context::Provider, html, prelude::*}; +use thaw_utils::{class_list, mount_style}; use web_sys::{DomRect, Element}; #[component] pub fn Anchor( #[prop(optional, into)] class: MaybeProp, - /// The element or selector used to calc offset of link elements. + /// The element or selector used to calc offset of link elements. /// If you are not scrolling the entire document but only a part of it, you may need to set this. - #[prop(into, optional)] offset_target: Option, + #[prop(into, optional)] + offset_target: Option, children: Children, ) -> impl IntoView { mount_style("anchor", include_str!("./anchor.css")); @@ -21,72 +21,102 @@ pub fn Anchor( let element_ids = RwSignal::new(Vec::::new()); let active_id = RwSignal::new(None::); - let offset_target = send_wrapper::SendWrapper::new(offset_target); - let on_scroll = move || { - element_ids.with(|ids| { - let offset_target_top = if let Some(offset_target) = offset_target.as_ref() { - if let Some(rect) = offset_target.get_bounding_client_rect() { - rect.top() - } else { - return; - } - } else { - 0.0 - }; + #[cfg(any(feature = "csr", feature = "hydrate"))] + { + use leptos::ev; + use std::cmp::Ordering; + use thaw_utils::{add_event_listener_with_bool, throttle}; - let mut links: Vec = vec![]; - for id in ids.iter() { - if let Some(link_el) = document().get_element_by_id(id) { - let link_rect = link_el.get_bounding_client_rect(); - links.push(LinkInfo { - top: link_rect.top() - offset_target_top, - id: id.clone(), - }); + struct LinkInfo { + top: f64, + id: String, + } + + impl OffsetTarget { + fn get_bounding_client_rect(&self) -> Option { + match self { + OffsetTarget::Selector(selector) => { + let el = document().query_selector(selector).ok().flatten()?; + Some(el.get_bounding_client_rect()) + } + OffsetTarget::Element(el) => Some(el.get_bounding_client_rect()), } } - links.sort_by(|a, b| { - if a.top > b.top { - Ordering::Greater - } else { - Ordering::Less - } - }); + } - let mut temp_link = None::; - for link in links.into_iter() { - if link.top >= 0.0 { - if link.top <= 12.0 { - temp_link = Some(link); - break; - } else if temp_link.is_some() { - break; + let offset_target = send_wrapper::SendWrapper::new(offset_target); + + let on_scroll = move || { + element_ids.with(|ids| { + let offset_target_top = if let Some(offset_target) = offset_target.as_ref() { + if let Some(rect) = offset_target.get_bounding_client_rect() { + rect.top() } else { - temp_link = None; + return; } } else { - temp_link = Some(link); + 0.0 + }; + + let mut links: Vec = vec![]; + for id in ids.iter() { + if let Some(link_el) = document().get_element_by_id(id) { + let link_rect = link_el.get_bounding_client_rect(); + links.push(LinkInfo { + top: link_rect.top() - offset_target_top, + id: id.clone(), + }); + } } - } - active_id.set(temp_link.map(|link| link.id)); + links.sort_by(|a, b| { + if a.top > b.top { + Ordering::Greater + } else { + Ordering::Less + } + }); + + let mut temp_link = None::; + for link in links.into_iter() { + if link.top >= 0.0 { + if link.top <= 12.0 { + temp_link = Some(link); + break; + } else if temp_link.is_some() { + break; + } else { + temp_link = None; + } + } else { + temp_link = Some(link); + } + } + active_id.set(temp_link.map(|link| link.id)); + }); + }; + let cb = throttle( + move || { + on_scroll(); + }, + std::time::Duration::from_millis(200), + ); + let scroll_handle = add_event_listener_with_bool( + document(), + ev::scroll, + move |_| { + cb(); + }, + true, + ); + on_cleanup(move || { + scroll_handle.remove(); }); - }; - let cb = throttle( - move || { - on_scroll(); - }, - std::time::Duration::from_millis(200), - ); - let scroll_handle = add_event_listener_with_bool( - document(), - ev::scroll, - move |_| { - cb(); - }, - true, - ); - on_cleanup(move || { - scroll_handle.remove(); - }); + } + #[cfg(not(any(feature = "csr", feature = "hydrate")))] + { + let _ = offset_target; + } + view! {
Option { - match self { - OffsetTarget::Selector(selector) => { - let el = document().query_selector(selector).ok().flatten()?; - Some(el.get_bounding_client_rect()) - } - OffsetTarget::Element(el) => Some(el.get_bounding_client_rect()), - } - } -} + impl From<&'static str> for OffsetTarget { fn from(value: &'static str) -> Self { diff --git a/thaw_utils/src/class_list.rs b/thaw_utils/src/class_list.rs index b32b492..c42b80b 100644 --- a/thaw_utils/src/class_list.rs +++ b/thaw_utils/src/class_list.rs @@ -3,7 +3,9 @@ use leptos::{ reactive_graph::traits::{Get, Update, With, WithUntracked}, tachys::renderer::DomRenderer, }; -use std::{collections::HashSet, sync::Arc}; +use std::collections::HashSet; +#[cfg(not(feature = "ssr"))] +use std::sync::Arc; #[derive(Clone, Default)] pub struct ClassList { @@ -21,6 +23,7 @@ impl ClassList { Default::default() } + #[allow(unused_mut)] pub fn add(mut self, value: impl IntoClass) -> Self { let class = value.into_class(); match class { @@ -188,11 +191,18 @@ where fn hydrate(self, el: &R::Element) -> Self::State { let el = el.to_owned(); RenderEffect::new(move |prev| { - if let Some(_) = prev { - unreachable!() + let mut class = String::new(); + self.write_class_string(&mut class); + + if let Some(state) = prev { + let (el, prev_class) = state; + if class != prev_class { + R::set_attribute(&el, "class", &class); + (el, class) + } else { + (el, prev_class) + } } else { - let mut class = String::new(); - self.write_class_string(&mut class); if !class.is_empty() { if !FROM_SERVER { R::set_attribute(&el, "class", &class); diff --git a/thaw_utils/src/dom/mount_style.rs b/thaw_utils/src/dom/mount_style.rs index e6cdb40..f2c9950 100644 --- a/thaw_utils/src/dom/mount_style.rs +++ b/thaw_utils/src/dom/mount_style.rs @@ -1,13 +1,14 @@ use cfg_if::cfg_if; pub fn mount_style(id: &str, content: &'static str) { + let id = format!("thaw-id-{id}"); cfg_if! { if #[cfg(feature = "ssr")] { - use leptos::{tachys::view::Render, view}; + use leptos::view; use leptos_meta::Style; let _ = view! { - }; @@ -15,7 +16,7 @@ pub fn mount_style(id: &str, content: &'static str) { use leptos::prelude::document; let head = document().head().expect("head no exist"); let style = head - .query_selector(&format!("style[data-thaw-id=\"{id}\"]")) + .query_selector(&format!("style#{id}")) .expect("query style element error"); if style.is_some() { @@ -25,7 +26,7 @@ pub fn mount_style(id: &str, content: &'static str) { let style = document() .create_element("style") .expect("create style element error"); - _ = style.set_attribute("data-thaw-id", id); + _ = style.set_attribute("id", &id); style.set_text_content(Some(content)); _ = head.prepend_with_node_1(&style); } @@ -33,13 +34,14 @@ pub fn mount_style(id: &str, content: &'static str) { } pub fn mount_dynamic_style String + Send + Sync + 'static>(id: String, f: T) { + let id = format!("thaw-id-{id}"); cfg_if! { if #[cfg(feature = "ssr")] { - use leptos::{tachys::view::Render, view}; + use leptos::view; use leptos_meta::Style; let _ = view! { - }; @@ -49,12 +51,12 @@ pub fn mount_dynamic_style String + Send + Sync + 'static>(id: String let head = document().head().expect("head no exist"); let style = head - .query_selector(&format!("style[data-thaw-id=\"{id}\"]")) + .query_selector(&format!("style#{id}")) .expect("query style element error").unwrap_or_else(|| { let style = document() .create_element("style") .expect("create style element error"); - _ = style.set_attribute("data-thaw-id", &id); + _ = style.set_attribute("id", &id); _ = head.prepend_with_node_1(&style); style