Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions packages/yew/src/dom_bundle/btag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use web_sys::{Element, HtmlTextAreaElement as TextAreaElement};

use super::{BNode, BSubtree, DomSlot, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
#[cfg(feature = "hydration")]
use crate::virtual_dom::vtag::HTML_NAMESPACE;
use crate::virtual_dom::vtag::{
InputFields, TextareaFields, VTagInner, Value, MATHML_NAMESPACE, SVG_NAMESPACE,
};
Expand Down Expand Up @@ -365,14 +367,31 @@ mod feat_hydration {
);
let el = node.dyn_into::<Element>().expect("expected an element.");

assert_eq!(
el.tag_name().to_lowercase(),
tag_name,
"expected element of kind {}, found {}.",
tag_name,
el.tag_name().to_lowercase(),
);

{
let el_tag_name = el.tag_name();
let parent_namespace = _parent.namespace_uri();

// In HTML namespace (or no namespace), createElement is case-insensitive
// In other namespaces (SVG, MathML), createElementNS is case-sensitive
let should_compare_case_insensitive = parent_namespace.is_none()
|| parent_namespace.as_deref() == Some(HTML_NAMESPACE);

if should_compare_case_insensitive {
// Case-insensitive comparison for HTML elements
let el_tag_lower = el_tag_name.to_lowercase();
let tag_name_lower = tag_name.to_lowercase();
assert_eq!(
el_tag_lower, tag_name_lower,
"expected element of kind {tag_name}, found {el_tag_name}.",
);
} else {
// Case-sensitive comparison for namespaced elements (SVG, MathML)
assert_eq!(
el_tag_name, tag_name,
"expected element of kind {tag_name}, found {el_tag_name}.",
);
}
}
// We simply register listeners and update all attributes.
let attributes = attributes.apply(root, &el);
let listeners = listeners.apply(root, &el);
Expand Down
97 changes: 97 additions & 0 deletions packages/yew/tests/hydration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1077,3 +1077,100 @@ async fn hydrate_empty() {
let result = obtain_result_by_id("output");
assert_eq!(result.as_str(), r#"<div>after</div><div>after</div>"#);
}

#[wasm_bindgen_test]
async fn hydration_with_camelcase_svg_elements() {
#[function_component]
fn SvgWithCamelCase() -> Html {
html! {
<svg width="100" height="100">
<defs>
<@{"linearGradient"} id="gradient1">
<stop offset="0%" stop-color="red" />
<stop offset="100%" stop-color="blue" />
</@>
<@{"radialGradient"} id="gradient2">
<stop offset="0%" stop-color="yellow" />
<stop offset="100%" stop-color="green" />
</@>
<@{"clipPath"} id="clip1">
<circle cx="50" cy="50" r="40" />
</@>
</defs>
<rect x="10" y="10" width="80" height="80" fill="url(#gradient1)" />
<circle cx="50" cy="50" r="30" fill="url(#gradient2)" clip-path="url(#clip1)" />
<@{"feDropShadow"} dx="2" dy="2" stdDeviation="2" />
</svg>
}
}

#[function_component]
fn App() -> Html {
let counter = use_state(|| 0);
let onclick = {
let counter = counter.clone();
Callback::from(move |_| counter.set(*counter + 1))
};

html! {
<div id="result">
<div class="counter">{"Count: "}{*counter}</div>
<button {onclick} class="increment">{"Increment"}</button>
<SvgWithCamelCase />
</div>
}
}

// Server render
let s = ServerRenderer::<App>::new().render().await;

// Set HTML
gloo::utils::document()
.query_selector("#output")
.unwrap()
.unwrap()
.set_inner_html(&s);

sleep(Duration::ZERO).await;

// Hydrate - this should not panic
Renderer::<App>::with_root(gloo::utils::document().get_element_by_id("output").unwrap())
.hydrate();

sleep(Duration::from_millis(10)).await;

// Verify the SVG elements are present and properly cased
let svg = gloo::utils::document()
.query_selector("svg")
.unwrap()
.unwrap();

let linear_gradient = svg.query_selector("linearGradient").unwrap().unwrap();
assert_eq!(linear_gradient.tag_name(), "linearGradient");

let radial_gradient = svg.query_selector("radialGradient").unwrap().unwrap();
assert_eq!(radial_gradient.tag_name(), "radialGradient");

let clip_path = svg.query_selector("clipPath").unwrap().unwrap();
assert_eq!(clip_path.tag_name(), "clipPath");

// Test interactivity still works after hydration
gloo::utils::document()
.query_selector(".increment")
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
.click();

sleep(Duration::from_millis(10)).await;

let counter_text = gloo::utils::document()
.query_selector(".counter")
.unwrap()
.unwrap()
.text_content()
.unwrap();

assert_eq!(counter_text, "Count: 1");
}
Loading