Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

percy-dom + virtual-node example wanted #208

Open
qknight opened this issue Jan 16, 2025 · 1 comment
Open

percy-dom + virtual-node example wanted #208

qknight opened this issue Jan 16, 2025 · 1 comment

Comments

@qknight
Copy link

qknight commented Jan 16, 2025

I want to use percy-dom to patch a webpage via patches over a websocket.

This is the position in the webpage I want to modify:

<div id="here">...</div>

There is already some html and this needs to be patched properly.

requirement

So the Rust code should basically:

  1. Extract the html at id here
  2. Receive an update via a websocket as String
  3. Convert that String based message into a VirtualNode
  4. Create a patch between the two different VirtualNodes
  5. Apply that patch to the local <div id=here>...

Later I want to create the diffs on the server and only stream patches via the websocket.

request for help

Could someone please help me to get a simple example working, ideally we could add this as a tests which are already there:

/// Verify that if we are patching over an old element that does not have an on_create_elem,

I've also studied the patch function signature:

pub fn patch<N: Into<Node>>(

But I don't get the documentation either.

experiment1

  • The code below already converts the websocket input: String into a VirtualNode
  • The here is not updated yet
  • it compiles and i see the modified real dom
let input: String = "<div>asdf</div>"; // from websocket

Initialize a div with id `here` in the DOM
let mut initial_div: VirtualNode = html! {<div id="here">{"bar"}</div>};

// Mount the initial VirtualNode to the actual DOM
let root_node = web_sys::window()
    .unwrap()
    .document()
    .unwrap()
    .get_element_by_id("here")
    .expect("`app` div not found in the DOM");
let mut percy_dom_root_node = PercyDom::new_replace_mount(initial_div, root_node);

// Render a new VirtualNode with the updated HTML
let mut updated_div: VirtualNode = html! {<div id="here"></div>};
updated_div
    .as_velement_mut()
    .unwrap()
    .special_attributes
    .dangerous_inner_html = Some(input);

// Patch the existing DOM with the new VirtualNode
percy_dom_root_node.update(updated_div);

experiment2

This code:

  • creates a patch and applies it, yet i never see the result as i don't yet understand how to apply this back to the real dom
  • it compiles and does something
let mut div: VirtualNode = html! {<div>here will be your message</div>};
div.as_velement_mut()
    .unwrap()
    .special_attributes
    .dangerous_inner_html = Some(input.to_string());

let mut events = VirtualEvents::new();

let mut events = VirtualEvents::new();
let mut old_div: VirtualNode = html! {<div>here will be your essage</div>}; 

let (root_node, enode) = old_div.create_dom_node(&mut events);
events.set_root(enode);

let old_vnode: VirtualNode = VirtualNode::from(old_div);
let patches = percy_dom::diff(&old_vnode, &div);
percy_dom::patch(root_node, &old_vnode, &mut events, &patches);

experiment3

this combines experiment 1 and experiment 2 but if i select text within the document where no change happened, it will remove the selection. if i select text outside that area it remains selected.

that means that the update removes everything and then adds the new document instead of applying a diff.

let document = web_sys::window().unwrap().document().unwrap();
let real_div: Element = document
    .get_element_by_id("ws-div")
    .expect("No element with id `ws-div` found in the DOM");

let real_div_html: String = real_div.inner_html();

let mut real_div_virtual_node: VirtualNode = html! {<div id="ws-div"></div>};
real_div_virtual_node
        .as_velement_mut()
        .unwrap()
        .special_attributes
        .dangerous_inner_html = Some(real_div_html.clone());
//log::info!("real_div_html: {}", real_div_html);
                
let mut new_div_virtual_node: VirtualNode = html! {<div id="ws-div"></div>};
new_div_virtual_node
    .as_velement_mut()
    .unwrap()
    .special_attributes
    .dangerous_inner_html = Some(input);

let mut events = VirtualEvents::new();
let (root_node, enode) = real_div_virtual_node.create_dom_node(&mut events);
events.set_root(enode);

let patches = percy_dom::diff(&real_div_virtual_node, &new_div_virtual_node);
percy_dom::patch(root_node, &real_div_virtual_node, &mut events, &patches);


let mut percy_dom_root_node = PercyDom::new_replace_mount(real_div_virtual_node, real_div);
percy_dom_root_node.update(new_div_virtual_node);
@qknight
Copy link
Author

qknight commented Jan 17, 2025

I've created another much smaller test-case so i can re-run the update manually and it seems that percy-dom handles dom-rewrites different from what I was using https://www.npmjs.com/package/diff-dom/v/1.0.0

diff-dom code implementation

https://github.com/qknight/blog.lastlog.de/blob/d4fbe58d05e525b858d211a80b2f29ff6d2025b8/js/pankat-websocket.js#L34C16-L34C22

noticed difference between implementations

Both implementations update the DOM-tree in a way that the outcome is 'correct', which is already great!

The subtle difference is text selection (didn't see scroll-buffers flipping):

  • diff-dom touches the real DOM tree only at places where there is a difference
  • percy-dom seems to remove the node with id=ws-div and recreates it with the updated version

as a consequence: if i have text selected, text inside the to-be-patched node which is not directly updated, it will loose the text selection anyways.

To make it abundantly clear:

I rewrite this:

<div id="ws-id">
<div>Hello World</div>
<div>this is fine, yeah, srly!</div>
</div>

Into this:

<div id="ws-id">
<div>Hello World</div>
<div>this is fine, yeah, srly!!!</div>
</div>

So I never changed the <div>Hello World</div>.

question

Is my conclusion about percy-dom correct, that it removes all the nodes and recreates them instead of updating them?
Is there some library function i am missing?

impl PercyDom {

    /// Create a new `PercyDom`.
    ///
    /// A root `Node` will be created and it will replace your passed in mount
    /// element.
    pub fn new_replace_mount(current_vdom: VirtualNode, mount: Element) -> PercyDom {
        let pdom = Self::new(current_vdom);

        mount
            .replace_with_with_node_1(&pdom.root_node)
            .expect("Could not replace mount element");

        pdom
    }

The comments confuse me a lot because I don't understand the difference between what is the real DOM and what is the virtual DOM.

percy-dom implementation

extern crate web_sys;
use log::info;

use percy_dom::event::VirtualEvents;
use percy_dom::patch;
use percy_dom::prelude::*;
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::Element;
use web_sys::{js_sys, MessageEvent, WebSocket};

#[wasm_bindgen]
pub fn foo() {
    static INIT: std::sync::Once = std::sync::Once::new();
    static mut PERCY_DOM_ROOT_NODE: Option<PercyDom> = None;

    INIT.call_once(|| {
        info!("WASM hello world foo");

        // Initialize a div with id `ws-div` in the DOM
        let initial_div: VirtualNode = html! {<div id="ws-div"></div>};

        // Mount the initial VirtualNode to the actual DOM
        let root_node: Element = web_sys::window()
            .unwrap()
            .document()
            .unwrap()
            .get_element_by_id("ws-div")
            .expect("`app` div not found in the DOM");
        let percy_dom_root_node = PercyDom::new_replace_mount(initial_div, root_node);

        unsafe {
            PERCY_DOM_ROOT_NODE = Some(percy_dom_root_node);
        }
    });

    unsafe {
        if let Some(percy_dom_root_node) = &mut PERCY_DOM_ROOT_NODE {
            info!("updating");

            // Code that uses percy_dom_root_node
            let input: String = "<div>Hello World</div><div>this is fine!</div>".to_string();
            let mut updated_div: VirtualNode = html! {<div id="ws-div"></div>};
            updated_div
                .as_velement_mut()
                .unwrap()
                .special_attributes
                .dangerous_inner_html = Some(input);
            percy_dom_root_node.update(updated_div);

            let input2 = "<div>Hello World</div><div>this is fine, yeah, srly!!!</div>".to_string();
            let mut updated_div2: VirtualNode = html! {<div id="ws-div"></div>};
            updated_div2
                .as_velement_mut()
                .unwrap()
                .special_attributes
                .dangerous_inner_html = Some(input2);
            percy_dom_root_node.update(updated_div2);
        }
    }
}
}

and with this code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World</title>
    <link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
<script type="module">
    import init, { foo } from './pkg/frontend.js';

    async function run() {
        await init(); // Initialize the WASM module
        console.log("Hello World from module init context!");
        window.foo = foo; // Make `foo` accessible from the console
        //foo(); // Call the `foo` function
    }

    run();
</script>
<body>
    <h1>Hello, World!</h1>
    <div><i>Some header text</i></div>
    <button onclick="foo()">asdf</button>
    <div id="ws-div"><div>xxx</div></div>
    <div><b>Some footer text</b></div>
</body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant