Add VNode::from_html_unchecked (#2842)

* Add VNode::html_from_raw

* Add docs for VNode::html_from_raw

* feature lock to available flags

* Actually raw

* Formatting + docs

* Tests

* More tests + docs

* fmt

* clippy

* CI

* No <div> around multi top-level nodes

* Update docs

* Fix braw detach

* Clippy & fmt

* Fix compile errors

* I hope you get attacked by Cow, Clippy

* Address review

* Reduce DOM calls

* improve detach bundle impl

* Add more tests

* Update example

* fmt

* Apply review suggestions

* fmt

* fix ci

* fix braw shift with multiple nodes

* rename function name

* fmt

* this should've been there

* ci be green
This commit is contained in:
Muhammad Hamza 2022-11-08 21:24:49 +05:00 committed by GitHub
parent a5f844ddcb
commit 90c7ff105a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 810 additions and 28 deletions

View File

@ -4,10 +4,6 @@
Rust source code. The code queries the DOM, creates a new element, and applies
this snippet of HTML to the element's innerHTML.
</p>
<p>
If you look at your browser's console you can see the DOM element (logged to
the console).
</p>
<svg height="250" width="500">
<polygon
points="220,10 300,210 170,250 123,234"

View File

@ -1,27 +1,19 @@
use web_sys::console;
use yew::{Component, Context, Html};
const HTML: &str = include_str!("document.html");
pub struct App {
pub value: i64,
}
pub struct App;
impl Component for App {
type Message = ();
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self { value: 0 }
Self
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let div = gloo::utils::document().create_element("div").unwrap();
div.set_inner_html(HTML);
// See <https://github.com/yewstack/yew/issues/1546>
console::log_1(&div);
Html::VRef(div.into())
Html::from_html_unchecked(HTML.into())
}
}

View File

@ -56,6 +56,7 @@ features = [
"FocusEvent",
"HtmlElement",
"HtmlInputElement",
"HtmlCollection",
"HtmlTextAreaElement",
"InputEvent",
"InputEventInit",
@ -91,6 +92,7 @@ version = "0.3"
features = [
"ShadowRootInit",
"ShadowRootMode",
"HtmlButtonElement"
]
[features]

View File

@ -4,7 +4,7 @@ use std::fmt;
use web_sys::{Element, Node};
use super::{BComp, BList, BPortal, BSubtree, BSuspense, BTag, BText};
use super::{BComp, BList, BPortal, BRaw, BSubtree, BSuspense, BTag, BText};
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::{Key, VNode};
@ -25,6 +25,8 @@ pub(super) enum BNode {
Ref(Node),
/// A suspendible document fragment.
Suspense(Box<BSuspense>),
/// A raw HTML string, represented by [`AttrValue`](crate::AttrValue).
Raw(BRaw),
}
impl BNode {
@ -38,6 +40,7 @@ impl BNode {
Self::Text(_) => None,
Self::Portal(bportal) => bportal.key(),
Self::Suspense(bsusp) => bsusp.key(),
Self::Raw(_) => None,
}
}
}
@ -58,6 +61,7 @@ impl ReconcileTarget for BNode {
}
Self::Portal(bportal) => bportal.detach(root, parent, parent_to_detach),
Self::Suspense(bsusp) => bsusp.detach(root, parent, parent_to_detach),
Self::Raw(raw) => raw.detach(root, parent, parent_to_detach),
}
}
@ -76,6 +80,7 @@ impl ReconcileTarget for BNode {
}
Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling),
Self::Raw(ref braw) => braw.shift(next_parent, next_sibling),
}
}
}
@ -120,6 +125,10 @@ impl Reconcilable for VNode {
vsuspsense.attach(root, parent_scope, parent, next_sibling);
(node_ref, suspsense.into())
}
VNode::VRaw(vraw) => {
let (node_ref, raw) = vraw.attach(root, parent_scope, parent, next_sibling);
(node_ref, raw.into())
}
}
}
@ -176,6 +185,9 @@ impl Reconcilable for VNode {
VNode::VSuspense(vsuspsense) => {
vsuspsense.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
}
VNode::VRaw(vraw) => {
vraw.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
}
}
}
}
@ -222,6 +234,13 @@ impl From<BSuspense> for BNode {
}
}
impl From<BRaw> for BNode {
#[inline]
fn from(braw: BRaw) -> Self {
Self::Raw(braw)
}
}
impl fmt::Debug for BNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
@ -232,6 +251,7 @@ impl fmt::Debug for BNode {
Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
Self::Portal(ref vportal) => vportal.fmt(f),
Self::Suspense(ref bsusp) => bsusp.fmt(f),
Self::Raw(ref braw) => braw.fmt(f),
}
}
}
@ -285,6 +305,9 @@ mod feat_hydration {
vsuspense.hydrate(root, parent_scope, parent, fragment);
(node_ref, suspense.into())
}
VNode::VRaw(_) => {
panic!("VRaw is not hydratable (raw HTML string cannot be hydrated)")
}
}
}
}

View File

@ -0,0 +1,400 @@
use wasm_bindgen::JsCast;
use web_sys::Element;
use crate::dom_bundle::bnode::BNode;
use crate::dom_bundle::traits::{Reconcilable, ReconcileTarget};
use crate::dom_bundle::utils::insert_node;
use crate::dom_bundle::BSubtree;
use crate::html::AnyScope;
use crate::virtual_dom::VRaw;
use crate::{AttrValue, NodeRef};
#[derive(Debug)]
pub struct BRaw {
reference: NodeRef,
children_count: usize,
html: AttrValue,
}
impl BRaw {
fn create_elements(html: &str) -> Vec<Element> {
let div = gloo::utils::document().create_element("div").unwrap();
div.set_inner_html(html);
let children = div.children();
let children = js_sys::Array::from(&children);
let children = children.to_vec();
children
.into_iter()
.map(|it| it.unchecked_into())
.collect::<Vec<_>>()
}
fn detach_bundle(&self, parent: &Element) {
let mut next_node = self.reference.get();
for _ in 0..self.children_count {
if let Some(node) = next_node {
next_node = node.next_sibling();
parent.remove_child(&node).unwrap();
}
}
}
}
impl ReconcileTarget for BRaw {
fn detach(self, _root: &BSubtree, parent: &Element, _parent_to_detach: bool) {
self.detach_bundle(parent);
}
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) -> NodeRef {
let mut next_node = match self.reference.get() {
Some(n) => n,
None => return NodeRef::default(),
};
let insert = |n| {
next_parent
.insert_before(&n, next_sibling.get().as_ref())
.unwrap()
};
for _ in 0..self.children_count {
let current = next_node;
next_node = match current.next_sibling() {
Some(n) => n,
None => {
// if nothing is next, add whatever is the current node and return early
insert(current.clone());
return NodeRef::new(current);
}
};
insert(current);
}
NodeRef::new(next_node)
}
}
impl Reconcilable for VRaw {
type Bundle = BRaw;
fn attach(
self,
_root: &BSubtree,
_parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
let elements = BRaw::create_elements(&self.html);
if elements.is_empty() {
return (
next_sibling.clone(),
BRaw {
reference: next_sibling,
children_count: 0,
html: self.html,
},
);
}
let node_ref = NodeRef::default();
let count = elements.len();
let mut iter = elements.into_iter();
let first = iter.next().unwrap();
insert_node(&first, parent, next_sibling.get().as_ref());
node_ref.set(Some(first.into()));
for child in iter {
insert_node(&child, parent, next_sibling.get().as_ref());
}
(
node_ref.clone(),
BRaw {
reference: node_ref,
children_count: count,
html: self.html,
},
)
}
fn reconcile_node(
self,
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
match bundle {
BNode::Raw(raw) if raw.html == self.html => raw.reference.clone(),
BNode::Raw(raw) => self.reconcile(root, parent_scope, parent, next_sibling, raw),
_ => self.replace(root, parent_scope, parent, next_sibling, bundle),
}
}
fn reconcile(
self,
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut Self::Bundle,
) -> NodeRef {
if self.html != bundle.html {
// we don't have a way to diff what's changed in the string so we remove the node and
// reattach it
bundle.detach_bundle(parent);
let (node_ref, braw) = self.attach(root, parent_scope, parent, next_sibling);
*bundle = braw;
node_ref
} else {
bundle.reference.clone()
}
}
}
#[cfg(target_arch = "wasm32")]
#[cfg(test)]
mod tests {
use gloo::utils::document;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
use super::*;
use crate::dom_bundle::utils::{setup_parent, setup_parent_and_sibling, SIBLING_CONTENT};
use crate::virtual_dom::VNode;
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn braw_works_one_node() {
let (root, scope, parent) = setup_parent();
const HTML: &str = "<span>text</span>";
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML)
}
#[test]
fn braw_works_no_node() {
let (root, scope, parent) = setup_parent();
const HTML: &str = "";
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML)
}
#[test]
fn braw_works_one_node_nested() {
let (root, scope, parent) = setup_parent();
const HTML: &str =
r#"<p>one <a href="https://yew.rs">link</a> more paragraph</p><div>here</div>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML)
}
#[test]
fn braw_works_multi_top_nodes() {
let (root, scope, parent) = setup_parent();
const HTML: &str = r#"<p>paragraph</p><a href="https://yew.rs">link</a>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML)
}
#[test]
fn braw_detach_works_multi_node() {
let (root, scope, parent) = setup_parent();
const HTML: &str = r#"<p>paragraph</p><a href="https://yew.rs">link</a>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML);
elem.detach(&root, &parent, false);
assert_eq!(parent.inner_html(), "");
}
#[test]
fn braw_detach_works_single_node() {
let (root, scope, parent) = setup_parent();
const HTML: &str = r#"<p>paragraph</p>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML);
elem.detach(&root, &parent, false);
assert_eq!(parent.inner_html(), "");
}
#[test]
fn braw_detach_works_empty() {
let (root, scope, parent) = setup_parent();
const HTML: &str = "";
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML);
elem.detach(&root, &parent, false);
assert_eq!(parent.inner_html(), "");
}
#[test]
fn braw_works_one_node_sibling_attached() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str = "<span>text</span>";
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
}
#[test]
fn braw_works_no_node_sibling_attached() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str = "";
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
}
#[test]
fn braw_works_one_node_nested_sibling_attached() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str =
r#"<p>one <a href="https://yew.rs">link</a> more paragraph</p><div>here</div>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
}
#[test]
fn braw_works_multi_top_nodes_sibling_attached() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str = r#"<p>paragraph</p><a href="https://yew.rs">link</a>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
}
#[test]
fn braw_detach_works_multi_node_sibling_attached() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str = r#"<p>paragraph</p><a href="https://yew.rs">link</a>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
elem.detach(&root, &parent, false);
assert_eq!(parent.inner_html(), format!("{}", SIBLING_CONTENT))
}
#[test]
fn braw_detach_works_single_node_sibling_attached() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str = r#"<p>paragraph</p>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
elem.detach(&root, &parent, false);
assert_eq!(parent.inner_html(), format!("{}", SIBLING_CONTENT))
}
#[test]
fn braw_detach_works_empty_sibling_attached() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str = "";
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
elem.detach(&root, &parent, false);
assert_eq!(parent.inner_html(), format!("{}", SIBLING_CONTENT))
}
#[test]
fn braw_shift_works() {
let (root, scope, parent) = setup_parent();
const HTML: &str = r#"<p>paragraph</p>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML);
let new_parent = document().create_element("section").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
elem.shift(&new_parent, NodeRef::default());
assert_eq!(new_parent.inner_html(), HTML);
assert_eq!(parent.inner_html(), "");
}
#[test]
fn braw_shift_with_sibling_works() {
let (root, scope, parent, sibling) = setup_parent_and_sibling();
const HTML: &str = r#"<p>paragraph</p>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, sibling);
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), format!("{}{}", HTML, SIBLING_CONTENT));
let new_parent = document().create_element("section").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let new_sibling = document().create_text_node(SIBLING_CONTENT);
new_parent.append_child(&new_sibling).unwrap();
let new_sibling_ref = NodeRef::new(new_sibling.into());
elem.shift(&new_parent, new_sibling_ref);
assert_eq!(parent.inner_html(), SIBLING_CONTENT);
assert_eq!(
new_parent.inner_html(),
format!("{}{}", HTML, SIBLING_CONTENT)
);
}
#[test]
fn braw_shift_works_multi_node() {
let (root, scope, parent) = setup_parent();
const HTML: &str = r#"<p>paragraph</p><a href="https://yew.rs">link</a>"#;
let elem = VNode::from_html_unchecked(HTML.into());
let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_braw(&mut elem);
assert_eq!(parent.inner_html(), HTML);
let new_parent = document().create_element("section").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
elem.shift(&new_parent, NodeRef::default());
assert_eq!(parent.inner_html(), "");
assert_eq!(new_parent.inner_html(), HTML);
}
fn assert_braw(node: &mut BNode) -> &mut BRaw {
if let BNode::Raw(braw) = node {
return braw;
}
panic!("should be braw");
}
}

View File

@ -385,30 +385,19 @@ mod feat_hydration {
#[cfg(target_arch = "wasm32")]
#[cfg(test)]
mod tests {
use gloo::utils::document;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
use web_sys::HtmlInputElement as InputElement;
use super::*;
use crate::dom_bundle::utils::setup_parent;
use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE};
use crate::virtual_dom::{AttrValue, VNode, VTag};
use crate::{html, Html, NodeRef};
wasm_bindgen_test_configure!(run_in_browser);
fn setup_parent() -> (BSubtree, AnyScope, Element) {
let scope = AnyScope::test();
let parent = document().create_element("div").unwrap();
let root = BSubtree::create_root(&parent);
document().body().unwrap().append_child(&parent).unwrap();
(root, scope, parent)
}
#[test]
fn it_compares_tags() {
let a = html! {

View File

@ -14,6 +14,7 @@ mod bcomp;
mod blist;
mod bnode;
mod bportal;
mod braw;
mod bsuspense;
mod btag;
mod btext;
@ -26,6 +27,7 @@ use bcomp::BComp;
use blist::BList;
use bnode::BNode;
use bportal::BPortal;
use braw::BRaw;
use bsuspense::BSuspense;
use btag::{BTag, Registry};
use btext::BText;

View File

@ -32,6 +32,8 @@ pub(super) trait Reconcilable {
/// - `next_sibling`: to find where to put the node.
///
/// Returns a reference to the newly inserted element.
/// The [`NodeRef`] points the first element (if there are multiple nodes created),
/// or is the passed in next_sibling if there are no element is created.
fn attach(
self,

View File

@ -77,3 +77,46 @@ mod feat_hydration {
#[cfg(feature = "hydration")]
pub(super) use feat_hydration::*;
#[cfg(test)]
mod tests {
#![allow(dead_code)]
use gloo::utils::document;
use web_sys::Element;
use crate::dom_bundle::BSubtree;
use crate::html::AnyScope;
use crate::NodeRef;
pub fn setup_parent() -> (BSubtree, AnyScope, Element) {
let scope = AnyScope::test();
let parent = document().create_element("div").unwrap();
let root = BSubtree::create_root(&parent);
document().body().unwrap().append_child(&parent).unwrap();
(root, scope, parent)
}
pub const SIBLING_CONTENT: &str = "END";
pub fn setup_parent_and_sibling() -> (BSubtree, AnyScope, Element, NodeRef) {
let scope = AnyScope::test();
let parent = document().create_element("div").unwrap();
let root = BSubtree::create_root(&parent);
document().body().unwrap().append_child(&parent).unwrap();
let end = document().create_text_node(SIBLING_CONTENT);
parent.append_child(&end).unwrap();
let sibling = NodeRef::new(end.into());
(root, scope, parent, sibling)
}
}
#[cfg(test)]
// this is needed because clippy doesn't like the import not being used
#[allow(unused_imports)]
pub(super) use tests::*;

View File

@ -13,6 +13,8 @@ pub mod vnode;
#[doc(hidden)]
pub mod vportal;
#[doc(hidden)]
pub mod vraw;
#[doc(hidden)]
pub mod vsuspense;
#[doc(hidden)]
pub mod vtag;
@ -36,6 +38,8 @@ pub use self::vnode::VNode;
#[doc(inline)]
pub use self::vportal::VPortal;
#[doc(inline)]
pub use self::vraw::VRaw;
#[doc(inline)]
pub use self::vsuspense::VSuspense;
#[doc(inline)]
pub use self::vtag::VTag;

View File

@ -8,6 +8,8 @@ use web_sys::Node;
use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText};
use crate::html::BaseComponent;
use crate::virtual_dom::VRaw;
use crate::AttrValue;
/// Bind virtual element to a DOM reference.
#[derive(Clone)]
@ -26,6 +28,10 @@ pub enum VNode {
VRef(Node),
/// A suspendible document fragment.
VSuspense(VSuspense),
/// A raw HTML string, represented by [`AttrValue`](crate::AttrValue).
///
/// Also see: [`VNode::from_html_unchecked`]
VRaw(VRaw),
}
impl VNode {
@ -38,6 +44,7 @@ impl VNode {
VNode::VText(_) => None,
VNode::VPortal(vportal) => vportal.node.key(),
VNode::VSuspense(vsuspense) => vsuspense.key.as_ref(),
VNode::VRaw(_) => None,
}
}
@ -45,6 +52,40 @@ impl VNode {
pub fn has_key(&self) -> bool {
self.key().is_some()
}
/// Create a [`VNode`] from a string of HTML
///
/// # Behavior in browser
///
/// In the browser, this function creates an element, sets the passed HTML to its `innerHTML`
/// and inserts the contents of it into the DOM.
///
/// # Behavior on server
///
/// When rendering on the server, the contents of HTML are directly injected into the HTML
/// stream.
///
/// ## Warning
///
/// The contents are **not** sanitized or validated. You, as the developer, are responsible to
/// ensure the HTML string passed to this method are _valid_ and _not malicious_
///
/// # Example
///
/// ```rust
/// use yew::{html, AttrValue, Html};
/// # fn _main() {
/// let parsed = Html::from_html_unchecked(AttrValue::from("<div>content</div>"));
/// let _: Html = html! {
/// <div>
/// {parsed}
/// </div>
/// };
/// # }
/// ```
pub fn from_html_unchecked(html: AttrValue) -> Self {
VNode::VRaw(VRaw { html })
}
}
impl Default for VNode {
@ -129,6 +170,7 @@ impl fmt::Debug for VNode {
VNode::VRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
VNode::VPortal(ref vportal) => vportal.fmt(f),
VNode::VSuspense(ref vsuspense) => vsuspense.fmt(f),
VNode::VRaw(ref vraw) => write!(f, "VRaw {{ {} }}", vraw.html),
}
}
}
@ -142,6 +184,7 @@ impl PartialEq for VNode {
(VNode::VRef(a), VNode::VRef(b)) => a == b,
// TODO: Need to improve PartialEq for VComp before enabling.
(VNode::VComp(_), VNode::VComp(_)) => false,
(VNode::VRaw(a), VNode::VRaw(b)) => a.html == b.html,
_ => false,
}
}
@ -194,6 +237,8 @@ mod feat_ssr {
.render_into_stream(w, parent_scope, hydratable)
.await
}
VNode::VRaw(vraw) => vraw.render_into_stream(w, parent_scope, hydratable).await,
}
}

View File

@ -0,0 +1,33 @@
use crate::AttrValue;
/// A raw HTML string to be used in VDOM.
#[derive(Clone, Debug)]
pub struct VRaw {
pub html: AttrValue,
}
impl From<AttrValue> for VRaw {
fn from(html: AttrValue) -> Self {
Self { html }
}
}
#[cfg(feature = "ssr")]
mod feat_ssr {
use std::fmt::Write;
use super::*;
use crate::html::AnyScope;
use crate::platform::fmt::BufWriter;
impl VRaw {
pub(crate) async fn render_into_stream(
&self,
w: &mut BufWriter,
_parent_scope: &AnyScope,
_hydratable: bool,
) {
let _ = w.write_str(self.html.as_ref());
}
}
}

View File

@ -0,0 +1,251 @@
mod common;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
use yew::prelude::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[cfg(not(target_arch = "wasm32"))]
use tokio::test;
macro_rules! create_test {
($name:ident, $html:expr) => {
create_test!($name, $html, $html);
};
($name:ident, $raw:expr, $expected:expr) => {
#[test]
async fn $name() {
#[function_component]
fn App() -> Html {
let raw = Html::from_html_unchecked(AttrValue::from($raw));
html! {
<div id="raw-container">
{raw}
</div>
}
}
#[cfg(target_arch = "wasm32")]
{
use std::time::Duration;
use yew::platform::time::sleep;
yew::Renderer::<App>::with_root(
gloo::utils::document().get_element_by_id("output").unwrap(),
)
.render();
// wait for render to finish
sleep(Duration::from_millis(100)).await;
let e = gloo::utils::document()
.get_element_by_id("raw-container")
.unwrap();
assert_eq!(e.inner_html(), $expected);
}
#[cfg(not(target_arch = "wasm32"))]
{
let actual = yew::ServerRenderer::<App>::new()
.hydratable(false)
.render()
.await;
assert_eq!(
actual,
format!(r#"<div id="raw-container">{}</div>"#, $expected)
);
}
}
};
}
create_test!(empty_string, "");
create_test!(one_node, "<span>text</span>");
create_test!(
one_but_nested_node,
r#"<p>one <a href="https://yew.rs">link</a> more paragraph</p>"#
);
create_test!(
multi_node,
r#"<p>paragraph</p><a href="https://yew.rs">link</a>"#
);
macro_rules! create_update_html_test {
($name:ident, $initial:expr, $updated:expr) => {
#[cfg(target_arch = "wasm32")]
#[test]
async fn $name() {
#[function_component]
fn App() -> Html {
let raw_html = use_state(|| ($initial));
let onclick = {
let raw_html = raw_html.clone();
move |_| raw_html.set($updated)
};
let raw = Html::from_html_unchecked(AttrValue::from(*raw_html));
html! {
<>
<div id="raw-container">
{raw}
</div>
<button id="click-me-btn" {onclick}>{"Click me"}</button>
</>
}
}
use std::time::Duration;
use yew::platform::time::sleep;
yew::Renderer::<App>::with_root(
gloo::utils::document().get_element_by_id("output").unwrap(),
)
.render();
// wait for render to finish
sleep(Duration::from_millis(100)).await;
let e = gloo::utils::document()
.get_element_by_id("raw-container")
.unwrap();
assert_eq!(e.inner_html(), $initial);
gloo::utils::document()
.get_element_by_id("click-me-btn")
.unwrap()
.unchecked_into::<web_sys::HtmlButtonElement>()
.click();
sleep(Duration::from_millis(100)).await;
let e = gloo::utils::document()
.get_element_by_id("raw-container")
.unwrap();
assert_eq!(e.inner_html(), $updated);
}
};
}
create_update_html_test!(
set_new_html_string,
"<span>first</span>",
"<span>second</span>"
);
create_update_html_test!(
set_new_html_string_multiple_children,
"<span>first</span><span>second</span>",
"<span>second</span>"
);
create_update_html_test!(
clear_html_string_multiple_children,
"<span>first</span><span>second</span>",
""
);
create_update_html_test!(
nothing_changes,
"<span>first</span><span>second</span>",
"<span>first</span><span>second</span>"
);
#[cfg(target_arch = "wasm32")]
#[test]
async fn change_vnode_types_from_other_to_vraw() {
#[function_component]
fn App() -> Html {
let node = use_state(|| html!("text"));
let onclick = {
let node = node.clone();
move |_| {
node.set(Html::from_html_unchecked(AttrValue::from(
"<span>second</span>",
)))
}
};
html! {
<>
<div id="raw-container">
{(*node).clone()}
</div>
<button id="click-me-btn" {onclick}>{"Click me"}</button>
</>
}
}
use std::time::Duration;
use yew::platform::time::sleep;
yew::Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
.render();
// wait for render to finish
sleep(Duration::from_millis(100)).await;
let e = gloo::utils::document()
.get_element_by_id("raw-container")
.unwrap();
assert_eq!(e.inner_html(), "text");
gloo::utils::document()
.get_element_by_id("click-me-btn")
.unwrap()
.unchecked_into::<web_sys::HtmlButtonElement>()
.click();
sleep(Duration::from_millis(100)).await;
let e = gloo::utils::document()
.get_element_by_id("raw-container")
.unwrap();
assert_eq!(e.inner_html(), "<span>second</span>");
}
#[cfg(target_arch = "wasm32")]
#[test]
async fn change_vnode_types_from_vraw_to_other() {
#[function_component]
fn App() -> Html {
let node = use_state(|| Html::from_html_unchecked(AttrValue::from("<span>second</span>")));
let onclick = {
let node = node.clone();
move |_| node.set(html!("text"))
};
html! {
<>
<div id="raw-container">
{(*node).clone()}
</div>
<button id="click-me-btn" {onclick}>{"Click me"}</button>
</>
}
}
use std::time::Duration;
use yew::platform::time::sleep;
yew::Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
.render();
// wait for render to finish
sleep(Duration::from_millis(100)).await;
let e = gloo::utils::document()
.get_element_by_id("raw-container")
.unwrap();
assert_eq!(e.inner_html(), "<span>second</span>");
gloo::utils::document()
.get_element_by_id("click-me-btn")
.unwrap()
.unchecked_into::<web_sys::HtmlButtonElement>()
.click();
sleep(Duration::from_millis(100)).await;
let e = gloo::utils::document()
.get_element_by_id("raw-container")
.unwrap();
assert_eq!(e.inner_html(), "text");
}