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

Creating VNode from web_sys types #1942

Closed
da-x opened this issue Jul 2, 2021 · 3 comments
Closed

Creating VNode from web_sys types #1942

da-x opened this issue Jul 2, 2021 · 3 comments

Comments

@da-x
Copy link

da-x commented Jul 2, 2021

The use case I have for is aimed at post-processing of HTML returned from pulldown_cmark, for example adding yew callbacks for links.

I ended up with the hack [1] below.

However I didn't find an existing implemention inside yew for this. Maybe I didn't search deep enough, or maybe I can fix it up and contribute? Using a From impl?

[1]

trait ToVNode {
    fn to_vnode(&self) -> VNode;
}

impl ToVNode for web_sys::Element {
    fn to_vnode(&self) -> VNode {
        let mut vtag = VTag::new(self.tag_name());
        let attrs = self.attributes();
        for idx in 0..attrs.length() {
            if let Some(attr) = attrs.item(idx) {
                // This is for interning of those strings:
                let stat = match attr.name().as_str() 
                    "href" => "href",
                    "class" => "class",
                    "id" => "id",
                    "style" => "style",
                    s => panic!("{}", s),
                };
                vtag.add_attribute(stat, std::borrow::Cow::Owned(attr.value()));
            }
        }
        let children = self.child_nodes();
        for idx in 0..children.length() {
            if let Some(child) = children.item(idx) {
                vtag.add_child(child.to_vnode());
            }
        }

        VNode::VTag(Box::new(vtag))
    }
}

impl ToVNode for web_sys::Node {
    fn to_vnode(&self) -> VNode {
        use wasm_bindgen::JsCast;
        if let Ok(item) = self.clone().dyn_into::<web_sys::Element>() {
            return item.to_vnode();
        }
        if let Ok(item) = self.clone().dyn_into::<web_sys::Text>() {
            return VNode::VText(VText::new(item.whole_text().unwrap()));
        }
        panic!();
    }
}
@mc1098
Copy link
Contributor

mc1098 commented Jul 2, 2021

I think you can use Html::VRef for this use case.

Then to add callbacks for Yew components in the following manner:

use yew::{prelude::*, web_sys::HtmlElement};
use wasm_bindgen::{prelude::*, JsCast};

pub struct Comp {
    link: ComponentLink<Self>,
    onclick_closure: Option<Closure<dyn Fn(MouseEvent)>>,
}

pub enum Msg {
    Clicked(MouseEvent),
}

impl Component for Comp {
    type Message = Msg;
    type Properties = ();

    fn create(_: yew::Properties, link: ComponentLink<Self>) -> Self {
        Self {
            link,
            onclick_closure: None,
        }
    }
    
    // fn change omitted for brevity 
    
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        yew::web_sys::console::_log_1(&"CLICKED!".into());
        // do something with msg here!
        false
    }
    
    fn view(&self) -> Html {
        let div = yew::utils::document()
            .get_element_by_id(YOUR_ELEMENT_ID)
            .unwrap();
       Html::VRef(div.into())
    }
    
    fn rendered(&mut self, first_render: bool) {
        if !first_render {
            return;
        }
        
        let element: HtmlElement = yew::utils::document()
            .get_element_by_id(YOUR_ELEMENT_ID)
            .unwrap()
            .unchecked_into();
        
        let local_cb = self.link.callback(|e: MouseEvent| Msg::Clicked(e));
        
        let onclick_closure = Closure::wrap(Box::new(move |e| {
            local_cb.emit(e);
        }) as Box<dyn Fn(MouseEvent)>);
        
        element.set_onclick(onclick_closure.as_ref().dyn_ref());
        
        // If the element and the onclick listener will last the duration of the app,
        // then you could just call onclick_closure.forget() instead of storing it 
        self.onclick_closure = Some(onclick_closure);
    }
}

The above assumes you have some element in the DOM that you can get by it's id (YOUR_ELEMENT_ID) - you can create an element in the view fn if it doesn't already exist.
Using Html:VRef is in the docs.

Hopefully this resolves the problem for you, or at least helps get you going :)
Let me know if I'm missing something for your use case, I'm not familiar with pulldown_cmark so this might be the case.

@da-x
Copy link
Author

da-x commented Jul 3, 2021

Thanks for investing time in this.

For converting Markdown to HTML, pulldown_cmark returns a String, which contains HTML. Now, to attach this to our DOM we should convert this into web_sys element tree, and then we may have a multitude of <a> elements in that HTML for which every one would need a slightly different callback implementation (different handling for each link).

So if I understand correctly the proposed solution, instead of converting everything to VNode, I can recursively iterate to find the <a> elements under the top HtmlElement, and have set_onclick be used on each of them. Then, attach the top HtmlElement via a VRef. Though I would like to avoid elements IDs and saving closures under self, and instead have all of this under fn view like I have now with the conversion, otherwise it feels a bit hacky. I'd need to experiment with this.

Anyway, I think I understand how to solve this better now. Please tell if you have more insights, otherwise I'll close the issue.

@hamza1311
Copy link
Member

A better solution would be to convert HTML String into VNode and attach listeners, etc to the vdom elements. You'll have to do this manually. See the virtual_dom module's documentation to learn more

@da-x da-x closed this as completed Jul 31, 2021
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

3 participants