Skip to content

Commit

Permalink
Start implementing individual browser events
Browse files Browse the repository at this point in the history
  • Loading branch information
chinedufn committed Nov 9, 2018
1 parent d968875 commit 51d6463
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 29 deletions.
16 changes: 10 additions & 6 deletions crates/virtual-dom-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ repository = "https://github.com/chinedufn/percy"
documentation = "https://chinedufn.github.io/percy/api/virtual_dom_rs/"
edition = "2018"

[dev-dependencies]
wasm-bindgen-test = "0.2"
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3"

[dependencies]

[dependencies.wasm-bindgen]
version = "0.2"
features = ["default", "nightly"]
wasm-bindgen = {version = "0.2", features = ["default", "nightly"]}

[dependencies.web-sys]
version = "0.3"
Expand All @@ -31,11 +29,17 @@ features = [
"NodeList",
"Text",
"Window",
"MouseEvent",
"InputEvent",
]

[dev-dependencies]
wasm-bindgen-test = "0.2"

[dev-dependencies.web-sys]
version = "0.3"
features = [
"Event",
"DomTokenList"
"DomTokenList",
"HtmlInputElement",
]
33 changes: 27 additions & 6 deletions crates/virtual-dom-rs/src/html_macro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::Closure;

#[cfg(target_arch = "wasm32")]
use js_sys::Function;

/// When parsing our HTML we keep track of whether the last tag that we saw was an open or
/// close tag.
///
Expand Down Expand Up @@ -166,6 +169,24 @@ macro_rules! recurse_html {
recurse_html! { $active_node $root_nodes $prev_tag_type $($remaining_html)* }
};

// InputEvent
//
// for <input oninput=|input_event| { do.something(); },></input> ths is:
// oninput=|input_event| { do.something(); }
($active_node:ident $root_nodes:ident $prev_tag_type:ident oninput = $callback:expr, $($remaining_html:tt)*) => {
// Closure::wrap only works on wasm32 targets, so we only support events when compiling to
// wasm at this time.
#[cfg(target_arch = "wasm32")]
{
let closure = $crate::Closure::wrap(Box::new($callback) as Box<FnMut(_)>);

$active_node.as_mut().unwrap().borrow_mut().browser_events.oninput = Some(closure);
}

recurse_html! { $active_node $root_nodes $prev_tag_type $($remaining_html)* }
};


// A property
// For <div id="10",> this is:
// id = "10",
Expand All @@ -178,18 +199,20 @@ macro_rules! recurse_html {
recurse_html! { $active_node $root_nodes $prev_tag_type $($remaining_html)* }
};


// An event
// for <div $onclick=|| { do.something(); },></div> ths is:
// $onclick=|| { do.something() }
// $onclick=|| { do.something(); }
($active_node:ident $root_nodes:ident $prev_tag_type:ident ! $event_name:tt = $callback:expr, $($remaining_html:tt)*) => {

// Closure::new only works on wasm32 targets, so we only support events when compiling to
// wasm at this time.
#[cfg(target_arch = "wasm32")]
{
$active_node.as_mut().unwrap().borrow_mut().events.0.insert(
let closure = $crate::Closure::wrap(Box::new($callback) as Box<FnMut()>);

$active_node.as_mut().unwrap().borrow_mut().custom_events.0.insert(
stringify!($event_name).to_string(),
$crate::RefCell::new(Some($crate::Closure::new($callback)))
$crate::RefCell::new(Some(closure))
);
}

Expand Down Expand Up @@ -261,9 +284,7 @@ macro_rules! recurse_html {
mod tests {
use super::*;
use crate::VirtualNode;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

struct HTMLMacroTest {
generated: VirtualNode,
Expand Down
6 changes: 6 additions & 0 deletions crates/virtual-dom-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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::*;
Expand All @@ -22,10 +24,14 @@ pub use crate::html_macro::*;
pub mod virtual_node;
pub use crate::virtual_node::*;

#[cfg(target_arch = "wasm32")]
mod diff;
#[cfg(target_arch = "wasm32")]
pub use crate::diff::*;

#[cfg(target_arch = "wasm32")]
mod patch;
#[cfg(target_arch = "wasm32")]
pub use crate::patch::*;

mod view;
Expand Down
91 changes: 74 additions & 17 deletions crates/virtual-dom-rs/src/virtual_node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ pub use std::rc::Rc;

pub mod virtual_node_test_utils;

#[cfg(target_arch = "wasm32")]
use web_sys;
use web_sys::{Element, Text};
#[cfg(target_arch = "wasm32")]
use web_sys::*;

#[cfg(target_arch = "wasm32")]
use js_sys::Function;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::Closure;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;

/// When building your views you'll typically use the `html!` macro to generate
Expand All @@ -25,14 +31,19 @@ use wasm_bindgen::JsCast;
///
/// 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
#[derive(PartialEq)]
pub struct VirtualNode {
/// The HTML tag, such as "div"
pub tag: String,
/// HTML props such as id, class, style, etc
pub props: HashMap<String, String>,
/// Events that will get added to your real DOM element via `.addEventListener`
pub events: Events,
pub custom_events: CustomEvents,
/// Events that are specified in web_sys such as `oninput` `onclick` `onmousemove`
/// etc...
pub browser_events: BrowserEvents,
/// The children of this `VirtualNode`. So a <div> <em></em> </div> structure would
/// have a parent div and one child, em.
pub children: Option<Vec<VirtualNode>>,
Expand All @@ -56,7 +67,9 @@ pub struct ParsedVirtualNode {
/// TODO: See if we can get rid of ParsedVirtualNode entirely in favor of only VirtualNode
pub props: HashMap<String, String>,
/// TODO: See if we can get rid of ParsedVirtualNode entirely in favor of only VirtualNode
pub events: Events,
pub custom_events: CustomEvents,
/// TODO: See if we can get rid of ParsedVirtualNode entirely in favor of only VirtualNode
pub browser_events: BrowserEvents,
/// TODO: See if we can get rid of ParsedVirtualNode entirely in favor of only VirtualNode
/// TODO: Don't think this needs to be an option
pub children: Option<Vec<Rc<RefCell<ParsedVirtualNode>>>>,
Expand All @@ -70,11 +83,12 @@ impl ParsedVirtualNode {
/// Create a virtual node that is meant to represent a DOM element
pub fn new(tag: &str) -> ParsedVirtualNode {
let props = HashMap::new();
let events = Events(HashMap::new());
let events = CustomEvents(HashMap::new());
ParsedVirtualNode {
tag: tag.to_string(),
props,
events,
custom_events: events,
browser_events: BrowserEvents::default(),
children: Some(vec![]),
parent: None,
text: None,
Expand All @@ -86,7 +100,8 @@ impl ParsedVirtualNode {
ParsedVirtualNode {
tag: "".to_string(),
props: HashMap::new(),
events: Events(HashMap::new()),
browser_events: BrowserEvents::default(),
custom_events: CustomEvents(HashMap::new()),
children: Some(vec![]),
parent: None,
text: Some(text.to_string()),
Expand All @@ -111,7 +126,8 @@ impl From<ParsedVirtualNode> for VirtualNode {
VirtualNode {
tag: parsed_node.tag,
props: parsed_node.props,
events: parsed_node.events,
browser_events: parsed_node.browser_events,
custom_events: parsed_node.custom_events,
children,
text: parsed_node.text,
}
Expand All @@ -130,11 +146,13 @@ impl VirtualNode {
/// ```
pub fn new(tag: &str) -> VirtualNode {
let props = HashMap::new();
let events = Events(HashMap::new());
let custom_events = CustomEvents(HashMap::new());
let browser_events = BrowserEvents::default();
VirtualNode {
tag: tag.to_string(),
props,
events,
custom_events,
browser_events,
children: Some(vec![]),
text: None,
}
Expand All @@ -153,7 +171,8 @@ impl VirtualNode {
VirtualNode {
tag: "".to_string(),
props: HashMap::new(),
events: Events(HashMap::new()),
custom_events: CustomEvents(HashMap::new()),
browser_events: BrowserEvents::default(),
children: Some(vec![]),
text: Some(text.to_string()),
}
Expand All @@ -163,6 +182,7 @@ impl VirtualNode {
impl VirtualNode {
/// Build a DOM element by recursively creating DOM nodes for this element and it's
/// children, it's children's children, etc.
#[cfg(target_arch = "wasm32")]
pub fn create_element(&self) -> Element {
let document = web_sys::window().unwrap().document().unwrap();

Expand All @@ -174,15 +194,16 @@ impl VirtualNode {
.expect("Set element attribute in create element");
});

self.events.0.iter().for_each(|(onevent, callback)| {
self.custom_events.0.iter().for_each(|(onevent, callback)| {
// onclick -> click
let event = &onevent[2..];

let mut callback = callback.borrow_mut();
let callback = callback.take().unwrap();
(current_elem.as_ref() as &web_sys::EventTarget)
.add_event_listener_with_callback(event, callback.as_ref().unchecked_ref())
.add_event_listener_with_callback(event, &callback.as_ref().unchecked_ref())
.unwrap();

callback.forget();
});

Expand Down Expand Up @@ -227,6 +248,7 @@ impl VirtualNode {
current_elem
}

#[cfg(target_arch = "wasm32")]
/// Return a `Text` element from a `VirtualNode`, typically right before adding it
/// into the DOM.
pub fn create_text_node(&self) -> Text {
Expand Down Expand Up @@ -263,7 +285,8 @@ impl From<VirtualNode> for ParsedVirtualNode {
ParsedVirtualNode {
tag: node.tag,
props: node.props,
events: node.events,
browser_events: node.browser_events,
custom_events: node.custom_events,
children,
parent: None,
text: node.text,
Expand Down Expand Up @@ -341,25 +364,59 @@ impl fmt::Display for VirtualNode {
}
}

/// We need a custom implementation of fmt::Debug since Fn() doesn't
#[cfg(target_arch = "wasm32")]
/// TODO: Private fields with set/get methods
#[derive(Default)]
pub struct BrowserEvents {
/// https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.HtmlElement.html#method.oninput
pub oninput: Option<Closure<FnMut(InputEvent) -> ()>>,
}

#[cfg(target_arch = "wasm32")]
impl PartialEq for BrowserEvents {
fn eq(&self, _rhs: &Self) -> bool {
true
}
}

#[cfg(target_arch = "wasm32")]
impl fmt::Debug for BrowserEvents {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", "TODO: browser event debug implementation")
}
}

/// Not used on the server side
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, PartialEq, Default)]
pub struct BrowserEvents;

/// We need a custom implementation of fmt::Debug since FnMut() doesn't
/// implement debug.
pub struct Events(pub HashMap<String, RefCell<Option<Closure<Fn() -> ()>>>>);
#[cfg(target_arch = "wasm32")]
pub struct CustomEvents(pub HashMap<String, RefCell<Option<Closure<FnMut() -> ()>>>>);

impl PartialEq for Events {
#[cfg(target_arch = "wasm32")]
impl PartialEq for CustomEvents {
// TODO: What should happen here..? And why?
fn eq(&self, _rhs: &Self) -> bool {
true
}
}

impl fmt::Debug for Events {
#[cfg(target_arch = "wasm32")]
impl fmt::Debug for CustomEvents {
// 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| format!("{} ", key)).collect();
write!(f, "{}", events)
}
}

#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, PartialEq)]
pub struct CustomEvents(pub HashMap<(), ()>);

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions crates/virtual-dom-rs/tests/create_element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ fn click_event() {

assert_eq!(*clicked, Cell::new(true));
}

39 changes: 39 additions & 0 deletions crates/virtual-dom-rs/tests/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 web_sys::*;

#[macro_use]
extern crate virtual_dom_rs;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn on_input() {
let text = Rc::new(RefCell::new("".to_string()));
let text_clone = Rc::clone(&text);

let input = html! {
<input
oninput=|input_event: InputEvent| {
// let input_text = ((input_event.as_ref() as Event).target().unwrap().as_ref() as HtmlInputElement).value();
// *text_clone.borrow_mut() = input_text;
},
>
</input>
};

let input_event = InputEvent::new("input").unwrap();
let input = input.create_element();

(web_sys::EventTarget::from(input))
.dispatch_event(input_event.as_ref() as &web_sys::Event)
.unwrap();

assert_eq!(&*text.borrow(), "hello world");
}

0 comments on commit 51d6463

Please sign in to comment.