mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Feat: include the helper
This commit is contained in:
parent
f2eec79fd4
commit
07341d2c65
22 changed files with 2990 additions and 1311 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
}
|
5
.vscode/spellright.dict
vendored
5
.vscode/spellright.dict
vendored
|
@ -11,3 +11,8 @@ transpiling
|
|||
fullstack
|
||||
Serverless
|
||||
vnode
|
||||
fn
|
||||
Dodrio
|
||||
fc
|
||||
Jemalloc
|
||||
Cloudfare
|
||||
|
|
31
README.md
31
README.md
|
@ -54,30 +54,34 @@ Here, the `Context` object is used to access hook state, create subscriptions, a
|
|||
```rust
|
||||
// A very terse component!
|
||||
#[fc]
|
||||
async fn Example(ctx: &Context, name: String) -> VNode {
|
||||
fn Example(ctx: &Context, name: String) -> VNode {
|
||||
html! { <div> "Hello {name}!" </div> }
|
||||
}
|
||||
|
||||
// or
|
||||
|
||||
#[functional_component]
|
||||
static Example: FC = |ctx, name: String| async html! { <div> "Hello {name}!" </div> };
|
||||
static Example: FC = |ctx, name: String| html! { <div> "Hello {name}!" </div> };
|
||||
```
|
||||
|
||||
The final output of components must be a tree of VNodes. We provide an html macro for using JSX-style syntax to write these, though, you could use any macro, DSL, templating engine, or the constructors directly.
|
||||
|
||||
## Concurrency
|
||||
In Dioxus, components are asynchronous and can their rendering can be paused at any time by awaiting a future. Hooks can combine this functionality with the Context and Subscription APIs to craft dynamic and efficient user experiences.
|
||||
In Dioxus, VNodes are asynchronous and can their rendering can be paused at any time by awaiting a future. Hooks can combine this functionality with the Context and Subscription APIs to craft dynamic and efficient user experiences.
|
||||
|
||||
```rust
|
||||
async fn user_data(ctx: &Context<()>) -> VNode {
|
||||
let Profile { name, birthday, .. } = fetch_data().await;
|
||||
html! {
|
||||
<div>
|
||||
{"Hello, {name}!"}
|
||||
{if birthday === std::Instant::now() {html! {"Happy birthday!"}}}
|
||||
</div>
|
||||
}
|
||||
fn user_data(ctx: &Context<()>) -> VNode {
|
||||
// Register this future as a task
|
||||
use_suspense(ctx, async {
|
||||
// Continue on with the component as usual, waiting for data to arrive
|
||||
let Profile { name, birthday, .. } = fetch_data().await;
|
||||
html! {
|
||||
<div>
|
||||
{"Hello, {name}!"}
|
||||
{if birthday === std::Instant::now() {html! {"Happy birthday!"}}}
|
||||
</div>
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
Asynchronous components are powerful but can also be easy to misuse as they pause rendering for the component and its children. Refer to the concurrent guide for information on how to best use async components.
|
||||
|
@ -112,6 +116,7 @@ Dioxus LiveHost is a paid service dedicated to hosting your Dioxus Apps - whethe
|
|||
- Analytics
|
||||
- Lighthouse optimization
|
||||
- On-premise support (see license terms)
|
||||
- Cloudfare/DDoS protection integrations
|
||||
|
||||
For small teams, LiveHost is free. Check out the pricing page to see if Dioxus LiveHost is good your team.
|
||||
|
||||
|
@ -121,9 +126,9 @@ We use the dedicated `dioxus-cli` to build and test dioxus web-apps. This can ru
|
|||
Alternatively, `trunk` works but can't run examples.
|
||||
|
||||
- tide_ssr: Handle an HTTP request and return an html body using the html! macro. `cargo run --example tide_ssr`
|
||||
- doc_generator: Use dioxus ssr to generate the website and docs. `cargo run --example doc_generator`
|
||||
- doc_generator: Use dioxus SSR to generate the website and docs. `cargo run --example doc_generator`
|
||||
- fc_macro: Use the functional component macro to build terse components. `cargo run --example fc_macro`
|
||||
- hello_web: Start a simple wasm app. Requires a webpacker like dioxus-cli or trunk `cargo run --example hello`
|
||||
- hello_web: Start a simple wasm app. Requires a web packer like dioxus-cli or trunk `cargo run --example hello`
|
||||
- router: `cargo run --example router`
|
||||
- tide_ssr: `cargo run --example tide_ssr`
|
||||
- webview: Use liveview to bridge into a webview context for a simple desktop application. `cargo run --example webview`
|
||||
|
|
159
SOLVEDPROBLEMS.md
Normal file
159
SOLVEDPROBLEMS.md
Normal file
|
@ -0,0 +1,159 @@
|
|||
# Solved problems while building Dioxus
|
||||
|
||||
## FC Macro for more elegant components
|
||||
Originally the syntax of the FC macro was meant to look like:
|
||||
|
||||
```rust
|
||||
#[fc]
|
||||
fn example(ctx: &Context<{ name: String }>) -> VNode {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
`Context` was originally meant to be more obviously parameterized around a struct definition. However, while this works with rustc, this does not work well with Rust Analyzer. Instead, the new form was chosen which works with Rust Analyzer and happens to be more ergonomic.
|
||||
|
||||
```rust
|
||||
#[fc]
|
||||
fn example(ctx: &Context, name: String) -> VNode {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
## Anonymous Components
|
||||
|
||||
In Yew, the function_component macro turns a struct into a Trait `impl` with associated type `props`. Like so:
|
||||
|
||||
```rust
|
||||
#[derive(Properties)]
|
||||
struct Props {
|
||||
// some props
|
||||
}
|
||||
|
||||
struct SomeComponent;
|
||||
impl FunctionProvider for SomeComponent {
|
||||
type TProps = Props;
|
||||
|
||||
fn run(&mut self, props: &Props) -> Html {
|
||||
// user's functional component goes here
|
||||
}
|
||||
}
|
||||
|
||||
pub type SomeComponent = FunctionComponent<function_name>;
|
||||
```
|
||||
By default, the underlying component is defined as a "functional" implementation of the `Component` trait with all the lifecycle methods. In Dioxus, we don't allow components as structs, and instead take a "hooks-only" approach. However, we still need props. To get these without dealing with traits, we just assume functional components are modules. This lets the macros assume an FC is a module, and `FC::Props` is its props and `FC::component` is the component. Yew's method does a similar thing, but with associated types on traits.
|
||||
|
||||
Perhaps one day we might use traits instead.
|
||||
|
||||
The FC macro needs to work like this to generate a final module signature:
|
||||
|
||||
```rust
|
||||
// "Example" can be used directly
|
||||
// The "associated types" are just children of the module
|
||||
// That way, files can just be components (yay, no naming craziness)
|
||||
mod Example {
|
||||
// Associated metadata important for liveview
|
||||
static NAME: &'static str = "Example";
|
||||
|
||||
struct Props {
|
||||
name: String
|
||||
}
|
||||
|
||||
fn component(ctx: &Context<Props>) -> VNode {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
}
|
||||
}
|
||||
|
||||
// or, Example.rs
|
||||
|
||||
static NAME: &'static str = "Example";
|
||||
|
||||
struct Props {
|
||||
name: String
|
||||
}
|
||||
|
||||
fn component(ctx: &Context<Props>) -> VNode {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
These definitions might be ugly, but the fc macro cleans it all up. The fc macro also allows some configuration
|
||||
|
||||
```rust
|
||||
#[fc]
|
||||
fn example(ctx: &Context, name: String) -> VNode {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
}
|
||||
|
||||
// .. expands to
|
||||
|
||||
mod Example {
|
||||
use super::*;
|
||||
static NAME: &'static str = "Example";
|
||||
struct Props {
|
||||
name: String
|
||||
}
|
||||
fn component(ctx: &Context<Props>) -> VNode {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Live Components
|
||||
Live components are a very important part of the Dioxus ecosystem. However, the goal with live components was to constrain their implementation purely to APIs available through Context (concurrency, context, subscription).
|
||||
|
||||
From a certain perspective, live components are simply server-side-rendered components that update when their props change. Here's more-or-less how live components work:
|
||||
|
||||
```rust
|
||||
#[fc]
|
||||
static LiveFc: FC = |ctx, refresh_handler: impl FnOnce| {
|
||||
// Grab the "live context"
|
||||
let live_context = ctx.use_context::<LiveContext>();
|
||||
|
||||
// Ensure this component is registered as "live"
|
||||
live_context.register_scope();
|
||||
|
||||
// send our props to the live context and get back a future
|
||||
let vnodes = live_context.request_update(ctx);
|
||||
|
||||
// Suspend the rendering of this component until the vnodes are finished arriving
|
||||
// Render them once available
|
||||
ctx.suspend(async move {
|
||||
let output = vnodes.await;
|
||||
|
||||
// inject any listener handles (ie button clicks, views, etc) to the parsed nodes
|
||||
output[1].add_listener("onclick", refresh_handler);
|
||||
|
||||
// Return these nodes
|
||||
// Nodes skip diffing and go straight to rendering
|
||||
output
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Notice that LiveComponent receivers (the client-side interpretation of a LiveComponent) are simply suspended components waiting for updates from the LiveContext (the context that wraps the app to make it "live").
|
||||
|
||||
## Allocation Strategy (ie incorporating Dodrio research)
|
||||
----
|
||||
The `VNodeTree` type is a very special type that allows VNodes to be created using a pluggable allocator. The html! macro creates something that looks like:
|
||||
|
||||
```rust
|
||||
static Example: FC<()> = |ctx| {
|
||||
html! { <div> "blah" </div> }
|
||||
};
|
||||
|
||||
// expands to...
|
||||
|
||||
static Example: FC<()> = |ctx| {
|
||||
// This function converts a Fn(allocator) -> VNode closure to a DomTree struct that will later be evaluated.
|
||||
html_macro_to_vnodetree(move |allocator| {
|
||||
let mut node0 = allocator.alloc(VElement::div);
|
||||
let node1 = allocator.alloc_text("blah");
|
||||
node0.children = [node1];
|
||||
node0
|
||||
})
|
||||
};
|
||||
```
|
||||
At runtime, the new closure is created that captures references to `ctx`. Therefore, this closure can only be evaluated while `ctx` is borrowed and in scope. However, this closure can only be evaluated with an `allocator`. Currently, the global and Bumpalo allocators are available, though in the future we will add support for creating a VDom with any allocator or arena system (IE Jemalloc, wee-alloc, etc). The intention here is to allow arena allocation of VNodes (no need to box nested VNodes). Between diffing phases, the arena will be overwritten as old nodes are replaced with new nodes. This saves allocation time and enables bump allocators.
|
||||
|
|
@ -20,3 +20,76 @@ fn main() {
|
|||
fn example(ctx: &Context, name: String) -> VNode {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
}
|
||||
|
||||
/*
|
||||
TODO
|
||||
|
||||
/// The macro can also be applied to statics in order to make components less verbose
|
||||
/// The FC type automatically adds the inference, and property fields are automatically added as function arguments
|
||||
#[fc]
|
||||
static Example: FC = |ctx, name: String| {
|
||||
html! { <div> "Hello, {name}!" </div> }
|
||||
};
|
||||
*/
|
||||
|
||||
// This trait is not exposed to users directly, though they could manually implement this for struct-style components
|
||||
trait Comp {
|
||||
type Props: Properties;
|
||||
fn render(&self, ctx: &mut Context<Self::Props>) -> VNode;
|
||||
fn builder(&self) -> Self::Props;
|
||||
}
|
||||
trait Properties {
|
||||
fn new() -> Self;
|
||||
}
|
||||
|
||||
impl<T: Properties> Comp for FC<T> {
|
||||
type Props = T;
|
||||
|
||||
fn render(&self, ctx: &mut Context<T>) -> VNode {
|
||||
let g = self(ctx);
|
||||
g
|
||||
}
|
||||
|
||||
fn builder(&self) -> T {
|
||||
T::new()
|
||||
}
|
||||
}
|
||||
|
||||
// impl<T: Properties, F: Fn(&Context<T>) -> VNode> Comp for F {
|
||||
// type Props = T;
|
||||
|
||||
// fn render(&self, ctx: &mut Context<T>) -> VNode {
|
||||
// let g = self(ctx);
|
||||
// g
|
||||
// }
|
||||
|
||||
// fn builder(&self) -> T {
|
||||
// T::new()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Properties for () {
|
||||
fn new() -> Self {
|
||||
()
|
||||
}
|
||||
}
|
||||
#[allow(unused, non_upper_case_globals)]
|
||||
static MyComp: FC<()> = |ctx| {
|
||||
html! {
|
||||
<div>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
fn my_comp(ctx: &Context<()>) -> VNode {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn test() {
|
||||
let mut ctx = Context { props: &() };
|
||||
let f = MyComp.render(&mut ctx);
|
||||
let props = MyComp.builder();
|
||||
|
||||
// let f = my_comp.render(&mut ctx);
|
||||
// let props = my_comp.builder();
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
//! thousands of simultaneous clients on minimal hardware.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_ssr::TextRenderer;
|
||||
use rand::Rng;
|
||||
use tide::{Request, Response};
|
||||
|
||||
|
@ -50,20 +51,17 @@ async fn fibsum(req: Request<()>) -> tide::Result<tide::Response> {
|
|||
// Generate another random number to try
|
||||
let other_fib_to_try = rand::thread_rng().gen_range(1..42);
|
||||
|
||||
let g = html! {
|
||||
<div>
|
||||
</div>
|
||||
};
|
||||
|
||||
let nodes = html! {
|
||||
let text = TextRenderer::<()>::to_text(html! {
|
||||
<html>
|
||||
|
||||
// Header
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" />
|
||||
<meta charset="UTF-8" />
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
// Body
|
||||
<body>
|
||||
<div class="flex items-center justify-center flex-col">
|
||||
<div class="flex items-center justify-center">
|
||||
|
@ -97,11 +95,10 @@ async fn fibsum(req: Request<()>) -> tide::Result<tide::Response> {
|
|||
</body>
|
||||
|
||||
</html>
|
||||
};
|
||||
});
|
||||
|
||||
Ok(Response::builder(203)
|
||||
.body(nodes.to_string())
|
||||
.header("custom-header", "value")
|
||||
.body(text)
|
||||
.content_type(tide::http::mime::HTML)
|
||||
.build())
|
||||
}
|
||||
|
|
|
@ -186,6 +186,48 @@ impl Parse for FunctionComponent {
|
|||
fn ensure_fn_block(item: Item) -> syn::Result<ItemFn> {
|
||||
match item {
|
||||
Item::Fn(it) => Ok(it),
|
||||
Item::Static(it) => {
|
||||
let syn::ItemStatic {
|
||||
attrs,
|
||||
vis,
|
||||
static_token,
|
||||
mutability,
|
||||
ident,
|
||||
colon_token,
|
||||
ty,
|
||||
eq_token,
|
||||
expr,
|
||||
semi_token,
|
||||
} = ⁢
|
||||
match ty.as_ref() {
|
||||
Type::BareFn(bare) => {}
|
||||
// Type::Array(_)
|
||||
// | Type::Group(_)
|
||||
// | Type::ImplTrait(_)
|
||||
// | Type::Infer(_)
|
||||
// | Type::Macro(_)
|
||||
// | Type::Never(_)
|
||||
// | Type::Paren(_)
|
||||
// | Type::Path(_)
|
||||
// | Type::Ptr(_)
|
||||
// | Type::Reference(_)
|
||||
// | Type::Slice(_)
|
||||
// | Type::TraitObject(_)
|
||||
// | Type::Tuple(_)
|
||||
// | Type::Verbatim(_)
|
||||
_ => {}
|
||||
};
|
||||
|
||||
// TODO: Add support for static block
|
||||
// Ensure that the contents of the static block can be extracted to a function
|
||||
// TODO: @Jon
|
||||
// Decide if statics should be converted to functions (under the hood) or stay as statics
|
||||
// They _do_ get promoted, but also have a &'static ref
|
||||
Err(syn::Error::new_spanned(
|
||||
it,
|
||||
"`function_component` attribute not ready for statics",
|
||||
))
|
||||
}
|
||||
other => Err(syn::Error::new_spanned(
|
||||
other,
|
||||
"`function_component` attribute can only be applied to functions",
|
||||
|
|
3
packages/core/.vscode/settings.json
vendored
Normal file
3
packages/core/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
}
|
|
@ -10,13 +10,13 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
generational-arena = "0.2.8"
|
||||
once_cell = "1.5.2"
|
||||
dioxus-html-macro = { path = "../html-macro", version = "0.1.0" }
|
||||
|
||||
# Backs some static data
|
||||
once_cell = "1.5.2"
|
||||
|
||||
# web-sys = "0.3.46"
|
||||
# js-sys = "0.3.46"
|
||||
# typed-arena = "2.0.1"
|
||||
# virtual-dom-rs = { path = "../virtual-dom-rs" }
|
||||
# virtual-node = { path = "../virtual-node" }
|
||||
# Backs the scope creation and reutilization
|
||||
generational-arena = "0.2.8"
|
||||
|
||||
# Bumpalo backs the VNode creation
|
||||
bumpalo = { version = "3.6.0", features = ["collections"] }
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
# Dioxus-core
|
||||
|
||||
In the world of Rust UI + wasm UI toolkits, Dioxus is taking a fairly opinionated stance by being functional-only with hooks/context as the primary method of interacting with state.
|
||||
This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website.
|
||||
|
||||
To focus on building an effective user experience,
|
||||
## Internals
|
||||
Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:
|
||||
|
||||
- React: hooks, concurrency, suspense
|
||||
- Dodrio: bump allocation / custom faster allocators, stack machine for fast changes
|
||||
- Percy: html! macro implementation and agnostic edit list
|
||||
- Yew: passion and inspiration ❤️
|
||||
|
||||
## Goals
|
||||
|
||||
We have big goals for Dioxus. The final implementation must:
|
||||
|
||||
- Be **fast**. Allocators are typically slow in WASM/Rust, so we should have a smart way of allocating.
|
||||
- Be concurrent. Components should be able to pause rendering using a threading mechanism.
|
||||
- Support "broadcasting". Edit lists should be separate from the Renderer implementation.
|
||||
- Support SSR. VNodes should render to a string that can be served via a web server.
|
||||
- Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs.
|
||||
- Be modular. Components and hooks should be work anywhere without worrying about target platform.
|
||||
|
||||
## Optimizations
|
||||
|
||||
Sources
|
||||
---
|
||||
percy: https://github.com/chinedufn/percy/blob/master/crates/virtual-dom-rs/src/patch/mod.rs
|
||||
yew:
|
||||
dodrio:
|
||||
rsx:
|
||||
- Support a pluggable allocation strategy that makes VNode creation **very** fast
|
||||
- Support lazy DomTrees (ie DomTrees that are not actually created when the view fn is ran)
|
||||
- Support advanced diffing strategies
|
||||
|
||||
react:
|
||||
fre:
|
||||
## Design Quirks
|
||||
|
||||
- Use of "Context" as a way of mitigating threading issues and the borrow checker. (JS relies on globals)
|
||||
- html! is lazy - needs to be used with a partner function to actually allocate the html.
|
||||
|
||||
```rust
|
||||
let text = TextRenderer::render(html! {<div>"hello world"</div>});
|
||||
// <div>hello world</div>
|
||||
```
|
||||
|
||||
|
|
49
packages/core/examples/dummy.rs
Normal file
49
packages/core/examples/dummy.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
// #![allow(unused, non_upper_case_globals)]
|
||||
// use bumpalo::Bump;
|
||||
// use dioxus_core::nodebuilder::*;
|
||||
// use dioxus_core::{nodes::DomTree, prelude::*};
|
||||
// use std::{collections::HashMap, future::Future, marker::PhantomData};
|
||||
|
||||
fn main() {}
|
||||
|
||||
// struct DummyRenderer {
|
||||
// alloc: Bump,
|
||||
// }
|
||||
|
||||
// impl DummyRenderer {
|
||||
// // "Renders" a domtree by logging its children and outputs
|
||||
// fn render() {}
|
||||
|
||||
// // Takes a domtree, an initial value, a new value, and produces the diff list
|
||||
// fn produce_diffs() {}
|
||||
// }
|
||||
|
||||
// struct Props<'a> {
|
||||
// name: &'a str,
|
||||
// }
|
||||
|
||||
// /// This component does "xyz things"
|
||||
// /// This is sample documentation
|
||||
// static Component: FC<Props> = |ctx| {
|
||||
// // This block mimics that output of the html! macro
|
||||
|
||||
// DomTree::new(move |bump| {
|
||||
// // parse into RSX structures
|
||||
// // regurgetate as rust types
|
||||
|
||||
// // <div> "Child 1" "Child 2"</div>
|
||||
// div(bump)
|
||||
// .attr("class", "edit")
|
||||
// .child(text("Child 1"))
|
||||
// .child(text("Child 2"))
|
||||
// .finish()
|
||||
// })
|
||||
// };
|
||||
|
||||
// /*
|
||||
// source
|
||||
// |> c1 -> VNode
|
||||
// |> c2 -> VNode
|
||||
// |> c3 -> VNode
|
||||
// |> c4 -> VNode
|
||||
// */
|
57
packages/core/examples/macrosrc.rs
Normal file
57
packages/core/examples/macrosrc.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
#![allow(unused, non_upper_case_globals)]
|
||||
use bumpalo::Bump;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core::{nodebuilder::*, virtual_dom::DomTree};
|
||||
use std::{collections::HashMap, future::Future, marker::PhantomData};
|
||||
|
||||
fn main() {}
|
||||
struct Props<'a> {
|
||||
use_macro: bool,
|
||||
|
||||
// todo uh not static
|
||||
// incorporate lifetimes into the thing somehow
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
fn Component(ctx: Context<Props>) -> VNode {
|
||||
// Write asynchronous rendering code that immediately returns a "suspended" VNode
|
||||
// The concurrent API will then progress this component when the future finishes
|
||||
// You can suspend the entire component, or just parts of it
|
||||
let product_list = ctx.suspend(async {
|
||||
// Suspend the rendering that completes when the future is done
|
||||
match fetch_data().await {
|
||||
Ok(data) => html! {<div> </div>},
|
||||
Err(_) => html! {<div> </div>},
|
||||
}
|
||||
});
|
||||
|
||||
// VNodes can be constructed via a builder or the html! macro
|
||||
// However, both of these are "lazy" - they need to be evaluated (aka, "viewed")
|
||||
// We can "view" them with Context for ultimate speed while inside components
|
||||
if ctx.props.use_macro {
|
||||
ctx.view(|bump| {
|
||||
div(bump)
|
||||
.attr("class", "edit")
|
||||
.child(text("Hello"))
|
||||
.child(text(ctx.props.text))
|
||||
.finish()
|
||||
})
|
||||
} else {
|
||||
// "View" indicates exactly *when* allocations take place, everything is lazy up to this point
|
||||
ctx.view(html! {
|
||||
<div>
|
||||
// TODO!
|
||||
// Get all this working again
|
||||
// <h1>"Products"</h1>
|
||||
// // Subnodes can even be suspended
|
||||
// // When completely rendered, they won't cause the component itself to re-render, just their slot
|
||||
// <p> {product_list} </p>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An example of a datafetching service
|
||||
async fn fetch_data() -> Result<String, ()> {
|
||||
todo!()
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
use std::future::Future;
|
||||
|
||||
use dioxus_core::{prelude::*, virtual_dom::Properties};
|
||||
// use virtual_dom_rs::Closure;
|
||||
|
||||
// Stop-gap while developing
|
||||
// Need to update the macro
|
||||
type VirtualNode = VNode;
|
||||
|
||||
pub fn main() {
|
||||
let dom = VirtualDom::new_with_props(root);
|
||||
// let mut renderer = TextRenderer::new(dom);
|
||||
// let output = renderer.render();
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct Props {
|
||||
name: String,
|
||||
}
|
||||
impl Properties for Props {}
|
||||
|
||||
fn root(ctx: &mut Context<Props>) -> VNode {
|
||||
// the regular html syntax
|
||||
|
||||
// html! {
|
||||
// <html>
|
||||
// <Head />
|
||||
// <Body />
|
||||
// <Footer />
|
||||
// </html>
|
||||
// }
|
||||
|
||||
// or a manually crated vnode
|
||||
{
|
||||
let mut node_0 = VNode::element("div");
|
||||
{
|
||||
if let Some(ref mut element_node) = node_0.as_velement_mut() {
|
||||
// element_node.attrs.insert("blah", "blah");
|
||||
// element_node.children.extend(node_0.into_iter());
|
||||
}
|
||||
}
|
||||
|
||||
let mut node_1: IterableNodes = ("Hello world!").into();
|
||||
|
||||
node_1.first().insert_space_before_text();
|
||||
let mut node_2 = VNode::element("button");
|
||||
|
||||
let node_3 = VNode::Component(VComponent::from_fn(
|
||||
Head,
|
||||
Props {
|
||||
name: "".to_string(),
|
||||
},
|
||||
));
|
||||
|
||||
{
|
||||
// let closure = Closure::wrap(Box::new(|_| {}) as Box<FnMut(_)>);
|
||||
// let closure_rc = std::rc::Rc::new(closure);
|
||||
// node_2
|
||||
// .as_velement_mut()
|
||||
// .expect("Not an element")
|
||||
// .events
|
||||
// .0
|
||||
// .insert("onclick".to_string(), closure_rc);
|
||||
}
|
||||
|
||||
if let Some(ref mut element_node) = node_0.as_velement_mut() {
|
||||
element_node.children.extend(node_1.into_iter());
|
||||
}
|
||||
if let Some(ref mut element_node) = node_0.as_velement_mut() {
|
||||
element_node.children.extend(node_2.into_iter());
|
||||
}
|
||||
|
||||
node_0
|
||||
}
|
||||
}
|
||||
|
||||
fn Head(ctx: &mut Context<Props>) -> VNode {
|
||||
html! {
|
||||
<head> "Head Section" </head>
|
||||
}
|
||||
}
|
||||
|
||||
fn Body(ctx: &mut Context<Props>) -> VNode {
|
||||
html! {
|
||||
<body> {"Footer Section"}</body>
|
||||
}
|
||||
}
|
||||
|
||||
fn Footer(ctx: &mut Context<Props>) -> VNode {
|
||||
let mut v = 10_i32;
|
||||
format!("Is this the real life, or is this fantasy, caught in a landslide");
|
||||
|
||||
html! {
|
||||
<div>
|
||||
"Footer Section"
|
||||
"Footer Section"
|
||||
"Footer Section"
|
||||
"Footer Section"
|
||||
"Footer Section"
|
||||
"Footer Section"
|
||||
</div>
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
1078
packages/core/src/nodebuilder.rs
Normal file
1078
packages/core/src/nodebuilder.rs
Normal file
File diff suppressed because it is too large
Load diff
359
packages/core/src/nodes.rs
Normal file
359
packages/core/src/nodes.rs
Normal file
|
@ -0,0 +1,359 @@
|
|||
//! Virtual Node Support
|
||||
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers.
|
||||
//!
|
||||
//! These VNodes should be *very* cheap and *very* fast to construct - building a full tree should be insanely quick.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bumpalo::Bump;
|
||||
pub use vcomponent::VComponent;
|
||||
pub use velement::VElement;
|
||||
pub use velement::{Attribute, Listener, NodeKey};
|
||||
pub use vnode::VNode;
|
||||
pub use vtext::VText;
|
||||
|
||||
/// Tools for the base unit of the virtual dom - the VNode
|
||||
/// VNodes are intended to be quickly-allocated, lightweight enum values.
|
||||
///
|
||||
/// Components will be generating a lot of these very quickly, so we want to
|
||||
/// limit the amount of heap allocations / overly large enum sizes.
|
||||
mod vnode {
|
||||
use super::*;
|
||||
|
||||
pub enum VNode<'src> {
|
||||
/// An element node (node type `ELEMENT_NODE`).
|
||||
Element(VElement<'src>),
|
||||
|
||||
/// A text node (node type `TEXT_NODE`).
|
||||
///
|
||||
/// Note: This wraps a `VText` instead of a plain `String` in
|
||||
/// order to enable custom methods like `create_text_node()` on the
|
||||
/// wrapped type.
|
||||
Text(VText<'src>),
|
||||
|
||||
/// A "suspended component"
|
||||
/// This is a masqeurade over an underlying future that needs to complete
|
||||
/// When the future is completed, the VNode will then trigger a render
|
||||
Suspended,
|
||||
|
||||
/// A User-defined componen node (node type COMPONENT_NODE)
|
||||
Component(VComponent),
|
||||
}
|
||||
|
||||
impl<'src> VNode<'src> {
|
||||
/// Create a new virtual element node with a given tag.
|
||||
///
|
||||
/// These get patched into the DOM using `document.createElement`
|
||||
///
|
||||
/// ```ignore
|
||||
/// let div = VNode::element("div");
|
||||
/// ```
|
||||
pub fn element(tag: &'static str) -> Self {
|
||||
VNode::Element(VElement::new(tag))
|
||||
}
|
||||
|
||||
/// Construct a new text node with the given text.
|
||||
#[inline]
|
||||
pub(crate) fn text(text: &'src str) -> VNode<'src> {
|
||||
VNode::Text(VText { text })
|
||||
}
|
||||
// /// Create a new virtual text node with the given text.
|
||||
// ///
|
||||
// /// These get patched into the DOM using `document.createTextNode`
|
||||
// ///
|
||||
// /// ```ignore
|
||||
// /// let div = VNode::text("div");
|
||||
// /// ```
|
||||
// pub fn text<S>(text: S) -> Self
|
||||
// where
|
||||
// S: Into<String>,
|
||||
// {
|
||||
// /*
|
||||
// TODO
|
||||
|
||||
// This is an opportunity to be extremely efficient when allocating/creating strings
|
||||
// To assemble a formatted string, we can, using the macro, borrow all the contents without allocating.
|
||||
|
||||
// String contents are therefore bump allocated automatically
|
||||
|
||||
// html!{
|
||||
// <>"Hello {world}"</>
|
||||
// }
|
||||
|
||||
// Should be
|
||||
|
||||
// ```
|
||||
// let mut root = VNode::text(["Hello", world]);
|
||||
|
||||
// ```
|
||||
|
||||
// */
|
||||
// VNode::Text(VText::new(text.into()))
|
||||
// }
|
||||
|
||||
// /// Return a [`VElement`] reference, if this is an [`Element`] variant.
|
||||
// ///
|
||||
// /// [`VElement`]: struct.VElement.html
|
||||
// /// [`Element`]: enum.VNode.html#variant.Element
|
||||
// pub fn as_velement_ref(&self) -> Option<&VElement> {
|
||||
// match self {
|
||||
// VNode::Element(ref element_node) => Some(element_node),
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// Return a mutable [`VElement`] reference, if this is an [`Element`] variant.
|
||||
// ///
|
||||
// /// [`VElement`]: struct.VElement.html
|
||||
// /// [`Element`]: enum.VNode.html#variant.Element
|
||||
// pub fn as_velement_mut(&mut self) -> Option<&mut VElement> {
|
||||
// match self {
|
||||
// VNode::Element(ref mut element_node) => Some(element_node),
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// Return a [`VText`] reference, if this is an [`Text`] variant.
|
||||
// ///
|
||||
// /// [`VText`]: struct.VText.html
|
||||
// /// [`Text`]: enum.VNode.html#variant.Text
|
||||
// pub fn as_vtext_ref(&self) -> Option<&VText> {
|
||||
// match self {
|
||||
// VNode::Text(ref text_node) => Some(text_node),
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// Return a mutable [`VText`] reference, if this is an [`Text`] variant.
|
||||
// ///
|
||||
// /// [`VText`]: struct.VText.html
|
||||
// /// [`Text`]: enum.VNode.html#variant.Text
|
||||
// pub fn as_vtext_mut(&mut self) -> Option<&mut VText> {
|
||||
// match self {
|
||||
// VNode::Text(ref mut text_node) => Some(text_node),
|
||||
// _ => None,
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// Used by html-macro to insert space before text that is inside of a block that came after
|
||||
// /// an open tag.
|
||||
// ///
|
||||
// /// html! { <div> {world}</div> }
|
||||
// ///
|
||||
// /// So that we end up with <div> world</div> when we're finished parsing.
|
||||
// pub fn insert_space_before_text(&mut self) {
|
||||
// match self {
|
||||
// VNode::Text(text_node) => {
|
||||
// text_node.text = " ".to_string() + &text_node.text;
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// Used by html-macro to insert space after braced text if we know that the next block is
|
||||
// /// another block or a closing tag.
|
||||
// ///
|
||||
// /// html! { <div>{Hello} {world}</div> } -> <div>Hello world</div>
|
||||
// /// html! { <div>{Hello} </div> } -> <div>Hello </div>
|
||||
// ///
|
||||
// /// So that we end up with <div>Hello world</div> when we're finished parsing.
|
||||
// pub fn insert_space_after_text(&mut self) {
|
||||
// match self {
|
||||
// VNode::Text(text_node) => {
|
||||
// text_node.text += " ";
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// Convert from DOM elements to the primary enum
|
||||
// -----------------------------------------------
|
||||
// impl From<VText> for VNode {
|
||||
// fn from(other: VText) -> Self {
|
||||
// VNode::Text(other)
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<VElement> for VNode {
|
||||
// fn from(other: VElement) -> Self {
|
||||
// VNode::Element(other)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
mod velement {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct VElement<'a> {
|
||||
/// The HTML tag, such as "div"
|
||||
pub tag: &'a str,
|
||||
|
||||
pub tag_name: &'a str,
|
||||
pub attributes: &'a [Attribute<'a>],
|
||||
// todo: hook up listeners
|
||||
// pub listeners: &'a [Listener<'a>],
|
||||
// / HTML attributes such as id, class, style, etc
|
||||
// pub attrs: HashMap<String, String>,
|
||||
// TODO: @JON Get this to not heap allocate, but rather borrow
|
||||
// pub attrs: HashMap<&'static str, &'static str>,
|
||||
|
||||
// TODO @Jon, re-enable "events"
|
||||
//
|
||||
// /// Events that will get added to your real DOM element via `.addEventListener`
|
||||
// pub events: Events,
|
||||
// pub events: HashMap<String, ()>,
|
||||
|
||||
// /// The children of this `VNode`. So a <div> <em></em> </div> structure would
|
||||
// /// have a parent div and one child, em.
|
||||
// pub children: Vec<VNode>,
|
||||
}
|
||||
|
||||
impl<'a> VElement<'a> {
|
||||
// The tag of a component MUST be known at compile time
|
||||
pub fn new(tag: &'a str) -> Self {
|
||||
todo!()
|
||||
// VElement {
|
||||
// tag,
|
||||
// attrs: HashMap::new(),
|
||||
// events: HashMap::new(),
|
||||
// // events: Events(HashMap::new()),
|
||||
// children: vec![],
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/// An attribute on a DOM node, such as `id="my-thing"` or
|
||||
/// `href="https://example.com"`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Attribute<'a> {
|
||||
pub(crate) name: &'a str,
|
||||
pub(crate) value: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Attribute<'a> {
|
||||
/// Get this attribute's name, such as `"id"` in `<div id="my-thing" />`.
|
||||
#[inline]
|
||||
pub fn name(&self) -> &'a str {
|
||||
self.name
|
||||
}
|
||||
|
||||
/// The attribute value, such as `"my-thing"` in `<div id="my-thing" />`.
|
||||
#[inline]
|
||||
pub fn value(&self) -> &'a str {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Certain attributes are considered "volatile" and can change via user
|
||||
/// input that we can't see when diffing against the old virtual DOM. For
|
||||
/// these attributes, we want to always re-set the attribute on the physical
|
||||
/// DOM node, even if the old and new virtual DOM nodes have the same value.
|
||||
#[inline]
|
||||
pub(crate) fn is_volatile(&self) -> bool {
|
||||
match self.name {
|
||||
"value" | "checked" | "selected" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An event listener.
|
||||
pub struct Listener<'a> {
|
||||
/// The type of event to listen for.
|
||||
pub(crate) event: &'a str,
|
||||
/// The callback to invoke when the event happens.
|
||||
pub(crate) callback: &'a (dyn Fn()),
|
||||
}
|
||||
|
||||
/// The key for keyed children.
|
||||
///
|
||||
/// Keys must be unique among siblings.
|
||||
///
|
||||
/// If any sibling is keyed, then they all must be keyed.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct NodeKey(pub(crate) u32);
|
||||
|
||||
impl Default for NodeKey {
|
||||
fn default() -> NodeKey {
|
||||
NodeKey::NONE
|
||||
}
|
||||
}
|
||||
impl NodeKey {
|
||||
/// The default, lack of a key.
|
||||
pub const NONE: NodeKey = NodeKey(u32::MAX);
|
||||
|
||||
/// Is this key `NodeKey::NONE`?
|
||||
#[inline]
|
||||
pub fn is_none(&self) -> bool {
|
||||
*self == Self::NONE
|
||||
}
|
||||
|
||||
/// Is this key not `NodeKey::NONE`?
|
||||
#[inline]
|
||||
pub fn is_some(&self) -> bool {
|
||||
!self.is_none()
|
||||
}
|
||||
|
||||
/// Create a new `NodeKey`.
|
||||
///
|
||||
/// `key` must not be `u32::MAX`.
|
||||
#[inline]
|
||||
pub fn new(key: u32) -> Self {
|
||||
debug_assert_ne!(key, u32::MAX);
|
||||
NodeKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
// todo
|
||||
// use zst enum for element type. Something like ValidElements::div
|
||||
}
|
||||
|
||||
mod vtext {
|
||||
#[derive(PartialEq)]
|
||||
pub struct VText<'a> {
|
||||
pub text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> VText<'a> {
|
||||
// / Create an new `VText` instance with the specified text.
|
||||
// pub fn new<S>(text: S) -> Self
|
||||
// where
|
||||
// S: Into<String>,
|
||||
// {
|
||||
// VText { text: text.into() }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
/// Virtual Components for custom user-defined components
|
||||
/// Only supports the functional syntax
|
||||
mod vcomponent {
|
||||
use crate::virtual_dom::Properties;
|
||||
use std::{any::TypeId, fmt, future::Future};
|
||||
|
||||
use super::VNode;
|
||||
#[derive(PartialEq)]
|
||||
pub struct VComponent {
|
||||
// props_id: TypeId,
|
||||
// callerIDs are unsafely coerced to function pointers
|
||||
// This is okay because #1, we store the props_id and verify and 2# the html! macro rejects components not made this way
|
||||
//
|
||||
// Manually constructing the VComponent is not possible from 3rd party crates
|
||||
}
|
||||
|
||||
impl VComponent {
|
||||
// /// Construct a VComponent directly from a function component
|
||||
// /// This should be *very* fast - we store the function pointer and props type ID. It should also be small on the stack
|
||||
// pub fn from_fn<P: Properties>(f: FC<P>, props: P) -> Self {
|
||||
// // // Props needs to be static
|
||||
// // let props_id = std::any::TypeId::of::<P>();
|
||||
|
||||
// // // Cast the caller down
|
||||
|
||||
// // Self { props_id }
|
||||
// Self {}
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -241,3 +241,766 @@ mod old2 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod text {
|
||||
//! Old methods that clouded the element implementation
|
||||
//! These all add a dedicated text renderer implementation
|
||||
|
||||
mod vnode {
|
||||
|
||||
impl From<&str> for VNode {
|
||||
fn from(other: &str) -> Self {
|
||||
VNode::text(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VNode {
|
||||
fn from(other: String) -> Self {
|
||||
VNode::text(other.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// Allow VNodes to be iterated for map-based UI
|
||||
// -----------------------------------------------
|
||||
impl IntoIterator for VNode {
|
||||
type Item = VNode;
|
||||
// TODO: Is this possible with an array [VNode] instead of a vec?
|
||||
type IntoIter = ::std::vec::IntoIter<VNode>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
vec![self].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<::std::vec::IntoIter<VNode>> for VNode {
|
||||
fn into(self) -> ::std::vec::IntoIter<VNode> {
|
||||
self.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// Allow debug/display adherent to the HTML spec
|
||||
// -----------------------------------------------
|
||||
use std::fmt;
|
||||
impl fmt::Debug for VNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VNode::Element(e) => write!(f, "Node::{:?}", e),
|
||||
VNode::Text(t) => write!(f, "Node::{:?}", t),
|
||||
VNode::Component(c) => write!(f, "Node::{:?}", c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a VNode into an HTML string (delegate impl to variants)
|
||||
impl fmt::Display for VNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VNode::Element(element) => write!(f, "{}", element),
|
||||
VNode::Text(text) => write!(f, "{}", text),
|
||||
VNode::Component(c) => write!(f, "{}", c),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod velement {
|
||||
// -----------------------------------------------
|
||||
// Allow debug/display adherent to the HTML spec
|
||||
// -----------------------------------------------
|
||||
use std::fmt;
|
||||
impl fmt::Debug for VElement {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Element(<{}>, attrs: {:?}, children: {:?})",
|
||||
self.tag, self.attrs, self.children,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VElement {
|
||||
// Turn a VElement and all of it's children (recursively) into an HTML string
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "<{}", self.tag).unwrap();
|
||||
|
||||
for (attr, value) in self.attrs.iter() {
|
||||
write!(f, r#" {}="{}""#, attr, value)?;
|
||||
}
|
||||
|
||||
write!(f, ">")?;
|
||||
|
||||
for child in self.children.iter() {
|
||||
write!(f, "{}", child.to_string())?;
|
||||
}
|
||||
|
||||
if !crate::validation::is_self_closing(&self.tag) {
|
||||
write!(f, "</{}>", self.tag)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod vtext {
|
||||
// -----------------------------------------------
|
||||
// Convert from primitives directly into VText
|
||||
// -----------------------------------------------
|
||||
impl From<&str> for VText {
|
||||
fn from(text: &str) -> Self {
|
||||
VText {
|
||||
text: text.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VText {
|
||||
fn from(text: String) -> Self {
|
||||
VText { text }
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// Allow debug/display adherent to the HTML spec
|
||||
// -----------------------------------------------
|
||||
use std::fmt;
|
||||
impl fmt::Debug for VText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Text({})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a VText into an HTML string
|
||||
impl fmt::Display for VText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod iterables {
|
||||
|
||||
// TODO @Jon
|
||||
// Set this up so instead of the view trait, we can just take functions
|
||||
// Functions with no context should just be rendered
|
||||
// But functions with a context should be treated as regular components
|
||||
|
||||
// impl<V: View> From<Vec<V>> for IterableNodes {
|
||||
// fn from(other: Vec<V>) -> Self {
|
||||
// IterableNodes(other.into_iter().map(|it| it.render()).collect())
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V: View> From<&Vec<V>> for IterableNodes {
|
||||
// fn from(other: &Vec<V>) -> Self {
|
||||
// IterableNodes(other.iter().map(|it| it.render()).collect())
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V: View> From<&[V]> for IterableNodes {
|
||||
// fn from(other: &[V]) -> Self {
|
||||
// IterableNodes(other.iter().map(|it| it.render()).collect())
|
||||
// }
|
||||
// }
|
||||
|
||||
impl From<&str> for IterableNodes {
|
||||
fn from(other: &str) -> Self {
|
||||
IterableNodes(vec![VNode::text(other)])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for IterableNodes {
|
||||
fn from(other: String) -> Self {
|
||||
IterableNodes(vec![VNode::text(other.as_str())])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn self_closing_tag_to_string() {
|
||||
let node = VNode::element("br");
|
||||
|
||||
// No </br> since self closing tag
|
||||
assert_eq!(&node.to_string(), "<br>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let mut node = VNode::Element(VElement::new("div"));
|
||||
node.as_velement_mut()
|
||||
.unwrap()
|
||||
.attrs
|
||||
.insert("id".into(), "some-id".into());
|
||||
|
||||
let mut child = VNode::Element(VElement::new("span"));
|
||||
|
||||
let mut text = VNode::Text(VText::new("Hello world"));
|
||||
|
||||
child.as_velement_mut().unwrap().children.push(text);
|
||||
|
||||
node.as_velement_mut().unwrap().children.push(child);
|
||||
|
||||
let expected = r#"<div id="some-id"><span>Hello world</span></div>"#;
|
||||
|
||||
assert_eq!(node.to_string(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod ddiff {
|
||||
/// The diffing algorithm to compare two VNode trees and generate a list of patches to update the VDom.
|
||||
/// Currently, using an index-based patching algorithm
|
||||
///
|
||||
pub mod diff {
|
||||
use super::*;
|
||||
use crate::nodes::{VNode, VText};
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
|
||||
// pub use apply_patches::patch;
|
||||
|
||||
/// A Patch encodes an operation that modifies a real DOM element.
|
||||
///
|
||||
/// To update the real DOM that a user sees you'll want to first diff your
|
||||
/// old virtual dom and new virtual dom.
|
||||
///
|
||||
/// This diff operation will generate `Vec<Patch>` with zero or more patches that, when
|
||||
/// applied to your real DOM, will make your real DOM look like your new virtual dom.
|
||||
///
|
||||
/// Each Patch has a u32 node index that helps us identify the real DOM node that it applies to.
|
||||
///
|
||||
/// Our old virtual dom's nodes are indexed depth first, as shown in this illustration
|
||||
/// (0 being the root node, 1 being it's first child, 2 being it's first child's first child).
|
||||
///
|
||||
/// ```text
|
||||
/// .─.
|
||||
/// ( 0 )
|
||||
/// `┬'
|
||||
/// ┌────┴──────┐
|
||||
/// │ │
|
||||
/// ▼ ▼
|
||||
/// .─. .─.
|
||||
/// ( 1 ) ( 4 )
|
||||
/// `┬' `─'
|
||||
/// ┌────┴───┐ │
|
||||
/// │ │ ├─────┬─────┐
|
||||
/// ▼ ▼ │ │ │
|
||||
/// .─. .─. ▼ ▼ ▼
|
||||
/// ( 2 ) ( 3 ) .─. .─. .─.
|
||||
/// `─' `─' ( 5 ) ( 6 ) ( 7 )
|
||||
/// `─' `─' `─'
|
||||
/// ```
|
||||
///
|
||||
/// The patching process is tested in a real browser in crates/virtual-dom-rs/tests/diff_patch.rs
|
||||
#[derive(PartialEq)]
|
||||
pub enum Patch<'a> {
|
||||
/// Append a vector of child nodes to a parent node id.
|
||||
AppendChildren(NodeIdx, Vec<&'a VNode>),
|
||||
/// For a `node_i32`, remove all children besides the first `len`
|
||||
TruncateChildren(NodeIdx, usize),
|
||||
/// Replace a node with another node. This typically happens when a node's tag changes.
|
||||
/// ex: <div> becomes <span>
|
||||
Replace(NodeIdx, &'a VNode),
|
||||
/// Add attributes that the new node has that the old node does not
|
||||
AddAttributes(NodeIdx, HashMap<&'a str, &'a str>),
|
||||
/// Remove attributes that the old node had that the new node doesn't
|
||||
RemoveAttributes(NodeIdx, Vec<&'a str>),
|
||||
/// Change the text of a Text node.
|
||||
ChangeText(NodeIdx, &'a VText),
|
||||
}
|
||||
|
||||
type NodeIdx = usize;
|
||||
|
||||
impl<'a> Patch<'a> {
|
||||
/// Every Patch is meant to be applied to a specific node within the DOM. Get the
|
||||
/// index of the DOM node that this patch should apply to. DOM nodes are indexed
|
||||
/// depth first with the root node in the tree having index 0.
|
||||
pub fn node_idx(&self) -> usize {
|
||||
match self {
|
||||
Patch::AppendChildren(node_idx, _) => *node_idx,
|
||||
Patch::TruncateChildren(node_idx, _) => *node_idx,
|
||||
Patch::Replace(node_idx, _) => *node_idx,
|
||||
Patch::AddAttributes(node_idx, _) => *node_idx,
|
||||
Patch::RemoveAttributes(node_idx, _) => *node_idx,
|
||||
Patch::ChangeText(node_idx, _) => *node_idx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given two VNode's generate Patch's that would turn the old virtual node's
|
||||
/// real DOM node equivalent into the new VNode's real DOM node equivalent.
|
||||
pub fn diff_vnodes<'a>(old: &'a VNode, new: &'a VNode) -> Vec<Patch<'a>> {
|
||||
diff_recursive(&old, &new, &mut 0)
|
||||
}
|
||||
|
||||
fn diff_recursive<'a, 'b>(
|
||||
old: &'a VNode,
|
||||
new: &'a VNode,
|
||||
cur_node_idx: &'b mut usize,
|
||||
) -> Vec<Patch<'a>> {
|
||||
let mut patches = vec![];
|
||||
let mut replace = false;
|
||||
|
||||
// Different enum variants, replace!
|
||||
// VNodes are of different types, and therefore will cause a re-render.
|
||||
// TODO: Handle previously-mounted children so they don't get re-mounted
|
||||
if mem::discriminant(old) != mem::discriminant(new) {
|
||||
replace = true;
|
||||
}
|
||||
|
||||
if let (VNode::Element(old_element), VNode::Element(new_element)) = (old, new) {
|
||||
// Replace if there are different element tags
|
||||
if old_element.tag != new_element.tag {
|
||||
replace = true;
|
||||
}
|
||||
|
||||
// Replace if two elements have different keys
|
||||
// TODO: More robust key support. This is just an early stopgap to allow you to force replace
|
||||
// an element... say if it's event changed. Just change the key name for now.
|
||||
// In the future we want keys to be used to create a Patch::ReOrder to re-order siblings
|
||||
if old_element.attrs.get("key").is_some()
|
||||
&& old_element.attrs.get("key") != new_element.attrs.get("key")
|
||||
{
|
||||
replace = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle replacing of a node
|
||||
if replace {
|
||||
patches.push(Patch::Replace(*cur_node_idx, &new));
|
||||
if let VNode::Element(old_element_node) = old {
|
||||
for child in old_element_node.children.iter() {
|
||||
increment_node_idx_for_children(child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
return patches;
|
||||
}
|
||||
|
||||
// The following comparison can only contain identical variants, other
|
||||
// cases have already been handled above by comparing variant
|
||||
// discriminants.
|
||||
match (old, new) {
|
||||
// We're comparing two text nodes
|
||||
(VNode::Text(old_text), VNode::Text(new_text)) => {
|
||||
if old_text != new_text {
|
||||
patches.push(Patch::ChangeText(*cur_node_idx, &new_text));
|
||||
}
|
||||
}
|
||||
|
||||
// We're comparing two element nodes
|
||||
(VNode::Element(old_element), VNode::Element(new_element)) => {
|
||||
let mut add_attributes: HashMap<&str, &str> = HashMap::new();
|
||||
let mut remove_attributes: Vec<&str> = vec![];
|
||||
|
||||
// TODO: -> split out into func
|
||||
for (new_attr_name, new_attr_val) in new_element.attrs.iter() {
|
||||
match old_element.attrs.get(new_attr_name) {
|
||||
Some(ref old_attr_val) => {
|
||||
if old_attr_val != &new_attr_val {
|
||||
add_attributes.insert(new_attr_name, new_attr_val);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
add_attributes.insert(new_attr_name, new_attr_val);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: -> split out into func
|
||||
for (old_attr_name, old_attr_val) in old_element.attrs.iter() {
|
||||
if add_attributes.get(&old_attr_name[..]).is_some() {
|
||||
continue;
|
||||
};
|
||||
|
||||
match new_element.attrs.get(old_attr_name) {
|
||||
Some(ref new_attr_val) => {
|
||||
if new_attr_val != &old_attr_val {
|
||||
remove_attributes.push(old_attr_name);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
remove_attributes.push(old_attr_name);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if add_attributes.len() > 0 {
|
||||
patches.push(Patch::AddAttributes(*cur_node_idx, add_attributes));
|
||||
}
|
||||
if remove_attributes.len() > 0 {
|
||||
patches.push(Patch::RemoveAttributes(*cur_node_idx, remove_attributes));
|
||||
}
|
||||
|
||||
let old_child_count = old_element.children.len();
|
||||
let new_child_count = new_element.children.len();
|
||||
|
||||
if new_child_count > old_child_count {
|
||||
let append_patch: Vec<&'a VNode> =
|
||||
new_element.children[old_child_count..].iter().collect();
|
||||
patches.push(Patch::AppendChildren(*cur_node_idx, append_patch))
|
||||
}
|
||||
|
||||
if new_child_count < old_child_count {
|
||||
patches.push(Patch::TruncateChildren(*cur_node_idx, new_child_count))
|
||||
}
|
||||
|
||||
let min_count = min(old_child_count, new_child_count);
|
||||
for index in 0..min_count {
|
||||
*cur_node_idx = *cur_node_idx + 1;
|
||||
let old_child = &old_element.children[index];
|
||||
let new_child = &new_element.children[index];
|
||||
patches.append(&mut diff_recursive(
|
||||
&old_child,
|
||||
&new_child,
|
||||
cur_node_idx,
|
||||
))
|
||||
}
|
||||
if new_child_count < old_child_count {
|
||||
for child in old_element.children[min_count..].iter() {
|
||||
increment_node_idx_for_children(child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
(VNode::Text(_), VNode::Element(_)) | (VNode::Element(_), VNode::Text(_)) => {
|
||||
unreachable!(
|
||||
"Unequal variant discriminants should already have been handled"
|
||||
);
|
||||
}
|
||||
_ => todo!("Diffing Not yet implemented for all node types"),
|
||||
};
|
||||
|
||||
// new_root.create_element()
|
||||
patches
|
||||
}
|
||||
|
||||
fn increment_node_idx_for_children<'a, 'b>(
|
||||
old: &'a VNode,
|
||||
cur_node_idx: &'b mut usize,
|
||||
) {
|
||||
*cur_node_idx += 1;
|
||||
if let VNode::Element(element_node) = old {
|
||||
for child in element_node.children.iter() {
|
||||
increment_node_idx_for_children(&child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use crate::prelude::*;
|
||||
// type VirtualNode = VNode;
|
||||
|
||||
// /// Test that we generate the right Vec<Patch> for some start and end virtual dom.
|
||||
// pub struct DiffTestCase<'a> {
|
||||
// // ex: "Patching root level nodes works"
|
||||
// pub description: &'static str,
|
||||
// // ex: html! { <div> </div> }
|
||||
// pub old: VNode,
|
||||
// // ex: html! { <strong> </strong> }
|
||||
// pub new: VNode,
|
||||
// // ex: vec![Patch::Replace(0, &html! { <strong></strong> })],
|
||||
// pub expected: Vec<Patch<'a>>,
|
||||
// }
|
||||
|
||||
// impl<'a> DiffTestCase<'a> {
|
||||
// pub fn test(&self) {
|
||||
// // ex: vec![Patch::Replace(0, &html! { <strong></strong> })],
|
||||
// let patches = diff_vnodes(&self.old, &self.new);
|
||||
|
||||
// assert_eq!(patches, self.expected, "{}", self.description);
|
||||
// }
|
||||
// }
|
||||
// use super::*;
|
||||
// use crate::nodes::{VNode, VText};
|
||||
// use std::collections::HashMap;
|
||||
|
||||
// #[test]
|
||||
// fn replace_node() {
|
||||
// DiffTestCase {
|
||||
// description: "Replace the root if the tag changed",
|
||||
// old: html! { <div> </div> },
|
||||
// new: html! { <span> </span> },
|
||||
// expected: vec![Patch::Replace(0, &html! { <span></span> })],
|
||||
// }
|
||||
// .test();
|
||||
// DiffTestCase {
|
||||
// description: "Replace a child node",
|
||||
// old: html! { <div> <b></b> </div> },
|
||||
// new: html! { <div> <strong></strong> </div> },
|
||||
// expected: vec![Patch::Replace(1, &html! { <strong></strong> })],
|
||||
// }
|
||||
// .test();
|
||||
// DiffTestCase {
|
||||
// description: "Replace node with a child",
|
||||
// old: html! { <div> <b>1</b> <b></b> </div> },
|
||||
// new: html! { <div> <i>1</i> <i></i> </div>},
|
||||
// expected: vec![
|
||||
// Patch::Replace(1, &html! { <i>1</i> }),
|
||||
// Patch::Replace(3, &html! { <i></i> }),
|
||||
// ], //required to check correct index
|
||||
// }
|
||||
// .test();
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn add_children() {
|
||||
// DiffTestCase {
|
||||
// description: "Added a new node to the root node",
|
||||
// old: html! { <div> <b></b> </div> },
|
||||
// new: html! { <div> <b></b> <span></span> </div> },
|
||||
// expected: vec![Patch::AppendChildren(0, vec![&html! { <span></span> }])],
|
||||
// }
|
||||
// .test();
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn remove_nodes() {
|
||||
// DiffTestCase {
|
||||
// description: "Remove all child nodes at and after child sibling index 1",
|
||||
// old: html! { <div> <b></b> <span></span> </div> },
|
||||
// new: html! { <div> </div> },
|
||||
// expected: vec![Patch::TruncateChildren(0, 0)],
|
||||
// }
|
||||
// .test();
|
||||
// DiffTestCase {
|
||||
// description: "Remove a child and a grandchild node",
|
||||
// old: html! {
|
||||
// <div>
|
||||
// <span>
|
||||
// <b></b>
|
||||
// // This `i` tag will get removed
|
||||
// <i></i>
|
||||
// </span>
|
||||
// // This `strong` tag will get removed
|
||||
// <strong></strong>
|
||||
// </div> },
|
||||
// new: html! {
|
||||
// <div>
|
||||
// <span>
|
||||
// <b></b>
|
||||
// </span>
|
||||
// </div> },
|
||||
// expected: vec![Patch::TruncateChildren(0, 1), Patch::TruncateChildren(1, 1)],
|
||||
// }
|
||||
// .test();
|
||||
// DiffTestCase {
|
||||
// description: "Removing child and change next node after parent",
|
||||
// old: html! { <div> <b> <i></i> <i></i> </b> <b></b> </div> },
|
||||
// new: html! { <div> <b> <i></i> </b> <i></i> </div>},
|
||||
// expected: vec![
|
||||
// Patch::TruncateChildren(1, 1),
|
||||
// Patch::Replace(4, &html! { <i></i> }),
|
||||
// ], //required to check correct index
|
||||
// }
|
||||
// .test();
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn add_attributes() {
|
||||
// let mut attributes = HashMap::new();
|
||||
// attributes.insert("id", "hello");
|
||||
|
||||
// DiffTestCase {
|
||||
// old: html! { <div> </div> },
|
||||
// new: html! { <div id="hello"> </div> },
|
||||
// expected: vec![Patch::AddAttributes(0, attributes.clone())],
|
||||
// description: "Add attributes",
|
||||
// }
|
||||
// .test();
|
||||
|
||||
// DiffTestCase {
|
||||
// old: html! { <div id="foobar"> </div> },
|
||||
// new: html! { <div id="hello"> </div> },
|
||||
// expected: vec![Patch::AddAttributes(0, attributes)],
|
||||
// description: "Change attribute",
|
||||
// }
|
||||
// .test();
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn remove_attributes() {
|
||||
// DiffTestCase {
|
||||
// old: html! { <div id="hey-there"></div> },
|
||||
// new: html! { <div> </div> },
|
||||
// expected: vec![Patch::RemoveAttributes(0, vec!["id"])],
|
||||
// description: "Add attributes",
|
||||
// }
|
||||
// .test();
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn change_attribute() {
|
||||
// let mut attributes = HashMap::new();
|
||||
// attributes.insert("id", "changed");
|
||||
|
||||
// DiffTestCase {
|
||||
// description: "Add attributes",
|
||||
// old: html! { <div id="hey-there"></div> },
|
||||
// new: html! { <div id="changed"> </div> },
|
||||
// expected: vec![Patch::AddAttributes(0, attributes)],
|
||||
// }
|
||||
// .test();
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn replace_text_node() {
|
||||
// DiffTestCase {
|
||||
// description: "Replace text node",
|
||||
// old: html! { Old },
|
||||
// new: html! { New },
|
||||
// expected: vec![Patch::ChangeText(0, &VText::new("New"))],
|
||||
// }
|
||||
// .test();
|
||||
// }
|
||||
|
||||
// // Initially motivated by having two elements where all that changed was an event listener
|
||||
// // because right now we don't patch event listeners. So.. until we have a solution
|
||||
// // for that we can just give them different keys to force a replace.
|
||||
// #[test]
|
||||
// fn replace_if_different_keys() {
|
||||
// DiffTestCase {
|
||||
// description: "If two nodes have different keys always generate a full replace.",
|
||||
// old: html! { <div key="1"> </div> },
|
||||
// new: html! { <div key="2"> </div> },
|
||||
// expected: vec![Patch::Replace(0, &html! {<div key="2"> </div>})],
|
||||
// }
|
||||
// .test()
|
||||
// }
|
||||
|
||||
// // // TODO: Key support
|
||||
// // #[test]
|
||||
// // fn reorder_chldren() {
|
||||
// // let mut attributes = HashMap::new();
|
||||
// // attributes.insert("class", "foo");
|
||||
// //
|
||||
// // let old_children = vec![
|
||||
// // // old node 0
|
||||
// // html! { <div key="hello", id="same-id", style="",></div> },
|
||||
// // // removed
|
||||
// // html! { <div key="gets-removed",> { "This node gets removed"} </div>},
|
||||
// // // old node 2
|
||||
// // html! { <div key="world", class="changed-class",></div>},
|
||||
// // // removed
|
||||
// // html! { <div key="this-got-removed",> { "This node gets removed"} </div>},
|
||||
// // ];
|
||||
// //
|
||||
// // let new_children = vec![
|
||||
// // html! { <div key="world", class="foo",></div> },
|
||||
// // html! { <div key="new",> </div>},
|
||||
// // html! { <div key="hello", id="same-id",></div>},
|
||||
// // ];
|
||||
// //
|
||||
// // test(DiffTestCase {
|
||||
// // old: html! { <div> { old_children } </div> },
|
||||
// // new: html! { <div> { new_children } </div> },
|
||||
// // expected: vec![
|
||||
// // // TODO: Come up with the patch structure for keyed nodes..
|
||||
// // // keying should only work if all children have keys..
|
||||
// // ],
|
||||
// // description: "Add attributes",
|
||||
// // })
|
||||
// // }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
mod vcomponent {
|
||||
// -----------------------------------------------
|
||||
// Allow debug/display adherent to the HTML spec
|
||||
// -----------------------------------------------
|
||||
|
||||
impl fmt::Debug for VComponent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// TODO: @JON Implement how components should be formatted when spit out to html
|
||||
// It probably can't be as straightforward as renderinng their VNodes
|
||||
// It _could_ be, but we can't really implement that directly
|
||||
// Instead, we should drop a vnode labeled with the component id/key
|
||||
|
||||
// write!(
|
||||
// f,
|
||||
// "Element(<{}>, attrs: {:?}, children: {:?})",
|
||||
// self.tag, self.attrs, self.children,
|
||||
// )
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VComponent {
|
||||
// Turn a VElement and all of it's children (recursively) into an HTML string
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// write!(f, "<{}", self.tag).unwrap();
|
||||
|
||||
// for (attr, value) in self.attrs.iter() {
|
||||
// write!(f, r#" {}="{}""#, attr, value)?;
|
||||
// }
|
||||
|
||||
// write!(f, ">")?;
|
||||
|
||||
// for child in self.children.iter() {
|
||||
// write!(f, "{}", child.to_string())?;
|
||||
// }
|
||||
|
||||
// if !crate::validation::is_self_closing(&self.tag) {
|
||||
// write!(f, "</{}>", self.tag)?;
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub mod iterables {
|
||||
// use super::*;
|
||||
|
||||
// /// Used by the html! macro for all braced child nodes so that we can use any type
|
||||
// /// that implements Into<IterableNodes>
|
||||
// ///
|
||||
// /// html! { <div> { nodes } </div> }
|
||||
// ///
|
||||
// /// nodes can be a String .. VNode .. Vec<VNode> ... etc
|
||||
// pub struct IterableNodes(Vec<VNode>);
|
||||
|
||||
// impl IterableNodes {
|
||||
// /// Retrieve the first node mutably
|
||||
// pub fn first(&mut self) -> &mut VNode {
|
||||
// self.0.first_mut().unwrap()
|
||||
// }
|
||||
|
||||
// /// Retrieve the last node mutably
|
||||
// pub fn last(&mut self) -> &mut VNode {
|
||||
// self.0.last_mut().unwrap()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl IntoIterator for IterableNodes {
|
||||
// type Item = VNode;
|
||||
// // TODO: Is this possible with an array [VNode] instead of a vec?
|
||||
// type IntoIter = ::std::vec::IntoIter<VNode>;
|
||||
|
||||
// fn into_iter(self) -> Self::IntoIter {
|
||||
// self.0.into_iter()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<VNode> for IterableNodes {
|
||||
// fn from(other: VNode) -> Self {
|
||||
// IterableNodes(vec![other])
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<Vec<VNode>> for IterableNodes {
|
||||
// fn from(other: Vec<VNode>) -> Self {
|
||||
// IterableNodes(other)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
37
packages/core/src/validation.rs
Normal file
37
packages/core/src/validation.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
/// TODO @Jon
|
||||
/// Figure out if validation should be its own crate, or embedded directly into dioxus
|
||||
///
|
||||
/// Should we even be bothered with validation?
|
||||
///
|
||||
///
|
||||
///
|
||||
mod validation {
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Used to uniquely identify elements that contain closures so that the DomUpdater can
|
||||
// look them up by their unique id.
|
||||
// When the DomUpdater sees that the element no longer exists it will drop all of it's
|
||||
// Rc'd Closures for those events.
|
||||
static SELF_CLOSING_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
[
|
||||
"area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "command",
|
||||
"keygen", "source",
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
});
|
||||
|
||||
/// Whether or not this tag is self closing
|
||||
///
|
||||
/// ```ignore
|
||||
/// use dioxus_core::validation::is_self_closing;
|
||||
/// assert_eq!(is_self_closing("br"), true);
|
||||
/// assert_eq!(is_self_closing("div"), false);
|
||||
/// ```
|
||||
pub fn is_self_closing(tag: &str) -> bool {
|
||||
SELF_CLOSING_TAGS.contains(tag)
|
||||
// SELF_CLOSING_TAGS.contains(tag) || is_self_closing_svg_tag(tag)
|
||||
}
|
||||
}
|
278
packages/core/src/virtual_dom.rs
Normal file
278
packages/core/src/virtual_dom.rs
Normal file
|
@ -0,0 +1,278 @@
|
|||
/*
|
||||
The Dioxus Virtual Dom integrates an event system and virtual nodes to create reactive user interfaces.
|
||||
|
||||
The Dioxus VDom uses the same underlying mechanics as Dodrio (double buffering, bump dom, etc).
|
||||
Instead of making the allocator very obvious, we choose to parametrize over the DomTree trait. For our purposes,
|
||||
the DomTree trait is simply an abstraction over a lazy dom builder, much like the iterator trait.
|
||||
|
||||
This means we can accept DomTree anywhere as well as return it. All components therefore look like this:
|
||||
```ignore
|
||||
function Component(ctx: Context<()>) -> impl DomTree {
|
||||
html! {<div> "hello world" </div>}
|
||||
}
|
||||
```
|
||||
It's not quite as sexy as statics, but there's only so much you can do. The goal is to get statics working with the FC macro,
|
||||
so types don't get in the way of you and your component writing. Fortunately, this is all generic enough to be split out
|
||||
into its own lib (IE, lazy loading wasm chunks by function (exciting stuff!))
|
||||
|
||||
```ignore
|
||||
#[fc] // gets translated into a function.
|
||||
static Component: FC = |ctx| {
|
||||
html! {<div> "hello world" </div>}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
This module includes all life-cycle related mechanics, including the virtual dom, scopes, properties, and lifecycles.
|
||||
---
|
||||
The VirtualDom is designed as so:
|
||||
|
||||
VDOM contains:
|
||||
- An arena of component scopes.
|
||||
- A scope contains
|
||||
- lifecycle data
|
||||
- hook data
|
||||
- Event queue
|
||||
- An event
|
||||
|
||||
A VDOM is
|
||||
- constructed from anything that implements "component"
|
||||
|
||||
A "Component" is anything (normally functions) that can be ran with a context to produce VNodes
|
||||
- Must implement properties-builder trait which produces a properties builder
|
||||
|
||||
A Context
|
||||
- Is a consumable struct
|
||||
- Made of references to properties
|
||||
- Holds a reference (lockable) to the underlying scope
|
||||
- Is partially threadsafe
|
||||
*/
|
||||
use crate::nodes::VNode;
|
||||
use crate::prelude::*;
|
||||
use bumpalo::Bump;
|
||||
use generational_arena::Arena;
|
||||
use std::future::Future;
|
||||
|
||||
/// An integrated virtual node system that progresses events and diffs UI trees.
|
||||
/// Differences are converted into patches which a renderer can use to draw the UI.
|
||||
pub struct VirtualDom {
|
||||
/// All mounted components are arena allocated to make additions, removals, and references easy to work with
|
||||
/// A generational arean is used to re-use slots of deleted scopes without having to resize the underlying arena.
|
||||
components: Arena<Scope>,
|
||||
|
||||
/// Components generate lifecycle events
|
||||
event_queue: Vec<LifecycleEvent>,
|
||||
|
||||
buffers: [Bump; 2],
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
|
||||
///
|
||||
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
|
||||
/// The root component can access things like routing in its context.
|
||||
pub fn new<F: DomTree>(root: FC<(), F>) -> Self {
|
||||
Self::new_with_props(root)
|
||||
}
|
||||
|
||||
/// Start a new VirtualDom instance with a dependent props.
|
||||
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
|
||||
///
|
||||
/// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive
|
||||
/// to toss out the entire tree.
|
||||
pub fn new_with_props<P: Properties, F: DomTree>(root: FC<P, F>) -> Self {
|
||||
Self {
|
||||
components: Arena::new(),
|
||||
event_queue: vec![],
|
||||
buffers: [Bump::new(), Bump::new()],
|
||||
}
|
||||
}
|
||||
|
||||
/// Pop an event off the even queue and process it
|
||||
pub fn progress_event() {}
|
||||
}
|
||||
|
||||
/// The internal lifecycle event system is managed by these
|
||||
/// All events need to be confused before swapping doms over
|
||||
pub enum LifecycleEvent {
|
||||
Add {},
|
||||
}
|
||||
|
||||
/// Anything that takes a "bump" and returns VNodes is a "DomTree"
|
||||
/// This is used as a "trait alias" for function return types to look less hair
|
||||
pub trait DomTree {
|
||||
fn render(self, b: &Bump) -> VNode;
|
||||
}
|
||||
|
||||
/// Implement DomTree for the type returned by the html! macro.
|
||||
/// This lets the caller of the static function evaluate the builder closure with its own bump.
|
||||
/// It keeps components pretty and removes the need for the user to get too involved with allocation.
|
||||
impl<F> DomTree for F
|
||||
where
|
||||
F: FnOnce(&Bump) -> VNode,
|
||||
{
|
||||
fn render(self, b: &Bump) -> VNode {
|
||||
self(b)
|
||||
}
|
||||
}
|
||||
|
||||
/// This type alias is an internal way of abstracting over the static functions that represent components.
|
||||
pub type FC<P: Properties, F: DomTree> = fn(&Context<P>) -> F;
|
||||
|
||||
/// A gross AF helper that connects the dots for lifetimes
|
||||
/// sigh.. todo: move this into the html macro or core crates
|
||||
/// https://github.com/rust-lang/rust/issues/41078
|
||||
#[inline]
|
||||
pub fn __domtree_helper(
|
||||
arg: impl for<'a> FnOnce(&'a Bump) -> VNode<'a>,
|
||||
) -> impl for<'a> FnOnce(&'a Bump) -> VNode<'a> {
|
||||
arg
|
||||
}
|
||||
|
||||
/// The `Component` trait refers to any struct or funciton that can be used as a component
|
||||
/// We automatically implement Component for FC<T>
|
||||
pub trait Component {
|
||||
type Props: Properties;
|
||||
fn builder(&'static self) -> Self::Props;
|
||||
}
|
||||
|
||||
// Auto implement component for a FC
|
||||
// Calling the FC is the same as "rendering" it
|
||||
impl<'a, P: Properties, F: DomTree> Component for FC<P, F> {
|
||||
type Props = P;
|
||||
|
||||
fn builder(&self) -> Self::Props {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Properties` trait defines any struct that can be constructed using a combination of default / optional fields.
|
||||
/// Components take a "properties" object
|
||||
pub trait Properties {
|
||||
fn new() -> Self;
|
||||
}
|
||||
|
||||
// Auto implement for no-prop components
|
||||
impl Properties for () {
|
||||
fn new() -> Self {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Compile Tests for FC/Component/Properties
|
||||
// ============================================
|
||||
#[cfg(test)]
|
||||
mod fc_test {
|
||||
use super::*;
|
||||
|
||||
// // Make sure this function builds properly.
|
||||
// fn test_static_fn<'a, P: Properties, F: DomTree>(b: &'a Bump, r: &FC<P, F>) -> VNode<'a> {
|
||||
// let p = P::new(); // new props
|
||||
// let c = Context { props: p }; // new context with props
|
||||
// let g = r(&c); // calling function with context
|
||||
// g.render(&b) // rendering closure with bump allocator
|
||||
// }
|
||||
|
||||
// fn test_component(ctx: &Context<()>) -> impl DomTree {
|
||||
// // todo: helper should be part of html! macro
|
||||
// html! { <div> </div> }
|
||||
// }
|
||||
|
||||
// fn test_component2(ctx: &Context<()>) -> impl DomTree {
|
||||
// __domtree_helper(move |bump: &Bump| VNode::text("blah"))
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn ensure_types_work() {
|
||||
// // TODO: Get the whole casting thing to work properly.
|
||||
// // For whatever reason, FC is not auto-implemented, depsite it being a static type
|
||||
// let b = Bump::new();
|
||||
|
||||
// let g: FC<_, _> = test_component;
|
||||
// let nodes0 = test_static_fn(&b, &g);
|
||||
// // Happiness! The VNodes are now allocated onto the bump vdom
|
||||
|
||||
// let g: FC<_, _> = test_component2;
|
||||
// let nodes1 = test_static_fn(&b, &g);
|
||||
// }
|
||||
}
|
||||
|
||||
/// The Scope that wraps a functional component
|
||||
/// Scopes are allocated in a generational arena. As components are mounted/unmounted, they will replace slots of dead components
|
||||
/// The actualy contents of the hooks, though, will be allocated with the standard allocator. These should not allocate as frequently.
|
||||
pub struct Scope {
|
||||
hook_idx: i32,
|
||||
hooks: Vec<()>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
fn new<T>() -> Self {
|
||||
Self {
|
||||
hook_idx: 0,
|
||||
hooks: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HookState {}
|
||||
|
||||
/// Components in Dioxus use the "Context" object to interact with their lifecycle.
|
||||
/// This lets components schedule updates, integrate hooks, and expose their context via the context api.
|
||||
///
|
||||
/// Properties passed down from the parent component are also directly accessible via the exposed "props" field.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Properties)]
|
||||
/// struct Props {
|
||||
/// name: String
|
||||
///
|
||||
/// }
|
||||
///
|
||||
/// fn example(ctx: &Context<Props>) -> VNode {
|
||||
/// html! {
|
||||
/// <div> "Hello, {ctx.props.name}" </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct Context<T> {
|
||||
// pub struct Context<'source, T> {
|
||||
/// Direct access to the properties used to create this component.
|
||||
pub props: T,
|
||||
// pub props: &'source T,
|
||||
}
|
||||
|
||||
impl<'a, T> Context<T> {
|
||||
// impl<'a, T> Context<'a, T> {
|
||||
/// Access the children elements passed into the component
|
||||
pub fn children(&self) -> Vec<VNode> {
|
||||
todo!("Children API not yet implemented for component Context")
|
||||
}
|
||||
|
||||
/// Access a parent context
|
||||
pub fn parent_context<C>(&self) -> C {
|
||||
todo!("Context API is not ready yet")
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
pub fn subscribe(&self) -> impl FnOnce() -> () {
|
||||
todo!("Subscription API is not ready yet");
|
||||
|| {}
|
||||
}
|
||||
|
||||
/// Create VNodes from macros that use efficient allcators
|
||||
/// The VDOM is configured to use a special allocator for VNodes
|
||||
pub fn view(&self, v: impl Fn(&'a Bump) -> VNode<'a>) -> VNode<'a> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Create a suspended component from a future.
|
||||
///
|
||||
/// When the future completes, the component will be renderered
|
||||
pub fn suspend(
|
||||
&self,
|
||||
fut: impl Future<Output = impl FnOnce(&'a Bump) -> VNode<'a>>,
|
||||
) -> VNode<'a> {
|
||||
todo!()
|
||||
}
|
||||
}
|
|
@ -5,12 +5,12 @@
|
|||
///
|
||||
// use crate::prelude::*;
|
||||
use dioxus_core::prelude::*;
|
||||
type VirtualNode = VNode;
|
||||
// type VirtualNode = VNode;
|
||||
|
||||
/// Test a basic usage of a virtual dom + text renderer combo
|
||||
#[test]
|
||||
fn simple_integration() {
|
||||
let dom = VirtualDom::new(|_| html! { <div>Hello World!</div> });
|
||||
// let dom = VirtualDom::new(|ctx| html! { <div>Hello World!</div> });
|
||||
// let mut renderer = TextRenderer::new(dom);
|
||||
// let output = renderer.render();
|
||||
}
|
||||
|
|
|
@ -147,9 +147,12 @@ impl HtmlParser {
|
|||
// Create a virtual node tree
|
||||
let node = quote! {
|
||||
{
|
||||
#(#tokens)*
|
||||
// Root node is always named node_0
|
||||
node_0
|
||||
move |bump| {
|
||||
// __domtree_helper(move |bump| {
|
||||
#(#tokens)*
|
||||
// Root node is always named node_0
|
||||
node_0
|
||||
}
|
||||
}
|
||||
};
|
||||
node
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
//! The `TextRenderer` is particularly useful when needing to cache a Virtual DOM in between requests
|
||||
//!
|
||||
|
||||
use dioxus_core::prelude::FC;
|
||||
use dioxus_core::prelude::{VNode, FC};
|
||||
|
||||
/// The `TextRenderer` provides a way of rendering a Dioxus Virtual DOM to a String.
|
||||
///
|
||||
|
@ -56,6 +56,11 @@ impl<T> TextRenderer<T> {
|
|||
todo!()
|
||||
}
|
||||
|
||||
/// Immediately render a DomTree to string
|
||||
pub fn to_text(root: VNode) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Render the virtual DOM to a string
|
||||
pub fn render(&self) -> String {
|
||||
let mut buffer = String::new();
|
||||
|
|
Loading…
Reference in a new issue