Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
546 changes: 546 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

35 changes: 21 additions & 14 deletions examples/hello-world/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import {useState} from 'react'

let n = 0

function App() {
const [name, setName] = useState(() => false)
const [age, setAge] = useState(() => 10)
if (n === 0) {
let tid = setTimeout(() => {
n++
setName(true)
setAge(11)
clearTimeout((tid))
}, 1000)
}
const [num, updateNum] = useState(0);

return name ? <Comp>{name + age}</Comp> : 'N/A'
const isOdd = num % 2;

return (
<h3
onClickCapture={(e) => {
e.stopPropagation()
console.log('click h3', e.currentTarget)
updateNum(prev => prev + 1);
}}
>
<div onClick={(e) => {
console.log('click div', e.currentTarget)
}}>
{isOdd ? <div>odd</div> : <p>even</p>}
</div>

</h3>
);
}

function Comp({children}) {
return <span><i>{`Hello world, ${children}`}</i></span>
function Child({num}: { num: number }) {
return <div>{num}</div>;
}

export default App
4 changes: 1 addition & 3 deletions examples/hello-world/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {createRoot} from 'react-dom'
import App from './App.tsx'
import App from "./App.tsx";

const root = createRoot(document.getElementById("root"))
const a = <App/>
console.log(a)
root.render(<App/>)

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"example": "examples"
},
"scripts": {
"build": "node scripts/build.js",
"build:dev": "ENV=dev node scripts/build.js",
"build:test": "node scripts/build.js --test",
"test": "npm run build:test && jest"
},
Expand Down
4 changes: 3 additions & 1 deletion packages/react-dom/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2.84"
web-sys = { version = "0.3.69", features = ["console", "Window", "Document", "Text", "Element"] }
web-sys = { version = "0.3.69", features = ["console", "Window", "Document", "Text", "Element", "EventListener"] }
react-reconciler = { path = "../react-reconciler" }
shared = { path = "../shared" }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
gloo = "0.11.0"
js-sys = "0.3.69"

[dev-dependencies]
wasm-bindgen-test = "0.3.34"
Expand Down
41 changes: 36 additions & 5 deletions packages/react-dom/src/host_config.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
use std::any::Any;
use std::rc::Rc;

use js_sys::JSON::stringify;
use wasm_bindgen::JsValue;
use web_sys::{Node, window};

use react_reconciler::HostConfig;
use shared::log;
use shared::{log, type_of};

use crate::synthetic_event::update_event_props;

pub struct ReactDomHostConfig;

pub fn to_string(js_value: &JsValue) -> String {
js_value.as_string().unwrap_or_else(|| {
if js_value.is_undefined() {
"undefined".to_owned()
} else if js_value.is_null() {
"null".to_owned()
} else if type_of(js_value, "boolean") {
let bool_value = js_value.as_bool().unwrap();
bool_value.to_string()
} else if js_value.as_f64().is_some() {
let num_value = js_value.as_f64().unwrap();
num_value.to_string()
} else {
let js_string = stringify(&js_value).unwrap();
js_string.into()
}
})
}

impl HostConfig for ReactDomHostConfig {
fn create_text_instance(&self, content: String) -> Rc<dyn Any> {
fn create_text_instance(&self, content: &JsValue) -> Rc<dyn Any> {
let window = window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
Rc::new(Node::from(document.create_text_node(content.as_str())))
Rc::new(Node::from(document.create_text_node(
to_string(content).as_str()
)))
}

fn create_instance(&self, _type: String) -> Rc<dyn Any> {
fn create_instance(&self, _type: String, props: Rc<dyn Any>) -> Rc<dyn Any> {
let window = window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
match document.create_element(_type.as_ref()) {
Ok(element) => Rc::new(Node::from(element)),
Ok(element) => {
let element = update_event_props(
element.clone(),
&*props.clone().downcast::<JsValue>().unwrap(),
);
Rc::new(Node::from(element))
}
Err(_) => todo!(),
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::utils::set_panic_hook;
mod host_config;
mod renderer;
mod utils;
mod synthetic_event;

#[wasm_bindgen(js_name = createRoot)]
pub fn create_root(container: &JsValue) -> Renderer {
Expand All @@ -24,6 +25,6 @@ pub fn create_root(container: &JsValue) -> Renderer {
}
};
let root = reconciler.create_container(Rc::new(node));
let renderer = Renderer::new(root, reconciler);
let renderer = Renderer::new(root, reconciler, container);
renderer
}
8 changes: 6 additions & 2 deletions packages/react-dom/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@ use wasm_bindgen::prelude::*;
use react_reconciler::fiber::FiberRootNode;
use react_reconciler::Reconciler;

use crate::synthetic_event::init_event;

#[wasm_bindgen]
pub struct Renderer {
container: JsValue,
root: Rc<RefCell<FiberRootNode>>,
reconciler: Reconciler,
}

impl Renderer {
pub fn new(root: Rc<RefCell<FiberRootNode>>, reconciler: Reconciler) -> Self {
Self { root, reconciler }
pub fn new(root: Rc<RefCell<FiberRootNode>>, reconciler: Reconciler, container: &JsValue) -> Self {
Self { root, reconciler, container: container.clone() }
}
}

#[wasm_bindgen]
impl Renderer {
pub fn render(&self, element: &JsValue) -> JsValue {
init_event(self.container.clone(), "click".to_string());
self.reconciler
.update_container(element.clone(), self.root.clone())
}
Expand Down
173 changes: 173 additions & 0 deletions packages/react-dom/src/synthetic_event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use gloo::events::EventListener;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen::closure::Closure;
use web_sys::{Element, Event};
use web_sys::js_sys::{Function, Object, Reflect};

use shared::{derive_from_js_value, is_dev, log};

static VALID_EVENT_TYPE_LIST: [&str; 1] = ["click"];
static ELEMENT_EVENT_PROPS_KEY: &str = "__props";

struct Paths {
capture: Vec<Function>,
bubble: Vec<Function>,
}

impl Paths {
fn new() -> Self {
Paths {
capture: vec![],
bubble: vec![],
}
}
}

fn create_synthetic_event(e: Event) -> Event {
Reflect::set(&*e, &"__stopPropagation".into(), &JsValue::from_bool(false));

let e_cloned = e.clone();
let origin_stop_propagation = derive_from_js_value(&*e, "stopPropagation");
let closure = Closure::wrap(Box::new(move || {
Reflect::set(
&*e_cloned,
&"__stopPropagation".into(),
&JsValue::from_bool(true),
);
if origin_stop_propagation.is_function() {
let origin_stop_propagation = origin_stop_propagation.dyn_ref::<Function>().unwrap();
origin_stop_propagation.call0(&JsValue::null());
}
}) as Box<dyn Fn()>);
let function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
Reflect::set(&*e.clone(), &"stopPropagation".into(), &function.into());
e
}

fn trigger_event_flow(paths: Vec<Function>, se: &Event) {
for callback in paths {
callback.call1(&JsValue::null(), se);
if derive_from_js_value(se, "__stopPropagation")
.as_bool()
.unwrap()
{
break;
}
}
}

fn dispatch_event(container: &Element, event_type: String, e: &Event) {
if e.target().is_none() {
log!("Target is none");
return;
}

let target_element = e.target().unwrap().dyn_into::<Element>().unwrap();
let Paths { capture, bubble } =
collect_paths(Some(target_element), container, event_type.as_str());

let se = create_synthetic_event(e.clone());

if is_dev() {
log!("Event {} capture phase", event_type);
}

trigger_event_flow(capture, &se);
if !derive_from_js_value(&se, "__stopPropagation")
.as_bool()
.unwrap()
{
if is_dev() {
log!("Event {} bubble phase", event_type);
}
trigger_event_flow(bubble, &se);
}
}

fn collect_paths(
mut target_element: Option<Element>,
container: &Element,
event_type: &str,
) -> Paths {
let mut paths = Paths::new();
while target_element.is_some() && !Object::is(target_element.as_ref().unwrap(), container) {
let event_props =
derive_from_js_value(target_element.as_ref().unwrap(), ELEMENT_EVENT_PROPS_KEY);
if event_props.is_object() {
let callback_name_list = get_event_callback_name_from_event_type(event_type);
if callback_name_list.is_some() {
for (i, callback_name) in callback_name_list.as_ref().unwrap().iter().enumerate() {
let event_callback = derive_from_js_value(&event_props, *callback_name);
if event_callback.is_function() {
let event_callback = event_callback.dyn_ref::<Function>().unwrap();
if i == 0 {
paths.capture.insert(0, event_callback.clone());
} else {
paths.bubble.push(event_callback.clone());
}
}
}
}
}
target_element = target_element.unwrap().parent_element();
}
paths
}

fn get_event_callback_name_from_event_type(event_type: &str) -> Option<Vec<&str>> {
if event_type == "click" {
return Some(vec!["onClickCapture", "onClick"]);
}
None
}

pub fn init_event(container: JsValue, event_type: String) {
if !VALID_EVENT_TYPE_LIST.contains(&event_type.clone().as_str()) {
log!("Unsupported event type: {:?}", event_type);
return;
}

if is_dev() {
log!("Init event {:?}", event_type);
}

let element = container
.clone()
.dyn_into::<Element>()
.expect("container is not element");
let on_click = EventListener::new(&element.clone(), event_type.clone(), move |event| {
dispatch_event(&element, event_type.clone(), event)
});
on_click.forget();
}

pub fn update_event_props(node: Element, props: &JsValue) -> Element {
let js_value = derive_from_js_value(&node, ELEMENT_EVENT_PROPS_KEY);
let element_event_props = if js_value.is_object() {
js_value.dyn_into::<Object>().unwrap()
} else {
Object::new()
};
for event_type in VALID_EVENT_TYPE_LIST {
let callback_name_list = get_event_callback_name_from_event_type(event_type);
if callback_name_list.is_none() {
break;
}

for callback_name in callback_name_list.clone().unwrap() {
if props.is_object()
&& props
.dyn_ref::<Object>()
.unwrap()
.has_own_property(&callback_name.into())
{
let callback = derive_from_js_value(props, callback_name);
Reflect::set(&element_event_props, &callback_name.into(), &callback);
}
}
}
Reflect::set(&node, &ELEMENT_EVENT_PROPS_KEY.into(), &element_event_props);

node
}
5 changes: 3 additions & 2 deletions packages/react-reconciler/src/child_fiber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::rc::Rc;
use wasm_bindgen::JsValue;
use web_sys::js_sys::{Object, Reflect};

use shared::{derive_from_js_value, log, REACT_ELEMENT_TYPE};
use shared::{derive_from_js_value, log, REACT_ELEMENT_TYPE, type_of};

use crate::fiber::FiberNode;
use crate::fiber_flags::Flags;
Expand Down Expand Up @@ -131,6 +131,7 @@ fn reconcile_single_text_node(
Rc::new(RefCell::new(created))
}


fn _reconcile_child_fibers(
return_fiber: Rc<RefCell<FiberNode>>,
current_first_child: Option<Rc<RefCell<FiberNode>>>,
Expand All @@ -140,7 +141,7 @@ fn _reconcile_child_fibers(
if new_child.is_some() {
let new_child = &new_child.unwrap();

if new_child.is_string() {
if type_of(new_child, "string") || type_of(new_child, "number") {
return Some(place_single_child(
reconcile_single_text_node(
return_fiber,
Expand Down
Loading