-
-
Notifications
You must be signed in to change notification settings - Fork 684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal for new implementation of leptos_dom
#116
Comments
I think this issue should be renamed, I just don't know to what lol. I'll leave it up to you @gbj. Also, in my research I also had found a very simple and fast technique for hydration. |
I think the name is fine, and I think this is very exciting. Short summary of my thoughts: if you're willing, I'd love to see you implement it so we can benchmark it against the current renderer. Longer list of thoughts:
So yeah, basically I think this is an exciting idea and would be very happy to see it. I assume the best thing to do is for you to implement it we can try it out. If you only want to work on the rendering part and leave the macro stuff to me, that's fine... If you come up with a manual/verbose version, I'm sure I can use
Here's a hypothetical approach to use template nodes. Feel completely free to ignore it let (name, set_name) = create_signal(cx, "Bob".to_string());
let (age, set_age) = create_signal(cx, 42);
// simplified output of the view macro
let template_str = "<h1><!></><!></></h1>";
// this is actually stored in a thread-local variable
// so we only create the template node once, lazily, per application
// then clone it every time we need it
let template = document.create_element("template").unchecked_into::<HtmlTemplateElement>();
template.set_inner_html(template_str);
let root = template.content().clone_node_with_deep(true);
// get the next start and end comments, starting from
let (name_start, name_end) = next_dyn_child(template_str);
let (age_start, age_end) = next_dyn_child(name_start);
insert_dyn_child_between(name_start, name_end, move || name.get());
insert_dyn_child_between(age_start, age_end, move || age.get().to_string()); |
Yay! Thanks! I'll definitely start working on it, hopefully today. I do need help with one thing, though...as you know, in computer science, there are only two hard things; cache invalidation, and naming things...what should I name it in the meantime...?
Sure! There are a few design approaches that can be taken, but a basic proof of concept renderer can be made in less than 500 LoC (I've made way too many to know lol), so it shouldn't be too long before we can start evaluating this approach vs the current one, and thus, the issue shouldn't be stalled for too long
Yes! As a matter of fact, the approach for the
I'll actually start off with a builder API, as it's the easiest to adapt to a macro, as opposed to the other way around, imho.
I will not ignore it! I will, however, not try to implement this for the initial evaluation, simply because I know adding this after the fact will be trivial, but will allow me to deliver something sooner if I skip this complexity for now. The reason I think this is because, let's say I have an API that looks something like this: let btn: HtmlElement<Button> = button(cx).class("btn btn-primary"); can later be collected into a let btn = HtmlElement<Button> = HtmlElement::from_template::<Button>("<button class=r#"btn btn-primary""#); or something similar... P.S. |
Merged! |
As promised, here is a little writeup. This is just the theoretical principals. I can make an RFC detailing how it would all work together and create a crate on my github for an implementation reference if you would like to hear more.
Disclaimer:
The code presented here is mostly pseudo code, so don't expect
it to compile as-is.
I had done some research on efficient ways of working with the DOM for
the purpose of a reactive web framework. I will resume the technique,
algorithm, and reasoning below.
When working with the DOM, there is surprisingly little work that needs
to be done in order to create a flexible and fast model for representing
application state. This is because the DOM already does all of the work for us.
We just need to exploit that within our respective paradigm, FRP, in our case.
Within FRP, we have values that asynchronously
change over time. These are called
signals
. The goal is to leverage the DOM withoutduplicating it in a VDOM. We have two simple notions of an element,
static and dynamic. Static nodes are nodes which never change, their value
is either known at compile time, or can be evaluated at runtime, but once
rendered, it's content will never change. For example:
or
Dynamic nodes, however, do change during runtime. For example,
In this previous case, the text content of the
<h1>
tag will change whenthe value of
title
changes. This is known as reactivity. Reactivity canbe split into boundaries, where the thing that will be changing, will have
it's own reactive area for it to change into.
Most modern web frameworks have the notion of components, that is a collection
of HTML markup and logic that comprises a more abstract piece of UI or application
logic. For example:
Creating basic elements and rendering them to the DOM is trivial. Let's
say I want to represent the following markup in Rust:
This can be achieved with the following Rust code:
Rendering static nodes is trivial; simply create the nodes, populate their
content with text or other nodes, and move on to the next one. The tricky bit
is working with dynamic data.
I will define one simple core component which is needed to represent all other
reactive elements, I will call it
DynChild
.DynChild
can have children thatis intended to be atomically changed when a signal changes. Let's take
our previous dynamic example, it can be represented with
DynChild
as follows:So far, so good. Now, how can we create a reactive abstraction which
will let us update the contents of the
<h1>
tag with doing as littlework as possible? Try this:
If we fence the reactive child within "opening" and "closing" comment nodes,
we will be able to insert and remove nodes by using only at most two DOM operations;
append_child
andbefore_with_node_1
, and better yet, with a guaranteed insertiontime of O(1).
Let's get into a bit more detail. For this, we'll need to introduce a few
new types.
For simplicity, children are not mentioned for
Element
, only forComponent
,as this is the part which interests us in the context of reactivity.
To remove any node from the DOM, it's as simple as:
Here is where the optimization comes into play for inserting dynamic children;
remember how I mentioned that
DynChild
was the only component required toimplement all reactivity? When we want to insert new nodes to replace the
old ones, just do these two steps
closing.before_with_node_1(child_node)
.That is, insert the nodes relative to the component boundaries, and you have
your own little isolated reactive context for children! In this example,
technically you only need the
closing
comment node, but you actually needboth in order to best represent another component built on
DynChild
,Each
,which can be made also into a O(n) insertion for each element changed, where
n
is the number of changes in the list. This, however, is outside the scopeof this writeup.
The text was updated successfully, but these errors were encountered: