+5!2p{&S>`I$D#_hrR;f_`BP
zUTDW}gWRw_!5_O!8mKrMuMk!)PLx9V{;ZhxMA`P9&sXFV{=D1uo}CqcG4b+cmB)$s
z@7fZ|O2Q@%$Y;dGMeHkiuPfSr>bp|P6MwVR_N|qd4=W!|l-qYjI4fm`+l%y9VNWjy
z+HtmgZ{3p5Yx_F1Uvt!Td9283_*FZ5{xbHDl!HsMYjuaEy8
zGqbYA>(p$Yn7>asLegM7$7NfM_IxzDT$a(NK5G4ak1^UOJHLId@j@_q(8;N{L&`5V
ztNsh0%c0L3AN%sT5SuQ
z-gdJ356&JhwHc3^;;rN8D4ygFSG%L=pAwB1f?AP^o+B8t{Rp^nHSVjIEpXk5Q
zuCwBC6!q|I%?pf{IXP*DpY^3nKRH~5K9o{bz8ufdy19>-u~!??zvXpJ(tmz!GNW`%
z?DjrZJDx5h!`s(4->=-9$g@AiYOfb7=IQ(X8rna!_S?~)&)21y#^<@w`re+O|Mz6Y
zafqO@XkNpS?Q3M9_0Fqv#^>vz(wO=Mt+|HTQ&TqQt?J@V@3N%J9J@Z2WmP37rT6XN
zw7L+h*!ES1RrQ=WR?`Je^kZq-T4*?yeHCTZ4o-^6063MOJC4Dt2&?vSqAk&1?mMep
zao(KszT4iA@{4}=&N0q^jm=jI5%p_>(6hqY4sW7P3f5r^K)%156fk`J{?E>
zsHfW>U=KL`Ze&Hji&p;vSGi`xZ`9ZBZ?Yep%bpM-
zmopo#pg)tD6*--!$ThJ6?Uruu?Z;$1A+kN?
z*+cB{d)=2x`!nU~j`d^{th&Tr9H^G&eo5evZTn3EUh(bE3D
zt<0*@N6V=qf9jL(Po9|{Oxmovc*n`=i^|tlV-OMJ|E8YhL{I?IEQsQKfkcgyt&OU
z)E2L6W|8Nk4=;~(C-h?jt8LMEd5r_W55wxuun~U9k^KSkS$X*(vtEMJwb;$$@cD9@
zw&u%;>j7M2GBM^K{mbp`->%y*u<9#iTOpjNhl>?|Zx=7D-_{I#kBxQTI}7omzc1Sn
z*VgLSlgCQ);&rZ#aw*^6j+$xfVh8p1Wt2`=e4W=8mp0X}^ZNSTrvARnxX!CfT$0v5
zG>q%%OB2?1vHxP*TJ1^e;$v4jZQi&L$`UajLqq3fNoj=Lxm*LBd7
z>*($Jrhnsm7T-(hKc4Rw9d1`?SZ
z3b*UKA6&nU_3l;jY0W$Nj$F)lA?x>Kob%>k_o*h>U%aeUmv)>d{Z-Y!{|{vdE4L{<
z+qj`#De2Ed|1bC0QiweEd0sBs&y2|u<+vJqdA|q~zp`<(rMkF2roLtKO2#MDc+SsJ
zY^*G!F0l`cRZZ8ng$leGP0>}p5}?uJm=5X_@Y+ouwy>^5!B
gX**Y$D*Km10$ZbAFe6=Gc(JZW>H3A5(WrO-1Fx
+
+
+
\ No newline at end of file
diff --git a/examples/ssr_axum/public/thaw/logo.svg b/examples/ssr_axum/public/thaw/logo.svg
new file mode 100644
index 0000000..2f77189
--- /dev/null
+++ b/examples/ssr_axum/public/thaw/logo.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/examples/ssr_axum/src/fileserv.rs b/examples/ssr_axum/src/fileserv.rs
new file mode 100644
index 0000000..eef7b71
--- /dev/null
+++ b/examples/ssr_axum/src/fileserv.rs
@@ -0,0 +1,40 @@
+use cfg_if::cfg_if;
+
+cfg_if! { if #[cfg(feature = "ssr")] {
+ use axum::{
+ body::{boxed, Body, BoxBody},
+ extract::State,
+ response::IntoResponse,
+ http::{Request, Response, StatusCode, Uri},
+ };
+ use axum::response::Response as AxumResponse;
+ use tower::ServiceExt;
+ use tower_http::services::ServeDir;
+ use leptos::*;
+ use demo::App;
+
+ pub async fn file_and_error_handler(uri: Uri, State(options): State, req: Request) -> AxumResponse {
+ let root = options.site_root.clone();
+ let res = get_static_file(uri.clone(), &root).await.unwrap();
+
+ if res.status() == StatusCode::OK {
+ res.into_response()
+ } else {
+ let handler = leptos_axum::render_app_to_stream(options.to_owned(), move || view!{});
+ handler(req).await.into_response()
+ }
+ }
+
+ async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> {
+ let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
+ // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
+ // This path is relative to the cargo root
+ match ServeDir::new(root).oneshot(req).await {
+ Ok(res) => Ok(res.map(boxed)),
+ Err(err) => Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Something went wrong: {err}"),
+ )),
+ }
+ }
+}}
diff --git a/examples/ssr_axum/src/lib.rs b/examples/ssr_axum/src/lib.rs
new file mode 100644
index 0000000..1bae92b
--- /dev/null
+++ b/examples/ssr_axum/src/lib.rs
@@ -0,0 +1,17 @@
+use cfg_if::cfg_if;
+pub mod fileserv;
+
+cfg_if! { if #[cfg(feature = "hydrate")] {
+ use leptos::*;
+ use wasm_bindgen::prelude::wasm_bindgen;
+ use demo::App;
+
+ #[wasm_bindgen]
+ pub fn hydrate() {
+ // initializes logging using the `log` crate
+ _ = console_log::init_with_level(log::Level::Debug);
+ console_error_panic_hook::set_once();
+
+ leptos::mount_to_body(App);
+ }
+}}
diff --git a/examples/ssr_axum/src/main.rs b/examples/ssr_axum/src/main.rs
new file mode 100644
index 0000000..8222418
--- /dev/null
+++ b/examples/ssr_axum/src/main.rs
@@ -0,0 +1,43 @@
+#[cfg(feature = "ssr")]
+#[tokio::main]
+async fn main() {
+ use axum::{routing::post, Router};
+ use demo::App;
+ use leptos::*;
+ use leptos_axum::{generate_route_list, LeptosRoutes};
+ use ssr_axum::fileserv::file_and_error_handler;
+
+ simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
+
+ // Setting get_configuration(None) means we'll be using cargo-leptos's env values
+ // For deployment these variables are:
+ //
+ // Alternately a file can be specified such as Some("Cargo.toml")
+ // The file would need to be included with the executable when moved to deployment
+ let conf = get_configuration(None).await.unwrap();
+ let leptos_options = conf.leptos_options;
+ let addr = leptos_options.site_addr;
+ let routes = generate_route_list(App);
+
+ // build our application with a route
+ let app = Router::new()
+ .route("/api/*fn_name", post(leptos_axum::handle_server_fns))
+ .leptos_routes(&leptos_options, routes, App)
+ .fallback(file_and_error_handler)
+ .with_state(leptos_options);
+
+ // run our app with hyper
+ // `axum::Server` is a re-export of `hyper::Server`
+ log::info!("listening on http://{}", &addr);
+ axum::Server::bind(&addr)
+ .serve(app.into_make_service())
+ .await
+ .unwrap();
+}
+
+#[cfg(not(feature = "ssr"))]
+pub fn main() {
+ // no client-side main function
+ // unless we want this to work with e.g., Trunk for a purely client-side app
+ // see lib.rs for hydration function instead
+}
diff --git a/src/color_picker/mod.rs b/src/color_picker/mod.rs
index 6897834..a046147 100644
--- a/src/color_picker/mod.rs
+++ b/src/color_picker/mod.rs
@@ -4,8 +4,8 @@ mod theme;
use crate::components::{Binder, Follower, FollowerPlacement};
use crate::{use_theme, utils::mount_style, Theme};
pub use color::*;
+use leptos::leptos_dom::helpers::WindowListenerHandle;
use leptos::*;
-use leptos::{leptos_dom::helpers::WindowListenerHandle, wasm_bindgen::__rt::IntoJsResult};
pub use theme::ColorPickerTheme;
#[component]
@@ -65,25 +65,30 @@ pub fn ColorPicker(#[prop(optional, into)] value: RwSignal) -> impl IntoVi
let show_popover = move |_| {
is_show_popover.set(true);
};
- let timer = window_event_listener(ev::click, move |ev| {
- let el = ev.target();
- let mut el: Option =
- el.into_js_result().map_or(None, |el| Some(el.into()));
- let body = document().body().unwrap();
- while let Some(current_el) = el {
- if current_el == *body {
- break;
- };
- if current_el == ***popover_ref.get().unwrap()
- || current_el == ***trigger_ref.get().unwrap()
- {
- return;
+
+ #[cfg(any(feature = "csr", feature = "hydrate"))]
+ {
+ use leptos::wasm_bindgen::__rt::IntoJsResult;
+ let timer = window_event_listener(ev::click, move |ev| {
+ let el = ev.target();
+ let mut el: Option =
+ el.into_js_result().map_or(None, |el| Some(el.into()));
+ let body = document().body().unwrap();
+ while let Some(current_el) = el {
+ if current_el == *body {
+ break;
+ };
+ if current_el == ***popover_ref.get().unwrap()
+ || current_el == ***trigger_ref.get().unwrap()
+ {
+ return;
+ }
+ el = current_el.parent_element();
}
- el = current_el.parent_element();
- }
- is_show_popover.set(false);
- });
- on_cleanup(move || timer.remove());
+ is_show_popover.set(false);
+ });
+ on_cleanup(move || timer.remove());
+ }
view! {
diff --git a/src/components/binder/mod.rs b/src/components/binder/mod.rs
index 36b7359..63b27cd 100644
--- a/src/components/binder/mod.rs
+++ b/src/components/binder/mod.rs
@@ -2,8 +2,8 @@ mod get_placement_style;
use crate::{
components::Teleport,
- utils::mount_style,
utils::{add_event_listener, EventListenerHandle},
+ utils::{mount_style, with_hydration_off},
};
use get_placement_style::get_follower_placement_style;
pub use get_placement_style::FollowerPlacement;
@@ -194,16 +194,19 @@ fn FollowerContainer(
is_show
});
- let children = html::div()
- .classes("thaw-binder-follower-container")
- .style("display", move || (!is_show.get()).then_some("none"))
- .child(
- html::div()
- .classes("thaw-binder-follower-content")
- .node_ref(content_ref)
- .attr("style", move || content_style.get())
- .child(children()),
- );
+ let children = with_hydration_off(|| {
+ html::div()
+ .classes("thaw-binder-follower-container")
+ .style("display", move || (!is_show.get()).then_some("none"))
+ .child(
+ html::div()
+ .classes("thaw-binder-follower-content")
+ .node_ref(content_ref)
+ .attr("style", move || content_style.get())
+ .child(children()),
+ )
+ });
+
view! { }
}
diff --git a/src/components/teleport/mod.rs b/src/components/teleport/mod.rs
index cfa976e..4c48b97 100644
--- a/src/components/teleport/mod.rs
+++ b/src/components/teleport/mod.rs
@@ -1,5 +1,6 @@
use cfg_if::cfg_if;
use leptos::{html::AnyElement, *};
+
/// https://github.com/solidjs/solid/blob/main/packages/solid/web/src/index.ts#L56
#[component]
pub fn Teleport(
@@ -7,8 +8,11 @@ pub fn Teleport(
#[prop(optional, into)] element: Option>,
#[prop(optional)] children: Option,
) -> impl IntoView {
- cfg_if! { if #[cfg(target_arch = "wasm32")] {
+ cfg_if! { if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
use leptos::wasm_bindgen::JsCast;
+ use leptos::leptos_dom::Mountable;
+ use crate::utils::with_hydration_off;
+
let mount = mount.unwrap_or_else(|| {
document()
.body()
@@ -16,21 +20,41 @@ pub fn Teleport(
.unchecked_into()
});
- let render_root = if let Some(element) = element {
- element
+ if let Some(element) = element {
+ let render_root = element;
+ let _ = mount.append_child(&render_root);
+ on_cleanup(move || {
+ let _ = mount.remove_child(&render_root);
+ });
} else if let Some(children) = children {
- html::div().child(children()).into_any()
+ let container = document()
+ .create_element("div")
+ .expect("element creation to work");
+ with_hydration_off(|| {
+ let _ = container.append_child(&children().into_view().get_mountable_node());
+ });
+
+ let render_root = container;
+ let _ = mount.append_child(&render_root);
+ on_cleanup(move || {
+ let _ = mount.remove_child(&render_root);
+ });
} else {
return;
};
-
- _ = mount.append_child(&render_root);
- on_cleanup(move || {
- _ = mount.remove_child(&render_root);
- });
} else {
- _ = mount;
- _ = element;
- _ = children;
+ let _ = mount;
+ #[cfg(not(feature = "ssr"))]
+ {
+ let _ = element;
+ let _ = children;
+ }
+ #[cfg(feature = "ssr")]
+ if element.is_none() {
+ if let Some(children) = children {
+ // Consumed hydration `id`
+ let _ = children();
+ }
+ }
}}
}
diff --git a/src/global_style/mod.rs b/src/global_style/mod.rs
index 36d2e2f..58e42e5 100644
--- a/src/global_style/mod.rs
+++ b/src/global_style/mod.rs
@@ -20,6 +20,7 @@ pub fn GlobalStyle() -> impl IntoView {
_ = body
.style()
.set_property("color-scheme", &theme.common.color_scheme);
+ _ = body.style().set_property("margin", "0");
}
});
});
diff --git a/src/select/mod.rs b/src/select/mod.rs
index 240bff7..011fe62 100644
--- a/src/select/mod.rs
+++ b/src/select/mod.rs
@@ -6,7 +6,6 @@ use crate::{
utils::mount_style,
Theme,
};
-use leptos::wasm_bindgen::__rt::IntoJsResult;
use leptos::*;
use std::hash::Hash;
pub use theme::SelectTheme;
@@ -73,25 +72,30 @@ where
let show_menu = move |_| {
is_show_menu.set(true);
};
- let timer = window_event_listener(ev::click, move |ev| {
- let el = ev.target();
- let mut el: Option =
- el.into_js_result().map_or(None, |el| Some(el.into()));
- let body = document().body().unwrap();
- while let Some(current_el) = el {
- if current_el == *body {
- break;
- };
- if current_el == ***menu_ref.get().unwrap()
- || current_el == ***trigger_ref.get().unwrap()
- {
- return;
+
+ #[cfg(any(feature = "csr", feature = "hydrate"))]
+ {
+ use leptos::wasm_bindgen::__rt::IntoJsResult;
+ let timer = window_event_listener(ev::click, move |ev| {
+ let el = ev.target();
+ let mut el: Option =
+ el.into_js_result().map_or(None, |el| Some(el.into()));
+ let body = document().body().unwrap();
+ while let Some(current_el) = el {
+ if current_el == *body {
+ break;
+ };
+ if current_el == ***menu_ref.get().unwrap()
+ || current_el == ***trigger_ref.get().unwrap()
+ {
+ return;
+ }
+ el = current_el.parent_element();
}
- el = current_el.parent_element();
- }
- is_show_menu.set(false);
- });
- on_cleanup(move || timer.remove());
+ is_show_menu.set(false);
+ });
+ on_cleanup(move || timer.remove());
+ }
let temp_options = options.clone();
let select_option_label = create_memo(move |_| match value.get() {
diff --git a/src/tabs/mod.rs b/src/tabs/mod.rs
index 39e56b8..2cae306 100644
--- a/src/tabs/mod.rs
+++ b/src/tabs/mod.rs
@@ -13,6 +13,24 @@ pub use tab::*;
pub fn Tabs(#[prop(optional, into)] value: RwSignal, children: Children) -> impl IntoView {
mount_style("tabs", include_str!("./tabs.css"));
let tab_options_vec = create_rw_signal(vec![]);
+
+ view! {
+
+
+
+ }
+}
+
+#[component]
+fn TabsInner(
+ value: RwSignal,
+ tab_options_vec: RwSignal>,
+ children: Children,
+) -> impl IntoView {
+ mount_style("tabs", include_str!("./tabs.css"));
let theme = use_theme(Theme::light);
let css_vars = create_memo(move |_| {
let mut css_vars = String::new();
@@ -35,72 +53,66 @@ pub fn Tabs(#[prop(optional, into)] value: RwSignal, children: Children)
});
let label_list_ref = create_node_ref::();
+ let children = children();
view! {
-
-
-
-
();
- let TabOption { key, label } = option;
- create_effect({
- let key = key.clone();
- move |_| {
- let Some(label) = label_ref.get() else {
- return;
- };
- let Some(label_list) = label_list_ref.get() else {
- return;
- };
- if key.clone() == value.get() {
- request_animation_frame(move || {
- let list_rect = label_list.get_bounding_client_rect();
- let rect = label.get_bounding_client_rect();
- label_line
- .set(
- Some(TabsLabelLine {
- width: rect.width(),
- left: rect.left() - list_rect.left(),
- }),
- );
- });
- }
+
+
+ ();
+ let TabOption { key, label } = option;
+ create_effect({
+ let key = key.clone();
+ move |_| {
+ let Some(label) = label_ref.get() else { return;
+ };
+ let Some(label_list) = label_list_ref.get() else { return;
+ };
+ if key.clone() == value.get() {
+ request_animation_frame(move || {
+ let list_rect = label_list.get_bounding_client_rect();
+ let rect = label.get_bounding_client_rect();
+ label_line
+ .set(
+ Some(TabsLabelLine {
+ width: rect.width(),
+ left: rect.left() - list_rect.left(),
+ }),
+ );
+ });
}
- });
- view! {
-
- {label}
-
}
- }
- />
+ });
+ view! {
+
-
-
{children()}
+ on:click={
+ let key = key.clone();
+ move |_| value.set(key.clone())
+ }
+
+ ref=label_ref
+ >
+ {label}
+
+ }
+ }
+ />
+
+
-
+ {children}
+
}
}
diff --git a/src/utils/event_listener.rs b/src/utils/event_listener.rs
index 0b9df50..8532e46 100644
--- a/src/utils/event_listener.rs
+++ b/src/utils/event_listener.rs
@@ -1,5 +1,5 @@
+use ::wasm_bindgen::{prelude::Closure, JsCast};
use leptos::{html::AnyElement, *};
-use wasm_bindgen::{prelude::Closure, JsCast};
pub fn add_event_listener
(
target: HtmlElement,
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 83bf0e2..9ca81a9 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -13,3 +13,15 @@ pub(crate) use mount_style::mount_style;
pub(crate) use provider::Provider;
pub use signal::SignalWatch;
pub(crate) use stored_maybe_signal::*;
+
+pub(crate) fn with_hydration_off(f: impl FnOnce() -> T) -> T {
+ #[cfg(feature = "hydrate")]
+ {
+ use leptos::leptos_dom::HydrationCtx;
+ HydrationCtx::with_hydration_off(f)
+ }
+ #[cfg(not(feature = "hydrate"))]
+ {
+ f()
+ }
+}
diff --git a/src/utils/mount_style.rs b/src/utils/mount_style.rs
index 3d7c6d4..4cf70ac 100644
--- a/src/utils/mount_style.rs
+++ b/src/utils/mount_style.rs
@@ -1,20 +1,34 @@
-use leptos::document;
+use cfg_if::cfg_if;
-pub fn mount_style(id: &str, content: &str) {
- let head = document().head().expect("head no exist");
- let style = head
- .query_selector(&format!("style[csr-id=\"thaw-{id}\"]"))
- .expect("query style element error");
+pub fn mount_style(id: &str, content: &'static str) {
+ cfg_if! {
+ if #[cfg(feature = "ssr")] {
+ use leptos::html::style;
+ use leptos_meta::use_head;
+ let meta = use_head();
+ let style_el = style().attr("csr-id", format!("thaw-{id}")).child(content);
+ meta.tags.register(format!("leptos-thaw-{id}").into(), style_el.into_any());
+ } else {
+ use leptos::document;
+ let head = document().head().expect("head no exist");
+ let style = head
+ .query_selector(&format!("style[csr-id=\"thaw-{id}\"]"))
+ .expect("query style element error");
- if style.is_some() {
- return;
+ #[cfg(feature = "hydrate")]
+ let _ = leptos::leptos_dom::HydrationCtx::id();
+
+ if style.is_some() {
+ return;
+ }
+
+ let style = document()
+ .create_element("style")
+ .expect("create style element error");
+ _ = style.set_attribute("csr-id", &format!("thaw-{id}"));
+ style.set_text_content(Some(content));
+
+ _ = head.append_child(&style);
+ }
}
-
- let style = document()
- .create_element("style")
- .expect("create style element error");
- _ = style.set_attribute("csr-id", &format!("thaw-{id}"));
- style.set_text_content(Some(content));
-
- _ = head.append_child(&style);
}