├── .gitignore ├── Cargo.toml ├── .travis.yml ├── papito_dom ├── Cargo.toml └── src │ ├── traits.rs │ ├── vdiff.rs │ ├── vtext.rs │ ├── events.rs │ ├── vnode.rs │ ├── vlist.rs │ ├── vcomponent.rs │ ├── velement.rs │ └── lib.rs ├── papito ├── Cargo.toml └── src │ └── lib.rs ├── papito_codegen ├── src │ ├── common.rs │ ├── lib.rs │ ├── events.rs │ └── component.rs ├── Cargo.toml └── tests │ └── component.rs ├── LICENSE.md ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | .idea/ 5 | *.iml -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "papito_dom", 4 | "papito", 5 | "papito_codegen" 6 | ] 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | nightly 4 | cache: cargo 5 | script: 6 | - cargo test --manifest-path papito_dom/Cargo.toml -------------------------------------------------------------------------------- /papito_dom/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "papito_dom" 3 | version = "0.1.1" 4 | authors = ["Sharad Chand "] 5 | description = "VDOM Library for the Papito WASM Framework" 6 | documentation = "https://docs.rs/papito_dom" 7 | homepage = "https://github.com/csharad/papito" 8 | repository = "https://github.com/csharad/papito" 9 | readme = "../README.md" 10 | keywords = ["papito", "web", "framework", "wasm", "dom"] 11 | license = "MIT" 12 | 13 | [dependencies] 14 | indexmap = "1.0.0" 15 | stdweb = "0.4.2" 16 | -------------------------------------------------------------------------------- /papito/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "papito" 3 | version = "0.1.1" 4 | authors = ["Sharad Chand "] 5 | description = "A Beginner Friendly Rusty WASM Framework" 6 | documentation = "https://docs.rs/papito" 7 | homepage = "https://github.com/csharad/papito" 8 | repository = "https://github.com/csharad/papito" 9 | readme = "../README.md" 10 | keywords = ["papito", "web", "framework", "wasm"] 11 | license = "MIT" 12 | 13 | [dependencies] 14 | stdweb = "0.4.2" 15 | papito_dom = { path = "../papito_dom", version = "0.1.1" } -------------------------------------------------------------------------------- /papito_codegen/src/common.rs: -------------------------------------------------------------------------------- 1 | use syn::{Path, Ident, TypePath, PathSegment}; 2 | use syn::punctuated::Pair; 3 | 4 | pub fn split_path(type_path: TypePath) -> (Path, PathSegment) { 5 | let TypePath { qself, mut path } = type_path; 6 | assert!(qself.is_none(), "No self-type allowed on the concrete type"); 7 | let last_segment = path.segments.pop().unwrap(); 8 | let last_segment = match last_segment { 9 | Pair::End(segment) => { 10 | segment 11 | } 12 | _ => unreachable!() 13 | }; 14 | (path, last_segment) 15 | } 16 | 17 | pub fn component_of_state(state: &Ident) -> Ident { 18 | Ident::from(format!("{}Component", state)) 19 | } -------------------------------------------------------------------------------- /papito_codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "papito_codegen" 3 | version = "0.1.1" 4 | authors = ["Sharad Chand "] 5 | description = "Codegen for the Papito WASM Framework" 6 | documentation = "https://docs.rs/papito_codegen" 7 | homepage = "https://github.com/csharad/papito" 8 | repository = "https://github.com/csharad/papito" 9 | readme = "../README.md" 10 | keywords = ["papito", "web", "framework", "wasm", "codegen"] 11 | license = "MIT" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { version = "0.12.14", features = ["full", "extra-traits"] } 18 | quote = "0.4.2" 19 | proc-macro2 = { version = "0.2.3" } 20 | heck = "0.3.0" 21 | 22 | [dev-dependencies] 23 | papito = "0.1.1" 24 | papito_dom = "0.1.1" 25 | stdweb = "0.4.2" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018-Present Sharad Chand 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 6 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 9 | of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 12 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 13 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 14 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 15 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /papito_codegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro)] 2 | 3 | extern crate proc_macro; 4 | #[macro_use] 5 | extern crate quote; 6 | extern crate syn; 7 | extern crate heck; 8 | extern crate proc_macro2; 9 | 10 | use proc_macro::TokenStream; 11 | use syn::{Item, DeriveInput}; 12 | 13 | mod events; 14 | mod common; 15 | mod component; 16 | 17 | #[proc_macro_attribute] 18 | pub fn component(_metadata: TokenStream, input: TokenStream) -> TokenStream { 19 | let item: Item = syn::parse(input).unwrap(); 20 | component::quote(item).into() 21 | } 22 | 23 | #[proc_macro_derive(Lifecycle)] 24 | pub fn derive_lifecycle(input: TokenStream) -> TokenStream { 25 | let derive: DeriveInput = syn::parse(input).unwrap(); 26 | let ident = &derive.ident; 27 | let expanded = quote! { 28 | impl ::papito::prelude::Lifecycle for #ident {} 29 | }; 30 | expanded.into() 31 | } 32 | 33 | #[proc_macro_attribute] 34 | pub fn events(_metadata: TokenStream, input: TokenStream) -> TokenStream { 35 | let state: Item = syn::parse(input).unwrap(); 36 | let wrapper = events::quote(&state); 37 | let expanded = quote! { 38 | #state 39 | 40 | #wrapper 41 | }; 42 | expanded.into() 43 | } 44 | 45 | // Just a placeholder attribute to be used by `#[events]` otherwise it does not compile 46 | #[proc_macro_attribute] 47 | pub fn event(_metadata: TokenStream, input: TokenStream) -> TokenStream { 48 | input 49 | } -------------------------------------------------------------------------------- /papito_dom/src/traits.rs: -------------------------------------------------------------------------------- 1 | use vnode::VNode; 2 | #[cfg(target_arch = "wasm32")] 3 | use stdweb::web::{Element, Node}; 4 | #[cfg(target_arch = "wasm32")] 5 | use events::RenderRequestSender; 6 | use std::any::Any; 7 | 8 | #[cfg(target_arch = "wasm32")] 9 | pub trait DOMRender { 10 | fn dom_render(&mut self, parent: &Element, next: Option<&Node>, render_req: RenderRequestSender); 11 | } 12 | 13 | #[cfg(not(target_arch = "wasm32"))] 14 | pub trait ServerRender { 15 | fn server_render(&mut self); 16 | } 17 | 18 | #[cfg(not(target_arch = "wasm32"))] 19 | pub trait RenderToString { 20 | fn render_to_string(&mut self) -> String; 21 | } 22 | 23 | pub trait Component: Lifecycle { 24 | type Props; 25 | 26 | fn create(props: Self::Props, notifier: Box) -> Self; 27 | 28 | fn update(&self, props: Self::Props); 29 | 30 | fn eq_props(&self, rhs: &Self::Props) -> bool; 31 | } 32 | 33 | pub trait Lifecycle: Render + AsAny { 34 | fn created(&self) {} 35 | 36 | fn mounted(&self) {} 37 | 38 | fn updated(&self) {} 39 | 40 | fn destroyed(&self) {} 41 | } 42 | 43 | pub trait Render { 44 | fn render(&self) -> VNode; 45 | } 46 | 47 | pub trait AsAny { 48 | fn as_any(&self) -> &Any; 49 | } 50 | 51 | impl AsAny for T { 52 | fn as_any(&self) -> &Any { 53 | self 54 | } 55 | } 56 | 57 | #[cfg(not(target_arch = "wasm32"))] 58 | impl RenderToString for T { 59 | fn render_to_string(&mut self) -> String { 60 | self.server_render(); 61 | self.to_string() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /papito_codegen/tests/component.rs: -------------------------------------------------------------------------------- 1 | #![feature(proc_macro, conservative_impl_trait)] 2 | 3 | extern crate papito; 4 | #[macro_use] 5 | extern crate papito_dom; 6 | #[macro_use] 7 | extern crate papito_codegen; 8 | #[macro_use] 9 | extern crate stdweb; 10 | 11 | use papito::prelude::{Lifecycle, Render}; 12 | use papito_dom::prelude::VNode; 13 | use papito_codegen::{component, render, events, event}; 14 | use stdweb::web::event::ClickEvent; 15 | 16 | #[test] 17 | fn should_impl_button_component() { 18 | #[component] 19 | struct Button; 20 | 21 | impl Lifecycle for Button {} 22 | #[render] 23 | impl Render for Button { 24 | fn render(&self) -> VNode { 25 | h!("button", h!("Click")) 26 | } 27 | } 28 | 29 | h!(comp Button); 30 | } 31 | 32 | #[test] 33 | fn should_derive_default_lifecycle() { 34 | #[derive(Lifecycle)] 35 | #[component] 36 | struct Button; 37 | 38 | #[render] 39 | impl Render for Button { 40 | fn render(&self) -> VNode { 41 | h!("button", h!("Click")) 42 | } 43 | } 44 | 45 | h!(comp Button); 46 | } 47 | 48 | #[test] 49 | fn should_create_event_wrappers() { 50 | #[derive(Lifecycle)] 51 | #[component] 52 | struct Button; 53 | 54 | #[events] 55 | impl Button { 56 | #[event] 57 | fn on_click(&self, _: ClickEvent) { 58 | console!(log, "Clicked"); 59 | } 60 | } 61 | 62 | #[render] 63 | impl Render for Button { 64 | fn render(&self) -> VNode { 65 | h!("button", [ self.on_click() ], h!("Click")) 66 | } 67 | } 68 | 69 | h!(comp Button); 70 | } -------------------------------------------------------------------------------- /papito_dom/src/vdiff.rs: -------------------------------------------------------------------------------- 1 | use stdweb::web::Element; 2 | use stdweb::web::Node; 3 | use events::RenderRequestSender; 4 | 5 | /// Required to update the DOM on the `parent` node. It is also tasked with Diffing along 6 | /// as it creates patches. 7 | pub trait DOMPatch { 8 | fn patch(self, parent: &Element, next: Option<&Node>, old_vnode: Option, render_req: RenderRequestSender) -> Self; 9 | } 10 | 11 | /// Required when removing stale `VNodes`. 12 | pub trait DOMRemove { 13 | fn remove(self, parent: &Element); 14 | } 15 | 16 | /// Required when re-ordering the `VList` children. Reordering is done by appending the dom node 17 | /// again in a new order. 18 | pub trait DOMReorder { 19 | fn move_to_last(&self, parent: &Element); 20 | 21 | fn move_before(&self, parent: &Element, next: &Node); 22 | } 23 | 24 | pub trait DOMNode { 25 | fn dom_node(&self) -> Option; 26 | } 27 | 28 | impl DOMPatch for Option where 29 | Q: DOMPatch, 30 | T: DOMRemove { 31 | fn patch(self, parent: &Element, next: Option<&Node>, old_vnode: Option, render_req: RenderRequestSender) -> Self { 32 | if let Some(this) = self { 33 | Some(this.patch(parent, next, old_vnode, render_req)) 34 | } else { 35 | old_vnode.remove(parent); 36 | None 37 | } 38 | } 39 | } 40 | 41 | impl DOMPatch for Box where 42 | T: DOMPatch { 43 | fn patch(self, parent: &Element, next: Option<&Node>, old_vnode: Option, render_req: RenderRequestSender) -> Self { 44 | let this = *self; 45 | Box::new(this.patch(parent, next, old_vnode, render_req)) 46 | } 47 | } 48 | 49 | impl DOMRemove for Option { 50 | fn remove(self, parent: &Element) { 51 | if let Some(inner) = self { 52 | inner.remove(parent); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /papito/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate papito_dom; 2 | #[cfg(target_arch = "wasm32")] 3 | #[macro_use] 4 | extern crate stdweb; 5 | 6 | use papito_dom::prelude::VNode; 7 | use papito_dom::{comp, h, Component}; 8 | #[cfg(target_arch = "wasm32")] 9 | use stdweb::web::{document, Element, INonElementParentNode}; 10 | #[cfg(target_arch = "wasm32")] 11 | use papito_dom::{DOMRender, RenderRequest}; 12 | use std::ops::Deref; 13 | 14 | pub mod prelude { 15 | pub use papito_dom::{Lifecycle, Render}; 16 | } 17 | 18 | pub struct App { 19 | vdom: VNode, 20 | #[cfg(target_arch = "wasm32")] 21 | render_req: RenderRequest, 22 | } 23 | 24 | impl App { 25 | pub fn new + 'static>() -> App { 26 | #[cfg(target_arch = "wasm32")] 27 | js! { @(no_return) 28 | window.__schedule_papito_render__ = function() {}; 29 | }; 30 | 31 | App { 32 | vdom: h(comp::(())), 33 | #[cfg(target_arch = "wasm32")] 34 | render_req: RenderRequest::new(|| { 35 | js! { @(no_return) 36 | window.__schedule_papito_render__(); 37 | } 38 | }), 39 | } 40 | } 41 | 42 | #[cfg(target_arch = "wasm32")] 43 | pub fn render>(mut self, app_root: T) { 44 | let app_root = app_root.into(); 45 | // Re-renders on requests from the components 46 | let rerender = move |initial_render: bool| { 47 | if initial_render || self.render_req.receive() { 48 | self.vdom.dom_render(&app_root, None, self.render_req.sender()); 49 | } 50 | js! { @(no_return) 51 | window.__is_rendering__ = false; 52 | } 53 | }; 54 | js! { @(no_return) 55 | var rerender = @{rerender}; 56 | window.__is_rendering__ = false; 57 | window.__schedule_papito_render__ = function() { 58 | if (!window.__is_rendering__) { 59 | setTimeout(function() { 60 | rerender(false) 61 | }); 62 | } 63 | }; 64 | // Initial render 65 | rerender(true); 66 | } 67 | } 68 | } 69 | 70 | #[cfg(target_arch = "wasm32")] 71 | pub struct AppRoot(Element); 72 | 73 | #[cfg(target_arch = "wasm32")] 74 | impl<'a> From<&'a str> for AppRoot { 75 | fn from(item: &'a str) -> Self { 76 | AppRoot(document().get_element_by_id(item) 77 | .expect(&format!("Could not find the element with id `#{}` to hoist the App", item))) 78 | } 79 | } 80 | 81 | #[cfg(target_arch = "wasm32")] 82 | impl From for AppRoot { 83 | fn from(item: Element) -> Self { 84 | AppRoot(item) 85 | } 86 | } 87 | 88 | #[cfg(target_arch = "wasm32")] 89 | impl Deref for AppRoot { 90 | type Target = Element; 91 | 92 | fn deref(&self) -> &Self::Target { 93 | &self.0 94 | } 95 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Papito (पपितो) = Papaya 2 | ## Deprecated : No Longer Maintained 3 | 4 | [![Build Status](https://travis-ci.org/csharad/papito.svg?branch=master)](https://travis-ci.org/csharad/papito) 5 | 6 | A Vue & React inspired Frontend Web Framework in Rust for the WASM platform. Developed by a beginner for beginners. 7 | 8 | ### Goals 9 | 10 | + Beginner Friendly. Draws cues from Vue and React. 11 | + Pure Rust web apps. 12 | + Cargo only (without webpack). Should provide optional tools that mimic loaders such as `file-loader`, `style-loader`, `url-loader`. 13 | 14 | It is still under active development. So tread carefully. 15 | 16 | ### Demo 17 | 18 | ```rust 19 | #![feature(proc_macro, conservative_impl_trait)] 20 | 21 | #[macro_use] 22 | extern crate papito_codegen; 23 | #[macro_use] 24 | extern crate papito_dom; 25 | extern crate papito; 26 | #[macro_use] 27 | extern crate stdweb; 28 | 29 | use papito_codegen::{component, render, events, event}; 30 | use papito::prelude::{Render, Lifecycle}; 31 | use papito_dom::prelude::VNode; 32 | use papito::App; 33 | use stdweb::web::event::ClickEvent; 34 | 35 | #[derive(Lifecycle)] 36 | #[component] 37 | struct Div; 38 | 39 | #[render] 40 | impl Render for Div { 41 | fn render(&self) -> VNode { 42 | h!("div", { "style" => "background-color: #fafafa; color: #666;" }, 43 | h!([ 44 | h!("This is the front page of web."), 45 | h!(comp Button, { style => "background-color: black; color: white;".to_string() }) 46 | ])) 47 | } 48 | } 49 | 50 | #[component] 51 | struct Button { 52 | #[prop] 53 | style: String, 54 | click: bool 55 | } 56 | 57 | #[events] 58 | impl Button { 59 | #[event] 60 | fn on_click(&mut self, _: ClickEvent) { 61 | let inv = !self.click; 62 | self.set_click(inv); 63 | } 64 | } 65 | 66 | impl Lifecycle for Button { 67 | fn created(&mut self) { 68 | console!(log, &format!("Initial click value: {}", self.click)); 69 | } 70 | 71 | fn updated(&mut self) { 72 | console!(log, &format!("You clicked the button: {}", self.click)); 73 | } 74 | } 75 | 76 | #[render] 77 | impl Render for Button { 78 | fn render(&self) -> VNode { 79 | let click = self.inner.borrow().click; 80 | let style = self.inner.borrow().style.clone(); 81 | h!([ 82 | h!("h1", h!("Hello World!")), 83 | h!("button", { "style" => style }, [ self.on_click() ], h!("Click")), 84 | h!("h3", h!(if click { "You clicked" } else { "You unclicked" })) 85 | ]) 86 | } 87 | } 88 | 89 | fn main() { 90 | App::new::
().render("app"); 91 | } 92 | ``` 93 | 94 | ### Features Supported 95 | 96 | * [x] Component props 97 | * [ ] Component Events 98 | * [x] DOM Events 99 | * [x] Reactive states 100 | * [x] Component Lifecycle (only some) 101 | * [x] Server Renderer 102 | * [x] Hyperscript macro h! 103 | * [ ] Vue-like template syntax 104 | * [ ] Context API? -------------------------------------------------------------------------------- /papito_dom/src/vtext.rs: -------------------------------------------------------------------------------- 1 | use CowStr; 2 | use std::fmt::{self, Formatter}; 3 | use std::fmt::Display; 4 | #[cfg(target_arch = "wasm32")] 5 | use stdweb::web::TextNode; 6 | 7 | #[derive(Debug, Eq, PartialEq)] 8 | pub struct VText { 9 | content: CowStr, 10 | #[cfg(target_arch = "wasm32")] 11 | dom_ref: Option, 12 | } 13 | 14 | impl VText { 15 | pub fn new(content: CowStr) -> VText { 16 | VText { 17 | content, 18 | #[cfg(target_arch = "wasm32")] 19 | dom_ref: None, 20 | } 21 | } 22 | 23 | #[cfg(target_arch = "wasm32")] 24 | pub fn dom_ref(&self) -> Option<&TextNode> { 25 | self.dom_ref.as_ref() 26 | } 27 | } 28 | 29 | impl Display for VText { 30 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 31 | write!(f, "{}", self.content) 32 | } 33 | } 34 | 35 | impl> From for VText { 36 | fn from(item: T) -> Self { 37 | VText::new(item.into()) 38 | } 39 | } 40 | 41 | #[cfg(target_arch = "wasm32")] 42 | mod wasm { 43 | use stdweb::web::{Element, document, INode}; 44 | use vdiff::{DOMPatch, DOMRemove}; 45 | use super::VText; 46 | use vdiff::DOMReorder; 47 | use vdiff::DOMNode; 48 | use stdweb::web::Node; 49 | use events::RenderRequestSender; 50 | 51 | impl DOMPatch for VText { 52 | fn patch(mut self, parent: &Element, next: Option<&Node>, old_vnode: Option, _: RenderRequestSender) -> Self { 53 | if let Some(old_vnode) = old_vnode { 54 | let text_node = old_vnode.dom_ref().unwrap().clone(); 55 | if old_vnode.content != self.content { 56 | text_node.set_text_content(&self.content); 57 | } 58 | self.dom_ref = Some(text_node); 59 | } else { 60 | let text_node = document().create_text_node(&self.content); 61 | self.dom_ref = Some(text_node); 62 | if let Some(next) = next { 63 | parent.insert_before(self.dom_ref().unwrap(), next).unwrap(); 64 | } else { 65 | parent.append_child(self.dom_ref().unwrap()); 66 | } 67 | } 68 | self 69 | } 70 | } 71 | 72 | impl DOMReorder for VText { 73 | fn move_to_last(&self, parent: &Element) { 74 | let dom_ref = self.dom_ref().expect("Cannot append previously non-existent text node."); 75 | parent.append_child(dom_ref); 76 | } 77 | 78 | fn move_before(&self, parent: &Element, next: &Node) { 79 | parent.insert_before(self.dom_ref().expect("Cannot insert previously non-existent text node."), next) 80 | .unwrap(); 81 | } 82 | } 83 | 84 | impl DOMRemove for VText { 85 | fn remove(mut self, parent: &Element) { 86 | parent.remove_child(&self.dom_ref.take() 87 | .expect("Cannot remove non-existent text node.") 88 | ).unwrap(); 89 | } 90 | } 91 | 92 | impl DOMNode for VText { 93 | fn dom_node(&self) -> Option { 94 | self.dom_ref.clone().map(|it| it.into()) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /papito_codegen/src/events.rs: -------------------------------------------------------------------------------- 1 | use quote::Tokens; 2 | use syn::{Item, Type, Ident, Path, ImplItem, ImplItemMethod, FnArg, ArgSelfRef, ArgCaptured, 3 | ReturnType}; 4 | use common::{split_path, component_of_state}; 5 | 6 | pub fn quote(item: &Item) -> Tokens { 7 | match *item { 8 | Item::Impl(ref item_impl) => { 9 | let self_ty = *item_impl.self_ty.clone(); 10 | let path = component_path_of(self_ty); 11 | impl_wrapper_for_any_events(item_impl.items.clone(), path) 12 | } 13 | _ => { 14 | panic!("`#[events]` attribute can only be used with an impl block"); 15 | } 16 | } 17 | } 18 | 19 | fn component_path_of(self_ty: Type) -> Path { 20 | match self_ty { 21 | Type::Path(type_path) => { 22 | let (mut path, mut last_segment) = split_path(type_path); 23 | last_segment.ident = component_of_state(&last_segment.ident); 24 | path.segments.push(last_segment); 25 | path 26 | } 27 | _ => panic!("Only type paths are allowed for impls attributed with `#[events]`") 28 | } 29 | } 30 | 31 | fn impl_wrapper_for_any_events(items: Vec, comp_path: Path) -> Tokens { 32 | let mut iter = items.into_iter(); 33 | let mut event_wrappers = vec![]; 34 | while let Some(ImplItem::Method(method_item)) = iter.next() { 35 | if has_event_attribute(&method_item) { 36 | let (fn_name, self_arg, event_arg) = get_metadata(method_item); 37 | let event_ty = &event_arg.ty; 38 | let event_ident = Ident::from("ev".to_string()); 39 | if self_arg.mutability.is_some() { 40 | event_wrappers.push(quote! { 41 | fn #fn_name(&self) -> impl FnMut(#event_ty) { 42 | let comp = self.inner.clone(); 43 | move |#event_ident| { 44 | comp.borrow_mut().#fn_name(#event_ident); 45 | } 46 | } 47 | }) 48 | } else { 49 | event_wrappers.push(quote! { 50 | fn #fn_name(&self) -> impl Fn(#event_ty) { 51 | let comp = self.inner.clone(); 52 | move |#event_ident| { 53 | comp.borrow().#fn_name(#event_ident); 54 | } 55 | } 56 | }) 57 | } 58 | } 59 | } 60 | quote! { 61 | impl #comp_path { 62 | #(#event_wrappers)* 63 | } 64 | } 65 | } 66 | 67 | fn has_event_attribute(item: &ImplItemMethod) -> bool { 68 | item.attrs.iter().any(|it| it.path == Path::from(Ident::from("event".to_string()))) 69 | } 70 | 71 | fn get_metadata(item: ImplItemMethod) -> (Ident, ArgSelfRef, ArgCaptured) { 72 | let sig = item.sig; 73 | let fn_name = sig.ident; 74 | let decl = sig.decl; 75 | if decl.output != ReturnType::Default { 76 | panic!("This event method `{}` cannot have a return type", &fn_name) 77 | } 78 | let mut args = decl.inputs.into_iter(); 79 | let first_arg = args.next().expect(&format!("This method `{}` has no argument", &fn_name)); 80 | let second_arg = args.next().expect(&format!("This method `{}` does not have second argument", &fn_name)); 81 | if let Some(_) = args.next() { 82 | panic!("This method `{}` cannot have any more that 2 arguments", &fn_name); 83 | } 84 | let first_arg = if let FnArg::SelfRef(self_arg) = first_arg { 85 | self_arg 86 | } else { 87 | panic!("The first arg of `{}` can only be `&self` or `&mut self`", &fn_name); 88 | }; 89 | let second_arg = if let FnArg::Captured(arg) = second_arg { 90 | arg 91 | } else { 92 | panic!("The second arg of `{}` must be an explicit type", &fn_name); 93 | }; 94 | (fn_name, first_arg, second_arg) 95 | } -------------------------------------------------------------------------------- /papito_dom/src/events.rs: -------------------------------------------------------------------------------- 1 | use stdweb::web::{Element, EventListenerHandle, IEventTarget}; 2 | use stdweb::web::event::*; 3 | use stdweb::unstable::TryInto; 4 | use std::marker::PhantomData; 5 | use std::fmt::Debug; 6 | use std::fmt::{Formatter, self}; 7 | use std::sync::mpsc::Sender; 8 | use std::sync::mpsc::Receiver; 9 | use std::sync::mpsc::channel; 10 | use stdweb::Reference; 11 | 12 | /// Add or remove events from the DOM 13 | pub trait DOMEvent { 14 | fn event_type(&self) -> &'static str; 15 | 16 | fn attach(&mut self, parent: &Element); 17 | 18 | fn detach(&mut self); 19 | } 20 | 21 | /// A wrapper construct to encapsulate all events 22 | pub struct DOMEventListener where 23 | F: FnMut(T) + 'static, 24 | T: ConcreteEvent { 25 | event_type: &'static str, 26 | listener: Option, 27 | listener_handle: Option, 28 | _phantom: PhantomData, 29 | } 30 | 31 | impl DOMEventListener where 32 | F: FnMut(T) + 'static, 33 | T: ConcreteEvent { 34 | pub fn new(listener: F) -> DOMEventListener { 35 | DOMEventListener { 36 | event_type: T::EVENT_TYPE, 37 | listener: Some(listener), 38 | listener_handle: None, 39 | _phantom: PhantomData, 40 | } 41 | } 42 | } 43 | 44 | impl DOMEvent for DOMEventListener where 45 | F: FnMut(T) + 'static, 46 | T: ConcreteEvent { 47 | fn event_type(&self) -> &'static str { 48 | self.event_type 49 | } 50 | 51 | fn attach(&mut self, parent: &Element) { 52 | let listener = self.listener.take() 53 | .expect("Event listener is either already attached or detached"); 54 | let listener_handle = parent.add_event_listener(listener); 55 | self.listener_handle = Some(listener_handle); 56 | } 57 | 58 | fn detach(&mut self) { 59 | let listener_handle = self.listener_handle.take() 60 | .expect("Event must be attached for it to detach"); 61 | listener_handle.remove(); 62 | } 63 | } 64 | 65 | macro_rules! convert_to_dom_ev_listener { 66 | ($( $listener:ty ),*) => { 67 | $( 68 | impl From for DOMEventListener<$listener, F> where 69 | F: FnMut($listener) { 70 | fn from(item: F) -> Self { 71 | DOMEventListener::new(item) 72 | } 73 | } 74 | )* 75 | }; 76 | } 77 | 78 | convert_to_dom_ev_listener!( 79 | ClickEvent, 80 | DoubleClickEvent, 81 | MouseDownEvent, 82 | MouseUpEvent, 83 | MouseMoveEvent, 84 | KeyPressEvent, 85 | KeyDownEvent, 86 | KeyUpEvent, 87 | ProgressEvent, 88 | LoadStartEvent, 89 | LoadEndEvent, 90 | ProgressLoadEvent, 91 | ProgressAbortEvent, 92 | ProgressErrorEvent, 93 | SocketCloseEvent, 94 | SocketErrorEvent, 95 | SocketOpenEvent, 96 | SocketMessageEvent, 97 | HashChangeEvent, 98 | PopStateEvent, 99 | ChangeEvent, 100 | ResourceLoadEvent, 101 | ResourceAbortEvent, 102 | ResourceErrorEvent, 103 | ResizeEvent, 104 | InputEvent, 105 | ReadyStateChangeEvent, 106 | FocusEvent, 107 | BlurEvent 108 | ); 109 | 110 | impl Debug for DOMEvent { 111 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 112 | write!(f, "EventType = \"{}\"", self.event_type()) 113 | } 114 | } 115 | 116 | // Implemented because of the requirements on VElement. Could not compare two closures 117 | // so a simple pass through `true`. 118 | impl PartialEq for DOMEvent { 119 | fn eq(&self, _: &DOMEvent) -> bool { 120 | true 121 | } 122 | } 123 | 124 | impl Eq for DOMEvent {} 125 | 126 | pub struct RenderRequest { 127 | tx: Sender, 128 | rx: Receiver, 129 | callback_ref: Reference 130 | } 131 | 132 | impl RenderRequest { 133 | pub fn new(on_send: T) -> RenderRequest { 134 | let (tx, rx) = channel(); 135 | let callback_ref = js! { 136 | var callback = @{on_send}; 137 | return callback; 138 | }.try_into().unwrap(); 139 | RenderRequest { 140 | rx, 141 | tx, 142 | callback_ref 143 | } 144 | } 145 | 146 | pub fn sender(&self) -> RenderRequestSender { 147 | RenderRequestSender { 148 | tx: self.tx.clone(), 149 | callback_ref: self.callback_ref.clone() 150 | } 151 | } 152 | 153 | pub fn receive(&self) -> bool { 154 | let received = self.rx.try_iter().collect::>(); 155 | !received.is_empty() 156 | } 157 | } 158 | 159 | #[derive(Clone)] 160 | pub struct RenderRequestSender { 161 | tx: Sender, 162 | callback_ref: Reference 163 | } 164 | 165 | impl RenderRequestSender { 166 | pub fn send(&self) { 167 | self.tx.send(true) 168 | .expect("The receiver of the app is not present which is impossible."); 169 | js!{ @(no_return) 170 | @{&self.callback_ref}(); 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /papito_dom/src/vnode.rs: -------------------------------------------------------------------------------- 1 | use velement::VElement; 2 | use vlist::VList; 3 | use vtext::VText; 4 | use std::fmt::Display; 5 | use std::fmt::{Formatter, self}; 6 | use vcomponent::VComponent; 7 | #[cfg(not(target_arch = "wasm32"))] 8 | use traits::ServerRender; 9 | 10 | #[derive(Debug, Eq, PartialEq)] 11 | pub enum VNode { 12 | Text(VText), 13 | Element(VElement), 14 | List(VList), 15 | Component(VComponent) 16 | } 17 | 18 | impl VNode { 19 | pub fn new>(content: T) -> VNode { 20 | content.into() 21 | } 22 | } 23 | 24 | impl Display for VNode { 25 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 26 | match *self { 27 | VNode::Text(ref text) => write!(f, "{}", text), 28 | VNode::Element(ref element) => write!(f, "{}", element), 29 | VNode::List(ref list) => write!(f, "{}", list), 30 | VNode::Component(ref component) => write!(f, "{}", component) 31 | } 32 | } 33 | } 34 | 35 | macro_rules! impl_conversion_to_vnode { 36 | ($variant:ident, $inner:ty) => { 37 | impl From<$inner> for VNode { 38 | fn from(item: $inner) -> Self { 39 | VNode::$variant(item) 40 | } 41 | } 42 | }; 43 | } 44 | 45 | impl_conversion_to_vnode!(Text, VText); 46 | impl_conversion_to_vnode!(Element, VElement); 47 | impl_conversion_to_vnode!(List, VList); 48 | impl_conversion_to_vnode!(Component, VComponent); 49 | 50 | #[cfg(not(target_arch = "wasm32"))] 51 | impl ServerRender for VNode { 52 | fn server_render(&mut self) { 53 | match *self { 54 | VNode::Component(ref mut component) => component.server_render(), 55 | VNode::List(ref mut list) => list.server_render(), 56 | VNode::Element(ref mut element) => element.server_render(), 57 | VNode::Text(_) => {} 58 | } 59 | } 60 | } 61 | 62 | #[cfg(target_arch = "wasm32")] 63 | mod wasm { 64 | use vdiff::{DOMPatch, DOMRemove}; 65 | use stdweb::web::Element; 66 | use super::VNode; 67 | use vdiff::DOMReorder; 68 | use vdiff::DOMNode; 69 | use stdweb::web::Node; 70 | use traits::DOMRender; 71 | use events::RenderRequestSender; 72 | 73 | macro_rules! match_for_vnode_patch { 74 | ($against:ident, $parent:ident, $next:ident, $old_vnode:ident, $render_req:ident, [$( $variant:ident ),*] ) => { 75 | match $against { 76 | $( 77 | VNode::$variant(node_like) => { 78 | if let Some(VNode::$variant(old_node_like)) = $old_vnode { 79 | node_like.patch($parent, $next, Some(old_node_like), $render_req).into() 80 | } else { 81 | $old_vnode.remove($parent); 82 | node_like.patch($parent, $next, None, $render_req).into() 83 | } 84 | } 85 | )* 86 | } 87 | }; 88 | } 89 | 90 | impl DOMPatch for VNode { 91 | fn patch(self, parent: &Element, next: Option<&Node>, old_vnode: Option, render_req: RenderRequestSender) -> Self { 92 | match_for_vnode_patch!(self, parent, next, old_vnode, render_req, [Text, Element, List, Component]) 93 | } 94 | } 95 | 96 | impl DOMRemove for VNode { 97 | fn remove(self, parent: &Element) { 98 | match self { 99 | VNode::Text(text) => text.remove(parent), 100 | VNode::Element(element) => element.remove(parent), 101 | VNode::List(list) => list.remove(parent), 102 | VNode::Component(component) => component.remove(parent) 103 | } 104 | } 105 | } 106 | 107 | impl DOMReorder for VNode { 108 | fn move_to_last(&self, parent: &Element) { 109 | match *self { 110 | VNode::Text(ref text) => text.move_to_last(parent), 111 | VNode::Element(ref element) => element.move_to_last(parent), 112 | VNode::List(ref list) => list.move_to_last(parent), 113 | VNode::Component(ref component) => component.move_to_last(parent) 114 | } 115 | } 116 | 117 | fn move_before(&self, parent: &Element, next: &Node) { 118 | match *self { 119 | VNode::Text(ref text) => text.move_before(parent, next), 120 | VNode::Element(ref element) => element.move_before(parent, next), 121 | VNode::List(ref list) => list.move_before(parent, next), 122 | VNode::Component(ref component) => component.move_before(parent, next) 123 | } 124 | } 125 | } 126 | 127 | impl DOMNode for VNode { 128 | fn dom_node(&self) -> Option { 129 | match *self { 130 | VNode::Text(ref text) => text.dom_node(), 131 | VNode::Element(ref element) => element.dom_node(), 132 | VNode::List(ref list) => list.dom_node(), 133 | VNode::Component(ref component) => component.dom_node() 134 | } 135 | } 136 | } 137 | 138 | impl DOMRender for VNode { 139 | fn dom_render(&mut self, parent: &Element, next: Option<&Node>, render_req: RenderRequestSender) { 140 | match *self { 141 | VNode::Component(ref mut component) => component.dom_render(parent, next, render_req), 142 | VNode::List(ref mut list) => list.dom_render(parent, next, render_req), 143 | VNode::Element(ref mut element) => element.dom_render(parent, next, render_req), 144 | VNode::Text(_) => {} 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /papito_dom/src/vlist.rs: -------------------------------------------------------------------------------- 1 | use vnode::VNode; 2 | use std::fmt::Display; 3 | use std::fmt::{Formatter, self}; 4 | use indexmap::IndexMap; 5 | use CowStr; 6 | #[cfg(not(target_arch = "wasm32"))] 7 | use traits::ServerRender; 8 | 9 | type Key = CowStr; 10 | 11 | #[derive(Debug, Eq, PartialEq)] 12 | pub struct VList { 13 | children: IndexMap 14 | } 15 | 16 | impl VList { 17 | pub fn new(children: IndexMap) -> VList { 18 | VList { 19 | children 20 | } 21 | } 22 | 23 | #[cfg(target_arch = "wasm32")] 24 | fn position(&self, key: &str) -> Option { 25 | self.children.iter().position(|(k, _)| k == key) 26 | } 27 | } 28 | 29 | impl Display for VList { 30 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 31 | for (_, v) in self.children.iter() { 32 | write!(f, "{}", v)?; 33 | } 34 | Ok(()) 35 | } 36 | } 37 | 38 | impl> From> for VList { 39 | fn from(item: Vec<(T, VNode)>) -> Self { 40 | let children = item.into_iter() 41 | .map(|(k, v)| (k.into(), v)) 42 | .collect(); 43 | VList::new(children) 44 | } 45 | } 46 | 47 | impl From> for VList { 48 | fn from(item: Vec) -> Self { 49 | let children = item.into_iter() 50 | .enumerate() 51 | .map(|(k, v)| (k.to_string().into(), v)) 52 | .collect(); 53 | VList::new(children) 54 | } 55 | } 56 | 57 | #[cfg(not(target_arch = "wasm32"))] 58 | impl ServerRender for VList { 59 | fn server_render(&mut self) { 60 | for (_, child) in self.children.iter_mut() { 61 | child.server_render(); 62 | } 63 | } 64 | } 65 | 66 | #[cfg(target_arch = "wasm32")] 67 | mod wasm { 68 | use super::VList; 69 | use vdiff::{DOMPatch, DOMRemove}; 70 | use stdweb::web::Element; 71 | use vdiff::DOMReorder; 72 | use vdiff::DOMNode; 73 | use stdweb::web::Node; 74 | use CowStr; 75 | use traits::DOMRender; 76 | use events::RenderRequestSender; 77 | use indexmap::IndexMap; 78 | 79 | impl DOMPatch for VList { 80 | fn patch(mut self, parent: &Element, next: Option<&Node>, old_vnodes: Option, render_req: RenderRequestSender) -> Self { 81 | if let Some(mut old_vnodes) = old_vnodes { 82 | let old_children_pos: IndexMap = old_vnodes.children.iter() 83 | .enumerate() 84 | .map(|(pos, (k, _))| (k.clone(), pos)) 85 | .collect(); 86 | let mut next_node = next.map(|it| it.clone()); 87 | let mut children = IndexMap::new(); 88 | for (k, v) in self.children.into_iter().rev() { 89 | let v = if let Some(mut pre_vnode) = old_vnodes.children.swap_remove(&k) { 90 | // Patch if any old VNode found 91 | v.patch(parent, next_node.as_ref(), Some(pre_vnode), render_req.clone()) 92 | } else { 93 | v.patch(parent, next_node.as_ref(), None, render_req.clone()) 94 | }; 95 | next_node = v.dom_node(); 96 | children.insert(k, v); 97 | } 98 | self.children = children.into_iter().rev().collect(); 99 | if has_dirty_order(&self, &old_children_pos) { 100 | update_dom_positions(&self, parent, &old_children_pos); 101 | } 102 | remove_old_vnodes(old_vnodes, parent); 103 | } else { 104 | let mut children = IndexMap::new(); 105 | for (k, v) in self.children { 106 | let v = v.patch(parent, None, None, render_req.clone()); 107 | children.insert(k, v); 108 | } 109 | self.children = children; 110 | } 111 | self 112 | } 113 | } 114 | 115 | fn has_dirty_order(new_vnodes: &VList, old_nodes: &IndexMap) -> bool { 116 | let mut old_last_position = 0; 117 | for (k, _) in new_vnodes.children.iter() { 118 | let old_pos = if let Some(pos) = old_nodes.get(k) { 119 | *pos 120 | } else { 121 | // new nodes not considered for order 122 | continue; 123 | }; 124 | if old_pos >= old_last_position { 125 | old_last_position = old_pos; 126 | } else { 127 | return true; 128 | } 129 | } 130 | false 131 | } 132 | 133 | fn update_dom_positions(new_vnodes: &VList, parent: &Element, old_vnodes: &IndexMap) { 134 | let mut next_key = None; 135 | for (k, new_node) in new_vnodes.children.iter().rev() { 136 | let new_pos = new_vnodes.position(k); 137 | let old_pos = old_vnodes.get(k).map(|it| *it); 138 | if old_pos.is_none() { 139 | // It is a new node and already inserted to the write place. 140 | } else if new_pos.unwrap() != old_pos.unwrap() { 141 | if let Some(next_key) = next_key { 142 | let next_vnode = new_vnodes.children.get(next_key).unwrap(); 143 | new_node.move_before(parent, &next_vnode.dom_node().unwrap()); 144 | } else { 145 | new_node.move_to_last(parent); 146 | } 147 | } 148 | next_key = Some(k); 149 | } 150 | } 151 | 152 | fn remove_old_vnodes(old_vnodes: VList, parent: &Element) { 153 | for (_, v) in old_vnodes.children { 154 | v.remove(parent); 155 | } 156 | } 157 | 158 | impl DOMRemove for VList { 159 | fn remove(self, parent: &Element) { 160 | for (_, child) in self.children { 161 | child.remove(parent); 162 | } 163 | } 164 | } 165 | 166 | impl DOMReorder for VList { 167 | fn move_to_last(&self, parent: &Element) { 168 | for (_, v) in self.children.iter() { 169 | v.move_to_last(parent); 170 | } 171 | } 172 | 173 | fn move_before(&self, parent: &Element, next: &Node) { 174 | for (_, v) in self.children.iter() { 175 | v.move_before(parent, next); 176 | } 177 | } 178 | } 179 | 180 | impl DOMNode for VList { 181 | fn dom_node(&self) -> Option { 182 | self.children.iter().next().and_then(|it| it.1.dom_node()) 183 | } 184 | } 185 | 186 | impl DOMRender for VList { 187 | fn dom_render(&mut self, parent: &Element, next: Option<&Node>, render_req: RenderRequestSender) { 188 | for (_, child) in self.children.iter_mut() { 189 | child.dom_render(parent, next, render_req.clone()); 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "base-x" 3 | version = "0.2.2" 4 | source = "registry+https://github.com/rust-lang/crates.io-index" 5 | 6 | [[package]] 7 | name = "dtoa" 8 | version = "0.4.2" 9 | source = "registry+https://github.com/rust-lang/crates.io-index" 10 | 11 | [[package]] 12 | name = "futures" 13 | version = "0.1.18" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | 16 | [[package]] 17 | name = "heck" 18 | version = "0.3.0" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | dependencies = [ 21 | "unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 22 | ] 23 | 24 | [[package]] 25 | name = "indexmap" 26 | version = "1.0.0" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | 29 | [[package]] 30 | name = "itoa" 31 | version = "0.3.4" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [[package]] 35 | name = "num-traits" 36 | version = "0.2.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | 39 | [[package]] 40 | name = "papito" 41 | version = "0.1.0" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | dependencies = [ 44 | "papito_dom 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 45 | "stdweb 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 46 | ] 47 | 48 | [[package]] 49 | name = "papito" 50 | version = "0.1.1" 51 | dependencies = [ 52 | "papito_dom 0.1.1", 53 | "stdweb 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 54 | ] 55 | 56 | [[package]] 57 | name = "papito_codegen" 58 | version = "0.1.1" 59 | dependencies = [ 60 | "heck 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 61 | "papito 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 62 | "papito_dom 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 63 | "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 64 | "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 65 | "stdweb 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 66 | "syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", 67 | ] 68 | 69 | [[package]] 70 | name = "papito_dom" 71 | version = "0.1.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | dependencies = [ 74 | "indexmap 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 75 | "stdweb 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 76 | ] 77 | 78 | [[package]] 79 | name = "papito_dom" 80 | version = "0.1.1" 81 | dependencies = [ 82 | "indexmap 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "stdweb 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 84 | ] 85 | 86 | [[package]] 87 | name = "proc-macro2" 88 | version = "0.2.3" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | dependencies = [ 91 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 92 | ] 93 | 94 | [[package]] 95 | name = "quote" 96 | version = "0.4.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | dependencies = [ 99 | "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 100 | ] 101 | 102 | [[package]] 103 | name = "serde" 104 | version = "1.0.33" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | 107 | [[package]] 108 | name = "serde_derive" 109 | version = "1.0.33" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | dependencies = [ 112 | "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 113 | "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 114 | "serde_derive_internals 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)", 115 | "syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", 116 | ] 117 | 118 | [[package]] 119 | name = "serde_derive_internals" 120 | version = "0.21.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | dependencies = [ 123 | "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 124 | "syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", 125 | ] 126 | 127 | [[package]] 128 | name = "serde_json" 129 | version = "1.0.11" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | dependencies = [ 132 | "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 133 | "itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 134 | "num-traits 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "serde 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", 136 | ] 137 | 138 | [[package]] 139 | name = "stdweb" 140 | version = "0.4.2" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | dependencies = [ 143 | "futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", 144 | "serde 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", 145 | "serde_json 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", 146 | "stdweb-derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 147 | "stdweb-internal-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 148 | ] 149 | 150 | [[package]] 151 | name = "stdweb-derive" 152 | version = "0.4.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | dependencies = [ 155 | "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 156 | "serde 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", 157 | "serde_derive 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", 158 | "syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", 159 | ] 160 | 161 | [[package]] 162 | name = "stdweb-internal-macros" 163 | version = "0.1.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | dependencies = [ 166 | "base-x 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 168 | "serde 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", 169 | "serde_derive 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", 170 | "serde_json 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", 171 | "syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)", 172 | ] 173 | 174 | [[package]] 175 | name = "syn" 176 | version = "0.12.14" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | dependencies = [ 179 | "proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 180 | "quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 182 | ] 183 | 184 | [[package]] 185 | name = "unicode-segmentation" 186 | version = "1.2.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | 189 | [[package]] 190 | name = "unicode-xid" 191 | version = "0.1.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | 194 | [metadata] 195 | "checksum base-x 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2f59103b47307f76e03bef1633aec7fa9e29bfb5aa6daf5a334f94233c71f6c1" 196 | "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" 197 | "checksum futures 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0bab5b5e94f5c31fc764ba5dd9ad16568aae5d4825538c01d6bca680c9bf94a7" 198 | "checksum heck 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea04fa3ead4e05e51a7c806fc07271fdbde4e246a6c6d1efd52e72230b771b82" 199 | "checksum indexmap 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9378f1f3923647a9aea6af4c6b5de68cc8a71415459ad25ef191191c48f5b7" 200 | "checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" 201 | "checksum num-traits 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3c2bd9b9d21e48e956b763c9f37134dc62d9e95da6edb3f672cacb6caf3cd3" 202 | "checksum papito 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "848475d1d514c1eb25e0a10f465894278b2a110ea4c0de87b679242b722bb6bc" 203 | "checksum papito_dom 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a3de0e4560f93fb3f9685f3f659c8240c2a3d0a867ff73f57b32cc2d991e3b9b" 204 | "checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0" 205 | "checksum quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408" 206 | "checksum serde 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "4fe95aa0d46f04ce5c3a88bdcd4114ecd6144ed0b2725ebca2f1127744357807" 207 | "checksum serde_derive 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "23b163a6ce7e1aa897919f9d8e40bd1f8a6f95342ed57727ae31387a01a7a356" 208 | "checksum serde_derive_internals 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "370aa477297975243dc914d0b0e1234927520ec311de507a560fbd1c80f7ab8c" 209 | "checksum serde_json 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "fab6c4d75bedcf880711c85e39ebf8ccc70d0eba259899047ec5d7436643ee17" 210 | "checksum stdweb 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d439538520bfe257b1fc2eeaa06b08dc4b56eee120b9d482c221f4bd267c9710" 211 | "checksum stdweb-derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aa46e9b38ea028a8a327ae6db35a486ace3eb834f5600bb3b6a71c0b6b1bd4b" 212 | "checksum stdweb-internal-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b0bb3289dfd46bba44d80ed47a9b3d4c43bf6c1d7931b29e2fa86bd6697ccf59" 213 | "checksum syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8c5bc2d6ff27891209efa5f63e9de78648d7801f085e4653701a692ce938d6fd" 214 | "checksum unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8083c594e02b8ae1654ae26f0ade5158b119bd88ad0e8227a5d8fcd72407946" 215 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 216 | -------------------------------------------------------------------------------- /papito_dom/src/vcomponent.rs: -------------------------------------------------------------------------------- 1 | use vnode::VNode; 2 | use std::any::TypeId; 3 | use std::fmt::Display; 4 | use std::fmt::{Formatter, self}; 5 | use std::fmt::Debug; 6 | use std::rc::Rc; 7 | use std::cell::RefCell; 8 | use traits::Component; 9 | use traits::Lifecycle; 10 | #[cfg(not(target_arch = "wasm32"))] 11 | use traits::ServerRender; 12 | #[cfg(target_arch = "wasm32")] 13 | use events::RenderRequestSender; 14 | use std::mem; 15 | 16 | struct Props; 17 | 18 | pub struct VComponent { 19 | type_id: TypeId, 20 | instance: Option>, 21 | props: Option<*mut Props>, 22 | #[cfg(target_arch = "wasm32")] 23 | initializer: Box Box>, 24 | #[cfg(not(target_arch = "wasm32"))] 25 | initializer: Box Box>, 26 | #[cfg(target_arch = "wasm32")] 27 | props_setter: Box, *mut Props)>, 28 | rendered: Option>, 29 | #[cfg(target_arch = "wasm32")] 30 | state_changed: Rc>, 31 | } 32 | 33 | impl VComponent { 34 | #[cfg(target_arch = "wasm32")] 35 | pub fn new(props: T::Props) -> VComponent { 36 | let state_changed = Rc::new(RefCell::new(false)); 37 | let state_changed_writer = state_changed.clone(); 38 | let props: *mut Props = unsafe { 39 | mem::transmute(Box::into_raw(Box::new(props))) 40 | }; 41 | VComponent { 42 | type_id: TypeId::of::(), 43 | instance: None, 44 | props: Some(props), 45 | initializer: Box::new(move |props, render_req| { 46 | let state_changed = state_changed_writer.clone(); 47 | let notifier = Box::new(move || { 48 | *state_changed.borrow_mut() = true; 49 | render_req.send(); 50 | }); 51 | let props: T::Props = unsafe { 52 | *Box::from_raw(mem::transmute(props)) 53 | }; 54 | Box::new(T::create(props, notifier)) 55 | }), 56 | props_setter: Box::new(|instance, props| { 57 | let props: T::Props = unsafe { 58 | *Box::from_raw(mem::transmute(props)) 59 | }; 60 | let instance = instance.as_any().downcast_ref::() 61 | .expect("Impossible. The instance cannot be of any other type"); 62 | let is_diff = instance.eq_props(&props); 63 | if is_diff { 64 | T::update(instance, props); 65 | } 66 | }), 67 | rendered: None, 68 | state_changed, 69 | } 70 | } 71 | 72 | #[cfg(not(target_arch = "wasm32"))] 73 | pub fn new(props: T::Props) -> VComponent { 74 | let state_changed = Rc::new(RefCell::new(false)); 75 | let state_changed_writer = state_changed.clone(); 76 | let props: *mut Props = unsafe { 77 | mem::transmute(Box::into_raw(Box::new(props))) 78 | }; 79 | VComponent { 80 | type_id: TypeId::of::(), 81 | instance: None, 82 | props: Some(props), 83 | initializer: Box::new(move |props| { 84 | let state_changed = state_changed_writer.clone(); 85 | let notifier = Box::new(move || { 86 | *state_changed.borrow_mut() = true; 87 | }); 88 | let props: T::Props = unsafe { 89 | *Box::from_raw(mem::transmute(props)) 90 | }; 91 | Box::new(T::create(props, notifier)) 92 | }), 93 | rendered: None 94 | } 95 | } 96 | 97 | #[cfg(target_arch = "wasm32")] 98 | fn init(&mut self, render_req: RenderRequestSender) { 99 | let initializer = &self.initializer; 100 | let props = self.props.take().expect("Impossible. The props are always provided"); 101 | let instance = initializer(props, render_req); 102 | instance.created(); 103 | self.instance = Some(instance); 104 | } 105 | 106 | #[cfg(not(target_arch = "wasm32"))] 107 | fn init(&mut self) { 108 | let initializer = &self.initializer; 109 | let props = self.props.take().expect("Impossible. The props are always provided"); 110 | let instance = initializer(props); 111 | instance.created(); 112 | self.instance = Some(instance); 113 | } 114 | 115 | // Only use this when the Type of the props is same as that of this Component's props 116 | #[cfg(target_arch = "wasm32")] 117 | unsafe fn set_props(&mut self, props: *mut Props) { 118 | debug_assert!(self.instance.is_some()); 119 | let props_setter = &self.props_setter; 120 | props_setter(self.instance.as_mut().unwrap(), props); 121 | } 122 | 123 | #[cfg(target_arch = "wasm32")] 124 | fn take_props(&mut self) -> *mut Props { 125 | self.props.take() 126 | .expect("Props already taken") 127 | } 128 | 129 | #[cfg(target_arch = "wasm32")] 130 | fn state_changed(&self) -> bool { 131 | *self.state_changed.borrow() 132 | } 133 | 134 | #[cfg(target_arch = "wasm32")] 135 | fn unset_state_changed(&self) { 136 | *self.state_changed.borrow_mut() = false; 137 | } 138 | } 139 | 140 | impl Eq for VComponent {} 141 | 142 | impl PartialEq for VComponent { 143 | fn eq(&self, other: &VComponent) -> bool { 144 | self.type_id == other.type_id && 145 | self.rendered == other.rendered 146 | } 147 | } 148 | 149 | impl Display for VComponent { 150 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 151 | if let Some(ref rendered) = self.rendered { 152 | write!(f, "{}", rendered) 153 | } else { 154 | Ok(()) 155 | } 156 | } 157 | } 158 | 159 | impl Debug for VComponent { 160 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 161 | if let Some(ref rendered) = self.rendered { 162 | write!(f, "{:?}", rendered) 163 | } else { 164 | Ok(()) 165 | } 166 | } 167 | } 168 | 169 | #[cfg(not(target_arch = "wasm32"))] 170 | impl ServerRender for VComponent { 171 | fn server_render(&mut self) { 172 | debug_assert!(self.instance.is_none()); 173 | debug_assert!(self.rendered.is_none()); 174 | self.init(); 175 | let instance = self.instance.as_mut().unwrap(); 176 | let mut rendered = instance.render(); 177 | rendered.server_render(); 178 | self.rendered = Some(Box::new(rendered)); 179 | } 180 | } 181 | 182 | #[cfg(target_arch = "wasm32")] 183 | mod wasm { 184 | use vdiff::DOMPatch; 185 | use vcomponent::VComponent; 186 | use stdweb::web::Element; 187 | use stdweb::web::Node; 188 | use vdiff::DOMRemove; 189 | use vdiff::DOMReorder; 190 | use vdiff::DOMNode; 191 | use traits::DOMRender; 192 | use events::RenderRequestSender; 193 | 194 | impl DOMPatch for VComponent { 195 | fn patch(mut self, parent: &Element, next: Option<&Node>, old_vnode: Option, render_req: RenderRequestSender) -> Self { 196 | // Those that are new here, are unrendered and those old require re-rendering 197 | if let Some(mut old_comp) = old_vnode { 198 | if self.type_id == old_comp.type_id { 199 | // Throw out the newer component, reuse older and pass the newer props 200 | unsafe { 201 | // Safe to use because both the props are of same type as both 202 | // components are of same type 203 | old_comp.set_props(self.take_props()); 204 | } 205 | old_comp.dom_render(parent, next, render_req); 206 | old_comp 207 | } else { 208 | old_comp.remove(parent); 209 | create_new_component_render(&mut self, parent, next, render_req); 210 | self 211 | } 212 | } else { 213 | create_new_component_render(&mut self, parent, next, render_req); 214 | self 215 | } 216 | } 217 | } 218 | 219 | fn create_new_component_render(vcomp: &mut VComponent, parent: &Element, next: Option<&Node>, render_req: RenderRequestSender) { 220 | debug_assert!(vcomp.instance.is_none()); 221 | debug_assert!(vcomp.rendered.is_none()); 222 | // Requires an initial render as they are very new 223 | vcomp.dom_render(parent, next, render_req); 224 | } 225 | 226 | impl DOMRemove for VComponent { 227 | fn remove(mut self, parent: &Element) { 228 | debug_assert!(self.instance.is_some()); 229 | debug_assert!(self.rendered.is_some()); 230 | self.rendered.unwrap().remove(parent); 231 | self.instance.as_mut().unwrap().destroyed(); 232 | } 233 | } 234 | 235 | impl DOMReorder for VComponent { 236 | fn move_to_last(&self, parent: &Element) { 237 | if let Some(ref rendered) = self.rendered { 238 | rendered.move_to_last(parent); 239 | } 240 | } 241 | 242 | fn move_before(&self, parent: &Element, next: &Node) { 243 | if let Some(ref rendered) = self.rendered { 244 | rendered.move_before(parent, next); 245 | } 246 | } 247 | } 248 | 249 | impl DOMNode for VComponent { 250 | fn dom_node(&self) -> Option { 251 | self.rendered.as_ref().and_then(|it| it.dom_node()) 252 | } 253 | } 254 | 255 | impl DOMRender for VComponent { 256 | fn dom_render(&mut self, parent: &Element, next: Option<&Node>, render_req: RenderRequestSender) { 257 | if self.instance.is_none() { 258 | self.init(render_req.clone()); 259 | } 260 | if self.rendered.is_none() { 261 | // First time being rendered 262 | let instance = self.instance.as_mut().unwrap(); 263 | let rendered = instance.render(); 264 | let rendered = rendered.patch(parent, next, None, render_req); 265 | self.rendered = Some(Box::new(rendered)); 266 | instance.mounted(); 267 | } else { 268 | if self.state_changed() { 269 | self.unset_state_changed(); 270 | let old_rendered = self.rendered.take().unwrap(); 271 | let instance = self.instance.as_mut().unwrap(); 272 | let newly_rendered = instance.render(); 273 | let newly_rendered = newly_rendered.patch(parent, next, Some(*old_rendered), render_req); 274 | self.rendered = Some(Box::new(newly_rendered)); 275 | instance.updated(); 276 | } else { 277 | // No change. Propagate till a changed/new component is found 278 | self.rendered.as_mut().unwrap().dom_render(parent, next, render_req); 279 | } 280 | } 281 | } 282 | } 283 | } -------------------------------------------------------------------------------- /papito_dom/src/velement.rs: -------------------------------------------------------------------------------- 1 | use CowStr; 2 | use indexmap::IndexMap; 3 | use std::fmt::{self, Formatter}; 4 | use std::fmt::Display; 5 | #[cfg(target_arch = "wasm32")] 6 | use stdweb::web::Element; 7 | #[cfg(target_arch = "wasm32")] 8 | use events::DOMEvent; 9 | use vnode::VNode; 10 | #[cfg(not(target_arch = "wasm32"))] 11 | use traits::ServerRender; 12 | 13 | #[derive(Debug, Eq, PartialEq)] 14 | pub struct ClassString(CowStr); 15 | 16 | impl Display for ClassString { 17 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 18 | write!(f, " class=\"{}\"", self.0) 19 | } 20 | } 21 | 22 | #[derive(Debug, Eq, PartialEq)] 23 | pub struct Attributes(IndexMap); 24 | 25 | impl Display for Attributes { 26 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 27 | for (k, v) in self.0.iter() { 28 | write!(f, " {}=\"{}\"", k, v)?; 29 | } 30 | Ok(()) 31 | } 32 | } 33 | 34 | #[cfg(target_arch = "wasm32")] 35 | #[derive(Debug, Eq, PartialEq)] 36 | pub struct Events(Vec>); 37 | 38 | #[derive(Debug, Eq, PartialEq)] 39 | pub struct VElement { 40 | tag: CowStr, 41 | class: Option, 42 | attrs: Option, 43 | child: Option>, 44 | is_self_closing: bool, 45 | #[cfg(target_arch = "wasm32")] 46 | events: Events, 47 | #[cfg(target_arch = "wasm32")] 48 | dom_ref: Option, 49 | } 50 | 51 | impl VElement { 52 | pub fn new(tag: CowStr, class: Option, attrs: Option, child: Option, is_self_closing: bool) -> VElement { 53 | VElement { 54 | // TODO: validate tag string first 55 | tag, 56 | class, 57 | attrs, 58 | child: child.map(|it| Box::new(it)), 59 | is_self_closing, 60 | #[cfg(target_arch = "wasm32")] 61 | events: Events(vec![]), 62 | #[cfg(target_arch = "wasm32")] 63 | dom_ref: None, 64 | } 65 | } 66 | 67 | #[cfg(target_arch = "wasm32")] 68 | pub fn dom_ref(&self) -> Option<&Element> { 69 | self.dom_ref.as_ref() 70 | } 71 | 72 | #[cfg(target_arch = "wasm32")] 73 | pub fn set_events(&mut self, events: Vec>) { 74 | self.events.0 = events; 75 | } 76 | } 77 | 78 | impl Display for VElement { 79 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 80 | write!(f, "<{}", self.tag)?; 81 | if let Some(ref class) = self.class { 82 | write!(f, "{}", class)?; 83 | } 84 | if let Some(ref attrs) = self.attrs { 85 | write!(f, "{}", attrs)?; 86 | } 87 | if self.is_self_closing { 88 | write!(f, ">") 89 | } else { 90 | write!(f, ">")?; 91 | if let Some(ref child) = self.child { 92 | write!(f, "{}", child)?; 93 | } 94 | write!(f, "", self.tag) 95 | } 96 | } 97 | } 98 | 99 | impl> From for ClassString { 100 | fn from(item: A) -> Self { 101 | ClassString(item.into()) 102 | } 103 | } 104 | 105 | impl From> for Attributes where 106 | A: Into, 107 | B: Into { 108 | fn from(item: Vec<(A, B)>) -> Self { 109 | Attributes(item.into_iter() 110 | .map(|(k, v)| (k.into(), v.into())) 111 | .collect()) 112 | } 113 | } 114 | 115 | impl From<(A, Vec<(B, C)>, VNode, bool)> for VElement where 116 | A: Into, 117 | B: Into, 118 | C: Into { 119 | fn from(item: (A, Vec<(B, C)>, VNode, bool)) -> Self { 120 | let tag = item.0.into(); 121 | let (class, attrs) = split_into_class_and_attrs(item.1.into()); 122 | VElement::new(tag, class, attrs, Some(item.2), item.3) 123 | } 124 | } 125 | 126 | impl From<(A, Vec<(B, C)>, VNode)> for VElement where 127 | A: Into, 128 | B: Into, 129 | C: Into { 130 | fn from(item: (A, Vec<(B, C)>, VNode)) -> Self { 131 | let tag = item.0.into(); 132 | let (class, attrs) = split_into_class_and_attrs(item.1.into()); 133 | VElement::new(tag, class, attrs, Some(item.2), false) 134 | } 135 | } 136 | 137 | impl From<(A, Vec<(B, C)>, bool)> for VElement where 138 | A: Into, 139 | B: Into, 140 | C: Into { 141 | fn from(item: (A, Vec<(B, C)>, bool)) -> Self { 142 | let tag = item.0.into(); 143 | let (class, attrs) = split_into_class_and_attrs(item.1.into()); 144 | VElement::new(tag, class, attrs, None, item.2) 145 | } 146 | } 147 | 148 | impl From<(A, Vec<(B, C)>)> for VElement where 149 | A: Into, 150 | B: Into, 151 | C: Into { 152 | fn from(item: (A, Vec<(B, C)>)) -> Self { 153 | let tag = item.0.into(); 154 | let (class, attrs) = split_into_class_and_attrs(item.1.into()); 155 | VElement::new(tag, class, attrs, None, false) 156 | } 157 | } 158 | 159 | impl From<(A, bool)> for VElement where 160 | A: Into { 161 | fn from(item: (A, bool)) -> Self { 162 | let tag = item.0.into(); 163 | VElement::new(tag, None, None, None, item.1) 164 | } 165 | } 166 | 167 | impl From<(A, ())> for VElement where 168 | A: Into { 169 | fn from(item: (A, ())) -> Self { 170 | let tag = item.0.into(); 171 | VElement::new(tag, None, None, None, false) 172 | } 173 | } 174 | 175 | impl From<(A, VNode, bool)> for VElement where 176 | A: Into { 177 | fn from(item: (A, VNode, bool)) -> Self { 178 | let tag = item.0.into(); 179 | VElement::new(tag, None, None, Some(item.1), item.2) 180 | } 181 | } 182 | 183 | impl From<(A, VNode)> for VElement where 184 | A: Into { 185 | fn from(item: (A, VNode)) -> Self { 186 | let tag = item.0.into(); 187 | VElement::new(tag, None, None, Some(item.1), false) 188 | } 189 | } 190 | 191 | fn split_into_class_and_attrs(mut attrs: Attributes) -> (Option, Option) { 192 | let class = attrs.0.swap_remove("class").map(|it| it.into()); 193 | (class, if attrs.0.len() == 0 { None } else { Some(attrs) }) 194 | } 195 | 196 | #[cfg(not(target_arch = "wasm32"))] 197 | impl ServerRender for VElement { 198 | fn server_render(&mut self) { 199 | if let Some(ref mut child) = self.child { 200 | child.server_render(); 201 | } 202 | } 203 | } 204 | 205 | #[cfg(target_arch = "wasm32")] 206 | mod wasm { 207 | use stdweb::web::{Element, document, INode, IElement}; 208 | use vdiff::{DOMPatch, DOMRemove}; 209 | use super::{VElement, ClassString, Attributes, Events}; 210 | use vdiff::DOMReorder; 211 | use vdiff::DOMNode; 212 | use stdweb::web::Node; 213 | use traits::DOMRender; 214 | use events::RenderRequestSender; 215 | 216 | impl DOMPatch for VElement { 217 | fn patch(mut self, parent: &Element, next: Option<&Node>, old_vnode: Option, render_req: RenderRequestSender) -> Self { 218 | if let Some(old_vnode) = old_vnode { 219 | if old_vnode.tag != self.tag { 220 | old_vnode.remove(parent); 221 | create_new_dom_node(self, parent, next, render_req) 222 | } else { 223 | let el = old_vnode.dom_ref().expect("Older element must have dom_ref") 224 | .clone(); 225 | self.class = self.class.patch(&el, None, old_vnode.class, render_req.clone()); 226 | self.attrs = self.attrs.patch(&el, None, old_vnode.attrs, render_req.clone()); 227 | self.child = self.child.patch(&el, None, old_vnode.child.map(|it| *it), render_req.clone()); 228 | self.events = self.events.patch(&el, None, Some(old_vnode.events), render_req); 229 | self.dom_ref = Some(el); 230 | self 231 | } 232 | } else { 233 | create_new_dom_node(self, parent, next, render_req) 234 | } 235 | } 236 | } 237 | 238 | impl DOMReorder for VElement { 239 | fn move_to_last(&self, parent: &Element) { 240 | let dom_ref = self.dom_ref().expect("Cannot append previously non-existent element."); 241 | parent.append_child(dom_ref); 242 | } 243 | 244 | fn move_before(&self, parent: &Element, next: &Node) { 245 | parent.insert_before(self.dom_ref().expect("Cannot insert previously non-existent text node."), next) 246 | .unwrap(); 247 | } 248 | } 249 | 250 | impl DOMRemove for VElement { 251 | fn remove(mut self, parent: &Element) { 252 | let dom_ref = self.dom_ref.take() 253 | .expect("Cannot remove non-existent element."); 254 | // Dismember the events 255 | self.events.remove(&dom_ref); 256 | // Remove child and their events 257 | if let Some(child) = self.child { 258 | child.remove(&dom_ref); 259 | } 260 | // Lastly remove self 261 | parent.remove_child(&dom_ref).unwrap(); 262 | } 263 | } 264 | 265 | fn create_new_dom_node(mut vel: VElement, parent: &Element, next: Option<&Node>, render_req: RenderRequestSender) -> VElement { 266 | let el_node = document().create_element(&vel.tag).unwrap(); 267 | vel.class = vel.class.patch(&el_node, None, None, render_req.clone()); 268 | vel.attrs = vel.attrs.patch(&el_node, None, None, render_req.clone()); 269 | vel.child = vel.child.patch(&el_node, None, None, render_req.clone()); 270 | vel.events = vel.events.patch(&el_node, None, None, render_req); 271 | if let Some(next) = next { 272 | parent.insert_before(&el_node, next).unwrap(); 273 | } else { 274 | parent.append_child(&el_node); 275 | } 276 | vel.dom_ref = Some(el_node); 277 | vel 278 | } 279 | 280 | impl DOMPatch for ClassString { 281 | fn patch(self, parent: &Element, _: Option<&Node>, old_value: Option, _: RenderRequestSender) -> Self { 282 | if Some(&self) != old_value.as_ref() { 283 | parent.set_attribute("class", &self.0) 284 | .unwrap(); 285 | } 286 | self 287 | } 288 | } 289 | 290 | impl DOMRemove for ClassString { 291 | fn remove(self, parent: &Element) { 292 | parent.remove_attribute("class"); 293 | } 294 | } 295 | 296 | impl DOMPatch for Attributes { 297 | fn patch(self, parent: &Element, _: Option<&Node>, old_vnode: Option, _: RenderRequestSender) -> Self { 298 | if let Some(mut old_attributes) = old_vnode { 299 | for (k, v) in self.0.clone() { 300 | let old_attr_val = old_attributes.0.swap_remove(&k); 301 | if Some(&v) != old_attr_val.as_ref() { 302 | parent.set_attribute(&k, &v).unwrap(); 303 | } 304 | } 305 | for (k, _) in old_attributes.0.iter() { 306 | parent.remove_attribute(&k); 307 | } 308 | } else { 309 | for (k, v) in self.0.clone() { 310 | parent.set_attribute(&k, &v).unwrap(); 311 | } 312 | } 313 | self 314 | } 315 | } 316 | 317 | impl DOMRemove for Attributes { 318 | fn remove(self, parent: &Element) { 319 | for (k, _) in self.0.iter() { 320 | parent.remove_attribute(k); 321 | } 322 | } 323 | } 324 | 325 | impl DOMPatch for Events { 326 | fn patch(mut self, parent: &Element, _: Option<&Node>, old_vnode: Option, _: RenderRequestSender) -> Self { 327 | // Remove older events because their is no way for Eq between two events. 328 | old_vnode.remove(parent); 329 | for ev in self.0.iter_mut() { 330 | ev.attach(parent); 331 | } 332 | self 333 | } 334 | } 335 | 336 | impl DOMRemove for Events { 337 | fn remove(mut self, _: &Element) { 338 | for ev in self.0.iter_mut() { 339 | ev.detach(); 340 | } 341 | } 342 | } 343 | 344 | impl DOMNode for VElement { 345 | fn dom_node(&self) -> Option { 346 | self.dom_ref.clone().map(|it| it.into()) 347 | } 348 | } 349 | 350 | impl DOMRender for VElement { 351 | fn dom_render(&mut self, parent: &Element, next: Option<&Node>, render_req: RenderRequestSender) { 352 | if let Some(ref mut child) = self.child { 353 | child.dom_render(parent, next, render_req); 354 | } 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /papito_codegen/src/component.rs: -------------------------------------------------------------------------------- 1 | use quote::Tokens; 2 | use syn::{Attribute, Field, Fields, Ident, Item, ItemStruct, Path, Type, Visibility}; 3 | 4 | pub fn quote(item: Item) -> Tokens { 5 | match item { 6 | Item::Struct(item_struct) => { 7 | let mut component_data = ComponentData::parse(&item_struct); 8 | component_data.quote() 9 | } 10 | _ => { 11 | panic!("`#[component]` can only be used on a struct"); 12 | } 13 | } 14 | } 15 | 16 | struct ComponentData { 17 | attrs: Vec, 18 | vis: Visibility, 19 | component: Ident, 20 | data: Option, 21 | props: Option, 22 | fields: DataFields, 23 | } 24 | 25 | impl ComponentData { 26 | fn parse(item: &ItemStruct) -> ComponentData { 27 | let fields = DataFields::parse(&item.fields); 28 | let component = item.ident.clone(); 29 | let attrs = item.attrs.clone(); 30 | let vis = item.vis.clone(); 31 | ComponentData { 32 | attrs, 33 | vis, 34 | data: None, 35 | props: None, 36 | component, 37 | fields, 38 | } 39 | } 40 | 41 | fn quote(&mut self) -> Tokens { 42 | let data_struct = self.quote_data_struct(); 43 | let props_struct = self.quote_props_struct(); 44 | let data_impl = self.quote_data_impl(); 45 | let component_struct = self.quote_component_struct(); 46 | let component_impl = self.quote_component_impl(); 47 | let impl_component_trait = self.quote_impl_component_trait(); 48 | quote! { 49 | #component_struct 50 | 51 | #props_struct 52 | 53 | #data_struct 54 | 55 | #data_impl 56 | 57 | #component_impl 58 | 59 | #impl_component_trait 60 | } 61 | } 62 | 63 | fn quote_component_struct(&self) -> Tokens { 64 | let attrs = &self.attrs; 65 | let vis = &self.vis; 66 | let component = &self.component; 67 | if let Some(data) = self.data { 68 | quote! { 69 | #(#attrs)* 70 | #vis struct #component { 71 | _data: ::std::rc::Rc<::std::cell::RefCell<#data>>, 72 | _notifier: Box 73 | } 74 | 75 | impl #component { 76 | fn _notify(&self) { 77 | (self._notifier)(); 78 | } 79 | } 80 | } 81 | } else { 82 | quote! { 83 | #(#attrs)* 84 | #vis struct #component; 85 | } 86 | } 87 | } 88 | 89 | fn quote_props_struct(&mut self) -> Tokens { 90 | let props_fields = self.fields.quote_props_fields(); 91 | if let Some(props_fields) = props_fields { 92 | let props = Ident::from(format!("_{}Props", &self.component)); 93 | self.props = Some(props); 94 | let props = self.props.as_ref().unwrap(); 95 | quote! { 96 | struct #props { 97 | #props_fields 98 | } 99 | } 100 | } else { 101 | quote!() 102 | } 103 | } 104 | 105 | fn quote_data_struct(&mut self) -> Tokens { 106 | let data_fields = self.fields.quote_data_fields(); 107 | if let Some(data_fields) = data_fields { 108 | let data = Ident::from(format!("_{}Data", &self.component)); 109 | self.data = Some(data); 110 | let data = self.data.as_ref().unwrap(); 111 | quote! { 112 | struct #data { 113 | #data_fields 114 | } 115 | } 116 | } else { 117 | quote!() 118 | } 119 | } 120 | 121 | fn quote_data_impl(&self) -> Tokens { 122 | if let Some(ref data) = self.data { 123 | let data_getters = self.fields.quote_getters(); 124 | let data_setters = self.fields.quote_setters(); 125 | quote! { 126 | impl #data { 127 | #data_getters 128 | 129 | #data_setters 130 | } 131 | } 132 | } else { 133 | quote!() 134 | } 135 | } 136 | 137 | fn quote_component_impl(&self) -> Tokens { 138 | if self.data.is_some() { 139 | let component = &self.component; 140 | let component_getters = self.fields.quote_component_getters(); 141 | let component_setters = self.fields.quote_component_setters(); 142 | quote! { 143 | impl #component { 144 | #component_getters 145 | 146 | #component_setters 147 | } 148 | } 149 | } else { 150 | quote!() 151 | } 152 | } 153 | 154 | fn quote_impl_component_trait(&self) -> Tokens { 155 | let component = &self.component; 156 | 157 | let props_ty = if let Some(ref props) = self.props { 158 | quote!( #props ) 159 | } else { 160 | quote!( () ) 161 | }; 162 | 163 | let create_fn = self.quote_create_fn(); 164 | let update_fn = self.quote_update_fn(); 165 | let eq_props_fn = self.quote_eq_props_fn(); 166 | 167 | quote! { 168 | impl ::papito_dom::Component for #component { 169 | type Props = #props_ty; 170 | 171 | #create_fn 172 | 173 | #update_fn 174 | 175 | #eq_props_fn 176 | } 177 | } 178 | } 179 | 180 | fn quote_create_fn(&self) -> Tokens { 181 | let component = &self.component; 182 | if let Some(ref data) = self.data { 183 | let data_init = self.fields.quote_data_init(); 184 | if self.props.is_some() { 185 | quote! { 186 | fn create(props: Self::Props, notifier: Box) -> Self { 187 | let _data = #data { 188 | #data_init 189 | }; 190 | #component { 191 | _data: ::std::rc::Rc::new(::std::cell::RefCell::new(_data)), 192 | _notifier: notifier 193 | } 194 | } 195 | } 196 | } else { 197 | quote! { 198 | fn create(_: Self::Props, notifier: Box) -> Self { 199 | let _data = #data { 200 | #data_init 201 | }; 202 | #component { 203 | _data: ::std::rc::Rc::new(::std::cell::RefCell::new(_data)), 204 | _notifier: notifier 205 | } 206 | } 207 | } 208 | } 209 | } else { 210 | quote! { 211 | fn create(_: Self::Props, _: Box) -> Self { 212 | #component 213 | } 214 | } 215 | } 216 | } 217 | 218 | fn quote_update_fn(&self) -> Tokens { 219 | if self.data.is_some() && self.props.is_some() { 220 | let props_update = self.fields.quote_props_update(); 221 | quote! { 222 | fn update(&self, props: Self::Props) { 223 | let _data = &mut self._data.borrow_mut(); 224 | #props_update 225 | self._notify(); 226 | } 227 | } 228 | } else { 229 | quote! { 230 | fn update(&self, _: Self::Props) {} 231 | } 232 | } 233 | } 234 | 235 | fn quote_eq_props_fn(&self) -> Tokens { 236 | if self.data.is_some() && self.props.is_some() { 237 | let props_eq = self.fields.quote_props_eq(); 238 | quote! { 239 | fn eq_props(&self, props: &Self::Props) -> bool { 240 | let _data = &*self._data.borrow(); 241 | #props_eq 242 | } 243 | } 244 | } else { 245 | // If there are no props, then the components have the same props 246 | quote! { 247 | fn eq_props(&self, _: &Self::Props) -> bool { 248 | true 249 | } 250 | } 251 | } 252 | } 253 | } 254 | 255 | struct DataFields { 256 | fields: Vec 257 | } 258 | 259 | impl DataFields { 260 | fn parse(fields: &Fields) -> DataFields { 261 | match *fields { 262 | Fields::Unit => DataFields { fields: vec![] }, 263 | Fields::Unnamed(_) => { 264 | panic!("Tuple structs are not allowed as a `#[component]`") 265 | } 266 | Fields::Named(ref named_fields) => { 267 | let fields = named_fields.named.iter() 268 | .map(|field| DataField::parse(field)) 269 | .collect(); 270 | DataFields { 271 | fields 272 | } 273 | } 274 | } 275 | } 276 | 277 | fn quote_data_fields(&self) -> Option { 278 | let fields: Vec<_> = self.fields.iter() 279 | .map(|it| it.quote_data_field()) 280 | .collect(); 281 | if !fields.is_empty() { 282 | Some(quote! { 283 | #(#fields),* 284 | }) 285 | } else { 286 | None 287 | } 288 | } 289 | 290 | fn quote_props_fields(&self) -> Option { 291 | let fields: Vec<_> = self.fields.iter() 292 | .map(|it| it.quote_props_field()) 293 | .filter(|it| it.is_some()) 294 | .map(|it| it.unwrap()) 295 | .collect(); 296 | if !fields.is_empty() { 297 | Some(quote! { 298 | #(#fields),* 299 | }) 300 | } else { 301 | None 302 | } 303 | } 304 | 305 | fn quote_getters(&self) -> Tokens { 306 | let getters: Vec<_> = self.fields.iter() 307 | .map(|it| it.quote_getters()) 308 | .collect(); 309 | quote! { 310 | #(#getters)* 311 | } 312 | } 313 | 314 | fn quote_component_getters(&self) -> Tokens { 315 | let getters: Vec<_> = self.fields.iter() 316 | .map(|it| it.quote_component_getters()) 317 | .collect(); 318 | quote! { 319 | #(#getters)* 320 | } 321 | } 322 | 323 | fn quote_setters(&self) -> Tokens { 324 | let setters: Vec<_> = self.fields.iter() 325 | .map(|it| it.quote_setters()) 326 | .collect(); 327 | quote! { 328 | #(#setters)* 329 | } 330 | } 331 | 332 | fn quote_component_setters(&self) -> Tokens { 333 | let setters: Vec<_> = self.fields.iter() 334 | .map(|it| it.quote_component_setters()) 335 | .collect(); 336 | quote! { 337 | #(#setters)* 338 | } 339 | } 340 | 341 | fn quote_data_init(&self) -> Tokens { 342 | let inits: Vec<_> = self.fields.iter() 343 | .map(|it| it.quote_data_init()) 344 | .collect(); 345 | quote! { 346 | #(#inits),* 347 | } 348 | } 349 | 350 | fn quote_props_update(&self) -> Tokens { 351 | let updates: Vec<_> = self.fields.iter() 352 | .map(|it| it.quote_props_update()) 353 | .collect(); 354 | quote! { 355 | #(#updates)* 356 | } 357 | } 358 | 359 | fn quote_props_eq(&self) -> Tokens { 360 | let eqs: Vec<_> = self.fields.iter() 361 | .map(|it| it.quote_props_eq()) 362 | .filter(|it| it.is_some()) 363 | .map(|it| it.unwrap()) 364 | .collect(); 365 | quote! { 366 | #(#eqs) && * 367 | } 368 | } 369 | } 370 | 371 | struct DataField { 372 | ident: Ident, 373 | ty: Type, 374 | is_prop: bool, 375 | } 376 | 377 | impl DataField { 378 | fn parse(field: &Field) -> DataField { 379 | if !field.vis.is_private() { 380 | panic!("Only private fields allowed."); 381 | } 382 | DataField { 383 | ident: field.ident.as_ref().unwrap().clone(), 384 | ty: field.ty.clone(), 385 | is_prop: field.attrs.has_prop_attribute(), 386 | } 387 | } 388 | 389 | fn quote_data_field(&self) -> Tokens { 390 | let ident = &self.ident; 391 | let ty = &self.ty; 392 | quote! { 393 | #ident: #ty 394 | } 395 | } 396 | 397 | fn quote_props_field(&self) -> Option { 398 | if self.is_prop { 399 | let ident = &self.ident; 400 | let ty = &self.ty; 401 | Some(quote! { 402 | #ident: #ty 403 | }) 404 | } else { 405 | None 406 | } 407 | } 408 | 409 | fn quote_getters(&self) -> Tokens { 410 | let ident = &self.ident; 411 | let ty = &self.ty; 412 | quote! { 413 | fn #ident(&self) -> #ty { 414 | self.#ident.clone() 415 | } 416 | } 417 | } 418 | 419 | fn quote_component_getters(&self) -> Tokens { 420 | let ident = &self.ident; 421 | let ty = &self.ty; 422 | quote! { 423 | fn #ident(&self) -> #ty { 424 | self._data.borrow().#ident() 425 | } 426 | } 427 | } 428 | 429 | fn quote_setters(&self) -> Option { 430 | if !self.is_prop { 431 | let ident = &self.ident; 432 | let fn_ident = Ident::from(format!("set_{}", ident)); 433 | let ty = &self.ty; 434 | Some(quote! { 435 | fn #fn_ident(&mut self, value: #ty) -> bool { 436 | if self.#ident != value { 437 | self.#ident = value; 438 | true 439 | } else { 440 | false 441 | } 442 | } 443 | }) 444 | } else { 445 | None 446 | } 447 | } 448 | 449 | fn quote_component_setters(&self) -> Option { 450 | if !self.is_prop { 451 | let ident = &self.ident; 452 | let fn_ident = Ident::from(format!("set_{}", ident)); 453 | let ty = &self.ty; 454 | Some(quote! { 455 | fn #fn_ident(&self, value: #ty) { 456 | let changed = self._data.borrow_mut().#fn_ident(value); 457 | if changed { 458 | self._notify(); 459 | } 460 | } 461 | }) 462 | } else { 463 | None 464 | } 465 | } 466 | 467 | fn quote_data_init(&self) -> Tokens { 468 | let ident = &self.ident; 469 | if self.is_prop { 470 | quote! { 471 | #ident: props.#ident 472 | } 473 | } else { 474 | quote! { 475 | #ident: Default::default() 476 | } 477 | } 478 | } 479 | 480 | fn quote_props_update(&self) -> Tokens { 481 | if self.is_prop { 482 | let ident = &self.ident; 483 | quote! { 484 | _data.#ident = props.#ident; 485 | } 486 | } else { 487 | quote!() 488 | } 489 | } 490 | 491 | fn quote_props_eq(&self) -> Option { 492 | if self.is_prop { 493 | let ident = &self.ident; 494 | Some(quote! { 495 | _data.#ident == props.#ident 496 | }) 497 | } else { 498 | None 499 | } 500 | } 501 | } 502 | 503 | trait HasPropAttribute { 504 | fn has_prop_attribute(&self) -> bool; 505 | } 506 | 507 | impl HasPropAttribute for Vec { 508 | fn has_prop_attribute(&self) -> bool { 509 | self.iter().any(|it| it.has_prop_attribute()) 510 | } 511 | } 512 | 513 | impl HasPropAttribute for Attribute { 514 | fn has_prop_attribute(&self) -> bool { 515 | if self.path == Path::from(Ident::from(format!("prop"))) { 516 | if !self.tts.is_empty() { 517 | panic!("No arguments supported"); 518 | } 519 | true 520 | } else { 521 | false 522 | } 523 | } 524 | } 525 | 526 | trait IsPrivate { 527 | fn is_private(&self) -> bool; 528 | } 529 | 530 | impl IsPrivate for Visibility { 531 | fn is_private(&self) -> bool { 532 | match *self { 533 | Visibility::Inherited => true, 534 | _ => false 535 | } 536 | } 537 | } -------------------------------------------------------------------------------- /papito_dom/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate indexmap; 2 | #[cfg(target_arch = "wasm32")] 3 | #[macro_use] 4 | extern crate stdweb; 5 | 6 | use std::borrow::Cow; 7 | use vnode::VNode; 8 | use vtext::VText; 9 | use velement::VElement; 10 | use vlist::VList; 11 | #[cfg(target_arch = "wasm32")] 12 | use stdweb::web::event::ConcreteEvent; 13 | use vcomponent::VComponent; 14 | 15 | type CowStr = Cow<'static, str>; 16 | 17 | mod vnode; 18 | mod vtext; 19 | mod velement; 20 | mod vlist; 21 | mod vcomponent; 22 | #[cfg(target_arch = "wasm32")] 23 | mod vdiff; 24 | #[cfg(target_arch = "wasm32")] 25 | mod events; 26 | mod traits; 27 | 28 | #[cfg(target_arch = "wasm32")] 29 | pub use traits::DOMRender; 30 | #[cfg(target_arch = "wasm32")] 31 | pub use events::{DOMEventListener, RenderRequest}; 32 | pub use traits::{Render, Component, Lifecycle}; 33 | 34 | pub mod prelude { 35 | pub use vnode::VNode; 36 | #[cfg(not(target_arch = "wasm32"))] 37 | pub use traits::RenderToString; 38 | } 39 | 40 | pub fn comp(props: C::Props) -> VComponent { 41 | VComponent::new::(props) 42 | } 43 | 44 | pub fn txt>(txt: T) -> VText { 45 | txt.into() 46 | } 47 | 48 | pub fn el>(el: T) -> VElement { 49 | el.into() 50 | } 51 | 52 | pub fn li>(li: T) -> VList { 53 | li.into() 54 | } 55 | 56 | pub fn h>(node_like: T) -> VNode { 57 | node_like.into() 58 | } 59 | 60 | #[cfg(target_arch = "wasm32")] 61 | pub fn ev(listener: E) -> Box where 62 | E: Into>, 63 | F: FnMut(T) + 'static, 64 | T: ConcreteEvent + 'static { 65 | Box::new(listener.into()) 66 | } 67 | 68 | #[macro_export] 69 | macro_rules! h { 70 | // Creates a component vnode with map as props where props is a struct 71 | (comp $t:ty, { $( $k:ident => $v:expr ),* } $(,)*) => {{ 72 | type T = <$t as $crate::Component>::Props; 73 | $crate::h($crate::comp::<$t>(T { 74 | $( $k: $v ),* 75 | })) 76 | }}; 77 | // Creates a component vnode with no props 78 | (comp $t:ty) => { 79 | $crate::h($crate::comp::<$t>(())) 80 | }; 81 | // Creates vnodes from a vec 82 | (vec $n:expr) => { 83 | $crate::h($crate::li($n)); 84 | }; 85 | // Creates keyed vnodes 86 | ({ $( $k:expr => $v:expr ),* $(,)* }) => { 87 | $crate::h($crate::li(vec![ $( ($k, $v) ),* ])) 88 | }; 89 | // Creates default-keyed vnodes 90 | ([ $( $v:expr ),* $(,)* ]) => { 91 | $crate::h($crate::li(vec![ $( $v ),* ])) 92 | }; 93 | // Creates text vnode 94 | ($n:expr) => { 95 | $crate::h($crate::txt($n)) 96 | }; 97 | // Creates an empty element 98 | ($n:expr, _ $(,)*) => { 99 | $crate::h($crate::el(($n, ()))) 100 | }; 101 | // Creates an element with map based attributes 102 | ($n:expr, { $($k:expr => $v:expr),* $(,)* } $(,)*) => { 103 | $crate::h($crate::el(($n, vec![ $( ($k, $v) ),* ]))) 104 | }; 105 | // Creates an element with event handlers 106 | ($n:expr, [ $( $ev:expr ),* $(,)* ] $(,)*) => {{ 107 | let mut el = $crate::el(($n, ())); 108 | #[cfg(target_arch = "wasm32")] 109 | el.set_events(vec![ $( $crate::ev( $ev ) ),* ]); 110 | $crate::h(el) 111 | }}; 112 | // Creates an element with map based attributes and event handlers 113 | ($n:expr, { $($k:expr => $v:expr),* $(,)* }, [ $( $ev:expr ),* $(,)* ] $(,)*) => {{ 114 | let mut el = $crate::el(($n, vec![ $( ($k, $v) ),* ])); 115 | #[cfg(target_arch = "wasm32")] 116 | el.set_events(vec![ $( $crate::ev( $ev ) ),* ]); 117 | $crate::h(el) 118 | }}; 119 | // Creates an element with map based attributes, event handlers and other arguments 120 | ($n:expr, { $($k:expr => $v:expr),* $(,)* }, [ $( $ev:expr ),* $(,)* ], $( $o:expr ),* $(,)*) => {{ 121 | let mut el = $crate::el(($n, vec![ $( ($k, $v) ),* ], $( $o ),*)); 122 | #[cfg(target_arch = "wasm32")] 123 | el.set_events(vec![ $( $crate::ev( $ev ) ),* ]); 124 | $crate::h(el) 125 | }}; 126 | // Creates an element with map based attributes along with other arguments 127 | ($n:expr, { $($k:expr => $v:expr),* $(,)* }, $( $o:expr ),* $(,)*) => { 128 | $crate::h($crate::el(($n, vec![ $( ($k, $v) ),* ], $( $o ),*))) 129 | }; 130 | // Creates an element with plain arguments, except attributes (not strictly), and event handlers 131 | ($n:expr, [ $( $ev:expr ),* $(,)* ], $( $m:expr ),* $(,)*) => {{ 132 | let mut el = $crate::el(($n, $( $m ),*)); 133 | #[cfg(target_arch = "wasm32")] 134 | el.set_events(vec![ $( $crate::ev( $ev ) ),* ]); 135 | $crate::h(el) 136 | }}; 137 | // Creates an element with plain arguments and event handlers 138 | ($n:expr, $s:expr, [ $( $ev:expr ),* $(,)* ], $( $m:expr ),* $(,)*) => {{ 139 | let mut el = $crate::el(($n, $s, $( $m ),*)); 140 | #[cfg(target_arch = "wasm32")] 141 | el.set_events(vec![ $( $crate::ev( $ev ) ),* ]); 142 | $crate::h(el) 143 | }}; 144 | // Creates an element with plain arguments 145 | ($n:expr, $( $m:expr ),* $(,)*) => { 146 | $crate::h($crate::el(($n, $( $m ),*))) 147 | }; 148 | } 149 | 150 | #[cfg(test)] 151 | mod test { 152 | use vtext::VText; 153 | use vnode::VNode; 154 | use velement::VElement; 155 | use std::borrow::Cow; 156 | #[cfg(target_arch = "wasm32")] 157 | use stdweb::web::event::InputEvent; 158 | use traits::{Component, Lifecycle, Render, RenderToString}; 159 | use vcomponent::VComponent; 160 | use std::rc::Rc; 161 | use std::cell::RefCell; 162 | 163 | #[test] 164 | fn should_create_text_vnode() { 165 | let node = h!("Hello World"); 166 | assert_eq!(VNode::Text(VText::new("Hello World".into())), node); 167 | } 168 | 169 | #[test] 170 | fn should_create_empty_velement() { 171 | let node = h!("div", _); 172 | assert_eq!(VNode::Element(VElement::new("div".into(), None, None, None, false)), node); 173 | } 174 | 175 | #[test] 176 | fn should_create_texted_velement() { 177 | let node = h!("span", h!("Hello World")); 178 | assert_eq!( 179 | VNode::Element(VElement::new( 180 | "span".into(), 181 | None, 182 | None, 183 | Some(VNode::Text(VText::new("Hello World".into()))), 184 | false, 185 | )), 186 | node 187 | ); 188 | } 189 | 190 | #[test] 191 | fn should_create_self_closing_velement() { 192 | let node = h!("br", true); 193 | assert_eq!( 194 | VNode::Element(VElement::new( 195 | "br".into(), 196 | None, 197 | None, 198 | None, 199 | true, 200 | )), 201 | node 202 | ); 203 | } 204 | 205 | #[test] 206 | fn should_create_vlist() { 207 | let node = h!({ "1" => h!("div", _), "2" => h!("div", _), "3" => h!("div", _) }); 208 | assert_eq!( 209 | VNode::List(vec![ 210 | (Cow::from("1"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 211 | (Cow::from("2"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 212 | (Cow::from("3"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 213 | ].into()), 214 | node 215 | ); 216 | } 217 | 218 | #[test] 219 | fn should_create_vlist_without_keys() { 220 | let node = h!([h!("div", _), h!("div", _), h!("div", _)]); 221 | assert_eq!( 222 | VNode::List(vec![ 223 | (Cow::from("0"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 224 | (Cow::from("1"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 225 | (Cow::from("2"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 226 | ].into()), 227 | node 228 | ); 229 | } 230 | 231 | #[test] 232 | fn should_create_vlist_from_vec() { 233 | let list = vec![h!("div", _), h!("div", _), h!("div", _)]; 234 | let node = h!(vec list); 235 | assert_eq!( 236 | VNode::List(vec![ 237 | (Cow::from("0"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 238 | (Cow::from("1"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 239 | (Cow::from("2"), VNode::Element(VElement::new("div".into(), None, None, None, false))), 240 | ].into()), 241 | node 242 | ); 243 | } 244 | 245 | #[test] 246 | fn should_create_velement_with_class() { 247 | let node = h!("div", vec![("class", "container")]); 248 | assert_eq!( 249 | VNode::Element(VElement::new("div".into(), Some("container".into()), None, None, false)), 250 | node 251 | ); 252 | } 253 | 254 | #[test] 255 | fn should_create_velement_with_class_with_alt_syntax() { 256 | let node = h!("div", { "class" => "container" }); 257 | assert_eq!( 258 | VNode::Element(VElement::new("div".into(), Some("container".into()), None, None, false)), 259 | node 260 | ); 261 | } 262 | 263 | #[test] 264 | fn should_create_velement_with_attributes() { 265 | let node = h!("div", { "style" => "background-color: black;" }); 266 | assert_eq!( 267 | VNode::Element(VElement::new("div".into(), None, Some(vec![("style", "background-color: black;")].into()), None, false)), 268 | node 269 | ); 270 | } 271 | 272 | #[test] 273 | fn should_create_nested_structure() { 274 | let node = h!("div", h!("span", _)); 275 | assert_eq!( 276 | VNode::Element(VElement::new( 277 | "div".into(), 278 | None, 279 | None, 280 | Some(VNode::Element(VElement::new( 281 | "span".into(), 282 | None, 283 | None, 284 | None, 285 | false))), 286 | false) 287 | ), 288 | node 289 | ); 290 | } 291 | 292 | #[test] 293 | fn should_create_heterogenous_vlist() { 294 | let node = h!([ 295 | h!("div", _), 296 | h!("Hello World"), 297 | h!([ 298 | h!("div", _), 299 | h!("Hello World"), 300 | ]) 301 | ]); 302 | assert_eq!( 303 | VNode::List(vec![ 304 | VNode::Element(VElement::new("div".into(), None, None, None, false)), 305 | VNode::Text(VText::new("Hello World".into())), 306 | VNode::List(vec![ 307 | VNode::Element(VElement::new("div".into(), None, None, None, false)), 308 | VNode::Text(VText::new("Hello World".into())) 309 | ].into()) 310 | ].into()), 311 | node 312 | ); 313 | } 314 | 315 | #[test] 316 | fn should_create_empty_input_with_event() { 317 | let node = h!("input", [ |_: InputEvent| {} ]); 318 | assert_eq!( 319 | VNode::Element(VElement::new("input".into(), None, None, None, false)), 320 | node 321 | ); 322 | } 323 | 324 | #[test] 325 | fn should_create_empty_input_with_attribute_and_event() { 326 | let node = h!("input", { "disabled" => "true" }, [ |_: InputEvent| {} ]); 327 | assert_eq!( 328 | VNode::Element(VElement::new( 329 | "input".into(), 330 | None, 331 | Some(vec![("disabled", "true")].into()), 332 | None, 333 | false) 334 | ), 335 | node 336 | ); 337 | } 338 | 339 | #[test] 340 | fn should_create_texted_div_with_attribute_and_event() { 341 | let node = h!("div", { "style" => "color: white;" }, [ |_: InputEvent| {} ], h!("Hello")); 342 | assert_eq!( 343 | VNode::Element(VElement::new( 344 | "div".into(), 345 | None, 346 | Some(vec![("style", "color: white;")].into()), 347 | Some(VNode::Text(VText::new("Hello".into()))), 348 | false) 349 | ), 350 | node 351 | ); 352 | } 353 | 354 | #[test] 355 | fn should_create_texted_div_with_event() { 356 | let node = h!("div", [ |_: InputEvent| {} ], h!("Hello")); 357 | assert_eq!( 358 | VNode::Element(VElement::new( 359 | "div".into(), 360 | None, 361 | None, 362 | Some(VNode::Text(VText::new("Hello".into()))), 363 | false) 364 | ), 365 | node 366 | ); 367 | } 368 | 369 | #[test] 370 | fn should_create_a_component() { 371 | struct Button; 372 | 373 | impl Lifecycle for Button {} 374 | impl Render for Button { 375 | fn render(&self) -> VNode { 376 | unimplemented!(); 377 | } 378 | } 379 | 380 | impl ComponentOf for Button { 381 | type Comp = ButtonComponent; 382 | } 383 | 384 | struct ButtonComponent { 385 | inner: Rc> 386 | } 387 | 388 | impl Component for ButtonComponent { 389 | type Props = (); 390 | 391 | fn create(_: (), _: Box) -> Self { 392 | ButtonComponent { 393 | inner: Rc::new(RefCell::new(Button)) 394 | } 395 | } 396 | fn update(&mut self, props: Self::Props) {} 397 | fn props(&self) -> &Self::Props { 398 | &() 399 | } 400 | } 401 | 402 | impl Lifecycle for ButtonComponent {} 403 | impl Render for ButtonComponent { 404 | fn render(&self) -> VNode { 405 | h!("button", h!("Click")) 406 | } 407 | } 408 | 409 | let node = h!(comp Button); 410 | assert_eq!( 411 | VNode::Component(VComponent::new::(())), 412 | node 413 | ); 414 | } 415 | 416 | #[test] 417 | fn should_print_html_for_empty_div() { 418 | let node = h!("div", _); 419 | assert_eq!(node.to_string(), "
"); 420 | } 421 | 422 | #[test] 423 | fn should_print_html_for_self_closing_br() { 424 | let node = h!("br", true); 425 | assert_eq!(node.to_string(), "
"); 426 | } 427 | 428 | #[test] 429 | fn should_print_html_for_texted_div() { 430 | let node = h!("div", h!("Hello World")); 431 | assert_eq!(node.to_string(), "
Hello World
"); 432 | } 433 | 434 | #[test] 435 | fn should_print_html_for_attributed_button() { 436 | let node = h!("button", { "class" => "container", "style" => "background-color: black;" }, h!("Click")); 437 | assert_eq!(node.to_string(), r#""#) 438 | } 439 | 440 | #[test] 441 | fn should_print_html_for_nested_spans() { 442 | let node = h!("span", h!("span", _)); 443 | assert_eq!(node.to_string(), ""); 444 | } 445 | 446 | #[test] 447 | fn should_print_html_for_ordered_list() { 448 | let node = h!("ol", h!([ 449 | h!("li", h!("content")), 450 | h!("li", h!("content")), 451 | h!("li", h!("content")), 452 | h!("li", h!("content")), 453 | ])); 454 | assert_eq!(node.to_string(), "
  1. content
  2. content
  3. content
  4. content
"); 455 | } 456 | 457 | #[test] 458 | fn should_print_html_for_list_of_text() { 459 | let node = h!("div", h!([ 460 | h!("Hello"), 461 | h!("Hello"), 462 | h!("Hello"), 463 | h!("Hello"), 464 | ])); 465 | assert_eq!(node.to_string(), "
HelloHelloHelloHello
"); 466 | } 467 | 468 | #[test] 469 | fn should_print_html_for_plain_list() { 470 | let node = h!([ 471 | h!("div", _), 472 | h!("div", _), 473 | h!("div", _), 474 | h!("div", _), 475 | ]); 476 | assert_eq!(node.to_string(), "
"); 477 | } 478 | 479 | #[test] 480 | fn should_print_html_for_component() { 481 | struct Button; 482 | 483 | impl Lifecycle for Button {} 484 | impl Render for Button { 485 | fn render(&self) -> VNode { 486 | unimplemented!(); 487 | } 488 | } 489 | 490 | impl ComponentOf for Button { 491 | type Comp = ButtonComponent; 492 | } 493 | 494 | struct ButtonComponent { 495 | inner: Rc> 496 | } 497 | 498 | impl Component for ButtonComponent { 499 | type Props = (); 500 | 501 | fn create(_: (), _: Box) -> Self { 502 | ButtonComponent { 503 | inner: Rc::new(RefCell::new(Button)) 504 | } 505 | } 506 | fn update(&mut self, props: Self::Props) {} 507 | fn props(&self) -> &Self::Props { 508 | &() 509 | } 510 | } 511 | 512 | impl Lifecycle for ButtonComponent {} 513 | impl Render for ButtonComponent { 514 | fn render(&self) -> VNode { 515 | h!("button", h!("Click")) 516 | } 517 | } 518 | 519 | let mut node = h!(comp Button); 520 | assert_eq!(node.render_to_string(), ""); 521 | } 522 | 523 | #[test] 524 | fn should_print_html_for_nested_components() { 525 | struct Button; 526 | 527 | impl Lifecycle for Button {} 528 | impl Render for Button { 529 | fn render(&self) -> VNode { 530 | unimplemented!(); 531 | } 532 | } 533 | 534 | impl ComponentOf for Button { 535 | type Comp = ButtonComponent; 536 | } 537 | 538 | struct ButtonComponent { 539 | inner: Rc> 540 | } 541 | 542 | impl Component for ButtonComponent { 543 | type Props = (); 544 | 545 | fn create(_: (), _: Box) -> Self { 546 | ButtonComponent { 547 | inner: Rc::new(RefCell::new(Button)) 548 | } 549 | } 550 | fn update(&mut self, props: Self::Props) {} 551 | fn props(&self) -> &Self::Props { 552 | &() 553 | } 554 | } 555 | 556 | impl Lifecycle for ButtonComponent {} 557 | impl Render for ButtonComponent { 558 | fn render(&self) -> VNode { 559 | h!("button", h!("Click")) 560 | } 561 | } 562 | 563 | struct Div; 564 | 565 | impl Lifecycle for Div {} 566 | impl Render for Div { 567 | fn render(&self) -> VNode { 568 | unimplemented!(); 569 | } 570 | } 571 | impl ComponentOf for Div { 572 | type Comp = DivComponent; 573 | } 574 | 575 | struct DivComponent { 576 | inner: Rc> 577 | } 578 | 579 | impl Component for DivComponent { 580 | type Props = (); 581 | 582 | fn create(_:(), _: Box) -> Self { 583 | DivComponent { 584 | inner: Rc::new(RefCell::new(Div)) 585 | } 586 | } 587 | fn update(&mut self, props: Self::Props) {} 588 | fn props(&self) -> &Self::Props { 589 | &() 590 | } 591 | } 592 | 593 | impl Lifecycle for DivComponent {} 594 | impl Render for DivComponent { 595 | fn render(&self) -> VNode { 596 | h!("div", h!(comp Button)) 597 | } 598 | } 599 | 600 | let mut node = h!(comp Div); 601 | assert_eq!(node.render_to_string(), "
"); 602 | } 603 | } 604 | --------------------------------------------------------------------------------