mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
a5f844ddcb
commit
90c7ff105a
@ -4,10 +4,6 @@
|
|||||||
Rust source code. The code queries the DOM, creates a new element, and applies
|
Rust source code. The code queries the DOM, creates a new element, and applies
|
||||||
this snippet of HTML to the element's innerHTML.
|
this snippet of HTML to the element's innerHTML.
|
||||||
</p>
|
</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">
|
<svg height="250" width="500">
|
||||||
<polygon
|
<polygon
|
||||||
points="220,10 300,210 170,250 123,234"
|
points="220,10 300,210 170,250 123,234"
|
||||||
|
|||||||
@ -1,27 +1,19 @@
|
|||||||
use web_sys::console;
|
|
||||||
use yew::{Component, Context, Html};
|
use yew::{Component, Context, Html};
|
||||||
|
|
||||||
const HTML: &str = include_str!("document.html");
|
const HTML: &str = include_str!("document.html");
|
||||||
|
|
||||||
pub struct App {
|
pub struct App;
|
||||||
pub value: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for App {
|
impl Component for App {
|
||||||
type Message = ();
|
type Message = ();
|
||||||
type Properties = ();
|
type Properties = ();
|
||||||
|
|
||||||
fn create(_ctx: &Context<Self>) -> Self {
|
fn create(_ctx: &Context<Self>) -> Self {
|
||||||
Self { value: 0 }
|
Self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||||
let div = gloo::utils::document().create_element("div").unwrap();
|
Html::from_html_unchecked(HTML.into())
|
||||||
div.set_inner_html(HTML);
|
|
||||||
// See <https://github.com/yewstack/yew/issues/1546>
|
|
||||||
console::log_1(&div);
|
|
||||||
|
|
||||||
Html::VRef(div.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,7 @@ features = [
|
|||||||
"FocusEvent",
|
"FocusEvent",
|
||||||
"HtmlElement",
|
"HtmlElement",
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
|
"HtmlCollection",
|
||||||
"HtmlTextAreaElement",
|
"HtmlTextAreaElement",
|
||||||
"InputEvent",
|
"InputEvent",
|
||||||
"InputEventInit",
|
"InputEventInit",
|
||||||
@ -91,6 +92,7 @@ version = "0.3"
|
|||||||
features = [
|
features = [
|
||||||
"ShadowRootInit",
|
"ShadowRootInit",
|
||||||
"ShadowRootMode",
|
"ShadowRootMode",
|
||||||
|
"HtmlButtonElement"
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use std::fmt;
|
|||||||
|
|
||||||
use web_sys::{Element, Node};
|
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::dom_bundle::{Reconcilable, ReconcileTarget};
|
||||||
use crate::html::{AnyScope, NodeRef};
|
use crate::html::{AnyScope, NodeRef};
|
||||||
use crate::virtual_dom::{Key, VNode};
|
use crate::virtual_dom::{Key, VNode};
|
||||||
@ -25,6 +25,8 @@ pub(super) enum BNode {
|
|||||||
Ref(Node),
|
Ref(Node),
|
||||||
/// A suspendible document fragment.
|
/// A suspendible document fragment.
|
||||||
Suspense(Box<BSuspense>),
|
Suspense(Box<BSuspense>),
|
||||||
|
/// A raw HTML string, represented by [`AttrValue`](crate::AttrValue).
|
||||||
|
Raw(BRaw),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BNode {
|
impl BNode {
|
||||||
@ -38,6 +40,7 @@ impl BNode {
|
|||||||
Self::Text(_) => None,
|
Self::Text(_) => None,
|
||||||
Self::Portal(bportal) => bportal.key(),
|
Self::Portal(bportal) => bportal.key(),
|
||||||
Self::Suspense(bsusp) => bsusp.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::Portal(bportal) => bportal.detach(root, parent, parent_to_detach),
|
||||||
Self::Suspense(bsusp) => bsusp.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::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
|
||||||
Self::Suspense(ref vsuspense) => vsuspense.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);
|
vsuspsense.attach(root, parent_scope, parent, next_sibling);
|
||||||
(node_ref, suspsense.into())
|
(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) => {
|
VNode::VSuspense(vsuspsense) => {
|
||||||
vsuspsense.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
|
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 {
|
impl fmt::Debug for BNode {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
@ -232,6 +251,7 @@ impl fmt::Debug for BNode {
|
|||||||
Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
|
Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
|
||||||
Self::Portal(ref vportal) => vportal.fmt(f),
|
Self::Portal(ref vportal) => vportal.fmt(f),
|
||||||
Self::Suspense(ref bsusp) => bsusp.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);
|
vsuspense.hydrate(root, parent_scope, parent, fragment);
|
||||||
(node_ref, suspense.into())
|
(node_ref, suspense.into())
|
||||||
}
|
}
|
||||||
|
VNode::VRaw(_) => {
|
||||||
|
panic!("VRaw is not hydratable (raw HTML string cannot be hydrated)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
400
packages/yew/src/dom_bundle/braw.rs
Normal file
400
packages/yew/src/dom_bundle/braw.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -385,30 +385,19 @@ mod feat_hydration {
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gloo::utils::document;
|
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||||
use web_sys::HtmlInputElement as InputElement;
|
use web_sys::HtmlInputElement as InputElement;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::dom_bundle::utils::setup_parent;
|
||||||
use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget};
|
use crate::dom_bundle::{BNode, Reconcilable, ReconcileTarget};
|
||||||
use crate::html::AnyScope;
|
|
||||||
use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE};
|
use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE};
|
||||||
use crate::virtual_dom::{AttrValue, VNode, VTag};
|
use crate::virtual_dom::{AttrValue, VNode, VTag};
|
||||||
use crate::{html, Html, NodeRef};
|
use crate::{html, Html, NodeRef};
|
||||||
|
|
||||||
wasm_bindgen_test_configure!(run_in_browser);
|
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]
|
#[test]
|
||||||
fn it_compares_tags() {
|
fn it_compares_tags() {
|
||||||
let a = html! {
|
let a = html! {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ mod bcomp;
|
|||||||
mod blist;
|
mod blist;
|
||||||
mod bnode;
|
mod bnode;
|
||||||
mod bportal;
|
mod bportal;
|
||||||
|
mod braw;
|
||||||
mod bsuspense;
|
mod bsuspense;
|
||||||
mod btag;
|
mod btag;
|
||||||
mod btext;
|
mod btext;
|
||||||
@ -26,6 +27,7 @@ use bcomp::BComp;
|
|||||||
use blist::BList;
|
use blist::BList;
|
||||||
use bnode::BNode;
|
use bnode::BNode;
|
||||||
use bportal::BPortal;
|
use bportal::BPortal;
|
||||||
|
use braw::BRaw;
|
||||||
use bsuspense::BSuspense;
|
use bsuspense::BSuspense;
|
||||||
use btag::{BTag, Registry};
|
use btag::{BTag, Registry};
|
||||||
use btext::BText;
|
use btext::BText;
|
||||||
|
|||||||
@ -32,6 +32,8 @@ pub(super) trait Reconcilable {
|
|||||||
/// - `next_sibling`: to find where to put the node.
|
/// - `next_sibling`: to find where to put the node.
|
||||||
///
|
///
|
||||||
/// Returns a reference to the newly inserted element.
|
/// 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(
|
fn attach(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
|
|||||||
@ -77,3 +77,46 @@ mod feat_hydration {
|
|||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
pub(super) use feat_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::*;
|
||||||
|
|||||||
@ -13,6 +13,8 @@ pub mod vnode;
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod vportal;
|
pub mod vportal;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
pub mod vraw;
|
||||||
|
#[doc(hidden)]
|
||||||
pub mod vsuspense;
|
pub mod vsuspense;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod vtag;
|
pub mod vtag;
|
||||||
@ -36,6 +38,8 @@ pub use self::vnode::VNode;
|
|||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use self::vportal::VPortal;
|
pub use self::vportal::VPortal;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
pub use self::vraw::VRaw;
|
||||||
|
#[doc(inline)]
|
||||||
pub use self::vsuspense::VSuspense;
|
pub use self::vsuspense::VSuspense;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use self::vtag::VTag;
|
pub use self::vtag::VTag;
|
||||||
|
|||||||
@ -8,6 +8,8 @@ use web_sys::Node;
|
|||||||
|
|
||||||
use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText};
|
use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText};
|
||||||
use crate::html::BaseComponent;
|
use crate::html::BaseComponent;
|
||||||
|
use crate::virtual_dom::VRaw;
|
||||||
|
use crate::AttrValue;
|
||||||
|
|
||||||
/// Bind virtual element to a DOM reference.
|
/// Bind virtual element to a DOM reference.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -26,6 +28,10 @@ pub enum VNode {
|
|||||||
VRef(Node),
|
VRef(Node),
|
||||||
/// A suspendible document fragment.
|
/// A suspendible document fragment.
|
||||||
VSuspense(VSuspense),
|
VSuspense(VSuspense),
|
||||||
|
/// A raw HTML string, represented by [`AttrValue`](crate::AttrValue).
|
||||||
|
///
|
||||||
|
/// Also see: [`VNode::from_html_unchecked`]
|
||||||
|
VRaw(VRaw),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VNode {
|
impl VNode {
|
||||||
@ -38,6 +44,7 @@ impl VNode {
|
|||||||
VNode::VText(_) => None,
|
VNode::VText(_) => None,
|
||||||
VNode::VPortal(vportal) => vportal.node.key(),
|
VNode::VPortal(vportal) => vportal.node.key(),
|
||||||
VNode::VSuspense(vsuspense) => vsuspense.key.as_ref(),
|
VNode::VSuspense(vsuspense) => vsuspense.key.as_ref(),
|
||||||
|
VNode::VRaw(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +52,40 @@ impl VNode {
|
|||||||
pub fn has_key(&self) -> bool {
|
pub fn has_key(&self) -> bool {
|
||||||
self.key().is_some()
|
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 {
|
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::VRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
|
||||||
VNode::VPortal(ref vportal) => vportal.fmt(f),
|
VNode::VPortal(ref vportal) => vportal.fmt(f),
|
||||||
VNode::VSuspense(ref vsuspense) => vsuspense.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,
|
(VNode::VRef(a), VNode::VRef(b)) => a == b,
|
||||||
// TODO: Need to improve PartialEq for VComp before enabling.
|
// TODO: Need to improve PartialEq for VComp before enabling.
|
||||||
(VNode::VComp(_), VNode::VComp(_)) => false,
|
(VNode::VComp(_), VNode::VComp(_)) => false,
|
||||||
|
(VNode::VRaw(a), VNode::VRaw(b)) => a.html == b.html,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,6 +237,8 @@ mod feat_ssr {
|
|||||||
.render_into_stream(w, parent_scope, hydratable)
|
.render_into_stream(w, parent_scope, hydratable)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VNode::VRaw(vraw) => vraw.render_into_stream(w, parent_scope, hydratable).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
packages/yew/src/virtual_dom/vraw.rs
Normal file
33
packages/yew/src/virtual_dom/vraw.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
packages/yew/tests/raw_html.rs
Normal file
251
packages/yew/tests/raw_html.rs
Normal 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");
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user