diff --git a/benches/benches.rs b/benches/benches.rs index b3d96fe..94ce1c3 100755 --- a/benches/benches.rs +++ b/benches/benches.rs @@ -17,16 +17,16 @@ use std::convert::TryInto; /// The simplest thing we can render: `
`. struct Empty; -impl Render for Empty { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Empty { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { div(&cx).finish() } } /// Render a list that is `self.0` items long, has attributes and listeners. struct SimpleList(usize); -impl Render for SimpleList { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for SimpleList { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { let mut children = bumpalo::collections::Vec::with_capacity_in(self.0, cx.bump); children.extend((0..self.0).map(|_| { li(&cx) diff --git a/crates/js-api/src/lib.rs b/crates/js-api/src/lib.rs index 6532695..54f9dec 100644 --- a/crates/js-api/src/lib.rs +++ b/crates/js-api/src/lib.rs @@ -99,8 +99,8 @@ impl GreetingViaJs { /// And finally the `Render` implementation! This adds a `

` element and some /// text around whatever the inner JS `Greeting` component renders. -impl Render for GreetingViaJs { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for GreetingViaJs { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; p(&cx) .children([ @@ -216,8 +216,8 @@ impl JsRender { } } -impl Render for JsRender { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for JsRender { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { create(cx, self.render()) } } diff --git a/examples/counter/src/lib.rs b/examples/counter/src/lib.rs index 0bbf035..ff44df3 100755 --- a/examples/counter/src/lib.rs +++ b/examples/counter/src/lib.rs @@ -26,8 +26,8 @@ impl Counter { // The `Render` implementation for `Counter`s displays the current count and has // buttons to increment and decrement the count. -impl Render for Counter { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Counter { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; // Stringify the count as a bump-allocated string. diff --git a/examples/game-of-life/src/lib.rs b/examples/game-of-life/src/lib.rs index 54e9064..e6b0039 100644 --- a/examples/game-of-life/src/lib.rs +++ b/examples/game-of-life/src/lib.rs @@ -126,8 +126,8 @@ impl Universe { } /// The rendering implementation for our Game of Life. -impl Render for Universe { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Universe { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; let mut rows = bumpalo::collections::Vec::with_capacity_in(self.height as usize, cx.bump); diff --git a/examples/hello-world/src/lib.rs b/examples/hello-world/src/lib.rs index f14c1f4..55f50da 100644 --- a/examples/hello-world/src/lib.rs +++ b/examples/hello-world/src/lib.rs @@ -10,8 +10,8 @@ struct Hello { // The `Render` implementation describes how to render a `Hello` component into // HTML. -impl Render for Hello { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Hello { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { let msg = bumpalo::format!(in cx.bump, "Hello, {}!", self.who); let msg = msg.into_bump_str(); p(&cx).children([text(msg)]).finish() diff --git a/examples/input-form/src/lib.rs b/examples/input-form/src/lib.rs index e7e5500..08073ac 100644 --- a/examples/input-form/src/lib.rs +++ b/examples/input-form/src/lib.rs @@ -23,8 +23,8 @@ impl SayHelloTo { // The `Render` implementation has a text `` and a `

` that shows a // greeting to the ``'s value. -impl Render for SayHelloTo { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for SayHelloTo { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; div(&cx) diff --git a/examples/js-component/src/lib.rs b/examples/js-component/src/lib.rs index 0629a71..d9c94f2 100644 --- a/examples/js-component/src/lib.rs +++ b/examples/js-component/src/lib.rs @@ -31,8 +31,8 @@ impl GreetingViaJs { /// Here's the `Render` implementation! This adds a `

` element and some text /// around whatever the inner JS `Greeting` component renders. -impl Render for GreetingViaJs { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for GreetingViaJs { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; p(&cx) .children([text("JavaScript says: "), self.js.render(cx)]) diff --git a/examples/moire/src/lib.rs b/examples/moire/src/lib.rs index df16018..abca33b 100644 --- a/examples/moire/src/lib.rs +++ b/examples/moire/src/lib.rs @@ -104,8 +104,8 @@ impl Moire { } } -impl Render for Moire { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Moire { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; let elapsed = web_sys::window() diff --git a/examples/sierpinski-triangle/src/lib.rs b/examples/sierpinski-triangle/src/lib.rs index 9c4b1b3..0ac3f96 100644 --- a/examples/sierpinski-triangle/src/lib.rs +++ b/examples/sierpinski-triangle/src/lib.rs @@ -85,8 +85,8 @@ impl Container { } } -impl Render for Container { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Container { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::div; let elapsed = web_sys::window() diff --git a/examples/todomvc/src/todo.rs b/examples/todomvc/src/todo.rs index e64e9a9..2ec4f9e 100644 --- a/examples/todomvc/src/todo.rs +++ b/examples/todomvc/src/todo.rs @@ -116,8 +116,8 @@ impl Todo { } } -impl Render for Todo { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a, C: TodoActions> Render<'a> for Todo { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::{ builder::*, bumpalo::{self, collections::String}, diff --git a/examples/todomvc/src/todos.rs b/examples/todomvc/src/todos.rs index 447e6b6..e9e709b 100755 --- a/examples/todomvc/src/todos.rs +++ b/examples/todomvc/src/todos.rs @@ -289,8 +289,8 @@ impl Todos { } } -impl Render for Todos { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a, C: TodosActions> Render<'a> for Todos { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; div(&cx) diff --git a/src/cached.rs b/src/cached.rs index 86d96dc..48d1137 100644 --- a/src/cached.rs +++ b/src/cached.rs @@ -45,8 +45,8 @@ where /// count: u32, /// } /// - /// impl Render for Counter { - /// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + /// impl<'a> Render<'a> for Counter { + /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { /// // ... /// # unimplemented!() /// } @@ -85,8 +85,8 @@ where /// who: String /// } /// - /// impl Render for Hello { - /// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + /// impl<'a> Render<'a> for Hello { + /// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { /// use dodrio::builder::*; /// let greeting = bumpalo::format!(in cx.bump, "Hello, {}!", self.who); /// p(&cx) @@ -143,11 +143,11 @@ where } } -impl Render for Cached +impl<'a, R> Render<'a> for Cached where - R: 'static + Default + Render, + R: 'static + Default + for<'b> Render<'b>, { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { let template = cx.template::(); let cached = match self.cached.get() { // This does-the-cache-contain-this-id check is necessary because diff --git a/src/change_list/mod.rs b/src/change_list/mod.rs index 41342b7..e3af69d 100644 --- a/src/change_list/mod.rs +++ b/src/change_list/mod.rs @@ -233,9 +233,9 @@ impl ChangeListBuilder<'_> { self.state.emitter.replace_with(); } - pub fn set_attribute(&mut self, name: &str, value: &str) { + pub fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) { debug_assert!(self.traversal_is_committed()); - if name == "class" { + if name == "class" && !is_namespaced { let class_id = self.ensure_string(value); debug!("emit: set_class({:?})", value); self.state.emitter.set_class(class_id.into()); diff --git a/src/diff.rs b/src/diff.rs index 15f199a..139dda4 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -78,7 +78,7 @@ pub(crate) fn diff( return; } diff_listeners(change_list, registry, old_listeners, new_listeners); - diff_attributes(change_list, old_attributes, new_attributes); + diff_attributes(change_list, old_attributes, new_attributes, new_namespace.is_some()); diff_children( cached_set, change_list, @@ -204,26 +204,26 @@ fn diff_listeners( // [... node] // // The change list stack is left unchanged. -fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute]) { +fn diff_attributes(change_list: &mut ChangeListBuilder, old: &[Attribute], new: &[Attribute], is_namespaced: bool) { // Do O(n^2) passes to add/update and remove attributes, since // there are almost always very few attributes. 'outer: for new_attr in new { if new_attr.is_volatile() { change_list.commit_traversal(); - change_list.set_attribute(new_attr.name, new_attr.value); + change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced); } else { for old_attr in old { if old_attr.name == new_attr.name { if old_attr.value != new_attr.value { change_list.commit_traversal(); - change_list.set_attribute(new_attr.name, new_attr.value); + change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced); } continue 'outer; } } change_list.commit_traversal(); - change_list.set_attribute(new_attr.name, new_attr.value); + change_list.set_attribute(new_attr.name, new_attr.value, is_namespaced); } } @@ -953,7 +953,7 @@ fn create( } for attr in attributes { - change_list.set_attribute(&attr.name, &attr.value); + change_list.set_attribute(&attr.name, &attr.value, namespace.is_some()); } // Fast path: if there is a single text child, it is faster to diff --git a/src/lib.rs b/src/lib.rs index 20aeb29..a6c7fd6 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,8 +18,8 @@ //! } //! } //! -//! impl<'who> Render for Hello<'who> { -//! fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +//! impl<'a, 'who> Render<'a> for Hello<'who> { +//! fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { //! use dodrio::builder::*; //! //! let id = bumpalo::format!(in cx.bump, "hello-{}", self.who); diff --git a/src/render.rs b/src/render.rs index 0e33beb..066758f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -21,8 +21,8 @@ use wasm_bindgen::UnwrapThrowExt; /// /// pub struct MyComponent; /// -/// impl Render for MyComponent { -/// fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +/// impl<'a> Render<'a> for MyComponent { +/// fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { /// use dodrio::builder::*; /// /// p(&cx) @@ -35,26 +35,26 @@ use wasm_bindgen::UnwrapThrowExt; /// } /// } /// ``` -pub trait Render { +pub trait Render<'a> { /// Render `self` as a virtual DOM. Use the given context's `Bump` for /// temporary allocations. - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a>; + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a>; } -impl<'r, R> Render for &'r R +impl<'a, 'r, R> Render<'a> for &'r R where - R: Render, + R: Render<'a>, { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { (**self).render(cx) } } -impl Render for Rc +impl<'a, R> Render<'a> for Rc where - R: Render, + R: Render<'a>, { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { (**self).render(cx) } } @@ -69,7 +69,7 @@ where /// You do not need to implement this trait by hand: there is a blanket /// implementation for all `Render` types that fulfill the `RootRender` /// requirements. -pub trait RootRender: Any + Render { +pub trait RootRender: Any + for<'a> Render<'a> { /// Get this `&RootRender` trait object as an `&Any` trait object reference. fn as_any(&self) -> &dyn Any; @@ -80,7 +80,7 @@ pub trait RootRender: Any + Render { impl RootRender for T where - T: Any + Render, + T: Any + for<'a> Render<'a>, { fn as_any(&self) -> &dyn Any { self @@ -138,4 +138,31 @@ mod tests { #[allow(dead_code)] fn takes_dyn_render(_: &dyn super::RootRender) {} } + + #[test] + fn render_bump_scoped_child() { + use crate::{builder::*, bumpalo::collections::String, Node, Render, RenderContext}; + + struct Child<'a> { + name: &'a str, + } + + impl<'a> Render<'a> for Child<'a> { + fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> { + text(self.name) + } + } + + struct Parent; + + impl<'a> Render<'a> for Parent { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + let child_name = String::from_str_in("child", cx.bump).into_bump_str(); + + div(&cx) + .children([Child { name: child_name }.render(cx)]) + .finish() + } + } + } } diff --git a/src/render_context.rs b/src/render_context.rs index 34abb63..674af4a 100644 --- a/src/render_context.rs +++ b/src/render_context.rs @@ -69,7 +69,7 @@ impl<'a> RenderContext<'a> { /// Get or create the cached template for `Cached`. pub(crate) fn template(&mut self) -> Option where - R: 'static + Default + Render, + R: 'static + Default + for<'b> Render<'b>, { let template_id = Cached::::template_id(); if let Some(cache_id) = self.templates.get(&template_id).cloned() { diff --git a/tests/web/cached.rs b/tests/web/cached.rs index 965f782..b2ebb8b 100644 --- a/tests/web/cached.rs +++ b/tests/web/cached.rs @@ -18,8 +18,8 @@ impl CountRenders { } } -impl Render for CountRenders { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for CountRenders { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { let count = self.render_count.get() + 1; self.render_count.set(count); @@ -153,8 +153,8 @@ impl Default for Id { } } -impl Render for Id { - fn render<'a>(&self, _cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Id { + fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> { text(self.0) } } diff --git a/tests/web/events.rs b/tests/web/events.rs index 000c669..a0edea4 100644 --- a/tests/web/events.rs +++ b/tests/web/events.rs @@ -24,8 +24,8 @@ impl EventContainer { } } -impl Render for EventContainer { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for EventContainer { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; div(&cx) .attr("id", "target") @@ -126,8 +126,8 @@ impl ListensOnlyOnFirstRender { } } -impl Render for ListensOnlyOnFirstRender { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for ListensOnlyOnFirstRender { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { use dodrio::builder::*; let count = self.count.get(); diff --git a/tests/web/js_api.rs b/tests/web/js_api.rs index 4e20f3f..cd43306 100644 --- a/tests/web/js_api.rs +++ b/tests/web/js_api.rs @@ -35,8 +35,8 @@ impl WrapJs { } } -impl Render for WrapJs { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for WrapJs { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { div(&cx) .attr("class", "wrap-js") .children([self.inner.render(cx)]) diff --git a/tests/web/keyed.rs b/tests/web/keyed.rs index ff2c536..73fd18f 100644 --- a/tests/web/keyed.rs +++ b/tests/web/keyed.rs @@ -7,8 +7,8 @@ use wasm_bindgen_test::*; struct Keyed(u16); -impl Render for Keyed { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { +impl<'a> Render<'a> for Keyed { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { let key = bumpalo::format!(in cx.bump, "{}", self.0).into_bump_str(); div(&cx) .attr("class", "keyed") @@ -33,8 +33,8 @@ where async fn assert_keyed(before: Before, after: After) -> Result<(), JsValue> where - Before: 'static + Render, - After: 'static + Render, + Before: 'static + for<'a> Render<'a>, + After: 'static + for<'a> Render<'a>, { #[wasm_bindgen(module = "/tests/web/keyed.js")] extern "C" { diff --git a/tests/web/main.rs b/tests/web/main.rs index 6cb1ead..9ac8872 100644 --- a/tests/web/main.rs +++ b/tests/web/main.rs @@ -50,7 +50,7 @@ pub fn init_logging() { /// Assert that the `container` contains the physical DOM tree that matches /// `r`'s rendered virtual DOM. -pub fn assert_rendered(container: &web_sys::Element, r: &R) { +pub fn assert_rendered Render<'a>>(container: &web_sys::Element, r: &R) { init_logging(); let cached_set = &RefCell::new(CachedSet::default()); @@ -155,11 +155,11 @@ pub struct RenderFn(F) where F: for<'a> Fn(&mut RenderContext<'a>) -> Node<'a>; -impl Render for RenderFn +impl<'a, F> Render<'a> for RenderFn where - F: for<'a> Fn(&mut RenderContext<'a>) -> Node<'a>, + F: for<'b> Fn(&mut RenderContext<'b>) -> Node<'b>, { - fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { (self.0)(cx) } } @@ -169,8 +169,8 @@ where /// the physical DOM tree correctly matches `after`. pub async fn assert_before_after(before: R, after: S) -> Result<(), JsValue> where - R: 'static + Render, - S: 'static + Render, + R: 'static + for<'a> Render<'a>, + S: 'static + for<'a> Render<'a>, { let container = create_element("div"); diff --git a/tests/web/render.rs b/tests/web/render.rs index 5ff4e12..13c3f03 100644 --- a/tests/web/render.rs +++ b/tests/web/render.rs @@ -1,6 +1,7 @@ use super::{assert_rendered, before_after, create_element, RenderFn}; -use dodrio::{builder::*, Vdom}; +use dodrio::{builder::*, bumpalo::collections::String, Node, Render, RenderContext, Vdom}; use std::rc::Rc; +use wasm_bindgen::{ JsCast}; use wasm_bindgen_test::*; #[wasm_bindgen_test] @@ -34,6 +35,93 @@ fn container_is_emptied_upon_drop() { assert!(container.first_child().is_none()); } +/// Renders a child with a lifetime scoped to the RenderContext bump arena. +#[wasm_bindgen_test] +fn render_bump_scoped_node() { + struct Child<'a> { + name: &'a str, + } + + impl<'a> Render<'a> for Child<'a> { + fn render(&self, _cx: &mut RenderContext<'a>) -> Node<'a> { + text(self.name) + } + } + + struct Parent; + + impl<'a> Render<'a> for Parent { + fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> { + let child_name = String::from_str_in("child", cx.bump).into_bump_str(); + + div(&cx) + .children([Child { name: child_name }.render(cx)]) + .finish() + } + } + + let parent = Rc::new(RenderFn(|cx| { + Parent.render(cx) + })); + + let container = create_element("div"); + let _vdom = Vdom::new(&container, parent.clone()); + + assert_rendered(&container, &parent); +} + +/// Originally, dodrio would use the className property for SVGs. +/// +/// This is problematic because when SVG elements are created, the className is flagged as a read +/// only property, so setting it causes an exception to be thrown. Here's an example of how this +/// happens: +/// +/// let elem = web_sys::window() +/// .unwrap() +/// .document() +/// .unwrap() +/// .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg") +/// .unwrap(); +/// +/// elem.set_class_name("does-not-work"); +/// +/// ----------------------------------------------------------------------------------------------- +/// +/// wasm-bindgen: imported JS function that was not marked as `catch` threw an error: +/// setting getter-only property "className" +/// +/// ----------------------------------------------------------------------------------------------- +/// +/// Now, dodrio passes the 'class' attribute of all namespaced elements into set_attribute. This +/// satisfies the restrictions on SVG and keeps the optimized path for non-namespaced elements +#[wasm_bindgen_test(async)] +async fn test_svg_set_class() { + let container = create_element("div"); + + + let valid_svg = Rc::new(RenderFn(|cx| { + ElementBuilder::new(cx.bump, "svg") + .namespace(Some("http://www.w3.org/2000/svg")) + .attr("class", "works") + .finish() + })); + + let vdom = Vdom::new(&container, valid_svg.clone()); + let weak = vdom.weak(); + + weak.render().await.unwrap(); + + assert_eq!( + "works", + container.first_child() + .expect("unable to get svg") + .dyn_ref::() + .expect("svg should be an element") + .get_attribute("class") + .expect("unable to get 'class' of svg") + ); +} + before_after! { same_text { before(_cx) {