Giter VIP home page Giter VIP logo

dodrio's Introduction

Dodrio

A fast, bump-allocated virtual DOM library for Rust and WebAssembly. Note that Dodrio is still experimental.

Warning

I reiterate that Dodrio is in a very experimental state. It probably has bugs, and no one is using it in production.

Examples

Here is the classic "Hello, World!" example:

struct Hello {
    who: String,
}

impl Render for Hello {
    fn render<'a>(&self, cx: &mut RenderContext<a>) -> Node<'a> {
        let who = bumpalo::format!(in cx.bump, "Hello, {}!", self.who);
        div(cx)
            .children([text(who.into_bump_str())])
            .finish()
    }
}

More examples can be found in the examples directory, including:

  • counter: Incrementing and decrementing a counter.
  • input-form: Reading an <input> and displaying its contents.
  • todomvc: An implementation of the infamous TodoMVC application.
  • moire: The WebRender Moiré patterns demo.
  • game-of-life: The Rust and WebAssembly book's Game of Life tutorial rendered with Dodrio instead of to 2D canvas.
  • js-component: Defines a rendering component in JavaScript with the dodrio-js-api crate.

Cargo Features

  • log — enable debugging-oriented log messages with the log crate's facade. You still have to initialize a logger for the messages to go anywhere, such as console_log.

  • serde — enable serde::{Serialize, Deserialize} implementations for Cached<R> where R is serializable and deserializable.

Design

Bump Allocation

Bump allocation is essentially the fastest method of allocating objects. It has constraints, but works particularly well when allocation lifetimes match program phases. And virtual DOMs are very phase oriented.

Dodrio maintains three bump allocation arenas:

  1. The newest, most up-to-date virtual DOM. The virtual DOM nodes themselves and any temporary containers needed while creating them are allocated into this arena.
  2. The previous virtual DOM. This reflects the current state of the physical DOM.
  3. The difference between (1) and (2). This is a sequence of DOM mutation operations — colloquially known as a "change list" — which if applied to the physical DOM, will make the physical DOM match (1).

Rendering happens as follows:

  1. The application state is rendered into bump allocation arena (1).
  2. (1) is diffed with (2) and the changes are emitted into (3).
  3. JavaScript code applies the change list in (3) to the physical DOM.
  4. (1) and (2) are swapped, double-buffering style, and the new (1) has its bump allocation pointer reset, as does (3).
  5. Rinse and repeat.

Change List as Stack Machine

The change list that represents the difference between how the physical DOM currently looks, and our ideal virtual DOM state is encoded in a tiny stack machine language. A stack machine works particularly well for applying DOM diffs, a task that is essentially a tree traversal.

Library — Not Framework

Dodrio is just a library. (And did I mention it is experimental?!) It is not a full-fledged, complete, batteries-included solution for all frontend Web development. And it never intends to become that either.

dodrio's People

Contributors

awestlake87 avatar bodil avatar dependabot-preview[bot] avatar dependabot-support avatar dependabot[bot] avatar fitzgen avatar ldesgoui avatar michalobi avatar rylev avatar sseg avatar tschneidereit avatar waywardmonkeys avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dodrio's Issues

API for JS

Hi, I am the author of a JS library called AppRun, which renders the virtual DOM to an DOM element:

const element = document.getElementById('my-app')
app.render(element, { tag: 'div', props: { id: ''}, children: []}

I wonder if dodrio has a API that allows JS code to pass in the element and VDOM and renders the VDOM, so that I can replace the current JS implementation with wasm/dodrio?

svg elements class attribute cannot use set_class(), but must use set_attribute()

I have a node like this
<svg class="myclass">...</svg>
It returns an error, that the object className is read-only.
I found that for svg element and its subelements we cannot call javascript className.
We must call javascript setAttribute instead.
It looks that this way works fine in general also for all other html elements.
https://stackoverflow.com/questions/33451050/classname-vs-get-setattribute-method
I tried this change in change_list/mod.rs and it works for what I tested.
And it also simplifies the code.

    pub fn set_attribute(&mut self, name: &str, value: &str) {
        debug_assert!(self.traversal_is_committed());
        // Luciano: if it is a svg element than also class must use set_attributes
        /*
        if name == "class" {
            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());
                /*
        }
        */
    }

Self mutation for children Node triggered by event.

Hi! I'm new to dodrio and started to like it very much. I feel like learning dodrio will help me better understand lifetimes, wasm and rust in general.

I have some questions about the behavior of RootRender, and would like to learn more about the design of dodrio.

In the example below, I'm able to trigger mutation.

struct App {
    count: isize
}

// under impl Render for App
div(bump)
  .on("click", |root, vdom, event| {
    // this one works.
    let handle = root.unwrap_mut::<Self>();
    self.count +=1;
  }).finish()

However, I'm getting error when trying to trigger the mutation in a nested node inside App.

struct App {
   inner: Dummy
}

struct Dummy {}

// under impl Render for Dummy
div(bump)
  .on("click", |root, vdom, event| {
    // next line failed...
    let handle = root.unwrap_mut::<Self>();
  }).finish()

I got the error of

Error: bad `RootRender::unwrap_ref` call

For now, If I want to trigger mutation of inner data, It's doable from the parent node event handler.


Here are the questions that I need help getting around.

1. Access of RootRender

It seems that RootRender can only be accessed by the root struct. Is there anyway I can trigger an event from a nested struct? Is it necessary to create multiple vdom so they all work independently?

2. Async mutation with RootRender

If I wrap my nested data under Rc<RefCell>>, and move my RootRender alongside VdomWeak, I can achieve mutation from an async loop.

|root, vdom, event| {
let handle = root.unwrap_mut::<Self>();
let fut = async move {
  loop {
    fut_wrap_setTimeout(100).await.unwrap();
    handle.count +=1;
    vdom.schedule_render();
   }
}
spawn_local(fut);
}

Although the example above works, I'm curious about how will be better to manage sync/async operations.

Since spawn_local requires 'static lifetime, it's a bit annoying that I have to wrap every data I'm interested to mutate under Rc<RefCell>. What's the proper way to setup program that respect RootRender but able to mutate data underneath it more freely?

Another option I'm considering. Will it be better to go async all the way and create some kinds of async event bus, and handle the mutation and schedule_render over there? If I create an async loop that skips all the time(by always setTimeout for a minimal time), maybe I can do the scheduling there by retrieving events from an async channel that binds to the struct.

Any advice is appreciated. Thx.

Systematic expansion of SVG builders

Looks like the current list of builders is hand-curated. It's great that I can create a tag manually via ElementBuilder, but it's more verbose.

I'm imagine automatically generating the all of builders in the svg namespace by pulling from sort of metadata depot, similar to how web_sys uses WebIDL. Ideally, typed-html could use the same - I found their dodrio!() macro similarly limiting as it whitelists the elements.

Investigate adding a change list ops for common attributes and listeners

The "class" and "id" attributes are used super frequently. We could have a SetKnownAttribute op that has an immediate that is an enum value for various common attributes instead of the string for the attribute. This would save time decoding strings in the change list implementation, which is pretty hot.

We could do a similar thing for known events, like "click" etc.

The trade off is that the more "known" events and attributes we support, the more we have to check whether an attribute is known or not when diffing and emitting the change list. But some initial profiling shows that we only spend about 5% of time in diffing, and much than that in applying diffs. Even just string decoding within diff application we seem to spend about 10% of time in!

Support SVG

There's currently no support for svg and it's related elements.

This has been started at #41

Investigate smarter diffing algorithm

Some initial profiling shows that we spend about

  • 10% of time in rendering,
  • 5% of time diffing to generate a change list, and
  • 75% of time applying a change list diff

Our current diffing algorithm is a rather simple, greedy, single-pass algorithm. It seems we could probably spend a little bit more time in diffing if it generated a smaller change list and then we would likely see some perf wins.

Investigate ways to avoid re-diffing cached nodes

If we diffed nodes by reference, rather than by value, we could use pointer identity to avoid both re-rendering and re-diffing Cached<R> sub-trees instead of just avoiding only re-rendering but not re-diffing.

The alternative to avoid re-diffing would be to add a flag or id to Node, as described in this TODO comment:

dodrio/src/cached.rs

Lines 7 to 12 in c5e1dfd

// TODO: This implementation will only cache the rendering (generation of the
// virtual DOM) but not the diffing of the cached subtree. We could skip diffing
// for cached tree by adding `fn is_cached(&self) -> bool` to `Node` that we can
// check during diffing. This comes at the cost of bloating `Node`'s size (since
// we don't have any padding to sneak an extra `bool` field into). We should
// investigate whether it is worth adding this or not.

async alternative to with_components

I don't have enough experience, so I have to ask.
Async/await and callbacks are a hard nut to crack.
it is about vdom.with_component...(...) ...
It executes a function passed as parameter with a reference to this virtual DOM's root rendering component.
Is it possible to use async/await to avoid passing a function?
So the code will be written just like a "normal sync" code flow.
Is it possible something like this :

//here we have vdom and y
spawn_local(async move {
    let root = vdom.get_root().await;
    let rrc = root.unwrap_mut::<RootRenderingComponent>();
    rrc.x = y;
})

Thank you for your patience.

Tutorial for Rust Webassembly/Wasm Virtual Dom Dodrio for webpage, electron and phonegap/cordova

I built a super simple memory game for kids in Rust Wasm + Virtual Dom Dodrio.
The code is on github.
It can be used as a tutorial how to use the same Rust Wasm code on webpage, electron and PhoneGap.
I would like to here some comments.

https://github.com/LucianoBestia/mem1
https://github.com/LucianoBestia/mem1_website
https://github.com/LucianoBestia/mem1_electron
https://github.com/LucianoBestia/mem1_phonegap

[feature request] Phantom node (or fragment to be the same as react)

When I'm using this library, sometimes I have problems flattening all my data into a bumpalo::*::Vec to use as the children for a node. react solves this problem with fragments, a special node type that, rather than rendering an element in the dom, transparently inserts its children into the parent element at the right point.

It might make the library faster as well, since there will be less accidental re-allocations from use of bumpalo::*::Vec::push.

Roadmap to being not experimental

Hey, this looks like a really cool project. Are there plans for moving it out of the experimental space? I'd love to use this in the future or at least something like iced-web. I've got spare time and could help with work but I don't see a roadmap anywhere in the repo. Thanks!

Restrictive lifetimes for attr function

The attr function currently has the following signature.

fn attr<'a>(name: &'a str, value: &'a str) -> Attribute<'a> 

Inside a render function, I'd like to create a String for the value of the attribute (using format!). It would be nice if I had the choice to pass ownership of this string to the Attribute.

Ownership of `RootRender`

When you want to respond to a user event, you use the following builder method

pub fn on<F>(self, event: &'a str, callback: F) -> Self where
    F: 'static + Fn(&mut dyn RootRender, VdomWeak, Event), 

The problem comes if you want to then pass the &mut dyn RootRender to a future, for example if you want to update state based on some asynchronous event like an api call. Because futures must be 'static, there's no way to do it.

A potential solution would be for that field to give you something like Rc<Box<dyn RootRender>>, thereby giving you ownership, so you can use it at any point in the future.

What do you think about this?

EDIT I guess it would need to be Rc<RefCell<dyn RootRender>> so you could mutate the state.

[feature request] Node::Null

Sometimes there is a code path where you don't want to render anything. An example:

fn render_error(error_msg: Option<&'static str>, bump: &'bump Bump) -> Node<'bump> {
    div(bump)
        .children([
            p(bump).children([text("Some text that is always there")]).finish(),
            match some_state {
                Some(msg) => p(bump).children([text(msg)]).finish(),
                None => Node::Null
            }
        ])
        .finish()
}

In this case the vdom sees Node::Null and doesn't render anything to the dom. react supports returning null in javascript for the same effect.
Currently you have to build the children as a Vec, which involves mutable state, and is less functionaly :).

Can you recommend a Simple http Server written in Rust?

In the examples for Dodrio it is suggested to use:
python -m SimpleHTTPServer

I would like to use a Simple http Server written in Rust.
I found many projects on Github, but it is very confusing.
Some are too complex for a simple development Server. Others are incomplete.
I would appreciate if somebody recommends a suitable Simple Server for this use-case.

new version publish to crates.io

When is in plan to publish the newer version of dodrio to crates.io?
Now the version is 0.1.0 and it doesn't understand async/await.
I would like to publish my crate, but it has a dependency to the newer dodrio.
In development I use the Github/master dependency or path dependency.
But for publishing a crate, all the dependencies (versions) must be in crates.io.

DOM Refs

I really like Dodrios design but one thing is holding me back a bit.
There should be a way to get DOM refs.
In some situations you NEED to have the actual DOM nodes that have been derived from the virtual one.
One quite easy example is the HTMLCanvasElement.

From what I've seen this could probably be done quite elegantly within the change-list interpreter.
One could just tag specific vdom nodes and let the interpreter maintain the dom refs globally.
That would result in something similar to the ref of React.

I'm just leaving this here now to start the discussion.
Right now, I'm compensating the lack of DOM refs with a hacky pseudo CSS animation.
I could add that if my pet project stays alive and if this is in your interest.

Sierpinski triangle example

It's a common example ever since it was used as a demo for react fiber. We should have an implementation.

can't find crate for `core`

Hello, I try to build the project, but got this error:

error[E0463]: can't find crate forcore| = note: thewasm32-unknown-unknown` target may not be installed

error: aborting due to previous error

For more information about this error, try rustc --explain E0463.
error: could not compile cfg-if.

To learn more, run the command again with --verbose.
warning: build failed, waiting for other jobs to finish...
error: build failed
`

Realworld

I really want to use this for my webdev in future. I want to define my routes as enums!! To that end, I've started implementing realworld for dodrio

Not much there yet - the routing framework (mostly stolen from the TodoMVC example) and copies of some of the templates. I intend to keep working on it and make a note of my experience in THOUGHTS.md. Just giving you a heads up.

I need sleep now.

To run

> wasm-pack build --target no-modules
> python -m http.server # or the python2 version that I can't remember

Dodrio in web worker

Would it be feasible to make Dodrio run in a web worker by doing the diffing and everything else in the worker and only applying the change list in the main thread? perhaps the bridging can use a SharedArrayBuffer if available or some efficient serialization mechanism also used to bridge user evens 🤔

Builder methods child and attr move self

Is it possible to change this methods to borrow and return a mut reference to self. To borrow self instead of moving it? Would this change things a lot?

pub fn child(mut self, child: Node<'a>) -> Self {
        self.children.push(child);
        self
    }

TodoMVC animation error

If you run the todomvc example, when you change the filter from say "all" to "complete", the "complete" animation will run based on position. It should only run when an action is marked complete.

dispatch_event failed

Greetings. I was trying to use web_sys::EventTarget to send custom event. And it failed with the following code.

.on("click", move |_, _, e| {

    let target: web_sys::EventTarget = e.current_target().unwrap();
    let event = web_sys::Event::new("hey").unwrap();
    target.dispatch_event(&event).unwrap();
})
.on("hey", |_, _, _| {
    log::info!("i got heyed!!!");
})

The error trace:

panicked at 'already borrowed: BorrowMutError', src/libcore/result.rs:1188:5

Stack:

init/imports.wbg.__wbg_new_59cb74e423758ede@http://127.0.0.1:8000/wasm-bindgen-test:559:23
console_error_panic_hook::Error::new::hf554e6f5eb6fb1b1@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[5206]:0x15819c
console_error_panic_hook::hook_impl::hee1fff08d7db4329@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[819]:0xb75ee
console_error_panic_hook::hook::h5c34a5c9072e6707@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[5659]:0x15f04f
core::ops::function::Fn::call::hda110851ec8d5d65@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[5243]:0x158bb8
std::panicking::rust_panic_with_hook::h4e529e530989255b@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[1336]:0xe06c1
rust_begin_unwind@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[3258]:0x12ea29
core::panicking::panic_fmt::h83dd057ea462878e@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[6341]:0x168403
core::result::unwrap_failed::h6d70f69438bf9954@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[2821]:0x122228
core::result::Result<T,E>::expect::h06c7e073675d0e5c@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[1349]:0xe1441
core::cell::RefCell<T>::borrow_mut::h78421ba0e9e39648@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[2400]:0x11404b
dodrio::events::EventsRegistry::new::{{closure}}::hd911a19e4fad900a@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[267]:0x6ae84
<dyn core::ops::function::Fn<(A, B, C)>+Output = R as wasm_bindgen::closure::WasmClosure>::describe::invoke::h7a13cc7fc1060c19@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[980]:0xc5e1a
__wbg_adapter_26@http://127.0.0.1:8000/wasm-bindgen-test:214:10
real@http://127.0.0.1:8000/wasm-bindgen-test:1016:28
initEventsTrampoline/this.eventHandler@http://127.0.0.1:8000/snippets/dodrio-fb10e775fcadd85a/js/change-list-interpreter.js:374:17
init/imports.wbg.__widl_f_dispatch_event_EventTarget@http://127.0.0.1:8000/wasm-bindgen-test:735:43
web_sys::EventTarget::dispatch_event::h5c69e1a8d00a5014@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[873]:0xbc80b
<dodrio_ext::tests::test_original::Hello as dodrio::render::Render>::render::{{closure}}::h68f0951748cab627@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[900]:0xbef25
core::ops::function::impls::<impl core::ops::function::Fn<A> for &F>::call::h94f90d2bf7912152@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[1592]:0xeff59
dodrio::events::EventsRegistry::new::{{closure}}::hd911a19e4fad900a@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[267]:0x6af4d
<dyn core::ops::function::Fn<(A, B, C)>+Output = R as wasm_bindgen::closure::WasmClosure>::describe::invoke::h7a13cc7fc1060c19@http://127.0.0.1:8000/wasm-bindgen-test_bg.wasm:wasm-function[980]:0xc5e1a
__wbg_adapter_26@http://127.0.0.1:8000/wasm-bindgen-test:214:10
real@http://127.0.0.1:8000/wasm-bindgen-test:1016:28
initEventsTrampoline/this.eventHandler@http://127.0.0.1:8000/snippets/dodrio-fb10e775fcadd85a/js/change-list-interpreter.js:374:17

Is this because every time we send mutation into the bump arena, which controls and mutate an actual HtmlElement, it either get rebuild or patched so that the event target is no longer the same? Therefore trying to obtain the event_target from the web_sys::Event won't get us the correct address. And the listener on the dodrio::div can't receive calls too because the event_listener is actually registered in the element inside the bump arena?

I tried to call obtain the element by element_select_by_id and it emits the same error as above.
Would like to know if there's a proper way to setup custom event listener(other than standard dom events).

Optimize `removeSelfAndNextSiblings` op

Probably slightly faster to use lastChild something like this:

    const node = changeList.stack.pop();
    const parent = node.parentNode;
    let last = parent.lastChild;
    while (last !== node) {
      last.remove();
      last = parent.lastChild;
    }
    // last === node
    node.remove();

than how we are currently using nextSibling.

Game Loop pattern

I can't tell if there's a way to create a loop which, for each animation frame, calculates the next state from the current time (provided via the animation frame callback) and immediately runs the diff/patch process inside that same animation task.

My use case is a little physics simulator, written in Rust, which would render an SVG document each tick. My initial version in React looks like the below; I've translated it into a psuedo-api that I imagine may be close to dodrio;

import { State, VDom } from wasm;

var state = State.initial();
var root = document.getElementById("root");
var vdom = VDom.new(state, root);

function draw(time) { // this is high accuracy provided by requestAnimationFrame
    // Do this inside the animation frame, because we have access to the timestamp
    // This mutates the state value
    state.tick(time);
    // Also do this inside the animation frame (doesn't make sense to wait for the next one)
    // State would implement something like the Render trait
    vdom.renderNow(state);

    requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

Is something like this currently possible, or would something need to be exposed? Perhaps said differently, what are your thoughts on exposing a synchronous render function that I can invoke while managing the loop myself from JS? It looks like a lot of this functionality exists (VDomInnerExclusive) but is private.

Serialize to HTML

Would it be possible to serialize a tree as an html string or some mechanism to create a dodrio dom in server side and serve it via http?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.