leptos_posthoc/lib.rs
1#![cfg_attr(feature = "ssr", allow(unused_variables))]
2#![cfg_attr(feature = "ssr", allow(unused_mut))]
3#![cfg_attr(feature = "ssr", allow(unused_imports))]
4
5/*! Allows for "hydrating" an existent DOM with reactive leptos components,
6 * without the entire DOM having to be generated by leptos components.
7 *
8 * ## Why would you want that?
9 * 1. **CSR:** It allows for building scripts that others can just embed in their arbitrary HTML documents, that add
10 * `<insert your favourite fancy feature here>`. For an example, see the `examples/csr` directory:
11 * the `index.html` has a node `<script src='csr_example.js'></script>`, which "hydrates" nodes with the
12 * `data-replace-with-leptos`-attribute with leptos components that add a hover-popup (using
13 * [thaw](https://docs.rs/thaw)).
14 * 2. **SSR:** Occasionally, you might want to dynamically insert some HTML string into the DOM, for example one that
15 * gets generated from some data and returned by a server function. This HTML might contain certain nodes that
16 * we want to attach reactive functionality to. For an example, see the `examples/ssr` directory.
17 *
18 * ## CSR Example
19 * Say we want to replace all elements with the attribute `data-replace-with-leptos` with a leptos component
20 * `MyReplacementComponent`, that simply wraps the original children in a `div` with a solid red border. This
21 * component would roughly look like this:
22 * ```
23 * #[component]
24 * fn MyReplacementComponent(orig:OriginalNode) -> impl IntoView {
25 * view! {
26 * <div style="border: 1px solid red;">
27 * <DomChildren orig />
28 * </div>
29 * }
30 * }
31 * ```
32 * This component takes an `orig:`[`OriginalNode`] that represents the, well, original [`Element`].
33 *
34 * So, where do we get `orig` from?
35 * - If we already have an `e:&`[`Element`], we can simply call `e.into()`.
36 * - More likely, we don't have an [`Element`] yet. Moreover, we probably want to iterate over the entire body
37 * *once* to find all nodes we want to make reactive, and we also need to set up a global reactive system for all
38 * our inserted components.
39 *
40 * To do that, we call [`hydrate_body`] (requires the `csr` feature flag) with a function that takes the
41 * [`OriginalNode`] of the body and returns some leptos view; e.g.:
42 *
43 * ```
44 * #[component]
45 * fn MainBody(orig:OriginalNode) -> impl IntoView {
46 * // set up some signals, provide context etc.
47 * view!{
48 * <DomChildren orig/>
49 * }
50 * }
51 * #[wasm_bindgen(start)]
52 * pub fn run() {
53 * console_error_panic_hook::set_once();
54 * hydrate_body(|orig| view!(<MainBody orig/>).into_any())
55 * }
56 * ```
57 *
58 * This sets up the reactive system, but does not yet replace any elements further down in the DOM. To do that,
59 * we provide a function that takes an `&`[`Element`] and optionally returns an
60 * [`FnOnce`]`() -> impl `[`IntoView`]`+'static`, if the element should be changed. This function is then passed to
61 * [`DomChildrenCont`], which will iterate over all children of the replaced element and replace them with the
62 * provided function.
63 *
64 * Let's modify our `MainBody` to replace all elements with the attribute `data-replace-with-leptos` with a
65 * `MyReplacementComponent`:
66 *
67 * ```
68 * fn replace(e:&Element) -> Option<impl FnOnce() -> AnyView> {
69 * e.get_attribute("data-replace-with-leptos").map(|_| {
70 * let orig: OriginalNode = e.clone().into();
71 * || view!(<MyReplacementComponent orig/>).into_any()
72 * })
73 * }
74 *
75 * #[component]
76 * fn MainBody(orig:OriginalNode) -> impl IntoView {
77 * // set up some signals, provide context etc.
78 * view!{
79 * <DomChildrenCont orig cont=replace/>
80 * }
81 * }
82 *
83 * #[component]
84 * fn MyReplacementComponent(orig:OriginalNode) -> impl IntoView {
85 * view! {
86 * <div style="border: 1px solid red;">
87 * <DomChildrenCont orig cont=replace/>
88 * </div>
89 * }
90 * }
91 * ```
92 *
93 * ...now, `replace` will get called on every element of the DOM, including those that were "moved around" in
94 * earlier `MyReplacementComponent`s, respecting the proper reactive graph (regardin signal inheritance etc.).
95 *
96 * ### SSR Example
97 *
98 * In general, for SSR we can simply use the normal leptos components to generate the entire DOM. We control the
99 * server, hence we control the DOM anyway.
100 *
101 * However, it might occasionally be the case that we want to dynamically *extend* the DOM at some point by
102 * retrieving HTML from elsewhere, and then want to do a similar "hydration" iteration over the freshly inserted
103 * nodes. This is what [`DomStringCont`] is for, and it does not require the `csr` feature:
104 *
105 * ```
106 * #[component]
107 * fn MyComponentThatGetsAStringFromSomewhere() -> impl IntoView {
108 * // get some HTML string from somewhere
109 * // e.g. some API call
110 * let html = "<div data-replace-with-leptos>...</div>".to_string();
111 * view! {
112 * <DomStringCont html cont=replace/>
113 * }
114 * }
115 * ```
116 *
117 * See the `examples/ssr` directory for a full example.
118*/
119
120mod dom;
121mod node;
122
123pub use node::OriginalNode;
124
125#[cfg(any(feature = "csr", feature = "hydrate"))]
126pub use dom::hydrate_node;
127
128use leptos::{html::Span, math::Mrow, prelude::*, web_sys::Element};
129
130/// A component that calls `cont` on `orig` and all its children,
131/// potentially "hydrating" them further, and reinserts the original
132/// element into the DOM.
133///
134/// If ``skip_head`` is set to true, `cont` will not be called on the head element itself.
135#[allow(unused_variables)]
136#[component]
137pub fn DomCont<
138 V: IntoView + 'static,
139 R: FnOnce() -> V,
140 F: Fn(&Element) -> Option<R> + 'static + Send,
141>(
142 orig: OriginalNode,
143 cont: F,
144 #[prop(optional)] skip_head: bool,
145 #[prop(optional, into)] class: MaybeProp<String>,
146 #[prop(optional, into)] style: MaybeProp<String>,
147) -> impl IntoView {
148 #[cfg(any(feature = "csr", feature = "hydrate"))]
149 {
150 let orig = orig
151 .add_any_attr(leptos::tachys::html::class::class(move || class.get()))
152 .add_any_attr(leptos::tachys::html::style::style(move || style.get()));
153 orig.as_view(move |e| {
154 if skip_head {
155 dom::hydrate_children(e.clone().into(), &cont);
156 } else {
157 dom::hydrate_node(e.clone().into(), &cont);
158 }
159 })
160 }
161}
162
163/// A component that inserts the children of some [`OriginalNode`]
164/// and renders them into the DOM.
165#[allow(unused_variables)]
166#[component]
167pub fn DomChildren(orig: OriginalNode) -> impl IntoView {
168 #[cfg(any(feature = "csr", feature = "hydrate"))]
169 {
170 orig.child_vec()
171 .into_iter()
172 .map(|c| match c {
173 leptos::either::Either::Left(c) => leptos::either::Either::Left(c.as_view(|_| ())),
174 leptos::either::Either::Right(c) => leptos::either::Either::Right(c),
175 })
176 .collect_view()
177 }
178}
179
180/// A component that inserts the children of some [`OriginalNode`] like [`DomChildren`],
181/// and then hydrates them using `cont` like [`DomCont`].
182#[allow(unused_variables)]
183#[component]
184pub fn DomChildrenCont<
185 V: IntoView + 'static,
186 R: FnOnce() -> V,
187 F: Fn(&Element) -> Option<R> + 'static + Send + Clone,
188>(
189 orig: OriginalNode,
190 cont: F,
191) -> impl IntoView {
192 #[cfg(any(feature = "csr", feature = "hydrate"))]
193 {
194 orig.child_vec()
195 .into_iter()
196 .map(|c| match c {
197 leptos::either::Either::Left(c) => leptos::either::Either::Left({
198 if let Some(r) = cont(&c) {
199 leptos::either::Either::Left(r())
200 } else {
201 let cont = cont.clone();
202 leptos::either::Either::Right(
203 c.as_view(move |e| dom::hydrate_children(e.clone().into(), &cont)),
204 )
205 }
206 }),
207 leptos::either::Either::Right(c) => leptos::either::Either::Right(c),
208 })
209 .collect_view()
210 }
211}
212
213/// A component that renders a string of valid HTML, and then hydrates the resulting DOM nodes
214/// using `cont` like [`DomCont`].
215#[allow(unused_variables)]
216#[component]
217pub fn DomStringCont<
218 V: IntoView + 'static,
219 R: FnOnce() -> V,
220 F: Fn(&Element) -> Option<R> + 'static,
221>(
222 html: String,
223 cont: F,
224 #[prop(optional)] on_load: Option<RwSignal<bool>>,
225 #[prop(optional, into)] class: MaybeProp<String>,
226 #[prop(optional, into)] style: MaybeProp<String>,
227) -> impl IntoView {
228 let rf = NodeRef::<Span>::new();
229 rf.on_load(move |e| {
230 #[cfg(any(feature = "csr", feature = "hydrate"))]
231 {
232 dom::hydrate_children(e.into(), &cont);
233 }
234 if let Some(on_load) = on_load {
235 on_load.set(true);
236 }
237 });
238 view!(<span node_ref=rf inner_html=html
239 class=move || class.get() style=move || style.get()
240 />)
241}
242
243/// Like [`DomStringCont`], but using `<mrow>` instead of `<span>` initially, in case we are
244/// in MathML (otherwise, there's a danger the browser will move the resulting nodes outside of the
245/// `<math>` node!).
246#[allow(unused_variables)]
247#[component]
248pub fn DomStringContMath<
249 V: IntoView + 'static,
250 R: FnOnce() -> V + 'static,
251 F: Fn(&Element) -> Option<R> + 'static + Send + Clone,
252>(
253 html: String,
254 cont: F,
255 #[prop(optional)] on_load: Option<RwSignal<bool>>,
256 #[prop(optional, into)] class: MaybeProp<String>,
257 #[prop(optional, into)] style: MaybeProp<String>,
258) -> impl IntoView {
259 let rf = NodeRef::<Mrow>::new();
260 let cnt = cont.clone();
261 rf.on_load(move |e| {
262 #[cfg(any(feature = "csr", feature = "hydrate"))]
263 {
264 dom::hydrate_children(e.into(), &cnt);
265 }
266 if let Some(on_load) = on_load {
267 on_load.set(true);
268 }
269 });
270 view!(<mrow node_ref=rf inner_html=html class=move || class.get() style=move || style.get()/>)
271}
272
273// need some check to not iterate over the entire body multiple times for some reason.
274// I'm not sure why this is necessary, but it seems to be.
275#[cfg(feature = "csr")]
276static DONE: std::sync::OnceLock<()> = std::sync::OnceLock::new();
277
278/// Hydrates the entire DOM with leptos components, starting at the body.
279///
280/// `v` is a function that takes the [`OriginalChildren`] of the `<body>` (likely reinserting them somewhere) and returns some
281/// leptos view replacing the original children(!) of the body.
282#[cfg(feature = "csr")]
283pub fn hydrate_body<N: IntoView>(v: impl FnOnce(OriginalNode) -> N + 'static) {
284 // make sure this only ever happens once.
285 if DONE.get().is_some() {
286 return;
287 }
288 DONE.get_or_init(|| ());
289 let document = leptos::tachys::dom::document();
290 // We check that the DOM has been fully loaded
291 let state = document.ready_state();
292 let go = move || {
293 let body = leptos::tachys::dom::body();
294 let nd = leptos::tachys::dom::document()
295 .create_element("div")
296 .expect("Error creating div");
297 while let Some(c) = body.child_nodes().get(0) {
298 nd.append_child(&c).expect("Error appending child");
299 }
300 mount_to_body(move || v(nd.into()));
301 };
302 if state == "complete" || state == "interactive" {
303 go();
304 } else {
305 use leptos::wasm_bindgen::JsCast;
306 let fun = std::rc::Rc::new(std::cell::Cell::new(Some(go)));
307 let closure = leptos::wasm_bindgen::closure::Closure::wrap(Box::new(
308 move |_: leptos::web_sys::Event| {
309 if let Some(f) = fun.take() {
310 f()
311 }
312 },
313 ) as Box<dyn FnMut(_)>);
314 document
315 .add_event_listener_with_callback("DOMContentLoaded", closure.as_ref().unchecked_ref())
316 .unwrap();
317 closure.forget();
318 }
319}
320
321// ------------------------------------------------------------
322
323#[cfg(any(feature = "csr", feature = "hydrate"))]
324fn cleanup(node: leptos::web_sys::Node) {
325 let c = send_wrapper::SendWrapper::new(node);
326 Owner::on_cleanup(move || {
327 if let Some(p) = c.parent_element() {
328 let _ = p.remove_child(&c);
329 }
330 });
331}
332
333/*
334#[cfg(any(feature="csr",feature="hydrate"))]
335fn prettyprint(node:&web_sys::Node) -> String {
336 use leptos::wasm_bindgen::JsCast;
337 if let Some(e) = node.dyn_ref::<Element>() {
338 e.outer_html()
339 } else if let Some(t) = node.dyn_ref::<web_sys::Text>() {
340 t.data()
341 } else if let Some(c) = node.dyn_ref::<web_sys::Comment>() {
342 c.data()
343 } else {
344 node.to_string().as_string().expect("wut")
345 }
346}
347 */