mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-22 04:03:04 +00:00
Feat: docs, code frm percy
This commit is contained in:
commit
2b9c8d09d9
72 changed files with 5130 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
.DS_Store
|
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[workspace]
|
||||
members = [
|
||||
# Built-n
|
||||
"packages/core",
|
||||
"packages/macro",
|
||||
"packages/hooks",
|
||||
"packages/recoil",
|
||||
"packages/redux",
|
||||
# Pulled from percy
|
||||
"packages/html-macro",
|
||||
"packages/html-macro-test",
|
||||
"packages/virtual-dom-rs",
|
||||
"packages/virtual-node",
|
||||
]
|
42
README.md
Normal file
42
README.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Dioxus: A concurrent, functional, arena-allocated VDOM implementation for creating UIs in Rust
|
||||
|
||||
Dioxus is a new approach for creating performant cross platform user experiences in Rust. In Dioxus, the UI is represented by a tree of Virtual Nodes not bound to any renderer framework. Instead, external renderers can leverage Dioxus' virtual DOM and event system as a source of truth for rendering to a medium of their choice. Developers used to crafting react-based experiences should feel comfortable with Dioxus.
|
||||
|
||||
## Hello World
|
||||
Dioxus should look and feel just like writing functional React components. In Dioxus, there are no class components with lifecycles. All state management is done via hooks. This encourages logic resusability and lessens the burden on Dioxus to maintain a non-breaking lifecycle API.
|
||||
|
||||
```rust
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct MyProps {
|
||||
name: String
|
||||
}
|
||||
|
||||
fn Example(ctx: Context<MyProps>) -> VNode {
|
||||
html! { <div> "Hello {ctx.props().name}!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
To build user interfaces, you must provide a way of creating VNodes. We provide a macro `dioxus-rsx` which makes it easy to drop in html templates and event listeners to make interactive user experiences.
|
||||
|
||||
Inspired by React's Concurrent Mode, components in Dioxus are asynchronous by default. When components need to load asynchronous data, their rendering will be halted until ready, leading to fewer DOM updates and greater performance. External crates can tap into this system using futures to craft useful transition-based hooks.
|
||||
|
||||
Rules of Dioxus:
|
||||
- Every component is asynchronous
|
||||
- Components will queued when completed
|
||||
-
|
||||
|
||||
Dioxus supports:
|
||||
- Hooks
|
||||
- Concurrent rendering
|
||||
- Context subscriptions
|
||||
|
||||
|
16
docs/1-hello-world.md
Normal file
16
docs/1-hello-world.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Hello, World!
|
||||
|
||||
Dioxus should look and feel just like writing functional React components. In Dioxus, there are no class components with lifecycles. All state management is done via hooks. This encourages logic resusability and lessens the burden on Dioxus to maintain a non-breaking lifecycle API.
|
||||
|
||||
```rust
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct MyProps {
|
||||
name: String
|
||||
}
|
||||
|
||||
fn Example(ctx: Context<MyProps>) -> VNode {
|
||||
html! { <div> "Hello {ctx.props().name}!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
For functions to be valid components, they must take the `Context` object which is generic over some properties. The properties parameter must implement the `Properties` trait, which can be automatically derived. Whenever the input properties of a component changes, the function component will be re-ran and a new set of VNodes will be generated.
|
84
docs/10-concurrent-mode.md
Normal file
84
docs/10-concurrent-mode.md
Normal file
|
@ -0,0 +1,84 @@
|
|||
# Concurrent mode
|
||||
|
||||
Concurrent mode provides a mechanism for building efficient asynchronous components. With this feature, components don't need to render immediately, and instead can schedule a future render by returning a future.
|
||||
|
||||
To make a component asynchronous, simply change its function signature to async.
|
||||
|
||||
```rust
|
||||
fn Example(ctx: Context<()>) -> Vnode {
|
||||
rsx!{ <div> "Hello world!" </div> }
|
||||
}
|
||||
```
|
||||
becomes
|
||||
|
||||
```rust
|
||||
async fn Example(ctx: Context<()>) -> Vnode {
|
||||
rsx!{ <div> "Hello world!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
Now, logic in components can be awaited to delay updates of the component and its children. Like so:
|
||||
|
||||
```rust
|
||||
async fn Example(ctx: Context<()>) -> Vnode {
|
||||
let name = fetch_name().await;
|
||||
rsx!{ <div> "Hello {name}" </div> }
|
||||
}
|
||||
|
||||
async fetch_name() -> String {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This component will only schedule its render once the fetch is complete. However, we *don't* recommend using async/await directly in your components.
|
||||
|
||||
Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, and cause confusion.
|
||||
|
||||
Instead, we suggest using hooks and future combinators that can safely utilize the safe guards of the component's Context when interacting with async tasks.
|
||||
|
||||
As part of our Dioxus hooks crate, we provide a data loader hook which pauses a component until its async dependencies are ready. This caches requests, reruns the fetch if dependencies have changed, and provides the option to render something else while the component is loading.
|
||||
|
||||
```rust
|
||||
async fn ExampleLoader(ctx: Context<()>) -> Vnode {
|
||||
/*
|
||||
Fetch, pause the component from rendering at all.
|
||||
|
||||
The component is locked while waiting for the request to complete
|
||||
While waiting, an alternate component is scheduled in its place.
|
||||
|
||||
This API stores the result on the Context object, so the loaded data is taken as reference.
|
||||
*/
|
||||
let name: &Result<SomeStructure> = use_fetch_data("http://example.com/json", ())
|
||||
.place_holder(|ctx| rsx!{<div> "loading..." </div>})
|
||||
.delayed_place_holder(1000, |ctx| rsx!{ <div> "still loading..." </div>})
|
||||
.await;
|
||||
|
||||
match name {
|
||||
Ok(name) => rsx! { <div> "Hello {something}" </div> },
|
||||
Err(e) => rsx! { <div> "An error occured :(" </div>}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
```rust
|
||||
async fn Example(ctx: Context<()>) -> VNode {
|
||||
// Diff this set between the last set
|
||||
// Check if we have any outstanding tasks?
|
||||
//
|
||||
// Eventually, render the component into the VDOM when the future completes
|
||||
<div>
|
||||
<Example />
|
||||
</div>
|
||||
|
||||
// Render a div, queue a component
|
||||
// Render the placeholder first, then when the component is ready, then render the component
|
||||
<div>
|
||||
<Suspense placeholder={html!{<div>"Loading"</div>}}>
|
||||
<Example />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
```
|
56
docs/11-arena-memo.md
Normal file
56
docs/11-arena-memo.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Memoization and the arena allocator
|
||||
|
||||
Dioxus differs slightly from other UI virtual doms in some subtle ways due to its memory allocator.
|
||||
|
||||
One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to any and allocated on the heap - not in the arena with the components.
|
||||
|
||||
With this system, we try to be more efficient when leaving the component arena and entering the heap. By default, props are memoized between renders using COW and context. This makes props comparisons fast - done via ptr comparisons on the cow pointer. Because memoization is done by default, parent re-renders will *not* cascade to children if the child's props did not change.
|
||||
|
||||
https://dmitripavlutin.com/use-react-memo-wisely/
|
||||
|
||||
This behavior is defined as an implicit attribute to user components. When in React land you might wrap a component is `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization.
|
||||
|
||||
```rust
|
||||
fn test() -> VNode {
|
||||
html! {
|
||||
<>
|
||||
<SomeComponent nomemo />
|
||||
// same as
|
||||
<SomeComponent nomemo=true />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
static TestComponent: FC<()> = |ctx| html!{<div>"Hello world"</div>};
|
||||
|
||||
static TestComponent: FC<()> = |ctx| {
|
||||
let g = "BLAH";
|
||||
html! {
|
||||
<div> "Hello world" </div>
|
||||
}
|
||||
};
|
||||
|
||||
#[functional_component]
|
||||
static TestComponent: FC<{ name: String }> = |ctx| html! { <div> "Hello {name}" </div> };
|
||||
```
|
||||
|
||||
## Why this behavior?
|
||||
|
||||
"This is different than React, why differ?".
|
||||
|
||||
Take a component likes this:
|
||||
|
||||
```rust
|
||||
fn test(ctx: Context<()>) -> VNode {
|
||||
let Bundle { alpha, beta, gamma } = use_context::<SomeContext>(ctx);
|
||||
html! {
|
||||
<div>
|
||||
<Component name=alpha />
|
||||
<Component name=beta />
|
||||
<Component name=gamma />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
While the contents of the destructued bundle might change, not every child component will need to be re-rendered.
|
58
docs/2-utilites.md
Normal file
58
docs/2-utilites.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Utilities
|
||||
|
||||
There are a few macros and utility functions that make life easier when writing Dioxus components.
|
||||
|
||||
|
||||
## The `functional_component` procedural macro
|
||||
The `functional_component` proc macro allows you to inline props into the generic parameter of the component's context. This is useful When writing "pure" components, or when you don't want the extra clutter of structs, derives, and burden of naming things.
|
||||
|
||||
This macro allows allows a classic struct definition to be embedded directly into the function arguments. The props are automatically pulled from the context and destructured into the function's body, saving an extra step.
|
||||
|
||||
```rust
|
||||
// Inlines and destructure props *automatically*
|
||||
#[functional_component]
|
||||
fn Example(ctx: &mut Context<{
|
||||
name: String
|
||||
pending: bool
|
||||
count: i32
|
||||
}>) -> VNode {
|
||||
html! {
|
||||
<div>
|
||||
<p> "Hello, {name}!" </p>
|
||||
<p> "Status: {pending}!" </p>
|
||||
<p> "Count {count}!" </p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
becomes this:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Properties, PartialEq)]
|
||||
struct ExampleProps {
|
||||
name: String
|
||||
pending: bool
|
||||
count: i32
|
||||
};
|
||||
|
||||
fn Example(ctx: &mut Context<ExampleProps>) -> VNode {
|
||||
let ExampleProps {
|
||||
name, pending, count
|
||||
} = ctx.props();
|
||||
|
||||
rsx! {
|
||||
<div>
|
||||
<p> "Hello, {name}!" </p>
|
||||
<p> "Status: {pending}!" </p>
|
||||
<p> "Count {count}!" </p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## The rsx! macro
|
||||
|
||||
The rsx! macacro is similar to the html! macro in other libraries, but with a few add-ons to make it fun and easy to work with. We'll cover the rsx macro more in depth in the [vnode-macro](3-vnode-macros.md) chapter.
|
||||
|
1
docs/3-vnode-macros.md
Normal file
1
docs/3-vnode-macros.md
Normal file
|
@ -0,0 +1 @@
|
|||
#
|
17
docs/4-hooks.md
Normal file
17
docs/4-hooks.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
```rust
|
||||
fn Example(ctx: &mut Context<()>) -> VNode {
|
||||
let service = use_combubulator(ctx);
|
||||
let Status { name, pending, count } = service.info();
|
||||
html! {
|
||||
<div>
|
||||
<p> "Hello, {name}!" </p>
|
||||
<p> "Status: {pending}!" </p>
|
||||
<p> "Count {count}!" </p>
|
||||
<button onclick=|_| service.update()>
|
||||
"Refresh services"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
47
docs/5-context-api.md
Normal file
47
docs/5-context-api.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Context API
|
||||
|
||||
|
||||
|
||||
```rust
|
||||
// Create contexts available to children
|
||||
// Only one context can be associated with any given component
|
||||
// This is known as "exposed state". Children can access this context,
|
||||
// but will not be automatically subscribed.
|
||||
fn ContextCreate(ctx: &mut Context<()>) -> VNode {
|
||||
let context = ctx.set_context(|| CustomContext::new());
|
||||
html! { <> {ctx.children()} </> }
|
||||
}
|
||||
|
||||
fn ContextRead(ctx: &mut Context<()>) -> VNode {
|
||||
// Panics if context is not available
|
||||
let some_ctx = ctx.get_context::<CustomContext>();
|
||||
let text = some_ctx.select("some_selector");
|
||||
html! { <div> "{text}" </div> }
|
||||
}
|
||||
|
||||
fn Subscription(ctx: &mut Context<()>) -> VNode {
|
||||
// Open a "port" on the component for actions to trigger a re-evaluation
|
||||
let subscription = ctx.new_subscription();
|
||||
|
||||
// A looping timer - the effect is re-called on every re-evaluation
|
||||
use_async_effect(ctx, move || async {
|
||||
timer::new(2000).await;
|
||||
subscription.call();
|
||||
}, None);
|
||||
|
||||
// A one-shot timer, the deps don't change so the effect only happens once
|
||||
use_async_effect_deps(ctx, move || async {
|
||||
timer::new(2000).await;
|
||||
subscription.call();
|
||||
}, ());
|
||||
}
|
||||
|
||||
// Mix subscriptions and context to make a simple Redux
|
||||
fn use_global_state<T: UserContextTrait>(ctx: &mut Context<()>) -> T {
|
||||
let some_ctx = ctx.get_context::<T>();
|
||||
let component_subscription = ctx.new_subscription();
|
||||
some_ctx.subscribe_component(component_subscription);
|
||||
some_ctx
|
||||
}
|
||||
|
||||
```
|
0
docs/6-subscription-api.md
Normal file
0
docs/6-subscription-api.md
Normal file
0
docs/7-state-management.md
Normal file
0
docs/7-state-management.md
Normal file
1
docs/8-custom-renderer.md
Normal file
1
docs/8-custom-renderer.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Custom Renderer
|
0
docs/9-interactivity.md
Normal file
0
docs/9-interactivity.md
Normal file
11
packages/core/Cargo.toml
Normal file
11
packages/core/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.1.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
generational-arena = "0.2.8"
|
||||
typed-arena = "2.0.1"
|
22
packages/core/README.md
Normal file
22
packages/core/README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# 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.
|
||||
|
||||
To focus on building an effective user experience,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Sources
|
||||
---
|
||||
percy: https://github.com/chinedufn/percy/blob/master/crates/virtual-dom-rs/src/patch/mod.rs
|
||||
yew:
|
||||
dodrio:
|
||||
rsx:
|
||||
|
||||
react:
|
||||
fre:
|
||||
|
||||
|
1
packages/core/src/lib.rs
Normal file
1
packages/core/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
243
packages/core/src/old.rs
Normal file
243
packages/core/src/old.rs
Normal file
|
@ -0,0 +1,243 @@
|
|||
mod old {
|
||||
|
||||
// #![feature(type_alias_impl_trait)]
|
||||
//
|
||||
use std::future::Future;
|
||||
|
||||
trait Props {}
|
||||
struct Context<T: Props> {
|
||||
_props: std::marker::PhantomData<T>,
|
||||
}
|
||||
struct VNode {}
|
||||
|
||||
// type FC<T: Props> = fn(&mut Context<T>) -> VNode;
|
||||
// type FC<T: Props> = fn(&mut Context<T>) -> Box<dyn Future<Output = VNode>>;
|
||||
|
||||
impl Props for () {}
|
||||
|
||||
// async fn some_component(g: &mut Context<()>) -> VNode {
|
||||
// rsx! {
|
||||
// <div>
|
||||
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
// Absolve ourselves of any type data about the context itself
|
||||
trait ContextApplier {
|
||||
fn use_hook<O, H>(
|
||||
&mut self,
|
||||
initializer: impl FnOnce() -> H,
|
||||
runner: impl Fn(&mut H) -> O,
|
||||
tear_down: impl Fn(&mut H),
|
||||
) -> O;
|
||||
}
|
||||
impl<T: Props> ContextApplier for Context<T> {
|
||||
fn use_hook<O, H>(
|
||||
&mut self,
|
||||
initializer: impl FnOnce() -> H,
|
||||
runner: impl Fn(&mut H) -> O,
|
||||
tear_down: impl Fn(&mut H),
|
||||
) -> O {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn use_state<T>(c: &mut impl ContextApplier, g: impl Fn() -> T) -> T {
|
||||
c.use_hook(|| {}, |_| {}, |_| {});
|
||||
g()
|
||||
}
|
||||
|
||||
enum SomeComponent {
|
||||
Imperative,
|
||||
Async,
|
||||
}
|
||||
|
||||
// impl<F, G> From<F> for SomeComponent
|
||||
// where
|
||||
// F: Fn() -> G,
|
||||
// G: Future<Output = ()>,
|
||||
// {
|
||||
// fn from(_: F) -> Self {
|
||||
// SomeComponent::Async
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<fn() -> ()> for SomeComponent {
|
||||
// fn from(_: F) -> Self {
|
||||
// SomeComponent::Async
|
||||
// }
|
||||
// }
|
||||
// impl<F> Into<SomeComponent> for fn() -> F
|
||||
// where
|
||||
// F: Future<Output = ()>,
|
||||
// {
|
||||
// fn into(self) -> SomeComponent {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test() {
|
||||
// let b: SomeComponent = test_comp.into();
|
||||
// }
|
||||
|
||||
// Does this make sense?
|
||||
// Any component labeled with async can halt its rendering, but won't be able to process updates?
|
||||
// Or, those updates can still happen virtually, just not propogated into the view?
|
||||
// async fn test_comp() -> () {
|
||||
// timer::new(300).await;
|
||||
// html! {
|
||||
// <div>
|
||||
// "hello world!"
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn use_state<T: Props>(c: &mut Context<T>) {}
|
||||
|
||||
// async fn another_component(ctx: &mut Context<()>) -> VNode {
|
||||
// // delay the re-render until component when the future is ready
|
||||
// // "use_future" loads the promise and provides a value (aka a loadable)
|
||||
// let value = use_effect(move || async {
|
||||
// get_value().join(timer::new(300));
|
||||
// set_value(blah);
|
||||
// });
|
||||
|
||||
// rsx! {
|
||||
// <Suspense fallback={<div>"Loading..."</div>}>
|
||||
// <div>
|
||||
// "hello {name}!"
|
||||
// </div>
|
||||
// <Suspense />
|
||||
// }
|
||||
// }
|
||||
|
||||
/*
|
||||
|
||||
Rationale
|
||||
Today, you can do use_async and do some async operations,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
// type FC<P: Props> = fn(&mut Context<P>) -> VNode;
|
||||
|
||||
// static Example: FC<()> = |_| async {
|
||||
// // some async work
|
||||
// };
|
||||
|
||||
// type FC2 = fn() -> impl Future<Output = ()>;
|
||||
// struct fc<P: Props>(fn(&mut Context<P>) -> G);
|
||||
// fn blah<P: Props, G: Future<Output = VNode>>(a: fn(&mut Context<P>) -> G) {}
|
||||
|
||||
// static Example2: FC2<()> = fc(|_| async { VNode {} });
|
||||
// static Example2: () = blah(|_: &mut Context<()>| async { VNode {} });
|
||||
|
||||
// static Example: FC<()> = |_| {
|
||||
// let g = async { VNode {} };
|
||||
// Box::new(g)
|
||||
// };
|
||||
|
||||
// static Example2: = || {};
|
||||
|
||||
// type FA<R: Future<Output = i32>> = fn(i32) -> R;
|
||||
|
||||
// async fn my_component()
|
||||
// static MyThing: FA<dyn Future<Output = i32>> = |_| async { 10 };
|
||||
|
||||
// type SomeFn = fn() -> ();
|
||||
|
||||
// static MyFn: SomeFn = || {};
|
||||
}
|
||||
|
||||
mod old2 {
|
||||
mod vdom {
|
||||
//! Virtual DOM implementation
|
||||
use super::*;
|
||||
|
||||
pub struct VDom {
|
||||
patches: Vec<Patch>,
|
||||
}
|
||||
|
||||
impl VDom {
|
||||
// fn new(root: ComponentFn) -> Self {
|
||||
// let scope = Scope::new();
|
||||
// Self {}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
mod nodes {}
|
||||
|
||||
mod patch {}
|
||||
|
||||
mod scope {
|
||||
//! Wrappers around components
|
||||
|
||||
pub struct Scope {}
|
||||
|
||||
impl Scope {
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod context {}
|
||||
|
||||
struct EventListener {}
|
||||
|
||||
struct VNode {
|
||||
/// key-value pairs of attributes
|
||||
attributes: Vec<(&'static str, &'static str)>,
|
||||
|
||||
/// onclick/onhover/on etc listeners
|
||||
/// goal is to standardize around a set of cross-platform listeners?
|
||||
listeners: Vec<EventListener>,
|
||||
|
||||
/// Direct children, non arena-allocated
|
||||
children: Vec<VNode>,
|
||||
}
|
||||
|
||||
enum ElementType {
|
||||
div,
|
||||
p,
|
||||
a,
|
||||
img,
|
||||
}
|
||||
|
||||
struct ComponentContext {}
|
||||
type ComponentFn = fn(ctx: &ComponentContext) -> VNode;
|
||||
|
||||
enum Patch {}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Ensure components can be made from the raw components
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
fn component(ctx: &ComponentContext) -> VNode {
|
||||
println!("Running component");
|
||||
VNode {}
|
||||
}
|
||||
|
||||
let dom = VDom::new(component);
|
||||
}
|
||||
|
||||
/// Ensure components can be made from the raw components
|
||||
#[test]
|
||||
fn simple_test_closure() {
|
||||
let component: ComponentFn = |ctx| {
|
||||
println!("Running component");
|
||||
VNode {}
|
||||
};
|
||||
|
||||
let dom = VDom::new(component);
|
||||
}
|
||||
}
|
||||
}
|
9
packages/hooks/Cargo.toml
Normal file
9
packages/hooks/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "dioxus-hooks"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
7
packages/hooks/src/lib.rs
Normal file
7
packages/hooks/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
11
packages/html-macro-test/Cargo.toml
Normal file
11
packages/html-macro-test/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "html-macro-test"
|
||||
version = "0.1.0"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
html-macro = {path = "../html-macro"}
|
||||
virtual-dom-rs = { path = "../virtual-dom-rs" }
|
||||
virtual-node = {path = "../virtual-node"}
|
||||
trybuild = "1.0"
|
3
packages/html-macro-test/README.md
Normal file
3
packages/html-macro-test/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# html-macro-test
|
||||
|
||||
Unit tests for the `html!` macro
|
13
packages/html-macro-test/src/lib.rs
Normal file
13
packages/html-macro-test/src/lib.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
//! Tests for our html! procedural macro
|
||||
//!
|
||||
//! To run all tests in this library:
|
||||
//!
|
||||
//! cargo test --color=always --package html-macro-test --lib "" -- --nocapture
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
// TODO: Deny warnings to ensure that the macro isn't creating any warnings.
|
||||
// #![deny(warnings)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
3
packages/html-macro-test/src/tests.rs
Normal file
3
packages/html-macro-test/src/tests.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod all_tests;
|
||||
mod text;
|
||||
mod ui;
|
377
packages/html-macro-test/src/tests/all_tests.rs
Normal file
377
packages/html-macro-test/src/tests/all_tests.rs
Normal file
|
@ -0,0 +1,377 @@
|
|||
//! This is a catch-all module to place new tests as we go.
|
||||
//!
|
||||
//! Over time we'll pull tests out of here and organize them.
|
||||
//!
|
||||
//! For example - there is a `text_tests.rs` module where all of our text node related
|
||||
//! tests live.
|
||||
|
||||
use html_macro::html;
|
||||
use std::collections::HashMap;
|
||||
use virtual_node::{IterableNodes, VElement, VText, View, VirtualNode};
|
||||
|
||||
struct HtmlMacroTest {
|
||||
generated: VirtualNode,
|
||||
expected: VirtualNode,
|
||||
}
|
||||
|
||||
impl HtmlMacroTest {
|
||||
/// Ensure that the generated and the expected virtual node are equal.
|
||||
fn test(self) {
|
||||
assert_eq!(self.expected, self.generated);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_div() {
|
||||
HtmlMacroTest {
|
||||
generated: html! { <div></div> },
|
||||
expected: VirtualNode::element("div"),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_attr() {
|
||||
let mut attrs = HashMap::new();
|
||||
attrs.insert("id".to_string(), "hello-world".to_string());
|
||||
let mut expected = VElement::new("div");
|
||||
expected.attrs = attrs;
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! { <div id="hello-world"></div> },
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
/// Events are ignored in non wasm-32 targets
|
||||
#[test]
|
||||
fn ignore_events_on_non_wasm32_targets() {
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div onclick=|_: u8|{}></div>
|
||||
},
|
||||
expected: html! {<div></div>},
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_node() {
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::element("span")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! { <div><span></span></div> },
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_child_nodes() {
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::element("span"), VirtualNode::element("b")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! { <div><span></span><b></b></div> },
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
/// Nested 3 nodes deep
|
||||
#[test]
|
||||
fn three_nodes_deep() {
|
||||
let mut child = VElement::new("span");
|
||||
child.children = vec![VirtualNode::element("b")];
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![child.into()];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! { <div><span><b></b></span></div> },
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_text_nodes() {
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::text("This is a text node")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! { <div>This is a text node</div> },
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_macro() {
|
||||
let child_2 = html! { <b></b> };
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::element("span"), VirtualNode::element("b")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>
|
||||
{ html! { <span></span> } }
|
||||
{ child_2 }
|
||||
</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
/// If the first thing we see is a block then we grab whatever is inside it.
|
||||
#[test]
|
||||
fn block_root() {
|
||||
let em = html! { <em></em> };
|
||||
|
||||
let expected = VirtualNode::element("em");
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
{ em }
|
||||
},
|
||||
expected,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
/// Text followed by a block
|
||||
#[test]
|
||||
fn text_next_to_block() {
|
||||
let child = html! { <ul></ul> };
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![
|
||||
VirtualNode::text(" A bit of text "),
|
||||
VirtualNode::element("ul"),
|
||||
];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>
|
||||
A bit of text
|
||||
{ child }
|
||||
</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
/// Ensure that we maintain the correct spacing around punctuation tokens, since
|
||||
/// they resolve into a separate TokenStream during parsing.
|
||||
#[test]
|
||||
fn punctuation_token() {
|
||||
let text = "Hello, World";
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! { Hello, World },
|
||||
expected: VirtualNode::text(text),
|
||||
}
|
||||
.test()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vec_of_nodes() {
|
||||
let children = vec![html! { <div> </div>}, html! { <strong> </strong>}];
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::element("div"), VirtualNode::element("strong")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! { <div> { children } </div> },
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
/// Just make sure that this compiles since async, for, loop, and type are keywords
|
||||
#[test]
|
||||
fn keyword_attribute() {
|
||||
html! { <script src="/app.js" async="async" /> };
|
||||
html! { <label for="username">Username:</label> };
|
||||
html! { <audio loop="loop"><source src="/beep.mp3" type="audio/mpeg" /></audio> };
|
||||
html! { <link rel="stylesheet" type="text/css" href="/app.css" /> };
|
||||
}
|
||||
|
||||
/// For unquoted text apostrophes should be parsed correctly
|
||||
#[test]
|
||||
fn apostrophe() {
|
||||
assert_eq!(html! { Aren't }, VText::new("Aren't").into());
|
||||
assert_eq!(html! { Aren'ttt }, VText::new("Aren'ttt").into());
|
||||
}
|
||||
|
||||
/// Verify that all of our self closing tags work without backslashes.
|
||||
#[test]
|
||||
fn self_closing_tag_without_backslash() {
|
||||
let mut expected = VElement::new("div");
|
||||
let children = vec![
|
||||
"area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "command",
|
||||
"keygen", "source",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|tag| VirtualNode::element(tag))
|
||||
.collect();
|
||||
expected.children = children;
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>
|
||||
<area> <base> <br> <col> <hr> <img> <input> <link> <meta> <param> <command>
|
||||
<keygen> <source>
|
||||
</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
/// Verify that our self closing tags work with backslashes
|
||||
#[test]
|
||||
fn self_closing_tag_with_backslace() {
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<br />
|
||||
},
|
||||
expected: VirtualNode::element("br"),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_true_block() {
|
||||
let child_valid = html! { <b></b> };
|
||||
let child_invalid = html! { <i></i> };
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::element("b")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>
|
||||
{if true {child_valid} else {child_invalid}}
|
||||
</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_false_block() {
|
||||
let child_valid = html! { <b></b> };
|
||||
let child_invalid = html! { <i></i> };
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::element("i")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>
|
||||
{if false {
|
||||
child_valid
|
||||
} else {
|
||||
child_invalid
|
||||
}}
|
||||
</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_branch_if_true_block() {
|
||||
let child_valid = html! { <b></b> };
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::element("b")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>{if true {child_valid}}</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_branch_if_false_block() {
|
||||
let child_valid = html! { <b></b> };
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
expected.children = vec![VirtualNode::text("")];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>{if false {child_valid}}</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_component_props() {
|
||||
struct Counter {
|
||||
count: u8,
|
||||
}
|
||||
|
||||
impl View for Counter {
|
||||
fn render(&self) -> VirtualNode {
|
||||
html! {
|
||||
<span>Counter = {format!("{}", self.count)}</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
let mut child = VElement::new("span");
|
||||
child.children = vec![VirtualNode::text("Counter = "), VirtualNode::text("1")];
|
||||
expected.children = vec![child.into()];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div><Counter count={1}/></div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_component_children() {
|
||||
struct Child;
|
||||
|
||||
impl View for Child {
|
||||
fn render(&self) -> VirtualNode {
|
||||
html! {
|
||||
<span></span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut expected = VElement::new("div");
|
||||
let mut child = VElement::new("span");
|
||||
child.children = vec![VirtualNode::text("Hello World")];
|
||||
expected.children = vec![child.into()];
|
||||
|
||||
HtmlMacroTest {
|
||||
generated: html! {
|
||||
<div>
|
||||
<Child>Hello World</Child>
|
||||
</div>
|
||||
},
|
||||
expected: expected.into(),
|
||||
}
|
||||
.test();
|
||||
}
|
171
packages/html-macro-test/src/tests/text.rs
Normal file
171
packages/html-macro-test/src/tests/text.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use html_macro::html;
|
||||
use virtual_node::{IterableNodes, VirtualNode};
|
||||
|
||||
#[test]
|
||||
fn text_root_node() {
|
||||
assert_eq!(&html! { some text }.to_string(), "some text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_variable_root() {
|
||||
let text = "hello world";
|
||||
|
||||
assert_eq!(&html! { { text } }.to_string(), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_string_literal() {
|
||||
assert_eq!(
|
||||
&html! { <div>{ r#"Hello World"# }</div> }.to_string(),
|
||||
"<div>Hello World</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_variable_child() {
|
||||
let text = "world";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div>{ text }</div> }.to_string(),
|
||||
"<div>world</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_space_after_start_tag() {
|
||||
assert_eq!(
|
||||
&html! { <div> After Start Tag</div> }.to_string(),
|
||||
"<div> After Start Tag</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_space_before_end_tag() {
|
||||
assert_eq!(
|
||||
&html! { <div>Before End Tag </div> }.to_string(),
|
||||
"<div>Before End Tag </div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_space_before_block() {
|
||||
let text = "Before Block";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div> {text}</div> }.to_string(),
|
||||
"<div> Before Block</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_space_after_block() {
|
||||
let text = "Hello";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div>{text} </div> }.to_string(),
|
||||
"<div>Hello </div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_space_in_block_ignored() {
|
||||
let text = "Hello";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div>{ text }</div> }.to_string(),
|
||||
"<div>Hello</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_multiple_text_no_space_between() {
|
||||
let hello = "Hello";
|
||||
let world = "World";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div>{ hello }{ world }</div> }.to_string(),
|
||||
"<div>HelloWorld</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_multiple_text_space_between() {
|
||||
let hello = "Hello";
|
||||
let world = "World";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div>{ hello } { world }</div> }.to_string(),
|
||||
"<div>Hello World</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_multiple_text_space_around() {
|
||||
let hello = "Hello";
|
||||
let world = "World";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div> { hello }{ world } </div> }.to_string(),
|
||||
"<div> HelloWorld </div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_multiple_text_space_between_around() {
|
||||
let hello = "Hello";
|
||||
let world = "World";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div> { hello } { world } </div> }.to_string(),
|
||||
"<div> Hello World </div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_tokens_in_between_vars_without_space() {
|
||||
let hello = "Hello";
|
||||
let world = "World";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div>{ hello }NoSpace{ world }</div> }.to_string(),
|
||||
"<div>HelloNoSpaceWorld</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_tokens_in_between_vars_with_space() {
|
||||
let hello = "Hello";
|
||||
let world = "World";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div>{ hello } Space { world }</div> }.to_string(),
|
||||
"<div>Hello Space World</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_tokens_in_between_vars_space_around_between() {
|
||||
let hello = "Hello";
|
||||
let world = "World";
|
||||
|
||||
assert_eq!(
|
||||
&html! { <div> { hello } Space { world } </div> }.to_string(),
|
||||
"<div> Hello Space World </div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_space_before_next_open_tag() {
|
||||
assert_eq!(
|
||||
&html! { <div>Hello <img /> world</div> }.to_string(),
|
||||
"<div>Hello <img> world</div>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_no_space_before_open_tag() {
|
||||
assert_eq!(
|
||||
&html! { <div>Hello<img /> world</div> }.to_string(),
|
||||
"<div>Hello<img> world</div>"
|
||||
)
|
||||
}
|
7
packages/html-macro-test/src/tests/ui.rs
Normal file
7
packages/html-macro-test/src/tests/ui.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
||||
let ui_tests = concat!(env!("CARGO_MANIFEST_DIR"), "/src/tests/ui/*.rs");
|
||||
t.compile_fail(ui_tests);
|
||||
}
|
11
packages/html-macro-test/src/tests/ui/invalid_html_tag.rs
Normal file
11
packages/html-macro-test/src/tests/ui/invalid_html_tag.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate virtual_dom_rs;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
// Used a tag name that does not exist in the HTML spec
|
||||
fn main() {
|
||||
html! {
|
||||
<invalidtagname></invalidtagname>
|
||||
};
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
error: invalidtagname is not a valid HTML tag.
|
||||
|
||||
If you are trying to use a valid HTML tag, perhaps there's a typo?
|
||||
|
||||
If you are trying to use a custom component, please capitalize the component name.
|
||||
|
||||
custom components: https://chinedufn.github.io/percy/html-macro/custom-components/index.html
|
||||
--> $DIR/invalid_html_tag.rs:9:10
|
||||
|
|
||||
9 | <invalidtagname></invalidtagname>
|
||||
| ^^^^^^^^^^^^^^
|
|
@ -0,0 +1,12 @@
|
|||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate virtual_dom_rs;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
// We are using open and close tags for a tag that should
|
||||
// actually be a self closing tag
|
||||
fn main () {
|
||||
html! {
|
||||
<br></br>
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
error: br is a self closing tag. Try "<br>" or "<br />"
|
||||
--> $DIR/should_be_self_closing_tag.rs:10:15
|
||||
|
|
||||
10 | <br></br>
|
||||
| ^^
|
11
packages/html-macro-test/src/tests/ui/wrong_closing_tag.rs
Normal file
11
packages/html-macro-test/src/tests/ui/wrong_closing_tag.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate virtual_dom_rs;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
// Expected a closing div tag, found a closing strong tag
|
||||
fn main () {
|
||||
html! {
|
||||
<div> </strong>
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
error: Wrong closing tag. Try changing "strong" into "div"
|
||||
--> $DIR/wrong_closing_tag.rs:9:17
|
||||
|
|
||||
9 | <div> </strong>
|
||||
| ^^^^^^
|
19
packages/html-macro/Cargo.toml
Normal file
19
packages/html-macro/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "html-macro"
|
||||
version = "0.1.9"
|
||||
description = "html macro"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
documentation = "https://chinedufn.github.io/percy/api/html_macro/"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "0.4", features = ["span-locations"] }
|
||||
quote = "0.6.11"
|
||||
syn = { version = "0.15", features = ["full", "extra-traits"] }
|
||||
html-validation = {path = "../html-validation", version = "0.1.2"}
|
20
packages/html-macro/README.md
Normal file
20
packages/html-macro/README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# html-macro
|
||||
|
||||
```rust
|
||||
use html_macro::*;
|
||||
|
||||
fn main () {
|
||||
let component = html! { <div id='component'>Some component</div> };
|
||||
|
||||
let text_var = "You can interpolate text variables";
|
||||
|
||||
let html = html! {
|
||||
<div onclick=|_ev: web_sys::MouseEvent| {}>
|
||||
You can type text right into the elements
|
||||
{ component }
|
||||
{ text_var }
|
||||
</div>
|
||||
};
|
||||
println!("{}", node);
|
||||
}
|
||||
```
|
55
packages/html-macro/src/lib.rs
Normal file
55
packages/html-macro/src/lib.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
extern crate proc_macro;
|
||||
|
||||
use crate::parser::HtmlParser;
|
||||
use crate::tag::Tag;
|
||||
use syn::parse::{Parse, ParseStream, Result};
|
||||
use syn::parse_macro_input;
|
||||
|
||||
mod parser;
|
||||
mod tag;
|
||||
|
||||
/// Used to generate VirtualNode's from a TokenStream.
|
||||
///
|
||||
/// html! { <div> Welcome to the html! procedural macro! </div> }
|
||||
#[proc_macro]
|
||||
pub fn html(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let parsed = parse_macro_input!(input as Html);
|
||||
|
||||
let mut html_parser = HtmlParser::new();
|
||||
|
||||
let parsed_tags_len = parsed.tags.len();
|
||||
|
||||
// Iterate over all of our parsed tags and push them into our HtmlParser one by one.
|
||||
//
|
||||
// As we go out HtmlParser will maintain some heuristics about what we've done so far
|
||||
// since that will sometimes inform how to parse the next token.
|
||||
for (idx, tag) in parsed.tags.iter().enumerate() {
|
||||
let mut next_tag = None;
|
||||
|
||||
if parsed_tags_len - 1 > idx {
|
||||
next_tag = Some(&parsed.tags[idx + 1])
|
||||
}
|
||||
|
||||
html_parser.push_tag(tag, next_tag);
|
||||
}
|
||||
|
||||
html_parser.finish().into()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Html {
|
||||
tags: Vec<Tag>,
|
||||
}
|
||||
|
||||
impl Parse for Html {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut tags = Vec::new();
|
||||
|
||||
while !input.is_empty() {
|
||||
let tag: Tag = input.parse()?;
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
Ok(Html { tags })
|
||||
}
|
||||
}
|
102
packages/html-macro/src/parser/braced.rs
Normal file
102
packages/html-macro/src/parser/braced.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use crate::parser::HtmlParser;
|
||||
use crate::tag::{Tag, TagKind};
|
||||
use proc_macro2::Span;
|
||||
use quote::quote;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::Block;
|
||||
|
||||
impl HtmlParser {
|
||||
/// Parse an incoming Tag::Braced text node
|
||||
pub(crate) fn parse_braced(
|
||||
&mut self,
|
||||
block: &Box<Block>,
|
||||
brace_span: &Span,
|
||||
next_tag: Option<&Tag>,
|
||||
) {
|
||||
// We'll check to see if there is a space between this block and the previous open
|
||||
// tag's closing brace.
|
||||
//
|
||||
// If so we'll then check if the node in this block is a text node. If it is we'll
|
||||
// insert a single white space before it.
|
||||
//
|
||||
// let some_var = "hello"
|
||||
// let another_var = "world";
|
||||
//
|
||||
// html! { <span>{some_var}</span> } -> would not get a " " inserted
|
||||
//
|
||||
// html! { <span> {some_var}</span> } -> would get a " " inserted
|
||||
let mut insert_whitespace_before_text = false;
|
||||
if let Some(open_tag_end) = self.recent_span_locations.most_recent_open_tag_end.as_ref() {
|
||||
if self.last_tag_kind == Some(TagKind::Open)
|
||||
&& self.separated_by_whitespace(open_tag_end, brace_span)
|
||||
{
|
||||
insert_whitespace_before_text = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If
|
||||
// 1. The next tag is a closing tag or another braced block
|
||||
// 2. There is space between this brace and that next tag / braced block
|
||||
//
|
||||
// Then
|
||||
// We'll insert some spacing after this brace.
|
||||
//
|
||||
// This ensures that we properly maintain spacing between two neighboring braced
|
||||
// text nodes
|
||||
//
|
||||
// html! { <div>{ This Brace } { Space WILL be inserted }</div>
|
||||
// -> <div>This Brace Space WILL be inserted</div>
|
||||
//
|
||||
// html! { <div>{ This Brace }{ Space WILL NOT be inserted }</div>
|
||||
// -> <div>This BraceSpace WILL NOT be inserted</div>
|
||||
let insert_whitespace_after_text = match next_tag {
|
||||
Some(Tag::Close {
|
||||
first_angle_bracket_span,
|
||||
..
|
||||
}) => self.separated_by_whitespace(brace_span, &first_angle_bracket_span),
|
||||
Some(Tag::Braced {
|
||||
brace_span: next_brace_span,
|
||||
..
|
||||
}) => self.separated_by_whitespace(brace_span, &next_brace_span),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// TODO: Only allow one statement per block. Put a quote_spanned! compiler error if
|
||||
// there is more than 1 statement. Add a UI test for this.
|
||||
block.stmts.iter().for_each(|stmt| {
|
||||
if self.current_node_idx == 0 {
|
||||
// Here we handle a block being the root node of an `html!` call
|
||||
//
|
||||
// html { { some_node } }
|
||||
let node = quote! {
|
||||
let node_0: VirtualNode = #stmt.into();
|
||||
};
|
||||
self.push_tokens(node);
|
||||
} else {
|
||||
self.parse_statement(stmt);
|
||||
|
||||
if insert_whitespace_before_text {
|
||||
let node = self.current_virtual_node_ident(stmt.span());
|
||||
|
||||
let insert_whitespace = quote! {
|
||||
#node.first().insert_space_before_text();
|
||||
};
|
||||
|
||||
self.push_tokens(insert_whitespace);
|
||||
}
|
||||
|
||||
if insert_whitespace_after_text {
|
||||
let node = self.current_virtual_node_ident(stmt.span());
|
||||
|
||||
let insert_whitespace = quote! {
|
||||
#node.last().insert_space_after_text();
|
||||
};
|
||||
|
||||
self.push_tokens(insert_whitespace);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.set_most_recent_block_start(brace_span.clone());
|
||||
}
|
||||
}
|
49
packages/html-macro/src/parser/close_tag.rs
Normal file
49
packages/html-macro/src/parser/close_tag.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use crate::parser::{is_self_closing, HtmlParser};
|
||||
use proc_macro2::Ident;
|
||||
use quote::quote_spanned;
|
||||
|
||||
impl HtmlParser {
|
||||
/// Parse an incoming Tag::Close
|
||||
pub(crate) fn parse_close_tag(&mut self, name: &Ident) {
|
||||
let parent_stack = &mut self.parent_stack;
|
||||
|
||||
let close_span = name.span();
|
||||
let close_tag = name.to_string();
|
||||
|
||||
// For example, this should have been <br /> instead of </br>
|
||||
if is_self_closing(&close_tag) {
|
||||
let error = format!(
|
||||
r#"{} is a self closing tag. Try "<{}>" or "<{} />""#,
|
||||
close_tag, close_tag, close_tag
|
||||
);
|
||||
let error = quote_spanned! {close_span=> {
|
||||
compile_error!(#error);
|
||||
}};
|
||||
|
||||
self.push_tokens(error);
|
||||
return;
|
||||
}
|
||||
|
||||
let last_open_tag = parent_stack.pop().expect("Last open tag");
|
||||
|
||||
let last_open_tag = last_open_tag.1.to_string();
|
||||
|
||||
// TODO: 2 compile_error!'s one pointing to the open tag and one pointing to the
|
||||
// closing tag. Update the ui test accordingly
|
||||
//
|
||||
// ex: if div != strong
|
||||
if last_open_tag != close_tag {
|
||||
let error = format!(
|
||||
r#"Wrong closing tag. Try changing "{}" into "{}""#,
|
||||
close_tag, last_open_tag
|
||||
);
|
||||
|
||||
let error = quote_spanned! {close_span=> {
|
||||
compile_error!(#error);
|
||||
}};
|
||||
// TODO: Abort early if we find an error. So we should be returning
|
||||
// a Result.
|
||||
self.push_tokens(error);
|
||||
}
|
||||
}
|
||||
}
|
270
packages/html-macro/src/parser/mod.rs
Normal file
270
packages/html-macro/src/parser/mod.rs
Normal file
|
@ -0,0 +1,270 @@
|
|||
use crate::tag::TagKind;
|
||||
use crate::Tag;
|
||||
use quote::{quote, quote_spanned};
|
||||
use std::collections::HashMap;
|
||||
use syn::export::Span;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{Ident, Stmt};
|
||||
|
||||
mod braced;
|
||||
mod close_tag;
|
||||
mod open_tag;
|
||||
mod statement;
|
||||
mod text;
|
||||
|
||||
pub enum NodesToPush<'a> {
|
||||
Stmt(&'a Stmt),
|
||||
TokenStream(&'a Stmt, proc_macro2::TokenStream),
|
||||
}
|
||||
|
||||
/// Used to parse [`Tag`]s that we've parsed and build a tree of `VirtualNode`s
|
||||
///
|
||||
/// [`Tag`]: enum.Tag.html
|
||||
pub struct HtmlParser {
|
||||
/// As we parse our macro tokens we'll generate new tokens to return back into the compiler
|
||||
/// when we're done.
|
||||
tokens: Vec<proc_macro2::TokenStream>,
|
||||
/// Everytime we encounter a new node we'll use the current_node_idx to name it.
|
||||
/// Then we'll increment the current_idx by one.
|
||||
/// This gives every node that we encounter a unique name that we can use to find
|
||||
/// it later when we want to push child nodes into parent nodes
|
||||
current_node_idx: usize,
|
||||
/// The order that we encountered nodes while parsing.
|
||||
node_order: Vec<usize>,
|
||||
/// Each time we encounter a new node that could possible be a parent node
|
||||
/// we push it's node index onto the stack.
|
||||
///
|
||||
/// Text nodes cannot be parent nodes.
|
||||
parent_stack: Vec<(usize, Ident)>,
|
||||
/// Key -> index of the parent node within the HTML tree
|
||||
/// Value -> vector of child node indices
|
||||
parent_to_children: HashMap<usize, Vec<usize>>,
|
||||
/// The locations of the most recent spans that we parsed.
|
||||
/// Used to determine whether or not to put space around text nodes.
|
||||
recent_span_locations: RecentSpanLocations,
|
||||
/// The last kind of tag that we parsed.
|
||||
/// Used to determine whether or not to put space around text nodes.
|
||||
last_tag_kind: Option<TagKind>,
|
||||
}
|
||||
|
||||
/// TODO: I've hit a good stopping point... but we can clean these methods up / split them up
|
||||
/// a bit...
|
||||
impl HtmlParser {
|
||||
/// Create a new HtmlParser
|
||||
pub fn new() -> HtmlParser {
|
||||
let mut parent_to_children: HashMap<usize, Vec<usize>> = HashMap::new();
|
||||
parent_to_children.insert(0, vec![]);
|
||||
|
||||
HtmlParser {
|
||||
tokens: vec![],
|
||||
current_node_idx: 0,
|
||||
node_order: vec![],
|
||||
parent_stack: vec![],
|
||||
parent_to_children,
|
||||
recent_span_locations: RecentSpanLocations::default(),
|
||||
last_tag_kind: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the tokens for the incoming Tag and update our parser's heuristics that keep
|
||||
/// track of information about what we've parsed.
|
||||
pub fn push_tag(&mut self, tag: &Tag, next_tag: Option<&Tag>) {
|
||||
match tag {
|
||||
Tag::Open {
|
||||
name,
|
||||
attrs,
|
||||
closing_bracket_span,
|
||||
is_self_closing,
|
||||
..
|
||||
} => {
|
||||
self.parse_open_tag(name, closing_bracket_span, attrs, *is_self_closing);
|
||||
self.last_tag_kind = Some(TagKind::Open);
|
||||
}
|
||||
Tag::Close { name, .. } => {
|
||||
self.parse_close_tag(name);
|
||||
self.last_tag_kind = Some(TagKind::Close);
|
||||
}
|
||||
Tag::Text {
|
||||
text,
|
||||
start_span,
|
||||
end_span,
|
||||
} => {
|
||||
self.parse_text(text, start_span.unwrap(), end_span.unwrap(), next_tag);
|
||||
self.last_tag_kind = Some(TagKind::Text);
|
||||
}
|
||||
Tag::Braced { block, brace_span } => {
|
||||
self.parse_braced(block, brace_span, next_tag);
|
||||
self.last_tag_kind = Some(TagKind::Braced);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 1. Pop a node off the stack
|
||||
/// 2. Look up all of it's children in parent_to_children
|
||||
/// 3. Append the children to this node
|
||||
/// 4. Move on to the next node (as in, go back to step 1)
|
||||
pub fn finish(&mut self) -> proc_macro2::TokenStream {
|
||||
let node_order = &mut self.node_order;
|
||||
let parent_to_children = &mut self.parent_to_children;
|
||||
let tokens = &mut self.tokens;
|
||||
|
||||
if node_order.len() > 1 {
|
||||
for _ in 0..(node_order.len()) {
|
||||
let parent_idx = node_order.pop().unwrap();
|
||||
|
||||
// TODO: Figure out how to really use spans
|
||||
let parent_name =
|
||||
Ident::new(format!("node_{}", parent_idx).as_str(), Span::call_site());
|
||||
|
||||
let parent_to_children_indices = match parent_to_children.get(&parent_idx) {
|
||||
Some(children) => children,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if parent_to_children_indices.len() > 0 {
|
||||
for child_idx in parent_to_children_indices.iter() {
|
||||
let children =
|
||||
Ident::new(format!("node_{}", child_idx).as_str(), Span::call_site());
|
||||
|
||||
let unreachable = quote_spanned!(Span::call_site() => {
|
||||
unreachable!("Non-elements cannot have children");
|
||||
});
|
||||
|
||||
let push_children = quote! {
|
||||
if let Some(ref mut element_node) = #parent_name.as_velement_mut() {
|
||||
element_node.children.extend(#children.into_iter());
|
||||
} else {
|
||||
#unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
tokens.push(push_children);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a virtual node tree
|
||||
let node = quote! {
|
||||
{
|
||||
#(#tokens)*
|
||||
// Root node is always named node_0
|
||||
node_0
|
||||
}
|
||||
};
|
||||
node
|
||||
}
|
||||
|
||||
/// Add more tokens to our tokens that we'll eventually return to the compiler.
|
||||
fn push_tokens(&mut self, tokens: proc_macro2::TokenStream) {
|
||||
self.tokens.push(tokens);
|
||||
}
|
||||
|
||||
/// Set the location of the most recent start tag's ending LineColumn
|
||||
fn set_most_recent_open_tag_end(&mut self, span: Span) {
|
||||
self.recent_span_locations.most_recent_open_tag_end = Some(span);
|
||||
}
|
||||
|
||||
/// Set the location of the most recent start tag's ending LineColumn
|
||||
fn set_most_recent_block_start(&mut self, span: Span) {
|
||||
self.recent_span_locations.most_recent_block_start = Some(span);
|
||||
}
|
||||
|
||||
/// Determine whether or not there is any space between the end of the first
|
||||
/// span and the beginning of the second span.
|
||||
///
|
||||
/// There is space if they are on separate lines or if they have different columns.
|
||||
///
|
||||
/// html! { <div>Hello</div> } <--- no space between end of div and Hello
|
||||
///
|
||||
/// html! { <div> Hello</div> } <--- space between end of div and Hello
|
||||
fn separated_by_whitespace(&self, first_span: &Span, second_span: &Span) -> bool {
|
||||
if first_span.end().line != second_span.end().line {
|
||||
return true;
|
||||
}
|
||||
|
||||
second_span.start().column - first_span.end().column > 0
|
||||
}
|
||||
|
||||
/// Create a new identifier for a VirtualNode and increment our node_idx so that next
|
||||
/// time we call this our node will get a different name.
|
||||
fn new_virtual_node_ident(&mut self, span: Span) -> Ident {
|
||||
let node_name = format!("node_{}", self.current_node_idx);
|
||||
|
||||
let node_ident = Ident::new(node_name.as_str(), span);
|
||||
|
||||
// TODO: Increment before creating the new node, not after.
|
||||
// This way the current virtual node ident won't need to do strange subtraction
|
||||
self.current_node_idx += 1;
|
||||
|
||||
node_ident
|
||||
}
|
||||
|
||||
/// Get the Ident for the current (last created) virtual node, without incrementing
|
||||
/// the node index.
|
||||
fn current_virtual_node_ident(&self, span: Span) -> Ident {
|
||||
// TODO: Increment before creating the new node, not after.
|
||||
// This way the current virtual node ident won't need to do strange subtraction
|
||||
let node_name = format!("node_{}", self.current_node_idx - 1);
|
||||
|
||||
Ident::new(node_name.as_str(), span)
|
||||
}
|
||||
|
||||
/// Generate virtual node tokens for a statement that came from in between braces
|
||||
///
|
||||
/// examples:
|
||||
///
|
||||
/// html! { <div> { some_var_in_braces } </div>
|
||||
/// html! { <div> { some_other_variable } </div>
|
||||
fn push_iterable_nodes(&mut self, nodes: NodesToPush) {
|
||||
let node_idx = self.current_node_idx;
|
||||
|
||||
match nodes {
|
||||
NodesToPush::Stmt(stmt) => {
|
||||
let node_ident = self.new_virtual_node_ident(stmt.span());
|
||||
|
||||
self.push_tokens(quote! {
|
||||
let mut #node_ident: IterableNodes = (#stmt).into();
|
||||
});
|
||||
}
|
||||
NodesToPush::TokenStream(stmt, tokens) => {
|
||||
let node_ident = self.new_virtual_node_ident(stmt.span());
|
||||
|
||||
self.push_tokens(quote! {
|
||||
let mut #node_ident: IterableNodes = #tokens.into();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let parent_idx = *&self.parent_stack[self.parent_stack.len() - 1].0;
|
||||
|
||||
self.parent_to_children
|
||||
.get_mut(&parent_idx)
|
||||
.expect("Parent of these iterable nodes")
|
||||
.push(node_idx);
|
||||
self.node_order.push(node_idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep track of the locations of different kinds of tokens that we encounter.
|
||||
///
|
||||
/// This helps us determine whether or not to insert space before or after text tokens
|
||||
/// in cases such as:
|
||||
///
|
||||
/// ```ignore
|
||||
/// html! { <div> { Hello World } </div>
|
||||
/// html! { <div>{Hello World}</div>
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
struct RecentSpanLocations {
|
||||
most_recent_open_tag_end: Option<Span>,
|
||||
most_recent_block_start: Option<Span>,
|
||||
}
|
||||
|
||||
fn is_self_closing(tag: &str) -> bool {
|
||||
html_validation::is_self_closing(tag)
|
||||
}
|
||||
|
||||
fn is_valid_tag(tag: &str) -> bool {
|
||||
html_validation::is_valid_tag(tag)
|
||||
}
|
152
packages/html-macro/src/parser/open_tag.rs
Normal file
152
packages/html-macro/src/parser/open_tag.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
use crate::parser::{is_self_closing, is_valid_tag, HtmlParser};
|
||||
use crate::tag::Attr;
|
||||
use proc_macro2::{Ident, Span};
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::Expr;
|
||||
|
||||
impl HtmlParser {
|
||||
/// Parse an incoming Tag::Open
|
||||
pub(crate) fn parse_open_tag(
|
||||
&mut self,
|
||||
name: &Ident,
|
||||
closing_span: &Span,
|
||||
attrs: &Vec<Attr>,
|
||||
is_self_closing_tag: bool,
|
||||
) {
|
||||
self.set_most_recent_open_tag_end(closing_span.clone());
|
||||
|
||||
let idx = &mut self.current_node_idx;
|
||||
let parent_to_children = &mut self.parent_to_children;
|
||||
let parent_stack = &mut self.parent_stack;
|
||||
let tokens = &mut self.tokens;
|
||||
let node_order = &mut self.node_order;
|
||||
|
||||
// The root node is named `node_0`. All of it's descendants are node_1.. node_2.. etc.
|
||||
// This just comes from the `idx` variable
|
||||
// TODO: Not sure what the span is supposed to be so I just picked something..
|
||||
let var_name_node = Ident::new(format!("node_{}", idx).as_str(), name.span());
|
||||
let html_tag = format!("{}", name);
|
||||
let is_html_tag = is_valid_tag(&html_tag);
|
||||
|
||||
// TODO: Maybe this could be split up into two functions at some point, would have to pass
|
||||
// a lot of vars around though, which isn't very nice.
|
||||
if is_html_tag {
|
||||
let node = quote! {
|
||||
let mut #var_name_node = VirtualNode::element(#html_tag);
|
||||
};
|
||||
|
||||
tokens.push(node);
|
||||
|
||||
for attr in attrs.iter() {
|
||||
let key = format!("{}", attr.key);
|
||||
let value = &attr.value;
|
||||
|
||||
match value {
|
||||
Expr::Closure(closure) => {
|
||||
// TODO: Use this to decide Box<FnMut(_, _, _, ...)
|
||||
// After we merge the DomUpdater
|
||||
let _arg_count = closure.inputs.len();
|
||||
|
||||
// NOTE: Closures don't work on non wasm32 targets so we only add
|
||||
// events on wasm32 targets.
|
||||
let add_closure = quote! {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let closure = Closure::wrap(
|
||||
Box::new(#value) as Box<FnMut(_)>
|
||||
);
|
||||
let closure_rc = std::rc::Rc::new(closure);
|
||||
#var_name_node.as_velement_mut().expect("Not an element")
|
||||
.events.0.insert(#key.to_string(), closure_rc);
|
||||
}
|
||||
};
|
||||
|
||||
tokens.push(add_closure);
|
||||
}
|
||||
_ => {
|
||||
let insert_attribute = quote! {
|
||||
#var_name_node.as_velement_mut().expect("Not an element")
|
||||
.attrs.insert(#key.to_string(), #value.to_string());
|
||||
};
|
||||
|
||||
tokens.push(insert_attribute);
|
||||
}
|
||||
};
|
||||
}
|
||||
} else if !html_tag.chars().next().unwrap().is_uppercase() {
|
||||
let error = format!(
|
||||
r#"{} is not a valid HTML tag.
|
||||
|
||||
If you are trying to use a valid HTML tag, perhaps there's a typo?
|
||||
|
||||
If you are trying to use a custom component, please capitalize the component name.
|
||||
|
||||
custom components: https://chinedufn.github.io/percy/html-macro/custom-components/index.html"#,
|
||||
html_tag,
|
||||
);
|
||||
let span = name.span();
|
||||
let invalid_tag_name_error = quote_spanned! {span=> {
|
||||
compile_error!(#error);
|
||||
}};
|
||||
tokens.push(invalid_tag_name_error);
|
||||
|
||||
let node = quote! {
|
||||
let mut #var_name_node = VirtualNode::text("error");
|
||||
};
|
||||
|
||||
tokens.push(node);
|
||||
} else {
|
||||
let var_name_component = Ident::new(format!("component_{}", idx).as_str(), name.span());
|
||||
let component_ident = Ident::new(format!("{}", html_tag).as_str(), name.span());
|
||||
|
||||
let component_props: Vec<proc_macro2::TokenStream> = attrs
|
||||
.into_iter()
|
||||
.map(|attr| {
|
||||
let key = Ident::new(format!("{}", attr.key).as_str(), name.span());
|
||||
let value = &attr.value;
|
||||
|
||||
quote! {
|
||||
#key: #value,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let node = quote! {
|
||||
let mut #var_name_component = #component_ident { #(#component_props),* };
|
||||
let mut #var_name_node = #var_name_component.render();
|
||||
};
|
||||
|
||||
tokens.push(node);
|
||||
}
|
||||
|
||||
// The first open tag that we see is our root node so we won't worry about
|
||||
// giving it a parent
|
||||
if *idx == 0 {
|
||||
node_order.push(0);
|
||||
|
||||
if !is_self_closing(&html_tag) && !is_self_closing_tag {
|
||||
parent_stack.push((0, name.clone()));
|
||||
}
|
||||
|
||||
*idx += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let parent_idx = *&parent_stack[parent_stack.len() - 1].0;
|
||||
|
||||
if !is_self_closing(&html_tag) && !is_self_closing_tag {
|
||||
parent_stack.push((*idx, name.clone()));
|
||||
}
|
||||
|
||||
node_order.push(*idx);
|
||||
|
||||
parent_to_children
|
||||
.get_mut(&parent_idx)
|
||||
.expect("Parent of this node")
|
||||
.push(*idx);
|
||||
|
||||
parent_to_children.insert(*idx, vec![]);
|
||||
|
||||
*idx += 1;
|
||||
}
|
||||
}
|
84
packages/html-macro/src/parser/statement.rs
Normal file
84
packages/html-macro/src/parser/statement.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use crate::parser::{HtmlParser, NodesToPush};
|
||||
use quote::quote;
|
||||
use syn::{Stmt, Expr, ExprIf};
|
||||
|
||||
impl HtmlParser {
|
||||
/// Parse an incoming syn::Stmt node inside a block
|
||||
pub(crate) fn parse_statement(
|
||||
&mut self,
|
||||
stmt: &Stmt,
|
||||
) {
|
||||
// Here we handle a block being a descendant within some html! call.
|
||||
//
|
||||
// The descendant should implement Into<IterableNodes>
|
||||
//
|
||||
// html { <div> { some_node } </div> }
|
||||
match stmt {
|
||||
Stmt::Expr(expr) => {
|
||||
self.parse_expr(stmt, expr);
|
||||
},
|
||||
_ => {
|
||||
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Parse an incoming syn::Expr node inside a block
|
||||
pub(crate) fn parse_expr(
|
||||
&mut self,
|
||||
stmt: &Stmt,
|
||||
expr: &Expr
|
||||
) {
|
||||
match expr {
|
||||
Expr::If(expr_if) => {
|
||||
self.expand_if(stmt, expr_if);
|
||||
},
|
||||
_ => {
|
||||
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand an incoming Expr::If block
|
||||
/// This enables us to use JSX-style conditions inside of blocks such as
|
||||
/// the following example.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// html! {
|
||||
/// <div>
|
||||
/// {if condition_is_true {
|
||||
/// html! { <span>Hello World</span> }
|
||||
/// }}
|
||||
/// </div>
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Traditionally this would be possible as an if statement in rust is an
|
||||
/// expression, so the then, and the else block have to return matching types.
|
||||
/// Here we identify whether the block is missing the else and fill it in with
|
||||
/// a blank VirtualNode::text("")
|
||||
pub(crate) fn expand_if(
|
||||
&mut self,
|
||||
stmt: &Stmt,
|
||||
expr_if: &ExprIf
|
||||
) {
|
||||
// Has else branch, we can parse the expression as normal.
|
||||
if let Some(_else_branch) = &expr_if.else_branch {
|
||||
self.push_iterable_nodes(NodesToPush::Stmt(stmt));
|
||||
} else {
|
||||
let condition = &expr_if.cond;
|
||||
let block = &expr_if.then_branch;
|
||||
let tokens = quote! {
|
||||
if #condition {
|
||||
#block.into()
|
||||
} else {
|
||||
VirtualNode::text("")
|
||||
}
|
||||
};
|
||||
|
||||
self.push_iterable_nodes(NodesToPush::TokenStream(stmt, tokens));
|
||||
}
|
||||
}
|
||||
}
|
96
packages/html-macro/src/parser/text.rs
Normal file
96
packages/html-macro/src/parser/text.rs
Normal file
|
@ -0,0 +1,96 @@
|
|||
use crate::parser::HtmlParser;
|
||||
use crate::tag::{Tag, TagKind};
|
||||
use proc_macro2::{Ident, Span};
|
||||
use quote::quote;
|
||||
|
||||
impl HtmlParser {
|
||||
/// Parse an incoming Tag::Text text node
|
||||
pub(crate) fn parse_text(
|
||||
&mut self,
|
||||
text: &str,
|
||||
text_start: Span,
|
||||
text_end: Span,
|
||||
next_tag: Option<&Tag>,
|
||||
) {
|
||||
let mut text = text.to_string();
|
||||
|
||||
if self.should_insert_space_before_text(&text_start) {
|
||||
text = " ".to_string() + &text;
|
||||
}
|
||||
|
||||
let should_insert_space_after_text = match next_tag {
|
||||
Some(Tag::Close {
|
||||
first_angle_bracket_span,
|
||||
..
|
||||
}) => self.separated_by_whitespace(&text_end, first_angle_bracket_span),
|
||||
Some(Tag::Braced { brace_span, .. }) => {
|
||||
self.separated_by_whitespace(&text_end, brace_span)
|
||||
}
|
||||
Some(Tag::Open {
|
||||
open_bracket_span, ..
|
||||
}) => self.separated_by_whitespace(&text_end, open_bracket_span),
|
||||
_ => false,
|
||||
};
|
||||
if should_insert_space_after_text {
|
||||
text += " ";
|
||||
}
|
||||
|
||||
let idx = &mut self.current_node_idx;
|
||||
let parent_to_children = &mut self.parent_to_children;
|
||||
let parent_stack = &mut self.parent_stack;
|
||||
let tokens = &mut self.tokens;
|
||||
let node_order = &mut self.node_order;
|
||||
|
||||
if *idx == 0 {
|
||||
node_order.push(0);
|
||||
// TODO: This is just a consequence of bad code. We're pushing this to make
|
||||
// things work but in reality a text node isn't a parent ever.
|
||||
// Just need to make the code DRY / refactor so that we can make things make
|
||||
// sense vs. just bolting things together.
|
||||
parent_stack.push((0, Ident::new("unused", Span::call_site())));
|
||||
}
|
||||
|
||||
let var_name = Ident::new(format!("node_{}", idx).as_str(), Span::call_site());
|
||||
|
||||
let text_node = quote! {
|
||||
let mut #var_name = VirtualNode::text(#text);
|
||||
};
|
||||
|
||||
tokens.push(text_node);
|
||||
|
||||
if *idx == 0 {
|
||||
*idx += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let parent_idx = &parent_stack[parent_stack.len() - 1];
|
||||
|
||||
node_order.push(*idx);
|
||||
|
||||
parent_to_children
|
||||
.get_mut(&parent_idx.0)
|
||||
.expect("Parent of this text node")
|
||||
.push(*idx);
|
||||
|
||||
*idx += 1;
|
||||
}
|
||||
|
||||
/// If the last TagKind was a block or an open tag we check to see if there is space
|
||||
/// between this text and that tag. If so we insert some space before this text.
|
||||
fn should_insert_space_before_text(&self, text_start: &Span) -> bool {
|
||||
if self.last_tag_kind == Some(TagKind::Braced) {
|
||||
let most_recent_block_start = self.recent_span_locations.most_recent_block_start;
|
||||
let most_recent_block_start = most_recent_block_start.as_ref().unwrap();
|
||||
|
||||
self.separated_by_whitespace(most_recent_block_start, text_start)
|
||||
} else if self.last_tag_kind == Some(TagKind::Open) {
|
||||
let most_recent_open_tag_end =
|
||||
self.recent_span_locations.most_recent_open_tag_end.as_ref();
|
||||
let most_recent_open_tag_end = most_recent_open_tag_end.unwrap();
|
||||
|
||||
self.separated_by_whitespace(most_recent_open_tag_end, text_start)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
310
packages/html-macro/src/tag.rs
Normal file
310
packages/html-macro/src/tag.rs
Normal file
|
@ -0,0 +1,310 @@
|
|||
use proc_macro2::{Span, TokenStream, TokenTree};
|
||||
use syn::parse::{Parse, ParseStream, Result};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::token::Brace;
|
||||
use syn::{braced, Block, Expr, Ident, Token};
|
||||
|
||||
/// The different kinds of tokens that we parse.
|
||||
///
|
||||
/// TODO: A better name than tag since not all of these are tags
|
||||
#[derive(Debug)]
|
||||
pub enum Tag {
|
||||
/// <div id="app" class=*CSS>
|
||||
/// <br />
|
||||
Open {
|
||||
name: Ident,
|
||||
attrs: Vec<Attr>,
|
||||
open_bracket_span: Span,
|
||||
closing_bracket_span: Span,
|
||||
is_self_closing: bool,
|
||||
},
|
||||
/// </div>
|
||||
Close {
|
||||
name: Ident,
|
||||
first_angle_bracket_span: Span,
|
||||
},
|
||||
/// html! { <div> Hello World </div> }
|
||||
///
|
||||
/// -> Hello world
|
||||
///
|
||||
/// start_span -> the span for the first token within the text
|
||||
/// end_span -> the span for the last token within the text
|
||||
Text {
|
||||
text: String,
|
||||
start_span: Option<Span>,
|
||||
end_span: Option<Span>,
|
||||
},
|
||||
/// let text_var = VirtualNode::text("3");
|
||||
///
|
||||
/// let iter_nodes =
|
||||
/// vec![
|
||||
/// html!{ <div></div> },
|
||||
/// html! {<span> </span>}
|
||||
/// ];
|
||||
///
|
||||
/// html! {
|
||||
/// <div>
|
||||
/// Here are some examples of blocks
|
||||
/// { text_var }
|
||||
/// { iter_nodes }
|
||||
/// { html! { <div> </div> }
|
||||
/// </div>
|
||||
/// }
|
||||
Braced { block: Box<Block>, brace_span: Span },
|
||||
}
|
||||
|
||||
/// The different kinds of tokens that we parse.
|
||||
///
|
||||
/// TODO: A better name than tag since not all of these are tags
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum TagKind {
|
||||
Open,
|
||||
Close,
|
||||
Text,
|
||||
Braced,
|
||||
}
|
||||
|
||||
/// id="my-id"
|
||||
/// class="some classes"
|
||||
/// etc...
|
||||
#[derive(Debug)]
|
||||
pub struct Attr {
|
||||
pub key: Ident,
|
||||
pub value: Expr,
|
||||
}
|
||||
|
||||
impl Parse for Tag {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut input = input;
|
||||
|
||||
// If it starts with a `<` it's either an open or close tag.
|
||||
// ex: <div>
|
||||
// ex: </em>
|
||||
if input.peek(Token![<]) {
|
||||
let first_angle_bracket_span = input.parse::<Token![<]>()?;
|
||||
let first_angle_bracket_span = first_angle_bracket_span.span();
|
||||
|
||||
let optional_close: Option<Token![/]> = input.parse()?;
|
||||
let is_open_tag = optional_close.is_none();
|
||||
|
||||
if is_open_tag {
|
||||
return parse_open_tag(&mut input, first_angle_bracket_span);
|
||||
} else {
|
||||
return parse_close_tag(&mut input, first_angle_bracket_span);
|
||||
}
|
||||
}
|
||||
|
||||
// { node_inside_block }
|
||||
if input.peek(Brace) {
|
||||
return parse_block(&mut input);
|
||||
}
|
||||
|
||||
return parse_text_node(&mut input);
|
||||
}
|
||||
}
|
||||
|
||||
/// `<div id="app" class=*CSS>`
|
||||
fn parse_open_tag(input: &mut ParseStream, open_bracket_span: Span) -> Result<Tag> {
|
||||
let name: Ident = input.parse()?;
|
||||
|
||||
let attrs = parse_attributes(input)?;
|
||||
|
||||
let is_self_closing: Option<Token![/]> = input.parse()?;
|
||||
let is_self_closing = is_self_closing.is_some();
|
||||
|
||||
let closing_bracket = input.parse::<Token![>]>()?;
|
||||
let closing_bracket_span = closing_bracket.span();
|
||||
|
||||
Ok(Tag::Open {
|
||||
name,
|
||||
attrs,
|
||||
open_bracket_span,
|
||||
closing_bracket_span,
|
||||
is_self_closing
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the attributes starting from something like:
|
||||
/// id="app" class=*CSS>
|
||||
///
|
||||
/// As soon as we see
|
||||
/// >
|
||||
/// We know that the element has no more attributes and our loop will end
|
||||
fn parse_attributes(input: &mut ParseStream) -> Result<Vec<Attr>> {
|
||||
let mut attrs = Vec::new();
|
||||
|
||||
// Do we see an identifier such as `id`? If so proceed
|
||||
while input.peek(Ident)
|
||||
|| input.peek(Token![async])
|
||||
|| input.peek(Token![for])
|
||||
|| input.peek(Token![loop])
|
||||
|| input.peek(Token![type])
|
||||
{
|
||||
// <link rel="stylesheet" type="text/css"
|
||||
// .. async, for, loop, type need to be handled specially since they are keywords
|
||||
let maybe_async_key: Option<Token![async]> = input.parse()?;
|
||||
let maybe_for_key: Option<Token![for]> = input.parse()?;
|
||||
let maybe_loop_key: Option<Token![loop]> = input.parse()?;
|
||||
let maybe_type_key: Option<Token![type]> = input.parse()?;
|
||||
|
||||
let key = if maybe_async_key.is_some() {
|
||||
Ident::new("async", maybe_async_key.unwrap().span())
|
||||
} else if maybe_for_key.is_some() {
|
||||
Ident::new("for", maybe_for_key.unwrap().span())
|
||||
} else if maybe_loop_key.is_some() {
|
||||
Ident::new("loop", maybe_loop_key.unwrap().span())
|
||||
} else if maybe_type_key.is_some() {
|
||||
Ident::new("type", maybe_type_key.unwrap().span())
|
||||
} else {
|
||||
input.parse()?
|
||||
};
|
||||
|
||||
// =
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
// Continue parsing tokens until we see the next attribute or a closing > tag
|
||||
let mut value_tokens = TokenStream::new();
|
||||
|
||||
loop {
|
||||
let tt: TokenTree = input.parse()?;
|
||||
value_tokens.extend(Some(tt));
|
||||
|
||||
let has_attrib_key = input.peek(Ident)
|
||||
|| input.peek(Token![async])
|
||||
|| input.peek(Token![for])
|
||||
|| input.peek(Token![loop])
|
||||
|| input.peek(Token![type]);
|
||||
let peek_start_of_next_attr = has_attrib_key && input.peek2(Token![=]);
|
||||
|
||||
let peek_end_of_tag = input.peek(Token![>]);
|
||||
|
||||
let peek_self_closing = input.peek(Token![/]);
|
||||
|
||||
if peek_end_of_tag || peek_start_of_next_attr || peek_self_closing {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let value: Expr = syn::parse2(value_tokens)?;
|
||||
|
||||
attrs.push(Attr { key, value });
|
||||
}
|
||||
|
||||
Ok(attrs)
|
||||
}
|
||||
|
||||
/// </div>
|
||||
fn parse_close_tag(input: &mut ParseStream, first_angle_bracket_span: Span) -> Result<Tag> {
|
||||
let name: Ident = input.parse()?;
|
||||
|
||||
input.parse::<Token![>]>()?;
|
||||
|
||||
Ok(Tag::Close {
|
||||
name,
|
||||
first_angle_bracket_span,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_block(input: &mut ParseStream) -> Result<Tag> {
|
||||
let content;
|
||||
let brace_token = braced!(content in input);
|
||||
|
||||
let brace_span = brace_token.span;
|
||||
|
||||
let block_expr = content.call(Block::parse_within)?;
|
||||
|
||||
let block = Box::new(Block {
|
||||
brace_token,
|
||||
stmts: block_expr,
|
||||
});
|
||||
|
||||
Ok(Tag::Braced { block, brace_span })
|
||||
}
|
||||
|
||||
/// Parse a sequence of tokens until we run into a closing tag
|
||||
/// html! { <div> Hello World </div> }
|
||||
/// or a brace
|
||||
/// html! { <div> Hello World { Braced } </div>
|
||||
///
|
||||
/// So, in the second case, there would be two VText nodes created. "Hello World" and "Braced".
|
||||
///
|
||||
/// Later in parser/text.rs we'll look at how close the VText nodes are to their neighboring tags
|
||||
/// to determine whether or not to insert spacing.
|
||||
///
|
||||
/// So, in the examples above, since the opening "<div>" has a space after it we'll later transform
|
||||
/// "Hello World" into " Hello World" in parser/tag.rs
|
||||
fn parse_text_node(input: &mut ParseStream) -> Result<Tag> {
|
||||
// Continue parsing tokens until we see a closing tag <
|
||||
let _text_tokens = TokenStream::new();
|
||||
|
||||
let mut text = "".to_string();
|
||||
|
||||
let mut idx = 0;
|
||||
|
||||
let mut start_span = None;
|
||||
|
||||
let mut most_recent_span: Option<Span> = None;
|
||||
|
||||
loop {
|
||||
if input.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let tt: TokenTree = input.parse()?;
|
||||
|
||||
if idx == 0 {
|
||||
start_span = Some(tt.span());
|
||||
most_recent_span = Some(tt.span());
|
||||
}
|
||||
|
||||
// TODO: Properly handle whitespace and new lines
|
||||
// https://github.com/chinedufn/percy/pull/97#discussion_r263039215
|
||||
if idx != 0 {
|
||||
if let Some(most_recent_span) = most_recent_span {
|
||||
let current_span_start = tt.span().start();
|
||||
let most_recent_span_end = most_recent_span.end();
|
||||
|
||||
let spans_on_different_lines = current_span_start.line != most_recent_span_end.line;
|
||||
|
||||
// Contraptions such as "Aren't" give the "'" and the "t" the
|
||||
// same span, even though they get parsed separately when calling
|
||||
// input.parse::<TokenTree>().
|
||||
// As in - it takes two input.parse calls to get the "'" and "t",
|
||||
// even though they have the same span.
|
||||
// This might be a bug in syn - but regardless we address this by
|
||||
// not inserting a space in this case.
|
||||
let span_comes_before_previous_span = current_span_start.column
|
||||
< most_recent_span_end.column
|
||||
&& !spans_on_different_lines;
|
||||
|
||||
// Spans are on different lines, insert space
|
||||
if spans_on_different_lines {
|
||||
text += " ";
|
||||
} else if !span_comes_before_previous_span
|
||||
&& current_span_start.column - most_recent_span_end.column > 0
|
||||
{
|
||||
text += " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text += &tt.to_string();
|
||||
|
||||
most_recent_span = Some(tt.span());
|
||||
|
||||
let peek_closing_tag = input.peek(Token![<]);
|
||||
let peek_start_block = input.peek(Brace);
|
||||
|
||||
if peek_closing_tag || peek_start_block {
|
||||
break;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
Ok(Tag::Text {
|
||||
text,
|
||||
start_span,
|
||||
end_span: most_recent_span,
|
||||
})
|
||||
}
|
12
packages/html-validation/Cargo.toml
Normal file
12
packages/html-validation/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "html-validation"
|
||||
version = "0.1.2"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
description = "Validation for HTML elements and attributes"
|
||||
keywords = ["html", "validation", "valid", "dom", "virtual"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.0"
|
42
packages/html-validation/src/lib.rs
Normal file
42
packages/html-validation/src/lib.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
//! The html-validation crate provides method that can be used when validating html elements
|
||||
//! and attributes.
|
||||
//!
|
||||
//! The original goal of this crate was to be used as a dependency in procedural macros that
|
||||
//! validate html at compile time, but it is general purpose and can be used in other problem
|
||||
//! spaces.
|
||||
//!
|
||||
//! ## Potential Strategy - Pessimistic Validation
|
||||
//!
|
||||
//! We might make the html-validation crate is pessimistic by nature.
|
||||
//!
|
||||
//! This means that as we develop the create we'll blacklist more and more things - but in general
|
||||
//! we default to not saying that something is invalid until we've specifically encoded that it is
|
||||
//! not allowed.
|
||||
//!
|
||||
//! This means that you'll see methods with names like `is_definitely_invalid_child` - hinting
|
||||
//! that we're telling you that we're certain that the relationship is not allowed.
|
||||
//!
|
||||
//! Over time we'll cover more and more cases and this should become a non issue, but at the
|
||||
//! beginning it will mean that our validation is less strict than it should really be.
|
||||
//!
|
||||
//! The reason behind this strategy is that it lets people get up and running from day one without
|
||||
//! needing to wait until our validation is perfect.
|
||||
//! A downside is that as we become more and more strict there might be situations where you have
|
||||
//! to go back and tweak your html if you had something that we are now calling invalid.
|
||||
//!
|
||||
//! ## Potential Strategy - Optimistic Validation
|
||||
//!
|
||||
//! In this case we'd make html! generate a compile time error for anything that isn't certainly valid.
|
||||
//! Then there would be a second macro such as html_unstrict! that would be a bit more permissive.
|
||||
//!
|
||||
//! Over time as our validation permitted more cases people could use html! more and more instead of html_loose!
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub use self_closing::is_self_closing;
|
||||
pub use svg_namespace::is_svg_namespace;
|
||||
pub use valid_tags::is_valid_tag;
|
||||
|
||||
mod self_closing;
|
||||
mod svg_namespace;
|
||||
mod valid_tags;
|
31
packages/html-validation/src/self_closing.rs
Normal file
31
packages/html-validation/src/self_closing.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use lazy_static::lazy_static;
|
||||
use std::collections::hash_set::HashSet;
|
||||
|
||||
use super::svg_namespace::is_self_closing_svg_tag;
|
||||
|
||||
// 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.
|
||||
lazy_static! {
|
||||
static ref SELF_CLOSING_TAGS: HashSet<&'static str> = [
|
||||
"area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "command",
|
||||
"keygen", "source",
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Whether or not this tag is self closing
|
||||
///
|
||||
/// ```
|
||||
/// use html_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) || is_self_closing_svg_tag(tag)
|
||||
}
|
114
packages/html-validation/src/svg_namespace.rs
Normal file
114
packages/html-validation/src/svg_namespace.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
|
||||
lazy_static! {
|
||||
// list of svg elements
|
||||
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element
|
||||
// a hashmap of `(tag, is_self_closing)`
|
||||
static ref SVG_NAMESPACED_TAGS: HashMap<&'static str, bool> = [
|
||||
// TODO: can cause conflict with html `a`
|
||||
//("a", true),
|
||||
("animate", true),
|
||||
("animateMotion", false),
|
||||
("animateTransform", true),
|
||||
("circle", true),
|
||||
("clipPath",false),
|
||||
// TODO: blocked with [issue](https://github.com/chinedufn/percy/issues/106)
|
||||
//("color-profile",),
|
||||
("defs", false),
|
||||
("desc", false),
|
||||
("discard", true),
|
||||
("ellipse",true),
|
||||
("feBlend", true),
|
||||
("feColorMatrix", true),
|
||||
("feComponentTransfer", false),
|
||||
("feComposite", true),
|
||||
("feConvolveMatrix", true),
|
||||
("feDiffuseLighting", false),
|
||||
("feDisplacementMap", true),
|
||||
("feDistantLight", true),
|
||||
("feDropShadow", true),
|
||||
("feFlood", true),
|
||||
("feFuncA", true),
|
||||
("feFuncB", true),
|
||||
("feFuncG", true),
|
||||
("feFuncR", true),
|
||||
("feGaussianBlur", true),
|
||||
("feImage", true),
|
||||
("feMerge", false),
|
||||
("feMergeNode", true),
|
||||
("feMorphology", true),
|
||||
("feOffset", true),
|
||||
("fePointLight", true),
|
||||
("feSpecularLighting", false),
|
||||
("feSpotLight", true),
|
||||
("feTile", true),
|
||||
("feTurbulence", true),
|
||||
("filter", false),
|
||||
("foreignObject", false),
|
||||
("g",false),
|
||||
("hatch", false),
|
||||
("hatchpath", true),
|
||||
("image", true),
|
||||
("line", true),
|
||||
("linearGradient", false),
|
||||
("marker", false),
|
||||
("mask", false),
|
||||
// TODO: undocumented
|
||||
//("mesh",),
|
||||
// TODO: undocumented
|
||||
//("meshgradient",),
|
||||
// TODO: undocumented
|
||||
//("meshpatch",),
|
||||
// TODO: undocumented
|
||||
//("meshrow",),
|
||||
("metadata", false),
|
||||
("mpath", true),
|
||||
("path", true),
|
||||
("pattern", false),
|
||||
("polygon", true),
|
||||
("polyline", true),
|
||||
("radialGradient", false),
|
||||
("rect", true),
|
||||
// TODO: can cause conflict with html `script` tag
|
||||
//("script", false),
|
||||
("set", true),
|
||||
("solidcolor", true),
|
||||
("stop", true),
|
||||
// TODO: can cause conflict with html `style` tag
|
||||
//("style", false),
|
||||
("svg", false),
|
||||
("switch", false),
|
||||
("symbol", false),
|
||||
("text", false),
|
||||
("textPath", false),
|
||||
// TODO: can cause conflict with html `title` tag
|
||||
//("title", false),
|
||||
("tspan", false),
|
||||
// TODO: undocumented
|
||||
//("unknown",),
|
||||
("use", true),
|
||||
("view", true),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
/// Whether or not this tag is part svg elements
|
||||
/// ```
|
||||
/// use html_validation::is_svg_namespace;
|
||||
///
|
||||
/// assert_eq!(is_svg_namespace("svg"), true);
|
||||
///
|
||||
/// assert_eq!(is_svg_namespace("circle"), true);
|
||||
///
|
||||
/// assert_eq!(is_svg_namespace("div"), false);
|
||||
/// ```
|
||||
pub fn is_svg_namespace(tag: &str) -> bool {
|
||||
SVG_NAMESPACED_TAGS.contains_key(tag)
|
||||
}
|
||||
|
||||
/// Whether or not this svg tag is self closing
|
||||
pub(crate) fn is_self_closing_svg_tag(tag: &str) -> bool {
|
||||
SVG_NAMESPACED_TAGS.get(tag).map(|v| *v).unwrap_or(false)
|
||||
}
|
35
packages/html-validation/src/valid_tags.rs
Normal file
35
packages/html-validation/src/valid_tags.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use lazy_static::lazy_static;
|
||||
use std::collections::hash_set::HashSet;
|
||||
|
||||
use super::svg_namespace::is_svg_namespace;
|
||||
|
||||
lazy_static! {
|
||||
static ref VALID_TAGS: HashSet<&'static str> = [
|
||||
"a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big",
|
||||
"blockquote","body","br","button","canvas","caption","cite","code","col","colgroup",
|
||||
"command", "data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed",
|
||||
"fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head",
|
||||
"header","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend",
|
||||
"li","link","main","map","mark","menu","menuitem","meta","meter","nav","noscript","object",
|
||||
"ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt",
|
||||
"ruby","s","samp","script","section","select","small","source","span","strong","style",
|
||||
"sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title",
|
||||
"tr","track","u","ul","var","video","wbr",
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Whether or not this tag is valid
|
||||
///
|
||||
/// ```
|
||||
/// use html_validation::is_valid_tag;
|
||||
///
|
||||
/// assert_eq!(is_valid_tag("br"), true);
|
||||
///
|
||||
/// assert_eq!(is_valid_tag("random"), false);
|
||||
/// ```
|
||||
pub fn is_valid_tag(tag: &str) -> bool {
|
||||
VALID_TAGS.contains(tag) || is_svg_namespace(tag)
|
||||
}
|
9
packages/macro/Cargo.toml
Normal file
9
packages/macro/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "dioxus-macro"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
0
packages/macro/src/lib.rs
Normal file
0
packages/macro/src/lib.rs
Normal file
9
packages/recoil/Cargo.toml
Normal file
9
packages/recoil/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "dioxus-recoil"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
7
packages/recoil/src/lib.rs
Normal file
7
packages/recoil/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
9
packages/redux/Cargo.toml
Normal file
9
packages/redux/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "dioxus-redux"
|
||||
version = "0.0.0"
|
||||
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
7
packages/redux/src/lib.rs
Normal file
7
packages/redux/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
58
packages/virtual-dom-rs/CHANGELOG.md
Normal file
58
packages/virtual-dom-rs/CHANGELOG.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
# virtual-dom-rs Changelog
|
||||
|
||||
Types of changes:
|
||||
|
||||
- `[added]` for new features.
|
||||
- `[changed]` for changes in existing functionality.
|
||||
- `[deprecated]` for once-stable features removed in upcoming releases.
|
||||
- `[removed]` for deprecated features removed in this release.
|
||||
- `[fixed]` for any bug fixes.
|
||||
- `[security]` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
## Not Yet Published
|
||||
|
||||
_Here we list notable things that have been merged into the master branch but have not been released yet._
|
||||
|
||||
- [added] SVG support [#104](https://github.com/chinedufn/percy/pull/104)
|
||||
- ...
|
||||
|
||||
## 0.6.9 - May 23, 2019
|
||||
|
||||
- [added] `on_create_elem` [Docs](https://chinedufn.github.io/percy/html-macro/real-elements-and-nodes/on-create-elem/index.html)
|
||||
- [added] `inner_html` [Docs](https://chinedufn.github.io/percy/html-macro/setting-inner-html/index.html)
|
||||
|
||||
## 0.6.7 - Mar 16, 2019
|
||||
|
||||
- [fixed] Spacing between elements is done by inserting space before and after existing text nodes instead of creating new ones.
|
||||
|
||||
## 0.6.6 - Mar 6, 2019
|
||||
|
||||
- [fixed] Proper spacing in between text nodes and elements in most use cases [PR](https://github.com/chinedufn/percy/pull/97)
|
||||
- Still need to address [#98](https://github.com/chinedufn/percy/issues/98) and then we should have all cases handled.
|
||||
|
||||
## 0.6.5 - Mar 4, 2019
|
||||
|
||||
- [added] Start supporting braced text in the `html!` macro [#96](https://github.com/chinedufn/percy/pull/96)
|
||||
- [removed] Removed the `text!` macro
|
||||
|
||||
```rust
|
||||
let hello = "hello world";
|
||||
html! { {hello} }
|
||||
```
|
||||
|
||||
|
||||
## 0.6.4 - Feb 24, 2019
|
||||
|
||||
- [fixed] Using the `html!` macro to create an event now uses the fully qualified path to `std::rc::Rc`
|
||||
- [added] Started adding key support. If a VirtualNode's key attribute changes it will lead to a `Replace` patch.
|
||||
|
||||
```rust
|
||||
// example
|
||||
html! { <div key="5"></div> }`;
|
||||
````
|
||||
|
||||
## 0.6.1 - Feb 22, 2019
|
||||
|
||||
- [fixed] Fix DomUpdater not storing closures for nodes that were created during `Patch::AppendChildren`
|
||||
and `Patch::Replace`
|
||||
- [Issue](https://github.com/chinedufn/percy/issues/70)
|
47
packages/virtual-dom-rs/Cargo.toml
Normal file
47
packages/virtual-dom-rs/Cargo.toml
Normal file
|
@ -0,0 +1,47 @@
|
|||
[package]
|
||||
name = "virtual-dom-rs"
|
||||
version = "0.6.14"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
description = "A standalone Virtual DOM creation, diffing and patching implementation"
|
||||
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
documentation = "https://chinedufn.github.io/percy/api/virtual_dom_rs/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2.33"
|
||||
virtual-node = { path = "../virtual-node", version = "0.2.7" }
|
||||
html-macro = { path = "../html-macro", version = "0.1.8"}
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Comment",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"EventTarget",
|
||||
"HtmlCollection",
|
||||
"Node",
|
||||
"NodeList",
|
||||
"Text",
|
||||
"CharacterData",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.2.33"
|
||||
console_error_panic_hook = "0.1.5"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"DomTokenList",
|
||||
"HtmlInputElement",
|
||||
"Event",
|
||||
"MouseEvent",
|
||||
"InputEvent",
|
||||
"console",
|
||||
]
|
3
packages/virtual-dom-rs/README.md
Normal file
3
packages/virtual-dom-rs/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# virtual-dom-rs
|
||||
|
||||
> A standalone Virtual DOM creation, diffing and patching implementation
|
26
packages/virtual-dom-rs/src/diff/diff_test_case.rs
Normal file
26
packages/virtual-dom-rs/src/diff/diff_test_case.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
//! Kept in it's own file to more easily import into the Percy book.
|
||||
|
||||
use crate::diff::diff;
|
||||
use crate::patch::Patch;
|
||||
use virtual_node::VirtualNode;
|
||||
|
||||
/// 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: VirtualNode,
|
||||
// ex: html! { <strong> </strong> }
|
||||
pub new: VirtualNode,
|
||||
// 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(&self.old, &self.new);
|
||||
|
||||
assert_eq!(patches, self.expected, "{}", self.description);
|
||||
}
|
||||
}
|
351
packages/virtual-dom-rs/src/diff/mod.rs
Normal file
351
packages/virtual-dom-rs/src/diff/mod.rs
Normal file
|
@ -0,0 +1,351 @@
|
|||
use crate::Patch;
|
||||
use crate::VirtualNode;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
|
||||
/// Given two VirtualNode's generate Patch's that would turn the old virtual node's
|
||||
/// real DOM node equivalent into the new VirtualNode's real DOM node equivalent.
|
||||
pub fn diff<'a>(old: &'a VirtualNode, new: &'a VirtualNode) -> Vec<Patch<'a>> {
|
||||
diff_recursive(&old, &new, &mut 0)
|
||||
}
|
||||
|
||||
fn diff_recursive<'a, 'b>(
|
||||
old: &'a VirtualNode,
|
||||
new: &'a VirtualNode,
|
||||
cur_node_idx: &'b mut usize,
|
||||
) -> Vec<Patch<'a>> {
|
||||
let mut patches = vec![];
|
||||
let mut replace = false;
|
||||
|
||||
// Different enum variants, replace!
|
||||
if mem::discriminant(old) != mem::discriminant(new) {
|
||||
replace = true;
|
||||
}
|
||||
|
||||
if let (VirtualNode::Element(old_element), VirtualNode::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 VirtualNode::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
|
||||
(VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
|
||||
if old_text != new_text {
|
||||
patches.push(Patch::ChangeText(*cur_node_idx, &new_text));
|
||||
}
|
||||
}
|
||||
|
||||
// We're comparing two element nodes
|
||||
(VirtualNode::Element(old_element), VirtualNode::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 VirtualNode> =
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
(VirtualNode::Text(_), VirtualNode::Element(_))
|
||||
| (VirtualNode::Element(_), VirtualNode::Text(_)) => {
|
||||
unreachable!("Unequal variant discriminants should already have been handled");
|
||||
}
|
||||
};
|
||||
|
||||
// new_root.create_element()
|
||||
patches
|
||||
}
|
||||
|
||||
fn increment_node_idx_for_children<'a, 'b>(old: &'a VirtualNode, cur_node_idx: &'b mut usize) {
|
||||
*cur_node_idx += 1;
|
||||
if let VirtualNode::Element(element_node) = old {
|
||||
for child in element_node.children.iter() {
|
||||
increment_node_idx_for_children(&child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod diff_test_case;
|
||||
#[cfg(test)]
|
||||
use self::diff_test_case::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{html, VText, VirtualNode};
|
||||
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",
|
||||
// })
|
||||
// }
|
||||
}
|
103
packages/virtual-dom-rs/src/dom_updater.rs
Normal file
103
packages/virtual-dom-rs/src/dom_updater.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
//! Diff virtual-doms and patch the real DOM
|
||||
|
||||
use crate::diff::diff;
|
||||
use crate::patch::patch;
|
||||
use std::collections::HashMap;
|
||||
use virtual_node::DynClosure;
|
||||
use virtual_node::VirtualNode;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// Closures that we are holding on to to make sure that they don't get invalidated after a
|
||||
/// VirtualNode is dropped.
|
||||
///
|
||||
/// The u32 is a unique identifier that is associated with the DOM element that this closure is
|
||||
/// attached to.
|
||||
///
|
||||
/// TODO: Periodically check if the DOM element is still there, and if not drop the closure.
|
||||
/// Maybe whenever a DOM node is replaced or truncated we figure out all of it's
|
||||
/// descendants somehow and invalidate those closures..? Need to plan this out..
|
||||
/// At it stands now this hashmap will grow anytime a new element with closures is
|
||||
/// appended or replaced and we will never free those closures.
|
||||
pub type ActiveClosures = HashMap<u32, Vec<DynClosure>>;
|
||||
|
||||
/// Used for keeping a real DOM node up to date based on the current VirtualNode
|
||||
/// and a new incoming VirtualNode that represents our latest DOM state.
|
||||
pub struct DomUpdater {
|
||||
current_vdom: VirtualNode,
|
||||
/// The closures that are currently attached to elements in the page.
|
||||
///
|
||||
/// We keep these around so that they don't get dropped (and thus stop working);
|
||||
///
|
||||
/// FIXME: Drop them when the element is no longer in the page. Need to figure out
|
||||
/// a good strategy for when to do this.
|
||||
pub active_closures: ActiveClosures,
|
||||
root_node: Node,
|
||||
}
|
||||
|
||||
impl DomUpdater {
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created but not added to your DOM.
|
||||
pub fn new(current_vdom: VirtualNode) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created and appended (as a child) to your passed
|
||||
/// in mount element.
|
||||
pub fn new_append_to_mount(current_vdom: VirtualNode, mount: &Element) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
mount
|
||||
.append_child(&created_node.node)
|
||||
.expect("Could not append child to mount");
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created and it will replace your passed in mount
|
||||
/// element.
|
||||
pub fn new_replace_mount(current_vdom: VirtualNode, mount: Element) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
mount
|
||||
.replace_with_with_node_1(&created_node.node)
|
||||
.expect("Could not replace mount element");
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Diff the current virtual dom with the new virtual dom that is being passed in.
|
||||
///
|
||||
/// Then use that diff to patch the real DOM in the user's browser so that they are
|
||||
/// seeing the latest state of the application.
|
||||
pub fn update(&mut self, new_vdom: VirtualNode) {
|
||||
let patches = diff(&self.current_vdom, &new_vdom);
|
||||
|
||||
let active_closures = patch(self.root_node.clone(), &patches).unwrap();
|
||||
|
||||
self.active_closures.extend(active_closures);
|
||||
|
||||
self.current_vdom = new_vdom;
|
||||
}
|
||||
|
||||
/// Return the root node of your application, the highest ancestor of all other nodes in
|
||||
/// your real DOM tree.
|
||||
pub fn root_node(&self) -> Node {
|
||||
// Note that we're cloning the `web_sys::Node`, not the DOM element.
|
||||
// So we're effectively cloning a pointer here, which is fast.
|
||||
self.root_node.clone()
|
||||
}
|
||||
}
|
44
packages/virtual-dom-rs/src/lib.rs
Normal file
44
packages/virtual-dom-rs/src/lib.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
//! virtual-dom-rs provides a virtual dom implementation as well as an `html!` macro
|
||||
//! that you can use to generate a virtual dom.
|
||||
//!
|
||||
//! The virtual dom works on both the client and server. On the client we'll render
|
||||
//! to an `HtmlElement`, and on the server we render to a `String`.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
// #![cfg_attr(test, feature(proc_macro_hygiene))]
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
|
||||
// Used so that `html!` calls work when people depend on this crate since `html!` needs
|
||||
// access to `Closure` when creating event handlers.
|
||||
pub use wasm_bindgen::prelude::Closure;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen::JsCast;
|
||||
|
||||
pub extern crate web_sys;
|
||||
pub use web_sys::*;
|
||||
|
||||
pub use virtual_node::*;
|
||||
|
||||
mod diff;
|
||||
pub use crate::diff::*;
|
||||
|
||||
mod patch;
|
||||
pub use crate::patch::*;
|
||||
|
||||
pub use html_macro::html;
|
||||
|
||||
mod dom_updater;
|
||||
pub use self::dom_updater::DomUpdater;
|
||||
|
||||
/// Exports structs and macros that you'll almost always want access to in a virtual-dom
|
||||
/// powered application
|
||||
pub mod prelude {
|
||||
pub use crate::dom_updater::DomUpdater;
|
||||
pub use crate::VirtualNode;
|
||||
pub use html_macro::html;
|
||||
pub use std::vec::IntoIter;
|
||||
pub use virtual_node::IterableNodes;
|
||||
pub use virtual_node::View;
|
||||
pub use wasm_bindgen::prelude::Closure;
|
||||
}
|
217
packages/virtual-dom-rs/src/patch/apply_patches.rs
Normal file
217
packages/virtual-dom-rs/src/patch/apply_patches.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
use crate::patch::Patch;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::dom_updater::ActiveClosures;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{Element, Node, Text};
|
||||
|
||||
/// Apply all of the patches to our old root node in order to create the new root node
|
||||
/// that we desire.
|
||||
/// This is usually used after diffing two virtual nodes.
|
||||
pub fn patch<N: Into<Node>>(root_node: N, patches: &Vec<Patch>) -> Result<ActiveClosures, JsValue> {
|
||||
let root_node: Node = root_node.into();
|
||||
|
||||
let mut cur_node_idx = 0;
|
||||
|
||||
let mut nodes_to_find = HashSet::new();
|
||||
|
||||
for patch in patches {
|
||||
nodes_to_find.insert(patch.node_idx());
|
||||
}
|
||||
|
||||
let mut element_nodes_to_patch = HashMap::new();
|
||||
let mut text_nodes_to_patch = HashMap::new();
|
||||
|
||||
// Closures that were added to the DOM during this patch operation.
|
||||
let mut active_closures = HashMap::new();
|
||||
|
||||
find_nodes(
|
||||
root_node,
|
||||
&mut cur_node_idx,
|
||||
&mut nodes_to_find,
|
||||
&mut element_nodes_to_patch,
|
||||
&mut text_nodes_to_patch,
|
||||
);
|
||||
|
||||
for patch in patches {
|
||||
let patch_node_idx = patch.node_idx();
|
||||
|
||||
if let Some(element) = element_nodes_to_patch.get(&patch_node_idx) {
|
||||
let new_closures = apply_element_patch(&element, &patch)?;
|
||||
active_closures.extend(new_closures);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(text_node) = text_nodes_to_patch.get(&patch_node_idx) {
|
||||
apply_text_patch(&text_node, &patch)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
unreachable!("Getting here means we didn't find the element or next node that we were supposed to patch.")
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
|
||||
fn find_nodes(
|
||||
root_node: Node,
|
||||
cur_node_idx: &mut usize,
|
||||
nodes_to_find: &mut HashSet<usize>,
|
||||
element_nodes_to_patch: &mut HashMap<usize, Element>,
|
||||
text_nodes_to_patch: &mut HashMap<usize, Text>,
|
||||
) {
|
||||
if nodes_to_find.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// We use child_nodes() instead of children() because children() ignores text nodes
|
||||
let children = root_node.child_nodes();
|
||||
let child_node_count = children.length();
|
||||
|
||||
// If the root node matches, mark it for patching
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
match root_node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
element_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
other => unimplemented!("Unsupported root node type: {}", other),
|
||||
}
|
||||
nodes_to_find.remove(&cur_node_idx);
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
|
||||
for i in 0..child_node_count {
|
||||
let node = children.item(i).unwrap();
|
||||
|
||||
match node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
find_nodes(
|
||||
node,
|
||||
cur_node_idx,
|
||||
nodes_to_find,
|
||||
element_nodes_to_patch,
|
||||
text_nodes_to_patch,
|
||||
);
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, node.unchecked_into());
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
}
|
||||
Node::COMMENT_NODE => {
|
||||
// At this time we do not support user entered comment nodes, so if we see a comment
|
||||
// then it was a delimiter created by virtual-dom-rs in order to ensure that two
|
||||
// neighboring text nodes did not get merged into one by the browser. So we skip
|
||||
// over this virtual-dom-rs generated comment node.
|
||||
}
|
||||
_other => {
|
||||
// Ignoring unsupported child node type
|
||||
// TODO: What do we do with this situation? Log a warning?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_element_patch(node: &Element, patch: &Patch) -> Result<ActiveClosures, JsValue> {
|
||||
let active_closures = HashMap::new();
|
||||
|
||||
match patch {
|
||||
Patch::AddAttributes(_node_idx, attributes) => {
|
||||
for (attrib_name, attrib_val) in attributes.iter() {
|
||||
node.set_attribute(attrib_name, attrib_val)?;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::RemoveAttributes(_node_idx, attributes) => {
|
||||
for attrib_name in attributes.iter() {
|
||||
node.remove_attribute(attrib_name)?;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
let created_node = new_node.create_dom_node();
|
||||
|
||||
node.replace_with_with_node_1(&created_node.node)?;
|
||||
|
||||
Ok(created_node.closures)
|
||||
}
|
||||
Patch::TruncateChildren(_node_idx, num_children_remaining) => {
|
||||
let children = node.child_nodes();
|
||||
let mut child_count = children.length();
|
||||
|
||||
// We skip over any separators that we placed between two text nodes
|
||||
// -> `<!--ptns-->`
|
||||
// and trim all children that come after our new desired `num_children_remaining`
|
||||
let mut non_separator_children_found = 0;
|
||||
|
||||
for index in 0 as u32..child_count {
|
||||
let child = children
|
||||
.get(min(index, child_count - 1))
|
||||
.expect("Potential child to truncate");
|
||||
|
||||
// If this is a comment node then we know that it is a `<!--ptns-->`
|
||||
// text node separator that was created in virtual_node/mod.rs.
|
||||
if child.node_type() == Node::COMMENT_NODE {
|
||||
continue;
|
||||
}
|
||||
|
||||
non_separator_children_found += 1;
|
||||
|
||||
if non_separator_children_found <= *num_children_remaining as u32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.remove_child(&child).expect("Truncated children");
|
||||
child_count -= 1;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::AppendChildren(_node_idx, new_nodes) => {
|
||||
let parent = &node;
|
||||
|
||||
let mut active_closures = HashMap::new();
|
||||
|
||||
for new_node in new_nodes {
|
||||
let created_node = new_node.create_dom_node();
|
||||
|
||||
parent.append_child(&created_node.node)?;
|
||||
|
||||
active_closures.extend(created_node.closures);
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::ChangeText(_node_idx, _new_node) => {
|
||||
unreachable!("Elements should not receive ChangeText patches.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_text_patch(node: &Text, patch: &Patch) -> Result<(), JsValue> {
|
||||
match patch {
|
||||
Patch::ChangeText(_node_idx, new_node) => {
|
||||
node.set_node_value(Some(&new_node.text));
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
node.replace_with_with_node_1(&new_node.create_dom_node().node)?;
|
||||
}
|
||||
other => unreachable!(
|
||||
"Text nodes should only receive ChangeText or Replace patches, not {:?}.",
|
||||
other,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
76
packages/virtual-dom-rs/src/patch/mod.rs
Normal file
76
packages/virtual-dom-rs/src/patch/mod.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
//! Our Patch enum is intentionally kept in it's own file for easy inclusion into
|
||||
//! The Percy Book.
|
||||
|
||||
use crate::{VText, VirtualNode};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod apply_patches;
|
||||
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(Debug, PartialEq)]
|
||||
pub enum Patch<'a> {
|
||||
/// Append a vector of child nodes to a parent node id.
|
||||
AppendChildren(NodeIdx, Vec<&'a VirtualNode>),
|
||||
/// 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 VirtualNode),
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
116
packages/virtual-dom-rs/tests/closures.rs
Normal file
116
packages/virtual-dom-rs/tests/closures.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
//! Ensure that our DomUpdater maintains Rc's to closures so that they work even
|
||||
//! after dropping virtual dom nodes.
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test closures
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::rc::Rc;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
use virtual_dom_rs::DomUpdater;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// TODO: This test current fails in headless browsers but works in non headless browsers
|
||||
// (tested in both geckodriver and chromedriver)
|
||||
// Need to figure out why
|
||||
#[wasm_bindgen_test]
|
||||
fn closure_not_dropped() {
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
let mut dom_updater = None;
|
||||
|
||||
{
|
||||
let mut input = make_input_component(Rc::clone(&text));
|
||||
input
|
||||
.as_velement_mut()
|
||||
.expect("Not an element")
|
||||
.attrs
|
||||
.insert("id".into(), "old-input-elem".into());
|
||||
|
||||
let mount = document.create_element("div").unwrap();
|
||||
mount.set_id("mount");
|
||||
document.body().unwrap().append_child(&mount).unwrap();
|
||||
|
||||
dom_updater = Some(DomUpdater::new_replace_mount(input, mount));
|
||||
|
||||
let mut dom_updater = dom_updater.as_mut().unwrap();
|
||||
|
||||
// Input VirtualNode from above gets dropped at the end of this block,
|
||||
// yet that element held Rc's to the Closure's that power the oninput event.
|
||||
//
|
||||
// We're patching the DOM with a new vdom, but since our new vdom doesn't contain any
|
||||
// new elements, `.create_element` won't get called and so no new Closures will be
|
||||
// created.
|
||||
//
|
||||
// So, we're testing that our old Closure's still work. The reason that they work is
|
||||
// that dom_updater maintains Rc's to those Closures.
|
||||
let mut new_node = make_input_component(Rc::clone(&text));
|
||||
new_node
|
||||
.as_velement_mut()
|
||||
.expect("Not an element")
|
||||
.attrs
|
||||
.insert("id".into(), "new-input-elem".into());
|
||||
|
||||
dom_updater.update(new_node);
|
||||
}
|
||||
|
||||
let dom_updater = dom_updater.as_ref().unwrap();
|
||||
|
||||
let input: HtmlInputElement = document
|
||||
.get_element_by_id("new-input-elem")
|
||||
.expect("Input element")
|
||||
.dyn_into()
|
||||
.unwrap();
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
|
||||
assert_eq!(
|
||||
dom_updater.active_closures.get(&1).as_ref().unwrap().len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
// We're just making sure that things compile - other tests give us confidence that the closure
|
||||
// will work just fine.
|
||||
//
|
||||
// https://github.com/chinedufn/percy/issues/81
|
||||
//
|
||||
//#[wasm_bindgen_test]
|
||||
//fn closure_with_no_params_compiles() {
|
||||
// let _making_sure_this_works = html! {
|
||||
// <div onclick=|| {}></div>
|
||||
// };
|
||||
//}
|
||||
|
||||
fn make_input_component(text_clone: Rc<RefCell<String>>) -> VirtualNode {
|
||||
html! {
|
||||
<input
|
||||
// On input we'll set our Rc<RefCell<String>> value to the input elements value
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
}
|
||||
}
|
118
packages/virtual-dom-rs/tests/create_element.rs
Normal file
118
packages/virtual-dom-rs/tests/create_element.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
//! Tests that ensure that we create the right DOM element from a VirtualNode
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
extern crate web_sys;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::{Element, Event, EventTarget, MouseEvent};
|
||||
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element nested_divs
|
||||
#[wasm_bindgen_test]
|
||||
fn nested_divs() {
|
||||
let vdiv = html! { <div> <div> <div></div> </div> </div> };
|
||||
let div: Element = vdiv.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(&div.inner_html(), "<div><div></div></div>");
|
||||
}
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element svg_element
|
||||
/// TODO: Temporarily disabled until we figure out why it's failing in CI but not failing locally
|
||||
// #[wasm_bindgen_test]
|
||||
// fn svg_element() {
|
||||
// let vdiv = html! { <div><svg xmlns="http://www.w3.org/2000/svg">
|
||||
// <circle cx="50" cy="50" r="50"/>
|
||||
// </svg></div> };
|
||||
// let div: Element = vdiv.create_dom_node().node.unchecked_into();
|
||||
|
||||
// assert_eq!(
|
||||
// &div.inner_html(),
|
||||
// r#"<svg xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="50"></circle></svg>"#
|
||||
// );
|
||||
// }
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element div_with_attributes
|
||||
#[wasm_bindgen_test]
|
||||
fn div_with_attributes() {
|
||||
let vdiv = html! { <div id="id-here" class="two classes"></div> };
|
||||
let div: Element = vdiv.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(&div.id(), "id-here");
|
||||
|
||||
assert!(div.class_list().contains("two"));
|
||||
assert!(div.class_list().contains("classes"));
|
||||
|
||||
assert_eq!(div.class_list().length(), 2);
|
||||
}
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element click_event
|
||||
#[wasm_bindgen_test]
|
||||
fn click_event() {
|
||||
let clicked = Rc::new(Cell::new(false));
|
||||
let clicked_clone = Rc::clone(&clicked);
|
||||
|
||||
let div = html! {
|
||||
<div
|
||||
onclick=move |_ev: MouseEvent| {
|
||||
clicked_clone.set(true);
|
||||
}
|
||||
>
|
||||
</div>
|
||||
};
|
||||
|
||||
let click_event = Event::new("click").unwrap();
|
||||
|
||||
let div = div.create_dom_node().node;
|
||||
|
||||
(EventTarget::from(div))
|
||||
.dispatch_event(&click_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(*clicked, Cell::new(true));
|
||||
}
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element inner_html
|
||||
/// @book start inner-html
|
||||
#[wasm_bindgen_test]
|
||||
fn inner_html() {
|
||||
let div = html! {
|
||||
<div
|
||||
unsafe_inner_html="<span>hi</span>"
|
||||
>
|
||||
</div>
|
||||
};
|
||||
let div: Element = div.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(div.inner_html(), "<span>hi</span>");
|
||||
}
|
||||
// @book end inner-html
|
||||
|
||||
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element on_create_elem
|
||||
/// @book start on-create-elem
|
||||
#[wasm_bindgen_test]
|
||||
fn on_create_elem() {
|
||||
let div = html! {
|
||||
<div
|
||||
on_create_elem=|elem: web_sys::Element| {
|
||||
elem.set_inner_html("Hello world");
|
||||
}
|
||||
>
|
||||
<span>This span should get replaced</span>
|
||||
</div>
|
||||
};
|
||||
let div: Element = div.create_dom_node().node.unchecked_into();
|
||||
|
||||
assert_eq!(div.inner_html(), "Hello world");
|
||||
}
|
||||
// @book end on-create-elem
|
197
packages/virtual-dom-rs/tests/diff_patch.rs
Normal file
197
packages/virtual-dom-rs/tests/diff_patch.rs
Normal file
|
@ -0,0 +1,197 @@
|
|||
//! Tests that ensure that diffing and patching work properly in a real browser.
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test diff_patch
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
extern crate web_sys;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
mod diff_patch_test_case;
|
||||
use self::diff_patch_test_case::DiffPatchTest;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_child() {
|
||||
DiffPatchTest {
|
||||
desc: "Replace a root node attribute attribute and a child text node",
|
||||
old: html! {
|
||||
<div>
|
||||
Original element
|
||||
</div>
|
||||
},
|
||||
new: html! { <div> Patched element</div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn truncate_children() {
|
||||
DiffPatchTest {
|
||||
desc: "Truncates extra children",
|
||||
old: html! {
|
||||
<div>
|
||||
<div> <div> <b></b> <em></em> </div> </div>
|
||||
</div>
|
||||
},
|
||||
new: html! {
|
||||
<div>
|
||||
<div> <div> <b></b> </div> </div>
|
||||
</div>
|
||||
},
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
|
||||
DiffPatchTest {
|
||||
desc: "https://github.com/chinedufn/percy/issues/48",
|
||||
old: html! {
|
||||
<div>
|
||||
ab <p></p> c
|
||||
</div>
|
||||
},
|
||||
new: html! {
|
||||
<div>
|
||||
ab <p></p>
|
||||
</div>
|
||||
},
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn remove_attributes() {
|
||||
DiffPatchTest {
|
||||
desc: "Removes attributes",
|
||||
old: html! { <div style=""> </div>
|
||||
},
|
||||
new: html! { <div></div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn append_children() {
|
||||
DiffPatchTest {
|
||||
desc: "Append a child node",
|
||||
old: html! { <div> </div>
|
||||
},
|
||||
new: html! { <div> <span></span> </div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn text_node_siblings() {
|
||||
// NOTE: Since there are two text nodes next to eachother we expect a `<!--ptns-->` separator in
|
||||
// between them.
|
||||
// @see virtual_node/mod.rs -> create_dom_node() for more information
|
||||
// TODO: A little more spacing than there should be in between the text nodes ... but doesn't
|
||||
// impact the user experience so we can look into that later..
|
||||
let override_expected = Some(
|
||||
r#"<div id="after"><span> The button has been clicked: <!--ptns--> world </span></div>"#,
|
||||
);
|
||||
|
||||
let old1 = VirtualNode::text("The button has been clicked: ");
|
||||
let old2 = VirtualNode::text("hello");
|
||||
|
||||
let new1 = VirtualNode::text("The button has been clicked: ");
|
||||
let new2 = VirtualNode::text("world");
|
||||
|
||||
DiffPatchTest {
|
||||
desc: "Diff patch on text node siblings",
|
||||
old: html! {
|
||||
<div id="before">
|
||||
<span> { {old1} {old2} } </span>
|
||||
</div>
|
||||
},
|
||||
new: html! {
|
||||
<div id="after">
|
||||
<span> { {new1} {new2} } </span>
|
||||
</div>
|
||||
},
|
||||
override_expected,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn append_text_node() {
|
||||
DiffPatchTest {
|
||||
desc: "Append text node",
|
||||
old: html! { <div> </div> },
|
||||
new: html! { <div> Hello </div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn append_sibling_text_nodes() {
|
||||
let text1 = VirtualNode::text("Hello");
|
||||
let text2 = VirtualNode::text("World");
|
||||
|
||||
DiffPatchTest {
|
||||
desc: "Append sibling text nodes",
|
||||
old: html! { <div> </div> },
|
||||
new: html! { <div> {text1} {text2} </div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_with_children() {
|
||||
DiffPatchTest {
|
||||
desc: "Replace node that has children",
|
||||
old: html! { <table><tr><th>0</th></tr><tr><td>1</td></tr></table> },
|
||||
new: html! { <table><tr><td>2</td></tr><tr><th>3</th></tr></table> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
// https://github.com/chinedufn/percy/issues/62
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_element_with_text_node() {
|
||||
DiffPatchTest {
|
||||
desc: "#62: Replace element with text node",
|
||||
old: html! { <span> <br> </span> },
|
||||
new: html! { <span> a </span> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
// https://github.com/chinedufn/percy/issues/68
|
||||
#[wasm_bindgen_test]
|
||||
fn text_root_node() {
|
||||
DiffPatchTest {
|
||||
desc: "Patching of text root node works",
|
||||
old: html! { Old text },
|
||||
new: html! { New text },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn replace_text_with_element() {
|
||||
DiffPatchTest {
|
||||
desc: "Replacing a text node with an element works",
|
||||
old: html! { <div>a</div> },
|
||||
new: html! { <div><br></div> },
|
||||
override_expected: None,
|
||||
}
|
||||
.test();
|
||||
}
|
53
packages/virtual-dom-rs/tests/diff_patch_test_case/mod.rs
Normal file
53
packages/virtual-dom-rs/tests/diff_patch_test_case/mod.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
//! Kept in its own file to more easily import into the book
|
||||
|
||||
use console_error_panic_hook;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// A test case that both diffing and patching are working in a real browser
|
||||
pub struct DiffPatchTest<'a> {
|
||||
/// Description of the test case.
|
||||
pub desc: &'static str,
|
||||
/// The old virtual node.
|
||||
pub old: VirtualNode,
|
||||
/// The new virtual node.
|
||||
pub new: VirtualNode,
|
||||
/// By default we generate the expected based on `new.to_string()`. You can
|
||||
/// use this field to override the expected HTML after patching.
|
||||
pub override_expected: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> DiffPatchTest<'a> {
|
||||
pub fn test(&mut self) {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
// Create a DOM node of the virtual root node
|
||||
let root_node: Node = self.old.create_dom_node().node;
|
||||
|
||||
// Clone since virtual_dom_rs::patch takes ownership of the root node.
|
||||
let patched_root_node: Node = root_node.clone();
|
||||
|
||||
// Generate patches
|
||||
let patches = virtual_dom_rs::diff(&self.old, &self.new);
|
||||
|
||||
// Patch our root node. It should now look like `self.new`
|
||||
virtual_dom_rs::patch(root_node, &patches);
|
||||
|
||||
// Determine the expected outer HTML
|
||||
let expected_outer_html = match self.override_expected {
|
||||
Some(ref expected) => expected.to_string(),
|
||||
None => self.new.to_string(),
|
||||
};
|
||||
|
||||
let actual_outer_html = match patched_root_node.node_type() {
|
||||
Node::ELEMENT_NODE => patched_root_node.unchecked_into::<Element>().outer_html(),
|
||||
Node::TEXT_NODE => patched_root_node.text_content().unwrap_or("".into()),
|
||||
_ => panic!("Unhandled node type"),
|
||||
};
|
||||
|
||||
assert_eq!(&actual_outer_html, &expected_outer_html, "{}", self.desc);
|
||||
}
|
||||
}
|
147
packages/virtual-dom-rs/tests/dom_updater.rs
Normal file
147
packages/virtual-dom-rs/tests/dom_updater.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
//! Ensure that our DomUpdater maintains Rc's to closures so that they work even
|
||||
//! after dropping virtual dom nodes.
|
||||
//!
|
||||
//! To run all tests in this file:
|
||||
//!
|
||||
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test dom_updater
|
||||
|
||||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
use console_error_panic_hook;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
use virtual_dom_rs::prelude::*;
|
||||
use virtual_dom_rs::DomUpdater;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test;
|
||||
use wasm_bindgen_test::*;
|
||||
use web_sys::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// Verify that our DomUpdater's patch method works.
|
||||
// We test a simple case here, since diff_patch.rs is responsible for testing more complex
|
||||
// diffing and patching.
|
||||
#[wasm_bindgen_test]
|
||||
fn patches_dom() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
let vdom = html! { <div></div> };
|
||||
|
||||
let mut dom_updater = DomUpdater::new(vdom);
|
||||
|
||||
let new_vdom = html! { <div id="patched"></div> };
|
||||
dom_updater.update(new_vdom);
|
||||
|
||||
document
|
||||
.body()
|
||||
.unwrap()
|
||||
.append_child(&dom_updater.root_node());
|
||||
assert_eq!(document.query_selector("#patched").unwrap().is_some(), true);
|
||||
}
|
||||
|
||||
// When you replace a DOM node with another DOM node we need to make sure that the closures
|
||||
// from the new DOM node are stored by the DomUpdater otherwise they'll get dropped and
|
||||
// won't work.
|
||||
#[wasm_bindgen_test]
|
||||
fn updates_active_closure_on_replace() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let body = document.body().unwrap();
|
||||
|
||||
let old = html! { <div> </div> };
|
||||
let mut dom_updater = DomUpdater::new_append_to_mount(old, &body);
|
||||
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
let text_clone = Rc::clone(&text);
|
||||
|
||||
let id = "update-active-closures-on-replace";
|
||||
|
||||
{
|
||||
let replace_node = html! {
|
||||
<input
|
||||
id=id
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
};
|
||||
|
||||
// New node replaces old node.
|
||||
// We are testing that we've stored this new node's closures even though `new` will be dropped
|
||||
// at the end of this block.
|
||||
dom_updater.update(replace_node);
|
||||
}
|
||||
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
let input = document.get_element_by_id(&id).unwrap();
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
}
|
||||
|
||||
// When you replace a DOM node with another DOM node we need to make sure that the closures
|
||||
// from the new DOM node are stored by the DomUpdater otherwise they'll get dropped and
|
||||
// won't work.
|
||||
#[wasm_bindgen_test]
|
||||
fn updates_active_closures_on_append() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
let body = document.body().unwrap();
|
||||
|
||||
let old = html! { <div> </div> };
|
||||
let mut dom_updater = DomUpdater::new_append_to_mount(old, &body);
|
||||
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
let text_clone = Rc::clone(&text);
|
||||
|
||||
let id = "update-active-closures-on-append";
|
||||
|
||||
{
|
||||
let append_node = html! {
|
||||
<div>
|
||||
<input
|
||||
id=id
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
</div>
|
||||
};
|
||||
|
||||
// New node gets appended into the DOM.
|
||||
// We are testing that we've stored this new node's closures even though `new` will be dropped
|
||||
// at the end of this block.
|
||||
dom_updater.update(append_node);
|
||||
}
|
||||
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
let input = document.get_element_by_id(id).unwrap();
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
}
|
47
packages/virtual-dom-rs/tests/events.rs
Normal file
47
packages/virtual-dom-rs/tests/events.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
// #![feature(proc_macro_hygiene)]
|
||||
|
||||
extern crate wasm_bindgen_test;
|
||||
extern crate web_sys;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::*;
|
||||
|
||||
use virtual_dom_rs::prelude::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
// Make sure that we successfully attach an event listener and see it work.
|
||||
#[wasm_bindgen_test]
|
||||
fn on_input() {
|
||||
let text = Rc::new(RefCell::new("Start Text".to_string()));
|
||||
let text_clone = Rc::clone(&text);
|
||||
|
||||
let input = html! {
|
||||
<input
|
||||
// On input we'll set our Rc<RefCell<String>> value to the input elements value
|
||||
oninput=move |event: Event| {
|
||||
let input_elem = event.target().unwrap();
|
||||
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
|
||||
*text_clone.borrow_mut() = input_elem.value();
|
||||
}
|
||||
value="End Text"
|
||||
>
|
||||
};
|
||||
|
||||
let input_event = InputEvent::new("input").unwrap();
|
||||
let input = input.create_dom_node().node;
|
||||
|
||||
assert_eq!(&*text.borrow(), "Start Text");
|
||||
|
||||
// After dispatching the oninput event our `text` should have a value of the input elements value.
|
||||
web_sys::EventTarget::from(input)
|
||||
.dispatch_event(&input_event)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&*text.borrow(), "End Text");
|
||||
}
|
35
packages/virtual-node/Cargo.toml
Normal file
35
packages/virtual-node/Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "virtual-node"
|
||||
version = "0.2.7"
|
||||
description = "A standalone Virtual DOM"
|
||||
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
|
||||
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/chinedufn/percy"
|
||||
documentation = "https://chinedufn.github.io/percy/api/virtual_node/"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2.33"
|
||||
html-validation = {path = "../html-validation", version = "0.1.1"}
|
||||
lazy_static = "1.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Comment",
|
||||
"Document",
|
||||
"Element",
|
||||
"HtmlElement",
|
||||
"EventTarget",
|
||||
"HtmlCollection",
|
||||
"Node",
|
||||
"NodeList",
|
||||
"Text",
|
||||
"Window",
|
||||
"Event",
|
||||
"MouseEvent",
|
||||
"InputEvent",
|
||||
]
|
||||
|
647
packages/virtual-node/src/lib.rs
Normal file
647
packages/virtual-node/src/lib.rs
Normal file
|
@ -0,0 +1,647 @@
|
|||
//! The virtual_node module exposes the `VirtualNode` struct and methods that power our
|
||||
//! virtual dom.
|
||||
|
||||
// TODO: A few of these dependencies (including js_sys) are used to power events.. yet events
|
||||
// only work on wasm32 targest. So we should start sprinkling some
|
||||
//
|
||||
// #[cfg(target_arch = "wasm32")]
|
||||
// #[cfg(not(target_arch = "wasm32"))]
|
||||
//
|
||||
// Around in order to get rid of dependencies that we don't need in non wasm32 targets
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub mod virtual_node_test_utils;
|
||||
|
||||
use web_sys::{self, Element, EventTarget, Node, Text};
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// 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.
|
||||
use lazy_static::lazy_static;
|
||||
lazy_static! {
|
||||
static ref ELEM_UNIQUE_ID: Mutex<u32> = Mutex::new(0);
|
||||
}
|
||||
|
||||
/// When building your views you'll typically use the `html!` macro to generate
|
||||
/// `VirtualNode`'s.
|
||||
///
|
||||
/// `html! { <div> <span></span> </div> }` really generates a `VirtualNode` with
|
||||
/// one child (span).
|
||||
///
|
||||
/// Later, on the client side, you'll use the `diff` and `patch` modules to
|
||||
/// update the real DOM with your latest tree of virtual nodes (virtual dom).
|
||||
///
|
||||
/// Or on the server side you'll just call `.to_string()` on your root virtual node
|
||||
/// in order to recursively render the node and all of its children.
|
||||
///
|
||||
/// TODO: Make all of these fields private and create accessor methods
|
||||
/// TODO: Create a builder to create instances of VirtualNode::Element with
|
||||
/// attrs and children without having to explicitly create a VElement
|
||||
#[derive(PartialEq)]
|
||||
pub enum VirtualNode {
|
||||
/// An element node (node type `ELEMENT_NODE`).
|
||||
Element(VElement),
|
||||
/// 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),
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct VElement {
|
||||
/// The HTML tag, such as "div"
|
||||
pub tag: String,
|
||||
/// HTML attributes such as id, class, style, etc
|
||||
pub attrs: HashMap<String, String>,
|
||||
/// Events that will get added to your real DOM element via `.addEventListener`
|
||||
pub events: Events,
|
||||
/// The children of this `VirtualNode`. So a <div> <em></em> </div> structure would
|
||||
/// have a parent div and one child, em.
|
||||
pub children: Vec<VirtualNode>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct VText {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl VirtualNode {
|
||||
/// Create a new virtual element node with a given tag.
|
||||
///
|
||||
/// These get patched into the DOM using `document.createElement`
|
||||
///
|
||||
/// ```ignore
|
||||
/// use virtual_dom_rs::VirtualNode;
|
||||
///
|
||||
/// let div = VirtualNode::element("div");
|
||||
/// ```
|
||||
pub fn element<S>(tag: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VirtualNode::Element(VElement::new(tag))
|
||||
}
|
||||
|
||||
/// Create a new virtual text node with the given text.
|
||||
///
|
||||
/// These get patched into the DOM using `document.createTextNode`
|
||||
///
|
||||
/// ```ignore
|
||||
/// use virtual_dom_rs::VirtualNode;
|
||||
///
|
||||
/// let div = VirtualNode::text("div");
|
||||
/// ```
|
||||
pub fn text<S>(text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VirtualNode::Text(VText::new(text.into()))
|
||||
}
|
||||
|
||||
/// Return a [`VElement`] reference, if this is an [`Element`] variant.
|
||||
///
|
||||
/// [`VElement`]: struct.VElement.html
|
||||
/// [`Element`]: enum.VirtualNode.html#variant.Element
|
||||
pub fn as_velement_ref(&self) -> Option<&VElement> {
|
||||
match self {
|
||||
VirtualNode::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.VirtualNode.html#variant.Element
|
||||
pub fn as_velement_mut(&mut self) -> Option<&mut VElement> {
|
||||
match self {
|
||||
VirtualNode::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.VirtualNode.html#variant.Text
|
||||
pub fn as_vtext_ref(&self) -> Option<&VText> {
|
||||
match self {
|
||||
VirtualNode::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.VirtualNode.html#variant.Text
|
||||
pub fn as_vtext_mut(&mut self) -> Option<&mut VText> {
|
||||
match self {
|
||||
VirtualNode::Text(ref mut text_node) => Some(text_node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create and return a `CreatedNode` instance (containing a DOM `Node`
|
||||
/// together with potentially related closures) for this virtual node.
|
||||
pub fn create_dom_node(&self) -> CreatedNode<Node> {
|
||||
match self {
|
||||
VirtualNode::Text(text_node) => {
|
||||
CreatedNode::without_closures(text_node.create_text_node())
|
||||
}
|
||||
VirtualNode::Element(element_node) => element_node.create_element_node().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
VirtualNode::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 {
|
||||
VirtualNode::Text(text_node) => {
|
||||
text_node.text += " ";
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VElement {
|
||||
pub fn new<S>(tag: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VElement {
|
||||
tag: tag.into(),
|
||||
attrs: HashMap::new(),
|
||||
events: Events(HashMap::new()),
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a DOM element by recursively creating DOM nodes for this element and it's
|
||||
/// children, it's children's children, etc.
|
||||
pub fn create_element_node(&self) -> CreatedNode<Element> {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
|
||||
let element = if html_validation::is_svg_namespace(&self.tag) {
|
||||
document
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), &self.tag)
|
||||
.unwrap()
|
||||
} else {
|
||||
document.create_element(&self.tag).unwrap()
|
||||
};
|
||||
|
||||
let mut closures = HashMap::new();
|
||||
|
||||
self.attrs.iter().for_each(|(name, value)| {
|
||||
if name == "unsafe_inner_html" {
|
||||
element.set_inner_html(value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
element
|
||||
.set_attribute(name, value)
|
||||
.expect("Set element attribute in create element");
|
||||
});
|
||||
|
||||
if self.events.0.len() > 0 {
|
||||
let unique_id = create_unique_identifier();
|
||||
|
||||
element
|
||||
.set_attribute("data-vdom-id".into(), &unique_id.to_string())
|
||||
.expect("Could not set attribute on element");
|
||||
|
||||
closures.insert(unique_id, vec![]);
|
||||
|
||||
self.events.0.iter().for_each(|(onevent, callback)| {
|
||||
// onclick -> click
|
||||
let event = &onevent[2..];
|
||||
|
||||
let current_elem: &EventTarget = element.dyn_ref().unwrap();
|
||||
|
||||
current_elem
|
||||
.add_event_listener_with_callback(
|
||||
event,
|
||||
callback.as_ref().as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
closures
|
||||
.get_mut(&unique_id)
|
||||
.unwrap()
|
||||
.push(Rc::clone(callback));
|
||||
});
|
||||
}
|
||||
|
||||
let mut previous_node_was_text = false;
|
||||
|
||||
self.children.iter().for_each(|child| {
|
||||
match child {
|
||||
VirtualNode::Text(text_node) => {
|
||||
let current_node = element.as_ref() as &web_sys::Node;
|
||||
|
||||
// We ensure that the text siblings are patched by preventing the browser from merging
|
||||
// neighboring text nodes. Originally inspired by some of React's work from 2016.
|
||||
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
|
||||
// -> https://github.com/facebook/react/pull/5753
|
||||
//
|
||||
// `ptns` = Percy text node separator
|
||||
if previous_node_was_text {
|
||||
let separator = document.create_comment("ptns");
|
||||
current_node
|
||||
.append_child(separator.as_ref() as &web_sys::Node)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
current_node
|
||||
.append_child(&text_node.create_text_node())
|
||||
.unwrap();
|
||||
|
||||
previous_node_was_text = true;
|
||||
}
|
||||
VirtualNode::Element(element_node) => {
|
||||
previous_node_was_text = false;
|
||||
|
||||
let child = element_node.create_element_node();
|
||||
let child_elem: Element = child.node;
|
||||
|
||||
closures.extend(child.closures);
|
||||
|
||||
element.append_child(&child_elem).unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(on_create_elem) = self.events.0.get("on_create_elem") {
|
||||
let on_create_elem: &js_sys::Function =
|
||||
on_create_elem.as_ref().as_ref().unchecked_ref();
|
||||
on_create_elem
|
||||
.call1(&wasm_bindgen::JsValue::NULL, &element)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
CreatedNode {
|
||||
node: element,
|
||||
closures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VText {
|
||||
/// Create an new `VText` instance with the specified text.
|
||||
pub fn new<S>(text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
VText { text: text.into() }
|
||||
}
|
||||
|
||||
/// Return a `Text` element from a `VirtualNode`, typically right before adding it
|
||||
/// into the DOM.
|
||||
pub fn create_text_node(&self) -> Text {
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
document.create_text_node(&self.text)
|
||||
}
|
||||
}
|
||||
|
||||
/// A node along with all of the closures that were created for that
|
||||
/// node's events and all of it's child node's events.
|
||||
pub struct CreatedNode<T> {
|
||||
/// A `Node` or `Element` that was created from a `VirtualNode`
|
||||
pub node: T,
|
||||
/// A map of a node's unique identifier along with all of the Closures for that node.
|
||||
///
|
||||
/// The DomUpdater uses this to look up nodes and see if they're still in the page. If not
|
||||
/// the reference that we maintain to their closure will be dropped, thus freeing the Closure's
|
||||
/// memory.
|
||||
pub closures: HashMap<u32, Vec<DynClosure>>,
|
||||
}
|
||||
|
||||
impl<T> CreatedNode<T> {
|
||||
pub fn without_closures<N: Into<T>>(node: N) -> Self {
|
||||
CreatedNode {
|
||||
node: node.into(),
|
||||
closures: HashMap::with_capacity(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for CreatedNode<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.node
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CreatedNode<Element>> for CreatedNode<Node> {
|
||||
fn from(other: CreatedNode<Element>) -> CreatedNode<Node> {
|
||||
CreatedNode {
|
||||
node: other.node.into(),
|
||||
closures: other.closures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_unique_identifier() -> u32 {
|
||||
let mut elem_unique_id = ELEM_UNIQUE_ID.lock().unwrap();
|
||||
|
||||
*elem_unique_id += 1;
|
||||
|
||||
*elem_unique_id
|
||||
}
|
||||
|
||||
/// A trait with common functionality for rendering front-end views.
|
||||
pub trait View {
|
||||
/// Render a VirtualNode, or any IntoIter<VirtualNode>
|
||||
fn render(&self) -> VirtualNode;
|
||||
}
|
||||
|
||||
impl<V> From<&V> for VirtualNode
|
||||
where
|
||||
V: View,
|
||||
{
|
||||
fn from(v: &V) -> Self {
|
||||
v.render()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 .. VirtualNode .. Vec<VirtualNode> ... etc
|
||||
pub struct IterableNodes(Vec<VirtualNode>);
|
||||
|
||||
impl IterableNodes {
|
||||
/// Retrieve the first node mutably
|
||||
pub fn first(&mut self) -> &mut VirtualNode {
|
||||
self.0.first_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Retrieve the last node mutably
|
||||
pub fn last(&mut self) -> &mut VirtualNode {
|
||||
self.0.last_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for IterableNodes {
|
||||
type Item = VirtualNode;
|
||||
// TODO: Is this possible with an array [VirtualNode] instead of a vec?
|
||||
type IntoIter = ::std::vec::IntoIter<VirtualNode>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VirtualNode> for IterableNodes {
|
||||
fn from(other: VirtualNode) -> Self {
|
||||
IterableNodes(vec![other])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for IterableNodes {
|
||||
fn from(other: &str) -> Self {
|
||||
IterableNodes(vec![VirtualNode::text(other)])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for IterableNodes {
|
||||
fn from(other: String) -> Self {
|
||||
IterableNodes(vec![VirtualNode::text(other.as_str())])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<VirtualNode>> for IterableNodes {
|
||||
fn from(other: Vec<VirtualNode>) -> Self {
|
||||
IterableNodes(other)
|
||||
}
|
||||
}
|
||||
|
||||
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<VText> for VirtualNode {
|
||||
fn from(other: VText) -> Self {
|
||||
VirtualNode::Text(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VElement> for VirtualNode {
|
||||
fn from(other: VElement) -> Self {
|
||||
VirtualNode::Element(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for VirtualNode {
|
||||
fn from(other: &str) -> Self {
|
||||
VirtualNode::text(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for VirtualNode {
|
||||
fn from(other: String) -> Self {
|
||||
VirtualNode::text(other.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for VirtualNode {
|
||||
type Item = VirtualNode;
|
||||
// TODO: Is this possible with an array [VirtualNode] instead of a vec?
|
||||
type IntoIter = ::std::vec::IntoIter<VirtualNode>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
vec![self].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<::std::vec::IntoIter<VirtualNode>> for VirtualNode {
|
||||
fn into(self) -> ::std::vec::IntoIter<VirtualNode> {
|
||||
self.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VirtualNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VirtualNode::Element(e) => write!(f, "Node::{:?}", e),
|
||||
VirtualNode::Text(t) => write!(f, "Node::{:?}", t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::Debug for VText {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Text({})", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
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 !html_validation::is_self_closing(&self.tag) {
|
||||
write!(f, "</{}>", self.tag)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a VirtualNode into an HTML string (delegate impl to variants)
|
||||
impl fmt::Display for VirtualNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
VirtualNode::Element(element) => write!(f, "{}", element),
|
||||
VirtualNode::Text(text) => write!(f, "{}", text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Box<dyn AsRef<JsValue>>> is our js_sys::Closure. Stored this way to allow us to store
|
||||
/// any Closure regardless of the arguments.
|
||||
pub type DynClosure = Rc<dyn AsRef<JsValue>>;
|
||||
|
||||
/// We need a custom implementation of fmt::Debug since JsValue doesn't
|
||||
/// implement debug.
|
||||
pub struct Events(pub HashMap<String, DynClosure>);
|
||||
|
||||
impl PartialEq for Events {
|
||||
// TODO: What should happen here..? And why?
|
||||
fn eq(&self, _rhs: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Events {
|
||||
// Print out all of the event names for this VirtualNode
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let events: String = self.0.keys().map(|key| " ".to_string() + key).collect();
|
||||
write!(f, "{}", events)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn self_closing_tag_to_string() {
|
||||
let node = VirtualNode::element("br");
|
||||
|
||||
// No </br> since self closing tag
|
||||
assert_eq!(&node.to_string(), "<br>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
let mut node = VirtualNode::Element(VElement::new("div"));
|
||||
node.as_velement_mut()
|
||||
.unwrap()
|
||||
.attrs
|
||||
.insert("id".into(), "some-id".into());
|
||||
|
||||
let mut child = VirtualNode::Element(VElement::new("span"));
|
||||
|
||||
let mut text = VirtualNode::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);
|
||||
}
|
||||
}
|
141
packages/virtual-node/src/virtual_node_test_utils.rs
Normal file
141
packages/virtual-node/src/virtual_node_test_utils.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
//! A collection of functions that are useful for unit testing your html! views.
|
||||
|
||||
use crate::VirtualNode;
|
||||
|
||||
impl VirtualNode {
|
||||
/// Get a vector of all of the VirtualNode children / grandchildren / etc of
|
||||
/// your virtual_node that have a label that matches your filter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # #[macro_use] extern crate virtual_dom_rs; fn main() {
|
||||
///
|
||||
/// let component = html! {<div>
|
||||
/// <span label="hello",> {"Hi!"} </span>
|
||||
/// <em label="world",> {"There!!"} </em>
|
||||
/// <em label="hello",></em>
|
||||
/// </div> };
|
||||
///
|
||||
/// let hello_nodes = component.filter_label(|label| {
|
||||
/// label.contains("hello")
|
||||
/// });
|
||||
///
|
||||
/// assert_eq!(hello_nodes.len(), 2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn filter_label<'a, F>(&'a self, filter: F) -> Vec<&'a VirtualNode>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
// Get descendants recursively
|
||||
let mut descendants: Vec<&'a VirtualNode> = vec![];
|
||||
match self {
|
||||
VirtualNode::Text(_) => { /* nothing to do */ }
|
||||
VirtualNode::Element(element_node) => {
|
||||
for child in element_node.children.iter() {
|
||||
get_descendants(&mut descendants, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter descendants
|
||||
descendants
|
||||
.into_iter()
|
||||
.filter(|vn: &&'a VirtualNode| match vn {
|
||||
VirtualNode::Text(_) => false,
|
||||
VirtualNode::Element(element_node) => match element_node.attrs.get("label") {
|
||||
Some(label) => filter(label),
|
||||
None => false,
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a vector of all of the descendants of this VirtualNode
|
||||
/// that have the provided `filter`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # #[macro_use] extern crate virtual_dom_rs; fn main() {
|
||||
///
|
||||
/// let component = html! {<div>
|
||||
/// <span label="hello",> {"Hi!"} </span>
|
||||
/// <em label="world",> {"There!!"} </em>
|
||||
/// <em label="hello",></em>
|
||||
/// </div> };
|
||||
///
|
||||
/// let hello_nodes = component.filter_label_equals("hello");
|
||||
///
|
||||
/// assert_eq!(hello_nodes.len(), 2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn filter_label_equals<'a>(&'a self, label: &str) -> Vec<&'a VirtualNode> {
|
||||
self.filter_label(|node_label| node_label == label)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_descendants<'a>(descendants: &mut Vec<&'a VirtualNode>, node: &'a VirtualNode) {
|
||||
descendants.push(node);
|
||||
match node {
|
||||
VirtualNode::Text(_) => { /* nothing to do */ }
|
||||
VirtualNode::Element(element_node) => {
|
||||
for child in element_node.children.iter() {
|
||||
get_descendants(descendants, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::VElement;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: Move this test somewhere that we can use the `html!` macro
|
||||
// #[test]
|
||||
// fn filter_label() {
|
||||
// let html = html! {
|
||||
// // Should not pick up labels on the root node
|
||||
// <div label="hello0",>
|
||||
// // This node gets picked up
|
||||
// <span label="hello1",>
|
||||
// </span>
|
||||
// // This node gets picked up
|
||||
// <em label="hello2",>
|
||||
// { "hello there :)!" }
|
||||
// </em>
|
||||
// <div label="world",></div>
|
||||
// </div>
|
||||
// };
|
||||
//
|
||||
// let hello_nodes = html.filter_label(|label| label.contains("hello"));
|
||||
//
|
||||
// assert_eq!(
|
||||
// hello_nodes.len(),
|
||||
// 2,
|
||||
// "2 elements with label containing 'hello'"
|
||||
// );
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn label_equals() {
|
||||
let span = VirtualNode::element("span");
|
||||
|
||||
let mut attrs = HashMap::new();
|
||||
attrs.insert("label".to_string(), "hello".to_string());
|
||||
let mut em = VElement::new("em");
|
||||
em.attrs = attrs;
|
||||
|
||||
let mut html = VElement::new("div");
|
||||
html.children.push(span);
|
||||
html.children.push(em.into());
|
||||
|
||||
let html_node = VirtualNode::Element(html);
|
||||
let hello_nodes = html_node.filter_label_equals("hello");
|
||||
|
||||
assert_eq!(hello_nodes.len(), 1);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue