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

Virtual DOM from DOM #111

Open
dbrgn opened this issue Apr 8, 2019 · 7 comments
Open

Virtual DOM from DOM #111

dbrgn opened this issue Apr 8, 2019 · 7 comments
Labels
enhancement New feature or request

Comments

@dbrgn
Copy link
Collaborator

dbrgn commented Apr 8, 2019

Is there already a way to initialize a Virtual DOM from an actual DOM element?

@dbrgn dbrgn added the enhancement New feature or request label Apr 8, 2019
@chinedufn
Copy link
Owner

Right now there's no way to get a virtual dom from a real DOM element.

I'm curious to hear more about the intended use case though?

@dbrgn
Copy link
Collaborator Author

dbrgn commented Apr 9, 2019

When a lot of transformation steps are being done, it might be cheaper to create a VDOM from the DOM, to apply those transformation steps to the VDOM and finally to apply the resulting changes to the real DOM.

@chinedufn
Copy link
Owner

Hmm I'm not quite following.

Why wouldn't you just start with a VDOM. Create a DOM. Do whatever you want with the VDOM and then whenever you want apply to the DOM?

@dbrgn
Copy link
Collaborator Author

dbrgn commented Apr 10, 2019

I originally tried to do that for this project: https://github.com/threema-ch/compose-area/blob/0c54358ba9e0f8d3ca6fc247d1b04e2cbb187e38/README.md (see "Concepts").

Unfortunately that attempt failed. When working with contenteditable elements, there are so many ways how they can be changed that I cannot capture all possible input events (especially on touch devices or with non-keyboard IMEs). Thus I'm resorting to letting the browser handle DOM changes.

The library would then only handle things like "insert an emoji at the current caret position". For that, I first have to manipulate the DOM directly.

Direct DOM calls through web_sys are probably the fastest way right now, but another approach would be parsing the current DOM into a VDOM, applying desired changes and writing back the result into the real DOM.

@chinedufn
Copy link
Owner

chinedufn commented Apr 11, 2019

Ohhh I see - interesting. Thanks for the context.

Cool - I'd love to use this thread to work out the requirements and potential design options.


Just to be on the same page... it sounds (correct me if I'm wrong) you want to be able to take all of the DOM nodes inside of a content editable div at any arbitrary time and be able to turn them into a VDOM.

At which point you'd update your in-Rust-memory VDOM's content editable div's children with the VDOM that you got from the real DOM.

So.. you'd need to be certain that the in-Rust-memory content-editable div VDOM didn't ever get used to update the real DOM unless you explicitly said so.


If the above is true .. it sounds like an option here is something similar to React's shouldComponentUpdate where we straight up don't patch your content editable div unless you explicitly say so....


And then separately there'd also be a need for DOM -> VDOM (we'd of course want to think about whether or not that belongs in Percy.)

?

(tried to fly at a high level just to make sure I understand the problem! Thanks for your patience!)

@dbrgn
Copy link
Collaborator Author

dbrgn commented Apr 11, 2019

Yeah, something similar to this function (which extracts text from an element) could work:

/// Process a DOM node recursively and extract text.
///
/// Convert elements like images to alt text.
#[wasm_bindgen]
pub fn extract_text(root_element: &Element, no_trim: bool) -> String {
    let mut text = String::new();
    visit_child_nodes(root_element, &mut text);
    if no_trim {
        text
    } else {
        text.trim().to_string()
    }
}

/// Used by `extract_text`.
///
/// TODO: This could be optimized by avoiding copies and re-allocations.
fn visit_child_nodes(parent_node: &Element, text: &mut String) {
    let mut last_node_type = "".to_string();
    let children = parent_node.child_nodes();
    for i in 0..children.length() {
        let node = match children.item(i) {
            Some(n) => n,
            None => {
                warn!("visit_child_nodes: Index out of bounds");
                return;
            },
        };
        match node.node_type() {
            Node::TEXT_NODE => {
                if last_node_type == "div" {
                    // An image following a div should go on a new line
                    text.push('\n');
                }
                last_node_type = "text".to_string();
                text.push_str(
                    // Append text, but strip leading and trailing newlines
                    node.node_value()
                        .unwrap_or_else(|| "".into())
                        .trim_matches(|c| c == '\n' || c == '\r')
                );
            }
            Node::ELEMENT_NODE => {
                let element: &Element = node.unchecked_ref();
                let tag = element.tag_name().to_lowercase();
                let last_node_type_clone = last_node_type.clone();
                last_node_type = tag.clone();
                match &*tag {
                    "span" => {
                        visit_child_nodes(element, text);
                    }
                    "div" => {
                        text.push('\n');
                        visit_child_nodes(element, text);
                    }
                    "img" => {
                        if last_node_type_clone == "div" {
                            // An image following a div should go on a new line
                            text.push('\n');
                        }
                        text.push_str(&node.unchecked_ref::<HtmlImageElement>().alt());
                    }
                    "br" => {
                        text.push('\n');
                    }
                    _other => {}
                }
            }
            other => warn!("visit_child_nodes: Unhandled node type: {}", other),
        }
    }
}

...just with the difference that a VDOM tree is created instead of a string.

I also have to be honest that I probably won't use any DOM -> VDOM function in my project, so it doesn't have high priority for me. But I thought I'd drop the request here, there might be others that need the same functionality.

@chinedufn
Copy link
Owner

Awesome - cool then let's leave this open for now then and see if anyone ends up needing to make use of something like this and we can go from there.

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

No branches or pull requests

2 participants