Skip to content
This repository was archived by the owner on Mar 2, 2021. It is now read-only.

Bump-Scoped rendering and SVG class attribute fix #136

Merged
merged 10 commits into from
Feb 24, 2020
8 changes: 4 additions & 4 deletions benches/benches.rs
Original file line number Diff line number Diff line change
@@ -17,16 +17,16 @@ use std::convert::TryInto;

/// The simplest thing we can render: `<div/>`.
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)
8 changes: 4 additions & 4 deletions crates/js-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -99,8 +99,8 @@ impl GreetingViaJs {
/// And finally the `Render` implementation! This adds a `<p>` 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())
}
}
4 changes: 2 additions & 2 deletions examples/counter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions examples/game-of-life/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 2 additions & 2 deletions examples/hello-world/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 2 additions & 2 deletions examples/input-form/src/lib.rs
Original file line number Diff line number Diff line change
@@ -23,8 +23,8 @@ impl SayHelloTo {

// The `Render` implementation has a text `<input>` and a `<div>` that shows a
// greeting to the `<input>`'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)
4 changes: 2 additions & 2 deletions examples/js-component/src/lib.rs
Original file line number Diff line number Diff line change
@@ -31,8 +31,8 @@ impl GreetingViaJs {

/// Here's the `Render` implementation! This adds a `<p>` 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)])
4 changes: 2 additions & 2 deletions examples/moire/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 2 additions & 2 deletions examples/sierpinski-triangle/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 2 additions & 2 deletions examples/todomvc/src/todo.rs
Original file line number Diff line number Diff line change
@@ -116,8 +116,8 @@ impl<C> Todo<C> {
}
}

impl<C: TodoActions> Render for Todo<C> {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a, C: TodoActions> Render<'a> for Todo<C> {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::{
builder::*,
bumpalo::{self, collections::String},
4 changes: 2 additions & 2 deletions examples/todomvc/src/todos.rs
Original file line number Diff line number Diff line change
@@ -289,8 +289,8 @@ impl<C: TodosActions> Todos<C> {
}
}

impl<C: TodosActions> Render for Todos<C> {
fn render<'a>(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
impl<'a, C: TodosActions> Render<'a> for Todos<C> {
fn render(&self, cx: &mut RenderContext<'a>) -> Node<'a> {
use dodrio::builder::*;

div(&cx)
14 changes: 7 additions & 7 deletions src/cached.rs
Original file line number Diff line number Diff line change
@@ -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<R> Render for Cached<R>
impl<'a, R> Render<'a> for Cached<R>
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::<R>();
let cached = match self.cached.get() {
// This does-the-cache-contain-this-id check is necessary because
4 changes: 2 additions & 2 deletions src/change_list/mod.rs
Original file line number Diff line number Diff line change
@@ -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());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where the SVG change begins and ends. I believe it's just this one if statement.

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());
12 changes: 6 additions & 6 deletions src/diff.rs
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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);
51 changes: 39 additions & 12 deletions src/render.rs
Original file line number Diff line number Diff line change
@@ -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<R> Render for Rc<R>
impl<'a, R> Render<'a> for Rc<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)
}
}
@@ -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> {
Copy link
Contributor Author

@awestlake87 awestlake87 Feb 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #46 @fitzgen mentioned this is the same as 'static, but I'm not sure it is in this case. It would be simpler without higher-order lifetimes, so I was trying to change it to Render<'static> or Render + 'static. The compiler didn't like either of those, which kinda makes sense to me.

The way I understand it, this trait bound says RootRender requires its types to also implement Any and Render for all lifetimes 'a. 'static is just the longest possible lifetime, so it wouldn't fulfill that requirement.

Am I missing something?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If something is valid for all lifetimes then it must be 'static which is the lifetime that is always valid. If it were not 'static then it would not be valid for all lifetimes, leading to a contradiction.

The only time that for<'a> is useful is when you are taking some generic function and calling it with parameters that contain "unknown" lifetimes or when you wish to call it multiple times with arguments of different lifetimes.

For this case, I think Render<'static> + 'static should do the trick here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think we both might be right.

Even though Render is a generic trait, it is object safe when used with higher-order lifetimes. That indicates to me that you are correct about for<'a> Render<'a> essentially being 'static from the perspective of the implementation of this trait. The compiler is effectively able to assume that the lifetime of the bump arena in RenderContext is static since it will always be inconsequential to the functionality inside of render.

That being said, the bound for<'a> Render<'a> does seem to be required, maybe not for the callee, but the caller. Here's an example that I've boiled down:

use dodrio::{bumpalo::{self, Bump}};

struct Context<'a> {
    bump: &'a Bump
}

#[derive(Debug, PartialEq, Eq)]
struct Node<'a> {
    name: &'a str
}

trait Render<'a> {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a>;
}

struct Test;

impl<'a> Render<'a> for Test {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a> {
        Node { name: bumpalo::format!(in cx.bump, "Test{}", 1).into_bump_str() }
    }
}

#[cfg(test)]
mod tests {
    use super::*;


    #[test]
    fn it_works() {
        let bump = Bump::default();
        let mut cx = Context { bump: &bump  };
        let comp: Box<dyn for<'a> Render<'a>> = Box::new(Test);

        assert_eq!(Node { name: "Test1" }, comp.render(&mut cx));
    }
}

This test compiles and runs as expected. It's emulating the situation I currently have in my fork of dodrio.

Now, when I switch the type of comp to Box<dyn Render<'static> + 'static>, the compiler doesn't like this example anymore.

use dodrio::{bumpalo::{self, Bump}};

struct Context<'a> {
    bump: &'a Bump
}

#[derive(Debug, PartialEq, Eq)]
struct Node<'a> {
    name: &'a str
}

trait Render<'a> {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a>;
}

struct Test;

impl<'a> Render<'a> for Test {
    fn render(&self, cx: &mut Context<'a>) -> Node<'a> {
        Node { name: bumpalo::format!(in cx.bump, "Test{}", 1).into_bump_str() }
    }
}

#[cfg(test)]
mod tests {
    use super::*;


    #[test]
    fn it_works() {
        let bump = Bump::default();
        let mut cx = Context { bump: &bump  };
        let comp: Box<dyn Render<'static> + 'static> = Box::new(Test);

        assert_eq!(Node { name: "Test1" }, comp.render(&mut cx));
    }
}

Compiler output:

   Compiling dodrio-test v0.1.0 (dodrio-test)
error[E0597]: `bump` does not live long enough
  --> src/lib.rs:32:38
   |
32 |         let mut cx = Context { bump: &bump  };
   |                                      ^^^^^ borrowed value does not live long enough
33 |         let comp: Box<dyn Render<'static> + 'static> = Box::new(Test);
   |                   ---------------------------------- type annotation requires that `bump` is borrowed for `'static`
...
36 |     }
   |     - `bump` dropped here while still borrowed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.
error: could not compile `dodrio-test`.
warning: build failed, waiting for other jobs to finish...
error: build failed

This is because while the Test component may treat its Context as 'static, the it_works function knows that cx has a lifetime that is shorter than 'static. Therefore, the trait bound of Render<'static> cannot be satisfied.

If I'm missing something, could you show me what you mean with this example? Maybe we're talking about different things.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, this makes sense. Thanks for breaking it down!

/// 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<T> 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()
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should act as a smoke test for bump-scoped lifetimes in child components

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I should have been more clear. This does ensure that code with bump-scoped child components compiles, but I was hoping for a rendering test that ensures that we also get the expected physical dom. Should be fine to put it in the render.rs test file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. I'll get that taken care of sometime tonight.

}
2 changes: 1 addition & 1 deletion src/render_context.rs
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ impl<'a> RenderContext<'a> {
/// Get or create the cached template for `Cached<R>`.
pub(crate) fn template<R>(&mut self) -> Option<CacheId>
where
R: 'static + Default + Render,
R: 'static + Default + for<'b> Render<'b>,
{
let template_id = Cached::<R>::template_id();
if let Some(cache_id) = self.templates.get(&template_id).cloned() {
8 changes: 4 additions & 4 deletions tests/web/cached.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
8 changes: 4 additions & 4 deletions tests/web/events.rs
Original file line number Diff line number Diff line change
@@ -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();
4 changes: 2 additions & 2 deletions tests/web/js_api.rs
Original file line number Diff line number Diff line change
@@ -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)])
8 changes: 4 additions & 4 deletions tests/web/keyed.rs
Original file line number Diff line number Diff line change
@@ -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, After>(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" {
12 changes: 6 additions & 6 deletions tests/web/main.rs
Original file line number Diff line number Diff line change
@@ -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<R: Render>(container: &web_sys::Element, r: &R) {
pub fn assert_rendered<R: for<'a> 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>(F)
where
F: for<'a> Fn(&mut RenderContext<'a>) -> Node<'a>;

impl<F> Render for RenderFn<F>
impl<'a, F> Render<'a> for RenderFn<F>
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<R, S>(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");

53 changes: 53 additions & 0 deletions tests/web/render.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{assert_rendered, before_after, create_element, RenderFn};
use dodrio::{builder::*, Vdom};
use std::rc::Rc;
use wasm_bindgen::{ JsCast};
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
@@ -34,6 +35,58 @@ fn container_is_emptied_upon_drop() {
assert!(container.first_child().is_none());
}

/// Originally, dodrio would use go through 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::<web_sys::Element>()
.expect("svg should be an element")
.get_attribute("class")
.expect("unable to get 'class' of svg")
);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the regression test. I wanted to test the failure case too, but web_sys doesn't mark set_class_name as catch, so I wasn't able to figure out how to do it.

You can verify that this works by running the following:

$ wasm-pack test --firefox --headless -- --features xxx-unstable-internal-use-only test_svg_set_class

Then, verify that the test fails when the new change is disabled:

// src/change_list/mod.rs - comment out !is_namespaced
    pub fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) {
        debug_assert!(self.traversal_is_committed());
        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());
        } else {
            let name_id = self.ensure_string(name);
            let value_id = self.ensure_string(value);
            debug!("emit: set_attribute({:?}, {:?})", name, value);
            self.state
                .emitter
                .set_attribute(name_id.into(), value_id.into());
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I wasn't sure where to put the test, so let me know if you want to move it somewhere else.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a fine location -- thanks!

before_after! {
same_text {
before(_cx) {