leptos-posthoc

Crates.ioleptos-posthoc
lib.rsleptos-posthoc
version0.1.0
created_at2025-06-04 09:57:15.351151+00
updated_at2025-06-04 09:57:15.351151+00
descriptionA crate for dynamically hydrating static/opaque HTML using leptos components
homepage
repositoryhttps://github.com/FlexiFormal/leptos-posthoc
max_upload_size
id1700066
size114,790
Dennis Müller (Jazzpirate)

documentation

README

leptos posthoc

Allows for "hydrating" an existent DOM with reactive leptos components, without the entire DOM having to be generated by leptos components.

Why would you want that?

  1. CSR: It allows for building scripts that others can just embed in their arbitrary HTML documents, that add <insert your favourite fancy feature here>. For an example, see the examples/csr directory: the index.html has a node <script src='csr_example.js'></script>, which "hydrates" nodes with the data-replace-with-leptos-attribute with leptos components that add a hover-popup (using thaw).
  2. SSR: Occasionally, you might want to dynamically insert some HTML string into the DOM, for example one that gets generated from some data and returned by a server function. This HTML might contain certain nodes that we want to attach reactive functionality to. For an example, see the examples/ssr directory.

CSR Example

Say we want to replace all elements with the attribute data-replace-with-leptos with a leptos component MyReplacementComponent, that simply wraps the original children in a div with a solid red border. This component would roughly look like this:

#[component]
fn MyReplacementComponent(orig:OriginalNode) -> impl IntoView {
   view! {
      <div style="border: 1px solid red;">
        <DomChildren orig />
     </div>
  }
}

This component takes an orig:[OriginalNode] that represents the, well, original [Element].

So, where do we get orig from?

  • If we already have an e:&[Element], we can simply call e.into().

  • More likely, we don't have an [Element] yet. Moreover, we probably want to iterate over the entire body once to find all nodes we want to make reactive, and we also need to set up a global reactive system for all our inserted components.

    To do that, we call [hydrate_body] (requires the csr feature flag) with a function that takes the [OriginalNode] of the body and returns some leptos view; e.g.:

#[component]
fn MainBody(orig:OriginalNode) -> impl IntoView {
  // set up some signals, provide context etc.
  view!{
    <DomChildren orig/>
  }
}
#[wasm_bindgen(start)]
pub fn run() {
  console_error_panic_hook::set_once();
  hydrate_body(|orig| view!(<MainBody orig/>).into_any())
}

This sets up the reactive system, but does not yet replace any elements further down in the DOM. To do that, we provide a function that takes an &[Element] and optionally returns an [FnOnce]() -> impl [IntoView]+'static, if the element should be changed. This function is then passed to [DomChildrenCont], which will iterate over all children of the replaced element and replace them with the provided function.

Let's modify our MainBody to replace all elements with the attribute data-replace-with-leptos with a MyReplacementComponent:

fn replace(e:&Element) -> Option<impl FnOnce() -> AnyView> {
  e.get_attribute("data-replace-with-leptos").map(|_| {
    let orig: OriginalNode = e.clone().into();
    || view!(<MyReplacementComponent orig/>).into_any()
  })
}

#[component]
fn MainBody(orig:OriginalNode) -> impl IntoView {
  // set up some signals, provide context etc.
  view!{
    <DomChildrenCont orig cont=replace/>
  }
}

#[component]
fn MyReplacementComponent(orig:OriginalNode) -> impl IntoView {
  view! {
    <div style="border: 1px solid red;">
      <DomChildrenCont orig cont=replace/>
    </div>
  }
}

...now, replace will get called on every element of the DOM, including those that were "moved around" in earlier MyReplacementComponents, respecting the proper reactive graph (regardin signal inheritance etc.).

SSR Example

In general, for SSR we can simply use the normal leptos components to generate the entire DOM. We control the server, hence we control the DOM anyway.

However, it might occasionally be the case that we want to dynamically extend the DOM at some point by retrieving HTML from elsewhere, and then want to do a similar "hydration" iteration over the freshly inserted nodes. This is what [DomStringCont] is for, and it does not require the csr feature:

#[component]
fn MyComponentThatGetsAStringFromSomewhere() -> impl IntoView {
  // get some HTML string from somewhere
  // e.g. some API call
  let html = "<div data-replace-with-leptos>...</div>".to_string();
  view! {
    <DomStringCont html cont=replace/>
  }
}

See the examples/ssr directory for a full example.

Commit count: 27

cargo fmt