From 635783445fa45a94048f5d53cae3b18a75cac32b Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Tue, 24 Dec 2019 12:01:41 -0600 Subject: [PATCH 01/28] Enable travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index de5cbeb08d1..8216fa57367 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ branches: - staging - trying - master + - web-sys dist: trusty language: rust From 0d29a2886c19ad4a10bb130ab3b185375ac05540 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Wed, 8 Jan 2020 07:38:08 +0100 Subject: [PATCH 02/28] `web-sys` general conversion (#826) * Moved patches from different PRs. * Add bits & pieces and some services. * Rename `stdweb` feature to `std_web`. * Move tests and examples to different PR. * Revert some `cargo_web` handling removal. * Missed something. * Implement `console_error_panic_hook`. * Update Cargo.toml Co-authored-by: Justin Starry --- Cargo.toml | 58 +++++++++++++++++++++++++++++++++++++++- build.rs | 6 +++++ src/app.rs | 35 +++++++++++++++++++----- src/components/select.rs | 6 ++++- src/lib.rs | 10 +++++++ src/utils.rs | 23 ++++++++++++++-- 6 files changed, 128 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 848d18a24c8..d56df56bce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,12 @@ travis-ci = { repository = "yewstack/yew" } [dependencies] anymap = "0.12" bincode = { version = "~1.2.1", optional = true } +console_error_panic_hook = { version = "0.1", optional = true } failure = "0.1" +gloo = { version = "0.2", optional = true } http = "0.2" indexmap = "1.0.2" +js-sys = { version = "0.3", optional = true } log = "0.4" proc-macro-hack = "0.5" proc-macro-nested = "0.1" @@ -33,10 +36,61 @@ serde_cbor = { version = "0.10.2", optional = true } serde_json = "1.0" serde_yaml = { version = "0.8.3", optional = true } slab = "0.4" -stdweb = "0.4.20" +stdweb = { version = "0.4.20", optional = true } toml = { version = "0.5", optional = true } yew-macro = { version = "0.10.1", path = "crates/macro" } +[dependencies.web-sys] +version = "0.3" +optional = true +features = [ + "AbortController", + "AbortSignal", + "BinaryType", + "Blob", + "console", + "DedicatedWorkerGlobalScope", + "Document", + "DomTokenList", + "DragEvent", + "Element", + "Event", + "EventTarget", + "File", + "FileList", + "FileReader", + "FocusEvent", + "Headers", + "HtmlElement", + "HtmlInputElement", + "HtmlSelectElement", + "HtmlTextAreaElement", + "KeyboardEvent", + "Location", + "MessageEvent", + "MouseEvent", + "Node", + "ObserverCallback", + "PointerEvent", + "ReferrerPolicy", + "Request", + "RequestCache", + "RequestCredentials", + "RequestInit", + "RequestMode", + "RequestRedirect", + "Response", + "Storage", + "Text", + "TouchEvent", + "UiEvent", + "WebSocket", + "WheelEvent", + "Window", + "Worker", + "WorkerOptions", +] + [target.'cfg(all(target_arch = "wasm32", not(target_os="wasi"), not(cargo_web)))'.dependencies] wasm-bindgen = "0.2.56" wasm-bindgen-futures = "0.4.4" @@ -54,6 +108,8 @@ rustversion = "1.0" [features] default = ["services", "agent"] +std_web = ["stdweb"] +web_sys = ["console_error_panic_hook", "gloo", "js-sys", "web-sys"] doc_test = [] web_test = [] wasm_test = [] diff --git a/build.rs b/build.rs index a0445febb0e..437ea43a5d1 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,12 @@ use std::env; pub fn main() { + if cfg!(all(feature = "web_sys", feature = "std_web")) { + panic!("don't use `web_sys` and `std_web` simultaneously") + } else if cfg!(not(any(feature = "web_sys", feature = "std_web"))) { + panic!("please select either `web_sys` or `std_web`") + } + let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let cargo_web = env::var("COMPILING_UNDER_CARGO_WEB").unwrap_or_default(); if target_arch == "wasm32" && cargo_web != "1" { diff --git a/src/app.rs b/src/app.rs index 3e6a9c2835e..295ae11b0ef 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,10 @@ //! a component in an isolated scope. use crate::html::{Component, NodeRef, Scope}; +#[cfg(feature = "std_web")] use stdweb::web::{document, Element, INode, IParentNode}; +#[cfg(feature = "web_sys")] +use web_sys::Element; /// An application instance. #[derive(Debug)] @@ -42,8 +45,13 @@ where /// Alias to `mount("body", ...)`. pub fn mount_to_body(self) -> Scope { + #[cfg(feature = "std_web")] + let document = document(); + #[cfg(feature = "web_sys")] + let document = web_sys::window().unwrap().document().unwrap(); + // Bootstrap the component for `Window` environment only (not for `Worker`) - let element = document() + let element = document .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); @@ -55,11 +63,16 @@ where /// need to manipulate the body element. For example, adding/removing app-wide /// CSS classes of the body element. pub fn mount_as_body(self) -> Scope { - let html_element = document() + #[cfg(feature = "std_web")] + let document = document(); + #[cfg(feature = "web_sys")] + let document = web_sys::window().unwrap().document().unwrap(); + + let html_element = document .query_selector("html") .expect("can't get html node for rendering") .expect("can't unwrap html node"); - let body_element = document() + let body_element = document .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); @@ -97,8 +110,13 @@ where /// Alias to `mount_with_props("body", ...)`. pub fn mount_to_body_with_props(self, props: COMP::Properties) -> Scope { + #[cfg(feature = "std_web")] + let document = document(); + #[cfg(feature = "web_sys")] + let document = web_sys::window().unwrap().document().unwrap(); + // Bootstrap the component for `Window` environment only (not for `Worker`) - let element = document() + let element = document .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); @@ -110,11 +128,16 @@ where /// when you need to manipulate the body element. For example, adding/removing app-wide /// CSS classes of the body element. pub fn mount_as_body_with_props(self, props: COMP::Properties) -> Scope { - let html_element = document() + #[cfg(feature = "std_web")] + let document = document(); + #[cfg(feature = "web_sys")] + let document = web_sys::window().unwrap().document().unwrap(); + + let html_element = document .query_selector("html") .expect("can't get html node for rendering") .expect("can't unwrap html node"); - let body_element = document() + let body_element = document .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); diff --git a/src/components/select.rs b/src/components/select.rs index a28f5d61892..cb919f2782c 100644 --- a/src/components/select.rs +++ b/src/components/select.rs @@ -121,7 +121,11 @@ where fn onchange(&self) -> Callback { self.link.callback(|event| match event { ChangeData::Select(elem) => { - let value = elem.selected_index().map(|x| x as usize); + let value = elem.selected_index(); + #[cfg(feature = "std_web")] + let value = value.map(|x| x as usize); + #[cfg(feature = "web_sys")] + let value = Some(value as usize); Msg::Selected(value) } _ => { diff --git a/src/lib.rs b/src/lib.rs index bc74e9b5b51..08e247834f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,7 @@ pub mod services; pub mod events { pub use crate::html::{ChangeData, InputData}; + #[cfg(feature = "std_web")] pub use stdweb::web::event::{ BlurEvent, ClickEvent, ContextMenuEvent, DoubleClickEvent, DragDropEvent, DragEndEvent, DragEnterEvent, DragEvent, DragExitEvent, DragLeaveEvent, DragOverEvent, DragStartEvent, @@ -106,15 +107,24 @@ pub mod events { PointerLeaveEvent, PointerMoveEvent, PointerOutEvent, PointerOverEvent, PointerUpEvent, ScrollEvent, SubmitEvent, TouchCancel, TouchEnd, TouchEnter, TouchMove, TouchStart, }; + #[cfg(feature = "web_sys")] + pub use web_sys::{ + DragEvent, Event, FocusEvent, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, UiEvent, + WheelEvent, + }; } /// Initializes yew framework. It should be called first. pub fn initialize() { + #[cfg(feature = "std_web")] stdweb::initialize(); + #[cfg(feature = "web_sys")] + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); } /// Starts event loop. pub fn run_loop() { + #[cfg(feature = "std_web")] stdweb::event_loop(); } diff --git a/src/utils.rs b/src/utils.rs index c56d43227ba..62f24e15bf1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,31 @@ //! This module contains useful utils to get information about the current document. use failure::{err_msg, Error}; +#[cfg(feature = "std_web")] use stdweb::web::document; /// Returns `host` for the current document. Useful to connect to a server that server the app. pub fn host() -> Result { - document() + #[cfg(feature = "std_web")] + { + document() + .location() + .ok_or_else(|| err_msg("can't get location")) + .and_then(|l| l.host().map_err(Error::from)) + } + #[cfg(feature = "web_sys")] + web_sys::window() + .unwrap() + .document() + .unwrap() .location() .ok_or_else(|| err_msg("can't get location")) - .and_then(|l| l.host().map_err(Error::from)) + .and_then(|l| { + l.host().map_err(|e| { + err_msg( + e.as_string() + .unwrap_or_else(|| String::from("error not recoverable")), + ) + }) + }) } From 05bb014750cf58d6213663c09f11f1a9bd4e3d36 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 8 Jan 2020 16:14:59 +0800 Subject: [PATCH 03/28] Move document creation to util convenience method (#855) --- build.rs | 4 ++-- src/app.rs | 35 ++++++++--------------------------- src/utils.rs | 46 +++++++++++++++++++++++++--------------------- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/build.rs b/build.rs index 437ea43a5d1..4d163e525ee 100644 --- a/build.rs +++ b/build.rs @@ -2,9 +2,9 @@ use std::env; pub fn main() { if cfg!(all(feature = "web_sys", feature = "std_web")) { - panic!("don't use `web_sys` and `std_web` simultaneously") + panic!("the `web_sys` and `std_web` cargo features cannot be used simultaneously") } else if cfg!(not(any(feature = "web_sys", feature = "std_web"))) { - panic!("please select either `web_sys` or `std_web`") + panic!("please select either the `web_sys` or `std_web` cargo feature") } let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); diff --git a/src/app.rs b/src/app.rs index 295ae11b0ef..7ef42b01b86 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,8 +2,9 @@ //! a component in an isolated scope. use crate::html::{Component, NodeRef, Scope}; +use crate::utils::document; #[cfg(feature = "std_web")] -use stdweb::web::{document, Element, INode, IParentNode}; +use stdweb::web::{Element, INode, IParentNode}; #[cfg(feature = "web_sys")] use web_sys::Element; @@ -45,13 +46,8 @@ where /// Alias to `mount("body", ...)`. pub fn mount_to_body(self) -> Scope { - #[cfg(feature = "std_web")] - let document = document(); - #[cfg(feature = "web_sys")] - let document = web_sys::window().unwrap().document().unwrap(); - // Bootstrap the component for `Window` environment only (not for `Worker`) - let element = document + let element = document() .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); @@ -63,16 +59,11 @@ where /// need to manipulate the body element. For example, adding/removing app-wide /// CSS classes of the body element. pub fn mount_as_body(self) -> Scope { - #[cfg(feature = "std_web")] - let document = document(); - #[cfg(feature = "web_sys")] - let document = web_sys::window().unwrap().document().unwrap(); - - let html_element = document + let html_element = document() .query_selector("html") .expect("can't get html node for rendering") .expect("can't unwrap html node"); - let body_element = document + let body_element = document() .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); @@ -110,13 +101,8 @@ where /// Alias to `mount_with_props("body", ...)`. pub fn mount_to_body_with_props(self, props: COMP::Properties) -> Scope { - #[cfg(feature = "std_web")] - let document = document(); - #[cfg(feature = "web_sys")] - let document = web_sys::window().unwrap().document().unwrap(); - // Bootstrap the component for `Window` environment only (not for `Worker`) - let element = document + let element = document() .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); @@ -128,16 +114,11 @@ where /// when you need to manipulate the body element. For example, adding/removing app-wide /// CSS classes of the body element. pub fn mount_as_body_with_props(self, props: COMP::Properties) -> Scope { - #[cfg(feature = "std_web")] - let document = document(); - #[cfg(feature = "web_sys")] - let document = web_sys::window().unwrap().document().unwrap(); - - let html_element = document + let html_element = document() .query_selector("html") .expect("can't get html node for rendering") .expect("can't unwrap html node"); - let body_element = document + let body_element = document() .query_selector("body") .expect("can't get body node for rendering") .expect("can't unwrap body node"); diff --git a/src/utils.rs b/src/utils.rs index 62f24e15bf1..c5fdc7f0f28 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,31 +1,35 @@ //! This module contains useful utils to get information about the current document. use failure::{err_msg, Error}; + #[cfg(feature = "std_web")] -use stdweb::web::document; +/// Returns current document. +pub fn document() -> stdweb::web::Document { + stdweb::web::document() +} + +#[cfg(feature = "web_sys")] +/// Returns current document. +pub fn document() -> web_sys::Document { + web_sys::window().unwrap().document().unwrap() +} /// Returns `host` for the current document. Useful to connect to a server that server the app. pub fn host() -> Result { + let location = document() + .location() + .ok_or_else(|| err_msg("can't get location"))?; + #[cfg(feature = "std_web")] - { - document() - .location() - .ok_or_else(|| err_msg("can't get location")) - .and_then(|l| l.host().map_err(Error::from)) - } + let host = location.host().map_err(Error::from)?; + #[cfg(feature = "web_sys")] - web_sys::window() - .unwrap() - .document() - .unwrap() - .location() - .ok_or_else(|| err_msg("can't get location")) - .and_then(|l| { - l.host().map_err(|e| { - err_msg( - e.as_string() - .unwrap_or_else(|| String::from("error not recoverable")), - ) - }) - }) + let host = location.host().map_err(|e| { + err_msg( + e.as_string() + .unwrap_or_else(|| String::from("error not recoverable")), + ) + })?; + + Ok(host) } From 7adfef1c89233c13770e47d00289a0663ffd2d64 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 9 Jan 2020 08:40:19 +0100 Subject: [PATCH 04/28] `web-sys` listener conversion (#813) * `web-sys` listener initial try. * Improve macros? * Remove generic from `EventListenerHandle`. * Fix build. * A cleaner solution? * Even cleaner. * Fix `build.rs`. * Minor improvements. * Following the yew toml style. * Fixing visibility. * Fix `rustfmt`. * Add `web-sys` re-exports. * Move general changes to different PR. * Remove compat. * Actually remove `compat.rs`. * Rename `stdweb` feature to `std_web`. * Move to gloo's `EventListener` and some polish. * Remove outdated comment. * Change `EventHandler` to be cancelled on drop. --- src/html/listener.rs | 170 -------------------------- src/html/listener/listener_stdweb.rs | 46 +++++++ src/html/listener/listener_web_sys.rs | 46 +++++++ src/html/listener/macros.rs | 68 +++++++++++ src/html/listener/mod.rs | 141 +++++++++++++++++++++ 5 files changed, 301 insertions(+), 170 deletions(-) delete mode 100644 src/html/listener.rs create mode 100644 src/html/listener/listener_stdweb.rs create mode 100644 src/html/listener/listener_web_sys.rs create mode 100644 src/html/listener/macros.rs create mode 100644 src/html/listener/mod.rs diff --git a/src/html/listener.rs b/src/html/listener.rs deleted file mode 100644 index 99d51de5c13..00000000000 --- a/src/html/listener.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::callback::Callback; -use crate::virtual_dom::Listener; -use stdweb::web::html_element::SelectElement; -#[allow(unused_imports)] -use stdweb::web::{EventListenerHandle, FileList, INode}; -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; - -macro_rules! impl_action { - ($($action:ident($event:ident : $type:ident) -> $ret:ty => $convert:expr)*) => {$( - /// An abstract implementation of a listener. - pub mod $action { - use stdweb::web::{IEventTarget, Element}; - use stdweb::web::event::{IEvent, $type}; - use super::*; - - /// A wrapper for a callback which attaches event listeners to elements. - #[derive(Clone, Debug)] - pub struct Wrapper { - callback: Callback, - } - - impl Wrapper { - /// Create a wrapper for an event-typed callback - pub fn new(callback: Callback) -> Self { - Wrapper { callback } - } - } - - /// And event type which keeps the returned type. - pub type Event = $ret; - - impl Listener for Wrapper { - fn kind(&self) -> &'static str { - stringify!($action) - } - - fn attach(&self, element: &Element) -> EventListenerHandle { - let this = element.clone(); - let callback = self.callback.clone(); - let listener = move |event: $type| { - event.stop_propagation(); - callback.emit($convert(&this, event)); - }; - element.add_event_listener(listener) - } - } - } - )*}; -} - -// Inspired by: http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Events -impl_action! { - onclick(event: ClickEvent) -> ClickEvent => |_, event| { event } - ondoubleclick(event: DoubleClickEvent) -> DoubleClickEvent => |_, event| { event } - onkeypress(event: KeyPressEvent) -> KeyPressEvent => |_, event| { event } - onkeydown(event: KeyDownEvent) -> KeyDownEvent => |_, event| { event } - onkeyup(event: KeyUpEvent) -> KeyUpEvent => |_, event| { event } - onmousemove(event: MouseMoveEvent) -> MouseMoveEvent => |_, event| { event } - onmousedown(event: MouseDownEvent) -> MouseDownEvent => |_, event| { event } - onmouseup(event: MouseUpEvent) -> MouseUpEvent => |_, event| { event } - onmouseover(event: MouseOverEvent) -> MouseOverEvent => |_, event| { event } - onmouseout(event: MouseOutEvent) -> MouseOutEvent => |_, event| { event } - onmouseenter(event: MouseEnterEvent) -> MouseEnterEvent => |_, event| { event } - onmouseleave(event: MouseLeaveEvent) -> MouseLeaveEvent => |_, event| { event } - onmousewheel(event: MouseWheelEvent) -> MouseWheelEvent => |_, event| { event } - ongotpointercapture(event: GotPointerCaptureEvent) -> GotPointerCaptureEvent => |_, event| { event } - onlostpointercapture(event: LostPointerCaptureEvent) -> LostPointerCaptureEvent => |_, event| { event } - onpointercancel(event: PointerCancelEvent) -> PointerCancelEvent => |_, event| { event } - onpointerdown(event: PointerDownEvent) -> PointerDownEvent => |_, event| { event } - onpointerenter(event: PointerEnterEvent) -> PointerEnterEvent => |_, event| { event } - onpointerleave(event: PointerLeaveEvent) -> PointerLeaveEvent => |_, event| { event } - onpointermove(event: PointerMoveEvent) -> PointerMoveEvent => |_, event| { event } - onpointerout(event: PointerOutEvent) -> PointerOutEvent => |_, event| { event } - onpointerover(event: PointerOverEvent) -> PointerOverEvent => |_, event| { event } - onpointerup(event: PointerUpEvent) -> PointerUpEvent => |_, event| { event } - onscroll(event: ScrollEvent) -> ScrollEvent => |_, event| { event } - onblur(event: BlurEvent) -> BlurEvent => |_, event| { event } - onfocus(event: FocusEvent) -> FocusEvent => |_, event| { event } - onsubmit(event: SubmitEvent) -> SubmitEvent => |_, event| { event } - ondragstart(event: DragStartEvent) -> DragStartEvent => |_, event| { event } - ondrag(event: DragEvent) -> DragEvent => |_, event| { event } - ondragend(event: DragEndEvent) -> DragEndEvent => |_, event| { event } - ondragenter(event: DragEnterEvent) -> DragEnterEvent => |_, event| { event } - ondragleave(event: DragLeaveEvent) -> DragLeaveEvent => |_, event| { event } - ondragover(event: DragOverEvent) -> DragOverEvent => |_, event| { event } - ondragexit(event: DragExitEvent) -> DragExitEvent => |_, event| { event } - ondrop(event: DragDropEvent) -> DragDropEvent => |_, event| { event } - oncontextmenu(event: ContextMenuEvent) -> ContextMenuEvent => |_, event| { event } - oninput(event: InputEvent) -> InputData => |this: &Element, _| { - use stdweb::web::html_element::{InputElement, TextAreaElement}; - use stdweb::unstable::TryInto; - // Normally only InputElement or TextAreaElement can have an oninput event listener. In - // practice though any element with `contenteditable=true` may generate such events, - // therefore here we fall back to just returning the text content of the node. - // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event. - let v1 = this.clone().try_into().map(|input: InputElement| input.raw_value()).ok(); - let v2 = this.clone().try_into().map(|input: TextAreaElement| input.value()).ok(); - let v3 = this.text_content(); - let value = v1.or(v2).or(v3) - .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); - InputData { value } - } - onchange(event: ChangeEvent) -> ChangeData => |this: &Element, _| { - use stdweb::web::{FileList, IElement}; - use stdweb::web::html_element::{InputElement, TextAreaElement, SelectElement}; - use stdweb::unstable::TryInto; - match this.node_name().as_ref() { - "INPUT" => { - let input: InputElement = this.clone().try_into().unwrap(); - let is_file = input.get_attribute("type").map(|value| { - value.eq_ignore_ascii_case("file") - }) - .unwrap_or(false); - if is_file { - let files: FileList = js!( return @{input}.files; ) - .try_into() - .unwrap(); - ChangeData::Files(files) - } else { - ChangeData::Value(input.raw_value()) - } - } - "TEXTAREA" => { - let tae: TextAreaElement = this.clone().try_into().unwrap(); - ChangeData::Value(tae.value()) - } - "SELECT" => { - let se: SelectElement = this.clone().try_into().unwrap(); - ChangeData::Select(se) - } - _ => { - panic!("only an InputElement, TextAreaElement or SelectElement can have an onchange event listener"); - } - } - } - touchcancel(event: TouchCancel) -> TouchCancel => |_, event| { event } - touchend(event: TouchEnd) -> TouchEnd => |_, event| { event } - touchenter(event: TouchEnter) -> TouchEnter => |_, event| { event } - touchmove(event: TouchMove) -> TouchMove => |_, event| { event } - touchstart(event: TouchStart) -> TouchStart => |_, event| { event } -} - -/// A type representing data from `oninput` event. -#[derive(Debug)] -pub struct InputData { - /// Inserted characters. Contains value from - /// [InputEvent](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/data). - pub value: String, -} - -// There is no '.../Web/API/ChangeEvent/data' (for onchange) similar to -// https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/data (for oninput). -// ChangeData actually contains the value of the InputElement/TextAreaElement -// after `change` event occured or contains the SelectElement (see more at the -// variant ChangeData::Select) - -/// A type representing change of value(s) of an element after committed by user -/// ([onchange event](https://developer.mozilla.org/en-US/docs/Web/Events/change)). -#[derive(Debug)] -pub enum ChangeData { - /// Value of the element in cases of ``, ` + + +

+ { nbsp(self.debugged_payload.as_str()) } +

+ + } + } +} + +fn nbsp(string: T) -> String +where + String: From, +{ + String::from(string).replace(' ', "\u{00a0}") +} + +#[wasm_bindgen] +extern "C" { + fn get_payload() -> String; +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "get_payload_later")] + fn get_payload_later_js(payload_callback: JsValue); +} + +fn get_payload_later(payload_callback: Callback) { + let callback = Closure::once_into_js(move |payload: String| payload_callback.emit(payload)); + get_payload_later_js(callback); +} diff --git a/examples/web_sys/js_callback/src/main.rs b/examples/web_sys/js_callback/src/main.rs new file mode 100644 index 00000000000..db451555d85 --- /dev/null +++ b/examples/web_sys/js_callback/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + yew::start_app::(); +} diff --git a/examples/web_sys/js_callback/static/get-payload-script.js b/examples/web_sys/js_callback/static/get-payload-script.js new file mode 100644 index 00000000000..2735ee185c3 --- /dev/null +++ b/examples/web_sys/js_callback/static/get-payload-script.js @@ -0,0 +1,9 @@ +function get_payload() { + return (new Date()).toString() +} + +function get_payload_later(callback) { + setTimeout(() => { + callback(get_payload()) + }, 1000) +} diff --git a/examples/web_sys/js_callback/static/index.html b/examples/web_sys/js_callback/static/index.html new file mode 100644 index 00000000000..8e47c484fcc --- /dev/null +++ b/examples/web_sys/js_callback/static/index.html @@ -0,0 +1,11 @@ + + + + + (Asynchronous) callback from JavaScript + + + + + + diff --git a/examples/web_sys/mount_point/Cargo.toml b/examples/web_sys/mount_point/Cargo.toml new file mode 100644 index 00000000000..83688ddc4b3 --- /dev/null +++ b/examples/web_sys/mount_point/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mount_point_web_sys" +version = "0.1.0" +authors = ["Ben Berman "] +edition = "2018" + +[dependencies] +wasm-bindgen = "0.2" +yew = { path = "../../..", features = ["web_sys"] } + +[dependencies.web-sys] +version = "0.3" +features = [ + "CanvasRenderingContext2d", + "Document", + "DomTokenList", + "Element", + "HtmlCanvasElement", + "Node", + "Window" +] diff --git a/examples/web_sys/mount_point/src/lib.rs b/examples/web_sys/mount_point/src/lib.rs new file mode 100644 index 00000000000..29c728b1ba3 --- /dev/null +++ b/examples/web_sys/mount_point/src/lib.rs @@ -0,0 +1,42 @@ +use yew::{html, Component, ComponentLink, Html, InputData, ShouldRender}; + +pub struct Model { + link: ComponentLink, + name: String, +} + +pub enum Msg { + UpdateName(String), +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { + link, + name: "Reversed".to_owned(), + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::UpdateName(new_name) => { + self.name = new_name; + } + } + true + } + + fn view(&self) -> Html { + html! { +
+ +

{ self.name.chars().rev().collect::() }

+
+ } + } +} diff --git a/examples/web_sys/mount_point/src/main.rs b/examples/web_sys/mount_point/src/main.rs new file mode 100644 index 00000000000..484d88d4403 --- /dev/null +++ b/examples/web_sys/mount_point/src/main.rs @@ -0,0 +1,31 @@ +use mount_point_web_sys::Model; +use wasm_bindgen::JsValue; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; +use yew::App; + +fn main() { + yew::initialize(); + let document = yew::utils::document(); + let body = document.query_selector("body").unwrap().unwrap(); + + // This canvas won't be overwritten by yew! + let canvas = document.create_element("canvas").unwrap(); + body.append_child(&canvas).unwrap(); + + let canvas = HtmlCanvasElement::from(JsValue::from(canvas)); + canvas.set_width(100); + canvas.set_height(100); + let ctx = + CanvasRenderingContext2d::from(JsValue::from(canvas.get_context("2d").unwrap().unwrap())); + ctx.set_fill_style(&JsValue::from_str("green")); + ctx.fill_rect(10., 10., 50., 50.); + + let mount_class = "mount-point"; + let mount_point = document.create_element("div").unwrap(); + let class_list = mount_point.class_list(); + class_list.add_1(mount_class).unwrap(); + body.append_child(&mount_point).unwrap(); + + App::::new().mount(mount_point); + yew::run_loop(); +} diff --git a/examples/web_sys/multi_thread/Cargo.toml b/examples/web_sys/multi_thread/Cargo.toml new file mode 100644 index 00000000000..dc2db7632b2 --- /dev/null +++ b/examples/web_sys/multi_thread/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "multi_thread_web_sys" +version = "0.1.0" +authors = ["Denis Kolodin "] +edition = "2018" + +[[bin]] +name = "main" +path = "src/bin/main.rs" + +[[bin]] +name = "native_worker" +path = "src/bin/native_worker.rs" + +[dependencies] +log = "0.4" +wasm-logger = "0.2" +serde = "1.0" +serde_derive = "1.0" +yew = { path = "../../..", features = ["web_sys"]} diff --git a/examples/web_sys/multi_thread/README.md b/examples/web_sys/multi_thread/README.md new file mode 100644 index 00000000000..efe100aad0d --- /dev/null +++ b/examples/web_sys/multi_thread/README.md @@ -0,0 +1,7 @@ +### multi_thread + +You should compile a worker which have to be spawned in a separate thread: + +```sh +wasm-pack build --target no-modules --release -- --features web_sys --bin native_worker +``` diff --git a/examples/web_sys/multi_thread/Web.toml b/examples/web_sys/multi_thread/Web.toml new file mode 100644 index 00000000000..813e27393a9 --- /dev/null +++ b/examples/web_sys/multi_thread/Web.toml @@ -0,0 +1 @@ +default-target = "wasm32-unknown-unknown" diff --git a/examples/web_sys/multi_thread/src/bin/main.rs b/examples/web_sys/multi_thread/src/bin/main.rs new file mode 100644 index 00000000000..28e93e8d54a --- /dev/null +++ b/examples/web_sys/multi_thread/src/bin/main.rs @@ -0,0 +1,7 @@ +fn main() { + #[cfg(feature = "std_web")] + web_logger::init(); + #[cfg(feature = "web_sys")] + wasm_logger::init(wasm_logger::Config::default()); + yew::start_app::(); +} diff --git a/examples/web_sys/multi_thread/src/bin/native_worker.rs b/examples/web_sys/multi_thread/src/bin/native_worker.rs new file mode 100644 index 00000000000..f1f40be3413 --- /dev/null +++ b/examples/web_sys/multi_thread/src/bin/native_worker.rs @@ -0,0 +1,11 @@ +use yew::agent::Threaded; + +fn main() { + #[cfg(feature = "std_web")] + web_logger::init(); + #[cfg(feature = "web_sys")] + wasm_logger::init(wasm_logger::Config::default()); + yew::initialize(); + multi_thread::native_worker::Worker::register(); + yew::run_loop(); +} diff --git a/examples/web_sys/multi_thread/src/context.rs b/examples/web_sys/multi_thread/src/context.rs new file mode 100644 index 00000000000..c93a9461cb8 --- /dev/null +++ b/examples/web_sys/multi_thread/src/context.rs @@ -0,0 +1,66 @@ +use log::info; +use serde_derive::{Deserialize, Serialize}; +use std::time::Duration; +use yew::worker::*; +// TODO use yew::services::{IntervalService, FetchService, Task}; +use yew::services::fetch::FetchService; +use yew::services::interval::IntervalService; +use yew::services::Task; + +#[derive(Serialize, Deserialize, Debug)] +pub enum Request { + GetDataFromServer, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + DataFetched, +} + +pub enum Msg { + Updating, +} + +pub struct Worker { + link: AgentLink, + interval: IntervalService, + task: Box, + fetch: FetchService, +} + +impl Agent for Worker { + type Reach = Context; + type Message = Msg; + type Input = Request; + type Output = Response; + + fn create(link: AgentLink) -> Self { + let mut interval = IntervalService::new(); + let duration = Duration::from_secs(3); + let callback = link.callback(|_| Msg::Updating); + let task = interval.spawn(duration, callback); + Worker { + link, + interval, + task: Box::new(task), + fetch: FetchService::new(), + } + } + + fn update(&mut self, msg: Self::Message) { + match msg { + Msg::Updating => { + info!("Tick..."); + } + } + } + + fn handle_input(&mut self, msg: Self::Input, who: HandlerId) { + info!("Request: {:?}", msg); + match msg { + Request::GetDataFromServer => { + self.link.respond(who, Response::DataFetched); + } + } + } +} diff --git a/examples/web_sys/multi_thread/src/job.rs b/examples/web_sys/multi_thread/src/job.rs new file mode 100644 index 00000000000..a9d21aa4144 --- /dev/null +++ b/examples/web_sys/multi_thread/src/job.rs @@ -0,0 +1,66 @@ +use log::info; +use serde_derive::{Deserialize, Serialize}; +use std::time::Duration; +use yew::worker::*; +// TODO use yew::services::{IntervalService, FetchService, Task}; +use yew::services::fetch::FetchService; +use yew::services::interval::IntervalService; +use yew::services::Task; + +#[derive(Serialize, Deserialize, Debug)] +pub enum Request { + GetDataFromServer, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + DataFetched, +} + +pub enum Msg { + Updating, +} + +pub struct Worker { + link: AgentLink, + interval: IntervalService, + task: Box, + fetch: FetchService, +} + +impl Agent for Worker { + type Reach = Job; + type Message = Msg; + type Input = Request; + type Output = Response; + + fn create(link: AgentLink) -> Self { + let mut interval = IntervalService::new(); + let duration = Duration::from_secs(3); + let callback = link.callback(|_| Msg::Updating); + let task = interval.spawn(duration, callback); + Worker { + link, + interval, + task: Box::new(task), + fetch: FetchService::new(), + } + } + + fn update(&mut self, msg: Self::Message) { + match msg { + Msg::Updating => { + info!("Tick..."); + } + } + } + + fn handle_input(&mut self, msg: Self::Input, who: HandlerId) { + info!("Request: {:?}", msg); + match msg { + Request::GetDataFromServer => { + self.link.respond(who, Response::DataFetched); + } + } + } +} diff --git a/examples/web_sys/multi_thread/src/lib.rs b/examples/web_sys/multi_thread/src/lib.rs new file mode 100644 index 00000000000..e86645d0332 --- /dev/null +++ b/examples/web_sys/multi_thread/src/lib.rs @@ -0,0 +1,82 @@ +#![recursion_limit = "128"] + +pub mod context; +pub mod job; +pub mod native_worker; + +use log::info; +use yew::worker::*; +use yew::{html, Component, ComponentLink, Html, ShouldRender}; + +pub struct Model { + link: ComponentLink, + worker: Box>, + job: Box>, + context: Box>, + context_2: Box>, +} + +pub enum Msg { + SendToWorker, + SendToJob, + SendToContext, + DataReceived, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + let callback = link.callback(|_| Msg::DataReceived); + let worker = native_worker::Worker::bridge(callback); + + let callback = link.callback(|_| Msg::DataReceived); + let job = job::Worker::bridge(callback); + + let callback = link.callback(|_| Msg::DataReceived); + let context = context::Worker::bridge(callback); + + let callback = link.callback(|_| Msg::DataReceived); + let context_2 = context::Worker::bridge(callback); + + Model { + link, + worker, + job, + context, + context_2, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::SendToWorker => { + self.worker.send(native_worker::Request::GetDataFromServer); + } + Msg::SendToJob => { + self.job.send(job::Request::GetDataFromServer); + } + Msg::SendToContext => { + self.context.send(context::Request::GetDataFromServer); + self.context_2.send(context::Request::GetDataFromServer); + } + Msg::DataReceived => { + info!("DataReceived"); + } + } + true + } + + fn view(&self) -> Html { + html! { +
+ +
+ } + } +} diff --git a/examples/web_sys/multi_thread/src/native_worker.rs b/examples/web_sys/multi_thread/src/native_worker.rs new file mode 100644 index 00000000000..723d54cec46 --- /dev/null +++ b/examples/web_sys/multi_thread/src/native_worker.rs @@ -0,0 +1,70 @@ +use log::info; +use serde_derive::{Deserialize, Serialize}; +use std::time::Duration; +use yew::worker::*; +// TODO use yew::services::{IntervalService, FetchService, Task}; +use yew::services::fetch::FetchService; +use yew::services::interval::IntervalService; +use yew::services::Task; + +#[derive(Serialize, Deserialize, Debug)] +pub enum Request { + GetDataFromServer, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + DataFetched, +} + +pub enum Msg { + Updating, +} + +pub struct Worker { + link: AgentLink, + interval: IntervalService, + task: Box, + fetch: FetchService, +} + +impl Agent for Worker { + type Reach = Public; + type Message = Msg; + type Input = Request; + type Output = Response; + + fn create(link: AgentLink) -> Self { + let mut interval = IntervalService::new(); + let duration = Duration::from_secs(3); + let callback = link.callback(|_| Msg::Updating); + let task = interval.spawn(duration, callback); + Worker { + link, + interval, + task: Box::new(task), + fetch: FetchService::new(), + } + } + + fn update(&mut self, msg: Self::Message) { + match msg { + Msg::Updating => { + info!("Tick..."); + } + } + } + + fn handle_input(&mut self, msg: Self::Input, who: HandlerId) { + info!("Request: {:?}", msg); + match msg { + Request::GetDataFromServer => { + self.link.respond(who, Response::DataFetched); + } + } + } + + fn name_of_resource() -> &'static str { + "bin/native_worker.js" + } +} diff --git a/examples/web_sys/multi_thread/static/bin b/examples/web_sys/multi_thread/static/bin new file mode 100644 index 00000000000..4a2105e009b --- /dev/null +++ b/examples/web_sys/multi_thread/static/bin @@ -0,0 +1 @@ +../../../target/wasm32-unknown-unknown/release \ No newline at end of file diff --git a/examples/web_sys/multi_thread/static/index.html b/examples/web_sys/multi_thread/static/index.html new file mode 100644 index 00000000000..11845751fc2 --- /dev/null +++ b/examples/web_sys/multi_thread/static/index.html @@ -0,0 +1,11 @@ + + + + + Yew • Multi-Thread + + + + + + diff --git a/examples/web_sys/node_refs/Cargo.toml b/examples/web_sys/node_refs/Cargo.toml new file mode 100644 index 00000000000..e6617be1864 --- /dev/null +++ b/examples/web_sys/node_refs/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "node_refs_web_sys" +version = "0.1.0" +authors = ["Justin Starry "] +edition = "2018" + +[dependencies] +yew = { path = "../../..", features = ["web_sys"] } +web-sys = { version = "0.3", features = ["HtmlElement", "HtmlInputElement", "Node"] } diff --git a/examples/web_sys/node_refs/src/input.rs b/examples/web_sys/node_refs/src/input.rs new file mode 100644 index 00000000000..e26f27c4ad4 --- /dev/null +++ b/examples/web_sys/node_refs/src/input.rs @@ -0,0 +1,43 @@ +use yew::prelude::*; + +pub struct InputComponent { + props: Props, + link: ComponentLink, +} + +#[derive(Clone, Properties)] +pub struct Props { + #[props(required)] + pub on_hover: Callback<()>, +} + +pub enum Msg { + Hover, +} + +impl Component for InputComponent { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + InputComponent { props, link } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Hover => { + self.props.on_hover.emit(()); + } + } + false + } + + fn view(&self) -> Html { + html! { + + } + } +} diff --git a/examples/web_sys/node_refs/src/lib.rs b/examples/web_sys/node_refs/src/lib.rs new file mode 100644 index 00000000000..472f52c0fa8 --- /dev/null +++ b/examples/web_sys/node_refs/src/lib.rs @@ -0,0 +1,74 @@ +#![recursion_limit = "256"] + +mod input; + +use input::InputComponent; +use web_sys::HtmlInputElement as InputElement; +use yew::prelude::*; + +pub struct Model { + link: ComponentLink, + refs: Vec, + focus_index: usize, +} + +pub enum Msg { + HoverIndex(usize), +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { + link, + focus_index: 0, + refs: vec![NodeRef::default(), NodeRef::default()], + } + } + + fn mounted(&mut self) -> ShouldRender { + if let Some(input) = self.refs[self.focus_index].try_into::() { + input.focus().unwrap(); + } + false + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::HoverIndex(index) => self.focus_index = index, + } + if let Some(input) = self.refs[self.focus_index].try_into::() { + input.focus().unwrap(); + } + true + } + + fn view(&self) -> Html { + html! { +
+

{ "Node Refs Demo" }

+

{ "Refs can be used to access and manipulate DOM elements directly" }

+
    +
  • { "First input will focus on mount" }
  • +
  • { "Each input will focus on hover" }
  • +
+
+ + +
+
+ + +
+
+ } + } +} diff --git a/examples/web_sys/node_refs/src/main.rs b/examples/web_sys/node_refs/src/main.rs new file mode 100644 index 00000000000..f771d6d5dad --- /dev/null +++ b/examples/web_sys/node_refs/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + yew::start_app::(); +} diff --git a/examples/web_sys/npm_and_rest/Cargo.toml b/examples/web_sys/npm_and_rest/Cargo.toml new file mode 100644 index 00000000000..7b44cfe7131 --- /dev/null +++ b/examples/web_sys/npm_and_rest/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "npm_and_rest_web_sys" +version = "0.1.0" +authors = ["Denis Kolodin "] +edition = "2018" + +[dependencies] +anyhow = "1" +js-sys = "0.3" +serde = "1" +serde_derive = "1" +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["console"] } +yew = { path = "../../..", features = ["web_sys"] } diff --git a/examples/web_sys/npm_and_rest/src/ccxt.rs b/examples/web_sys/npm_and_rest/src/ccxt.rs new file mode 100644 index 00000000000..ae3f9de2b32 --- /dev/null +++ b/examples/web_sys/npm_and_rest/src/ccxt.rs @@ -0,0 +1,34 @@ +use js_sys::{Array, Reflect}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use web_sys::console; + +#[derive(Default)] +pub struct CcxtService(Option<&'static JsValue>); + +#[wasm_bindgen] +extern "C" { + static ccxt: JsValue; +} + +impl CcxtService { + pub fn new() -> Self { + let lib: &JsValue = &ccxt; + CcxtService(Some(lib)) + } + + pub fn exchanges(&mut self) -> Vec { + let lib = self.0.as_ref().expect("ccxt library object lost"); + let v = { + let exchanges = Reflect::get(lib, &JsValue::from_str("exchanges")).unwrap(); + console::log_1(&exchanges); + exchanges + }; + let v: Vec = Array::from(&v) + .to_vec() + .into_iter() + .map(|v| v.as_string().expect("can't extract exchanges")) + .collect(); + v + } +} diff --git a/examples/web_sys/npm_and_rest/src/gravatar.rs b/examples/web_sys/npm_and_rest/src/gravatar.rs new file mode 100644 index 00000000000..845a9d71785 --- /dev/null +++ b/examples/web_sys/npm_and_rest/src/gravatar.rs @@ -0,0 +1,51 @@ +use anyhow::{anyhow, Error}; +use serde_derive::Deserialize; +use yew::callback::Callback; +use yew::format::{Json, Nothing}; +use yew::services::fetch::{FetchService, FetchTask, Request, Response}; + +#[derive(Deserialize, Debug)] +pub struct Profile { + entry: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Entry { + id: String, + hash: String, + request_hash: String, + profile_url: String, + preferred_username: String, +} + +#[derive(Default)] +pub struct GravatarService { + web: FetchService, +} + +impl GravatarService { + pub fn new() -> Self { + Self { + web: FetchService::new(), + } + } + + pub fn profile(&mut self, hash: &str, callback: Callback>) -> FetchTask { + let url = format!("https://en.gravatar.com/{}.json", hash); + let handler = move |response: Response>>| { + let (meta, Json(data)) = response.into_parts(); + if meta.status.is_success() { + callback.emit(data) + } else { + // format_err! is a macro in crate `failure` + callback.emit(Err(anyhow!( + "{}: error getting profile https://gravatar.com/", + meta.status + ))) + } + }; + let request = Request::get(url.as_str()).body(Nothing).unwrap(); + self.web.fetch(request, handler.into()).unwrap() + } +} diff --git a/examples/web_sys/npm_and_rest/src/lib.rs b/examples/web_sys/npm_and_rest/src/lib.rs new file mode 100644 index 00000000000..97b6a16c116 --- /dev/null +++ b/examples/web_sys/npm_and_rest/src/lib.rs @@ -0,0 +1,83 @@ +#![recursion_limit = "128"] + +// Own services implementation +pub mod ccxt; +pub mod gravatar; + +use anyhow::Error; +use yew::services::fetch::FetchTask; +use yew::{html, Callback, Component, ComponentLink, Html, ShouldRender}; + +use ccxt::CcxtService; +use gravatar::{GravatarService, Profile}; + +pub struct Model { + link: ComponentLink, + gravatar: GravatarService, + ccxt: CcxtService, + callback: Callback>, + profile: Option, + exchanges: Vec, + task: Option, +} + +pub enum Msg { + Gravatar, + GravatarReady(Result), + Exchanges, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { + link: link.clone(), + gravatar: GravatarService::new(), + ccxt: CcxtService::new(), + callback: link.callback(Msg::GravatarReady), + profile: None, + exchanges: Vec::new(), + task: None, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Gravatar => { + let task = self + .gravatar + .profile("205e460b479e2e5b48aec07710c08d50", self.callback.clone()); + self.task = Some(task); + } + Msg::GravatarReady(Ok(profile)) => { + self.profile = Some(profile); + } + Msg::GravatarReady(Err(_)) => { + // Can't load gravatar profile + } + Msg::Exchanges => { + self.exchanges = self.ccxt.exchanges(); + } + } + true + } + + fn view(&self) -> Html { + let view_exchange = |exchange| { + html! { +
  • { exchange }
  • + } + }; + html! { +
    + + +
      + { for self.exchanges.iter().map(view_exchange) } +
    +
    + } + } +} diff --git a/examples/web_sys/npm_and_rest/src/main.rs b/examples/web_sys/npm_and_rest/src/main.rs new file mode 100644 index 00000000000..d9c7a9efb69 --- /dev/null +++ b/examples/web_sys/npm_and_rest/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + yew::start_app::(); +} diff --git a/examples/web_sys/npm_and_rest/static/index.html b/examples/web_sys/npm_and_rest/static/index.html new file mode 100644 index 00000000000..a0e34651c2b --- /dev/null +++ b/examples/web_sys/npm_and_rest/static/index.html @@ -0,0 +1,12 @@ + + + + + Yew • npm and REST + + + + + + + diff --git a/examples/web_sys/todomvc/Cargo.toml b/examples/web_sys/todomvc/Cargo.toml new file mode 100644 index 00000000000..0af8a7727ba --- /dev/null +++ b/examples/web_sys/todomvc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "todomvc_web_sys" +version = "0.1.0" +authors = ["Denis Kolodin "] +edition = "2018" + +[dependencies] +strum = "0.13" +strum_macros = "0.13" +serde = "1" +serde_derive = "1" +yew = { path = "../../..", features = ["web_sys"] } diff --git a/examples/web_sys/todomvc/README.md b/examples/web_sys/todomvc/README.md new file mode 100644 index 00000000000..5249541a236 --- /dev/null +++ b/examples/web_sys/todomvc/README.md @@ -0,0 +1,6 @@ +## Yew TodoMVC Demo + +This it an implementationt of [TodoMVC](http://todomvc.com/) app. + +Unlike other implementations, this stores the full state of the model, +including: all entries, entered text and chosen filter. diff --git a/examples/web_sys/todomvc/src/lib.rs b/examples/web_sys/todomvc/src/lib.rs new file mode 100644 index 00000000000..fb0760ab248 --- /dev/null +++ b/examples/web_sys/todomvc/src/lib.rs @@ -0,0 +1,358 @@ +#![recursion_limit = "512"] + +use serde_derive::{Deserialize, Serialize}; +use strum::IntoEnumIterator; +use strum_macros::{EnumIter, ToString}; +use yew::events::KeyboardEvent; +use yew::format::Json; +use yew::services::storage::{Area, StorageService}; +use yew::{html, Component, ComponentLink, Href, Html, InputData, ShouldRender}; + +const KEY: &'static str = "yew.todomvc.self"; + +pub struct Model { + link: ComponentLink, + storage: StorageService, + state: State, +} + +#[derive(Serialize, Deserialize)] +pub struct State { + entries: Vec, + filter: Filter, + value: String, + edit_value: String, +} + +#[derive(Serialize, Deserialize)] +struct Entry { + description: String, + completed: bool, + editing: bool, +} + +pub enum Msg { + Add, + Edit(usize), + Update(String), + UpdateEdit(String), + Remove(usize), + SetFilter(Filter), + ToggleAll, + ToggleEdit(usize), + Toggle(usize), + ClearCompleted, + Nope, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + let storage = StorageService::new(Area::Local).expect("storage was disabled by the user"); + let entries = { + if let Json(Ok(restored_model)) = storage.restore(KEY) { + restored_model + } else { + Vec::new() + } + }; + let state = State { + entries, + filter: Filter::All, + value: "".into(), + edit_value: "".into(), + }; + Model { + link, + storage, + state, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Add => { + let entry = Entry { + description: self.state.value.clone(), + completed: false, + editing: false, + }; + self.state.entries.push(entry); + self.state.value = "".to_string(); + } + Msg::Edit(idx) => { + let edit_value = self.state.edit_value.clone(); + self.state.complete_edit(idx, edit_value); + self.state.edit_value = "".to_string(); + } + Msg::Update(val) => { + println!("Input: {}", val); + self.state.value = val; + } + Msg::UpdateEdit(val) => { + println!("Input: {}", val); + self.state.edit_value = val; + } + Msg::Remove(idx) => { + self.state.remove(idx); + } + Msg::SetFilter(filter) => { + self.state.filter = filter; + } + Msg::ToggleEdit(idx) => { + self.state.edit_value = self.state.entries[idx].description.clone(); + self.state.toggle_edit(idx); + } + Msg::ToggleAll => { + let status = !self.state.is_all_completed(); + self.state.toggle_all(status); + } + Msg::Toggle(idx) => { + self.state.toggle(idx); + } + Msg::ClearCompleted => { + self.state.clear_completed(); + } + Msg::Nope => {} + } + self.storage.store(KEY, Json(&self.state.entries)); + true + } + + fn view(&self) -> Html { + html! { +
    +
    +
    +

    { "todos" }

    + { self.view_input() } +
    +
    + +
      + { for self.state.entries.iter().filter(|e| self.state.filter.fit(e)).enumerate().map(|e| self.view_entry(e)) } +
    +
    +
    + + { self.state.total() } + { " item(s) left" } + +
      + { for Filter::iter().map(|flt| self.view_filter(flt)) } +
    + +
    +
    + +
    + } + } +} + +impl Model { + fn view_filter(&self, filter: Filter) -> Html { + let flt = filter.clone(); + html! { +
  • + + { filter } + +
  • + } + } + + fn view_input(&self) -> Html { + html! { + // You can use standard Rust comments. One line: + //
  • + + /* Or multiline: +
      +
    • +
    + */ + } + } + + fn view_entry(&self, (idx, entry): (usize, &Entry)) -> Html { + let mut class = "todo".to_string(); + if entry.editing { + class.push_str(" editing"); + } + if entry.completed { + class.push_str(" completed"); + } + html! { +
  • +
    + + +
    + { self.view_entry_edit_input((idx, &entry)) } +
  • + } + } + + fn view_entry_edit_input(&self, (idx, entry): (usize, &Entry)) -> Html { + if entry.editing { + html! { + + } + } else { + html! { } + } + } +} + +#[derive(EnumIter, ToString, Clone, PartialEq, Serialize, Deserialize)] +pub enum Filter { + All, + Active, + Completed, +} + +impl<'a> Into for &'a Filter { + fn into(self) -> Href { + match *self { + Filter::All => "#/".into(), + Filter::Active => "#/active".into(), + Filter::Completed => "#/completed".into(), + } + } +} + +impl Filter { + fn fit(&self, entry: &Entry) -> bool { + match *self { + Filter::All => true, + Filter::Active => !entry.completed, + Filter::Completed => entry.completed, + } + } +} + +impl State { + fn total(&self) -> usize { + self.entries.len() + } + + fn total_completed(&self) -> usize { + self.entries + .iter() + .filter(|e| Filter::Completed.fit(e)) + .count() + } + + fn is_all_completed(&self) -> bool { + let mut filtered_iter = self + .entries + .iter() + .filter(|e| self.filter.fit(e)) + .peekable(); + + if filtered_iter.peek().is_none() { + return false; + } + + filtered_iter.all(|e| e.completed) + } + + fn toggle_all(&mut self, value: bool) { + for entry in self.entries.iter_mut() { + if self.filter.fit(entry) { + entry.completed = value; + } + } + } + + fn clear_completed(&mut self) { + let entries = self + .entries + .drain(..) + .filter(|e| Filter::Active.fit(e)) + .collect(); + self.entries = entries; + } + + fn toggle(&mut self, idx: usize) { + let filter = self.filter.clone(); + let mut entries = self + .entries + .iter_mut() + .filter(|e| filter.fit(e)) + .collect::>(); + let entry = entries.get_mut(idx).unwrap(); + entry.completed = !entry.completed; + } + + fn toggle_edit(&mut self, idx: usize) { + let filter = self.filter.clone(); + let mut entries = self + .entries + .iter_mut() + .filter(|e| filter.fit(e)) + .collect::>(); + let entry = entries.get_mut(idx).unwrap(); + entry.editing = !entry.editing; + } + + fn complete_edit(&mut self, idx: usize, val: String) { + let filter = self.filter.clone(); + let mut entries = self + .entries + .iter_mut() + .filter(|e| filter.fit(e)) + .collect::>(); + let entry = entries.get_mut(idx).unwrap(); + entry.description = val; + entry.editing = !entry.editing; + } + + fn remove(&mut self, idx: usize) { + let idx = { + let filter = self.filter.clone(); + let entries = self + .entries + .iter() + .enumerate() + .filter(|&(_, e)| filter.fit(e)) + .collect::>(); + let &(idx, _) = entries.get(idx).unwrap(); + idx + }; + self.entries.remove(idx); + } +} diff --git a/examples/web_sys/todomvc/src/main.rs b/examples/web_sys/todomvc/src/main.rs new file mode 100644 index 00000000000..ba7deffbd24 --- /dev/null +++ b/examples/web_sys/todomvc/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + yew::start_app::(); +} diff --git a/examples/web_sys/todomvc/static/index.html b/examples/web_sys/todomvc/static/index.html new file mode 100644 index 00000000000..4f2cf27d37f --- /dev/null +++ b/examples/web_sys/todomvc/static/index.html @@ -0,0 +1,13 @@ + + + + + Yew • TodoMVC + + + + + + + + diff --git a/examples/npm_and_rest/Cargo.toml b/examples/web_sys/two_apps/Cargo.toml similarity index 59% rename from examples/npm_and_rest/Cargo.toml rename to examples/web_sys/two_apps/Cargo.toml index 146b3b7a753..36ff90bd306 100644 --- a/examples/npm_and_rest/Cargo.toml +++ b/examples/web_sys/two_apps/Cargo.toml @@ -1,12 +1,9 @@ [package] -name = "npm_and_rest" +name = "two_apps_web_sys" version = "0.1.0" authors = ["Denis Kolodin "] edition = "2018" [dependencies] -anyhow = "1" -serde = "1" -serde_derive = "1" stdweb = "0.4.20" -yew = { path = "../.." } +yew = { path = "../../..", features = ["web_sys"] } diff --git a/examples/web_sys/two_apps/src/lib.rs b/examples/web_sys/two_apps/src/lib.rs new file mode 100644 index 00000000000..232ad785fd4 --- /dev/null +++ b/examples/web_sys/two_apps/src/lib.rs @@ -0,0 +1,83 @@ +#![recursion_limit = "256"] + +use yew::html::Scope; +/// This example demonstrates low-level usage of scopes. +use yew::{html, Component, ComponentLink, Html, ShouldRender}; + +pub struct Model { + link: ComponentLink, + scope: Option>, + selector: &'static str, + title: String, +} + +pub enum Msg { + SetScope(Scope), + SendToOpposite(String), + SetTitle(String), +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + Model { + link, + scope: None, + selector: "", + title: "Nothing".into(), + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::SetScope(scope) => { + self.scope = Some(scope); + } + Msg::SendToOpposite(title) => { + self.scope + .as_mut() + .unwrap() + .send_message(Msg::SetTitle(title)); + } + Msg::SetTitle(title) => { + match title.as_ref() { + "Ping" => { + self.scope + .as_mut() + .unwrap() + .send_message(Msg::SetTitle("Pong".into())); + } + "Pong" => { + self.scope + .as_mut() + .unwrap() + .send_message(Msg::SetTitle("Pong Done".into())); + } + "Pong Done" => { + self.scope + .as_mut() + .unwrap() + .send_message(Msg::SetTitle("Ping Done".into())); + } + _ => {} + } + self.title = title; + } + } + true + } + + fn view(&self) -> Html { + html! { +
    +

    { format!("{} received <{}>", self.selector, self.title) }

    + + + + +
    + } + } +} diff --git a/examples/two_apps/src/main.rs b/examples/web_sys/two_apps/src/main.rs similarity index 59% rename from examples/two_apps/src/main.rs rename to examples/web_sys/two_apps/src/main.rs index c306990c97b..a0f3cc8018c 100644 --- a/examples/two_apps/src/main.rs +++ b/examples/web_sys/two_apps/src/main.rs @@ -1,10 +1,10 @@ -use stdweb::web::{document, IParentNode}; -use two_apps::{Model, Msg}; +use two_apps_web_sys::{Model, Msg}; use yew::html::Scope; use yew::App; fn mount_app(selector: &'static str, app: App) -> Scope { - let element = document().query_selector(selector).unwrap().unwrap(); + let document = yew::utils::document(); + let element = document.query_selector(selector).unwrap().unwrap(); app.mount(element) } @@ -12,8 +12,8 @@ fn main() { yew::initialize(); let first_app = App::new(); let second_app = App::new(); - let mut to_first = mount_app(".first-app", first_app); - let mut to_second = mount_app(".second-app", second_app); + let to_first = mount_app(".first-app", first_app); + let to_second = mount_app(".second-app", second_app); to_first.send_message(Msg::SetScope(to_second.clone())); to_second.send_message(Msg::SetScope(to_first.clone())); yew::run_loop(); diff --git a/examples/web_sys/two_apps/static/index.html b/examples/web_sys/two_apps/static/index.html new file mode 100644 index 00000000000..c01b03cda9c --- /dev/null +++ b/examples/web_sys/two_apps/static/index.html @@ -0,0 +1,14 @@ + + + + + Yew • Two Apps + + + +
    +
    + + + + diff --git a/tests/vtag_test.rs b/tests/vtag_test.rs index 8b5daf35ce2..96c75d4d30b 100644 --- a/tests/vtag_test.rs +++ b/tests/vtag_test.rs @@ -1,5 +1,4 @@ #![recursion_limit = "128"] - #[cfg(feature = "std_web")] use stdweb::web::{document, IElement}; #[cfg(feature = "wasm_test")] From 0ed024144bd9c0ad7ed7c00cafc999b0f7287d2e Mon Sep 17 00:00:00 2001 From: daxpedda Date: Wed, 29 Jan 2020 15:19:30 +0100 Subject: [PATCH 16/28] Fix spawning workers in combination with `wasm-bindgen`. (#901) --- Cargo.toml | 2 ++ examples/web_sys/multi_thread/Cargo.toml | 2 +- examples/web_sys/multi_thread/src/bin/main.rs | 5 +--- .../multi_thread/src/bin/native_worker.rs | 5 +--- src/agent.rs | 26 ++++++++++++++++--- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3d5df1598d8..dcbd78fc8c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ features = [ "AbortSignal", "BinaryType", "Blob", + "BlobPropertyBag", "console", "DedicatedWorkerGlobalScope", "Document", @@ -91,6 +92,7 @@ features = [ "Text", "TouchEvent", "UiEvent", + "Url", "WebSocket", "WheelEvent", "Window", diff --git a/examples/web_sys/multi_thread/Cargo.toml b/examples/web_sys/multi_thread/Cargo.toml index dc2db7632b2..37fe4483317 100644 --- a/examples/web_sys/multi_thread/Cargo.toml +++ b/examples/web_sys/multi_thread/Cargo.toml @@ -17,4 +17,4 @@ log = "0.4" wasm-logger = "0.2" serde = "1.0" serde_derive = "1.0" -yew = { path = "../../..", features = ["web_sys"]} +yew = { path = "../../..", features = ["agent", "services", "web_sys"]} diff --git a/examples/web_sys/multi_thread/src/bin/main.rs b/examples/web_sys/multi_thread/src/bin/main.rs index 28e93e8d54a..3cf99c96aeb 100644 --- a/examples/web_sys/multi_thread/src/bin/main.rs +++ b/examples/web_sys/multi_thread/src/bin/main.rs @@ -1,7 +1,4 @@ fn main() { - #[cfg(feature = "std_web")] - web_logger::init(); - #[cfg(feature = "web_sys")] wasm_logger::init(wasm_logger::Config::default()); - yew::start_app::(); + yew::start_app::(); } diff --git a/examples/web_sys/multi_thread/src/bin/native_worker.rs b/examples/web_sys/multi_thread/src/bin/native_worker.rs index f1f40be3413..80f6ddf266e 100644 --- a/examples/web_sys/multi_thread/src/bin/native_worker.rs +++ b/examples/web_sys/multi_thread/src/bin/native_worker.rs @@ -1,11 +1,8 @@ use yew::agent::Threaded; fn main() { - #[cfg(feature = "std_web")] - web_logger::init(); - #[cfg(feature = "web_sys")] wasm_logger::init(wasm_logger::Config::default()); yew::initialize(); - multi_thread::native_worker::Worker::register(); + multi_thread_web_sys::native_worker::Worker::register(); yew::run_loop(); } diff --git a/src/agent.rs b/src/agent.rs index c827708b0f1..c40b38b1288 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -22,9 +22,10 @@ cfg_if! { #[allow(unused_imports)] use stdweb::{_js_impl, js}; } else if #[cfg(feature = "web_sys")] { - use js_sys::{Reflect, Uint8Array}; + use crate::utils; + use js_sys::{Array, Reflect, Uint8Array}; use wasm_bindgen::{closure::Closure, JsCast, JsValue}; - use web_sys::{DedicatedWorkerGlobalScope, MessageEvent, Worker, WorkerOptions}; + use web_sys::{Blob, BlobPropertyBag, DedicatedWorkerGlobalScope, MessageEvent, Url, Worker, WorkerOptions}; } } @@ -983,6 +984,23 @@ where #[cfg(feature = "web_sys")] fn worker_new(name_of_resource: &str, is_module: bool) -> Worker { + let href = utils::document().location().unwrap().href().unwrap(); + + let array = Array::new(); + array.push( + &format!( + "importScripts(\"{}{}\");onmessage=e=>{{wasm_bindgen(e.data)}}", + href, name_of_resource, + ) + .into(), + ); + let blob = Blob::new_with_str_sequence_and_options( + &array, + BlobPropertyBag::new().type_("application/javascript"), + ) + .unwrap(); + let url = Url::create_object_url_with_blob(&blob).unwrap(); + if is_module { let options = WorkerOptions::new(); Reflect::set( @@ -991,9 +1009,9 @@ fn worker_new(name_of_resource: &str, is_module: bool) -> Worker { &JsValue::from_str("module"), ) .unwrap(); - Worker::new_with_options(name_of_resource, &options).expect("failed to spawn worker") + Worker::new_with_options(&url, &options).expect("failed to spawn worker") } else { - Worker::new(name_of_resource).expect("failed to spawn worker") + Worker::new(&url).expect("failed to spawn worker") } } From ecf79b24a2191648dd85b199390788712a64c381 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sun, 2 Feb 2020 23:12:45 +0800 Subject: [PATCH 17/28] Fix component rendering process (#913) * wip * Fix component rendering process --- src/agent.rs | 2 +- src/html/mod.rs | 18 +++++++++++--- src/html/scope.rs | 25 ++++++++++++------- src/scheduler.rs | 54 ++++++++++++++++++++++++---------------- src/virtual_dom/vcomp.rs | 24 ++++++------------ 5 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index c40b38b1288..54707096d47 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -813,7 +813,7 @@ impl AgentScope { update, }; let runnable: Box = Box::new(envelope); - scheduler().put_and_try_run(runnable); + scheduler().push(runnable); } } diff --git a/src/html/mod.rs b/src/html/mod.rs index 9fa8e43330f..d79aa031fdb 100644 --- a/src/html/mod.rs +++ b/src/html/mod.rs @@ -315,12 +315,19 @@ where /// } /// } #[derive(PartialEq, Debug, Default, Clone)] -pub struct NodeRef(Rc>>); +pub struct NodeRef(Rc>); + +#[derive(PartialEq, Debug, Default, Clone)] +struct NodeRefInner { + node: Option, + link: Option, +} impl NodeRef { /// Get the wrapped Node reference if it exists pub fn get(&self) -> Option { - self.0.borrow().clone() + let inner = self.0.borrow(); + inner.node.clone().or_else(|| inner.link.as_ref()?.get()) } /// Try converting the node reference into another form @@ -339,7 +346,12 @@ impl NodeRef { /// Place a Node in a reference for later use pub(crate) fn set(&self, node: Option) { - *self.0.borrow_mut() = node; + self.0.borrow_mut().node = node; + } + + /// Link a downstream `NodeRef` + pub(crate) fn link(&self, node_ref: Self) { + self.0.borrow_mut().link = Some(node_ref); } } diff --git a/src/html/scope.rs b/src/html/scope.rs index 45952daea7e..af9a319961e 100644 --- a/src/html/scope.rs +++ b/src/html/scope.rs @@ -84,15 +84,15 @@ impl Scope { /// Schedules a task to call the mounted method on a component and optionally re-render pub(crate) fn mounted(&mut self) { let shared_state = self.shared_state.clone(); - let mounted = Box::new(MountedComponent { shared_state }); - scheduler().put_and_try_run(mounted); + let mounted = MountedComponent { shared_state }; + scheduler().push_mount(Box::new(mounted)); } /// Schedules a task to create and render a component and then mount it to the DOM pub(crate) fn create(&mut self) { let shared_state = self.shared_state.clone(); let create = CreateComponent { shared_state }; - scheduler().put_and_try_run(Box::new(create)); + scheduler().push_create(Box::new(create)); } /// Schedules a task to send a message or new props to a component @@ -101,14 +101,14 @@ impl Scope { shared_state: self.shared_state.clone(), update, }; - scheduler().put_and_try_run(Box::new(update)); + scheduler().push(Box::new(update)); } /// Schedules a task to destroy a component pub(crate) fn destroy(&mut self) { let shared_state = self.shared_state.clone(); let destroy = DestroyComponent { shared_state }; - scheduler().put_and_try_run(Box::new(destroy)); + scheduler().push(Box::new(destroy)); } /// Send a message to the component @@ -180,10 +180,17 @@ impl CreatedState { } fn update(mut self) -> Self { - let mut vnode = self.component.render(); - let node = vnode.apply(&self.element, None, self.last_frame); - self.node_ref.set(node); - self.last_frame = Some(vnode); + let mut root = self.component.render(); + if let Some(node) = root.apply(&self.element, None, self.last_frame) { + self.node_ref.set(Some(node)); + } else if let VNode::VComp(child) = &root { + // If the root VNode is a VComp, we won't have access to the rendered DOM node + // because components render asynchronously. In order to bubble up the DOM node + // from the VComp, we need to link the currently rendering component with its + // root child component. + self.node_ref.link(child.node_ref.clone()); + } + self.last_frame = Some(root); self } } diff --git a/src/scheduler.rs b/src/scheduler.rs index a6c3d3129bd..779ddd65b45 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -3,7 +3,6 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; pub(crate) type Shared = Rc>; @@ -23,44 +22,57 @@ pub(crate) trait Runnable { } /// This is a global scheduler suitable to schedule and run any tasks. +#[derive(Clone)] pub(crate) struct Scheduler { - lock: Rc, - sequence: Shared>>, -} - -impl Clone for Scheduler { - fn clone(&self) -> Self { - Scheduler { - lock: self.lock.clone(), - sequence: self.sequence.clone(), - } - } + lock: Rc>, + main: Shared>>, + create_component: Shared>>, + mount_component: Shared>>, } impl Scheduler { - /// Creates a new scheduler with a context. fn new() -> Self { - let sequence = VecDeque::new(); Scheduler { - lock: Rc::new(AtomicBool::new(false)), - sequence: Rc::new(RefCell::new(sequence)), + lock: Rc::new(RefCell::new(())), + main: Rc::new(RefCell::new(VecDeque::new())), + create_component: Rc::new(RefCell::new(VecDeque::new())), + mount_component: Rc::new(RefCell::new(Vec::new())), } } - pub(crate) fn put_and_try_run(&self, runnable: Box) { - self.sequence.borrow_mut().push_back(runnable); - if self.lock.compare_and_swap(false, true, Ordering::Relaxed) { + pub(crate) fn push(&self, runnable: Box) { + self.main.borrow_mut().push_back(runnable); + self.start(); + } + + pub(crate) fn push_create(&self, runnable: Box) { + self.create_component.borrow_mut().push_back(runnable); + self.start(); + } + + pub(crate) fn push_mount(&self, runnable: Box) { + self.mount_component.borrow_mut().push(runnable); + self.start(); + } + + pub(crate) fn start(&self) { + let lock = self.lock.try_borrow_mut(); + if lock.is_err() { return; } loop { - let do_next = self.sequence.borrow_mut().pop_front(); + let do_next = self + .create_component + .borrow_mut() + .pop_front() + .or_else(|| self.mount_component.borrow_mut().pop()) + .or_else(|| self.main.borrow_mut().pop_front()); if let Some(runnable) = do_next { runnable.run(); } else { break; } } - self.lock.store(false, Ordering::Relaxed); } } diff --git a/src/virtual_dom/vcomp.rs b/src/virtual_dom/vcomp.rs index 36c03be4ad5..8cdc3924b0b 100644 --- a/src/virtual_dom/vcomp.rs +++ b/src/virtual_dom/vcomp.rs @@ -26,18 +26,11 @@ enum GeneratorType { } /// A virtual component. +#[derive(Clone)] pub struct VComp { type_id: TypeId, state: Rc>, -} - -impl Clone for VComp { - fn clone(&self) -> Self { - VComp { - type_id: self.type_id, - state: self.state.clone(), - } - } + pub(crate) node_ref: NodeRef, } /// A virtual child component. @@ -100,6 +93,7 @@ impl VComp { where COMP: Component, { + let node_ref_clone = node_ref.clone(); let generator = move |generator_type: GeneratorType| -> Mounted { match generator_type { GeneratorType::Mount(element, dummy_node) => { @@ -108,12 +102,12 @@ impl VComp { let mut scope = scope.mount_in_place( element, Some(VNode::VRef(dummy_node.into())), - node_ref.clone(), + node_ref_clone.clone(), props.clone(), ); Mounted { - node_ref: node_ref.clone(), + node_ref: node_ref_clone.clone(), scope: scope.clone().into(), destroyer: Box::new(move || scope.destroy()), } @@ -123,7 +117,7 @@ impl VComp { scope.update(ComponentUpdate::Properties(props.clone())); Mounted { - node_ref: node_ref.clone(), + node_ref: node_ref_clone.clone(), scope: scope.clone().into(), destroyer: Box::new(move || scope.destroy()), } @@ -136,6 +130,7 @@ impl VComp { state: Rc::new(RefCell::new(MountState::Unmounted(Unmounted { generator: Box::new(generator), }))), + node_ref, } } } @@ -236,16 +231,13 @@ impl VDiff for VComp { this.mount(parent.to_owned(), dummy_node) } }; - - let node = mounted.node_ref.get(); self.state.replace(MountState::Mounted(mounted)); - node } state => { self.state.replace(state); - None } } + None } } From 9e603cece63bb6f4e3d5f98c08e98f91f3f518bd Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 29 Jan 2020 15:18:01 +0100 Subject: [PATCH 18/28] Simplify yew-macro a bit (#902) * yew-macro: Simplify Properties validation * Fix most clippy warnings --- crates/macro/Cargo.toml | 1 - crates/macro/build.rs | 7 ---- crates/macro/src/derive_props/builder.rs | 7 +--- crates/macro/src/html_tree/html_component.rs | 39 ++++++-------------- crates/macro/src/html_tree/html_iterable.rs | 2 +- crates/macro/src/html_tree/html_prop.rs | 2 +- 6 files changed, 15 insertions(+), 43 deletions(-) delete mode 100644 crates/macro/build.rs diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index b24411a0b5c..d5b5e81d6b7 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -32,7 +32,6 @@ trybuild = "1.0" yew = { path = "../..", features = ["std_web"] } [build-dependencies] -autocfg = "1.0.0" [features] doc_test = [] diff --git a/crates/macro/build.rs b/crates/macro/build.rs deleted file mode 100644 index a5de3dc6e7c..00000000000 --- a/crates/macro/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -extern crate autocfg; - -pub fn main() { - if autocfg::new().probe_rustc_version(1, 36) { - println!("cargo:rustc-cfg=has_maybe_uninit"); - } -} diff --git a/crates/macro/src/derive_props/builder.rs b/crates/macro/src/derive_props/builder.rs index d17ae1ca343..dd28914d194 100644 --- a/crates/macro/src/derive_props/builder.rs +++ b/crates/macro/src/derive_props/builder.rs @@ -46,11 +46,8 @@ impl ToTokens for PropsBuilder<'_> { // Each builder step implements the `BuilderStep` trait and `step_generics` is used to // enforce that. let step_generic_param = Ident::new("YEW_PROPS_BUILDER_STEP", Span::call_site()); - let step_generics = with_param_bounds( - &generics, - step_generic_param.clone(), - step_trait.clone().to_owned(), - ); + let step_generics = + with_param_bounds(&generics, step_generic_param.clone(), (*step_trait).clone()); let builder = quote! { #( diff --git a/crates/macro/src/html_tree/html_component.rs b/crates/macro/src/html_tree/html_component.rs index ff4f53b1cce..7893c93aab2 100644 --- a/crates/macro/src/html_tree/html_component.rs +++ b/crates/macro/src/html_tree/html_component.rs @@ -88,38 +88,21 @@ impl ToTokens for HtmlComponent { } = self; let validate_props = if let Props::List(ListProps { props, .. }) = props { - let prop_ref = Ident::new("__yew_prop_ref", Span::call_site()); let check_props = props.iter().map(|HtmlProp { label, .. }| { - quote! { #prop_ref.#label; } + quote! { props.#label; } }); let check_children = if !children.is_empty() { - quote! { #prop_ref.children; } + quote! { props.children; } } else { quote! {} }; - // This is a hack to avoid allocating memory but still have a reference to a props - // struct so that attributes can be checked against it - - #[cfg(has_maybe_uninit)] - let unallocated_prop_ref = quote! { - let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { - ::std::mem::MaybeUninit::uninit().assume_init() - }; - }; - - #[cfg(not(has_maybe_uninit))] - let unallocated_prop_ref = quote! { - let #prop_ref: <#ty as ::yew::html::Component>::Properties = unsafe { - ::std::mem::uninitialized() - }; - }; - quote! { - #unallocated_prop_ref - #check_children - #(#check_props)* + let _ = |props: <#ty as ::yew::html::Component>::Properties| { + #check_children + #(#check_props)* + }; } } else { quote! {} @@ -283,7 +266,7 @@ impl PeekValue for HtmlComponentOpen { let (punct, cursor) = cursor.punct()?; (punct.as_char() == '<').as_option()?; let (typ, _) = HtmlComponent::peek_type(cursor)?; - return Some(typ); + Some(typ) } } @@ -333,7 +316,7 @@ impl PeekValue for HtmlComponentClose { let (punct, _) = cursor.punct()?; (punct.as_char() == '>').as_option()?; - return Some(typ); + Some(typ) } } impl Parse for HtmlComponentClose { @@ -378,7 +361,7 @@ impl Props { impl PeekValue for Props { fn peek(cursor: Cursor) -> Option { let (ident, _) = cursor.ident()?; - let prop_type = if ident.to_string() == "with" { + let prop_type = if ident == "with" { PropType::With } else { PropType::List @@ -411,7 +394,7 @@ impl Parse for ListProps { } let ref_position = props.iter().position(|p| p.label.to_string() == "ref"); - let node_ref = ref_position.and_then(|i| Some(props.remove(i).value)); + let node_ref = ref_position.map(|i| props.remove(i).value); for prop in &props { if prop.label.to_string() == "ref" { return Err(syn::Error::new_spanned(&prop.label, "too many refs set")); @@ -452,7 +435,7 @@ struct WithProps { impl Parse for WithProps { fn parse(input: ParseStream) -> ParseResult { let with = input.parse::()?; - if with.to_string() != "with" { + if with != "with" { return Err(input.error("expected to find `with` token")); } let props = input.parse::()?; diff --git a/crates/macro/src/html_tree/html_iterable.rs b/crates/macro/src/html_tree/html_iterable.rs index ba80943d2b5..712a018f55a 100644 --- a/crates/macro/src/html_tree/html_iterable.rs +++ b/crates/macro/src/html_tree/html_iterable.rs @@ -12,7 +12,7 @@ pub struct HtmlIterable(Expr); impl PeekValue<()> for HtmlIterable { fn peek(cursor: Cursor) -> Option<()> { let (ident, _) = cursor.ident()?; - (ident.to_string() == "for").as_option() + (ident == "for").as_option() } } diff --git a/crates/macro/src/html_tree/html_prop.rs b/crates/macro/src/html_tree/html_prop.rs index 8a47abdd0f7..1744f710854 100644 --- a/crates/macro/src/html_tree/html_prop.rs +++ b/crates/macro/src/html_tree/html_prop.rs @@ -16,7 +16,7 @@ impl PeekValue<()> for HtmlProp { fn peek(cursor: Cursor) -> Option<()> { let (_, cursor) = HtmlPropLabel::peek(cursor)?; let (punct, _) = cursor.punct()?; - return (punct.as_char() == '=').as_option(); + (punct.as_char() == '=').as_option() } } From ac4b6970d180f2fe17c1ea1bef05d4a1ceb12b12 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sun, 2 Feb 2020 15:45:09 +0800 Subject: [PATCH 19/28] Fix clippy warnings (#912) --- crates/macro/src/html_tree/html_block.rs | 8 +++--- crates/macro/src/html_tree/html_component.rs | 25 +++++++++++-------- crates/macro/src/html_tree/html_node.rs | 8 +++--- .../src/html_tree/html_tag/tag_attributes.rs | 4 +-- crates/macro/src/html_tree/mod.rs | 24 +++++++++--------- examples/dashboard/src/lib.rs | 1 - 6 files changed, 36 insertions(+), 34 deletions(-) diff --git a/crates/macro/src/html_tree/html_block.rs b/crates/macro/src/html_tree/html_block.rs index 0c0fbd57ca6..8b7e5d393ac 100644 --- a/crates/macro/src/html_tree/html_block.rs +++ b/crates/macro/src/html_tree/html_block.rs @@ -14,8 +14,8 @@ pub struct HtmlBlock { } enum BlockContent { - Node(HtmlNode), - Iterable(HtmlIterable), + Node(Box), + Iterable(Box), } impl PeekValue<()> for HtmlBlock { @@ -29,9 +29,9 @@ impl Parse for HtmlBlock { let content; let brace = braced!(content in input); let content = if HtmlIterable::peek(content.cursor()).is_some() { - BlockContent::Iterable(content.parse()?) + BlockContent::Iterable(Box::new(content.parse()?)) } else { - BlockContent::Node(content.parse()?) + BlockContent::Node(Box::new(content.parse()?)) }; Ok(HtmlBlock { brace, content }) diff --git a/crates/macro/src/html_tree/html_component.rs b/crates/macro/src/html_tree/html_component.rs index 7893c93aab2..a1afac7c122 100644 --- a/crates/macro/src/html_tree/html_component.rs +++ b/crates/macro/src/html_tree/html_component.rs @@ -87,8 +87,8 @@ impl ToTokens for HtmlComponent { children, } = self; - let validate_props = if let Props::List(ListProps { props, .. }) = props { - let check_props = props.iter().map(|HtmlProp { label, .. }| { + let validate_props = if let Props::List(list_props) = props { + let check_props = list_props.props.iter().map(|HtmlProp { label, .. }| { quote! { props.#label; } }); @@ -121,8 +121,8 @@ impl ToTokens for HtmlComponent { }; let init_props = match props { - Props::List(ListProps { props, .. }) => { - let set_props = props.iter().map(|HtmlProp { label, value }| { + Props::List(list_props) => { + let set_props = list_props.props.iter().map(|HtmlProp { label, value }| { quote_spanned! { value.span()=> .#label( <::yew::virtual_dom::vcomp::VComp as ::yew::virtual_dom::Transformer<_, _>>::transform( #value @@ -137,7 +137,10 @@ impl ToTokens for HtmlComponent { .build() } } - Props::With(WithProps { props, .. }) => quote! { #props }, + Props::With(with_props) => { + let props = &with_props.props; + quote! { #props } + } Props::None => quote! { <<#ty as ::yew::html::Component>::Properties as ::yew::html::Properties>::builder() #set_children @@ -343,16 +346,16 @@ enum PropType { } enum Props { - List(ListProps), - With(WithProps), + List(Box), + With(Box), None, } impl Props { fn node_ref(&self) -> Option<&Expr> { match self { - Props::List(ListProps { node_ref, .. }) => node_ref.as_ref(), - Props::With(WithProps { node_ref, .. }) => node_ref.as_ref(), + Props::List(list_props) => list_props.node_ref.as_ref(), + Props::With(with_props) => with_props.node_ref.as_ref(), Props::None => None, } } @@ -374,8 +377,8 @@ impl PeekValue for Props { impl Parse for Props { fn parse(input: ParseStream) -> ParseResult { match Props::peek(input.cursor()) { - Some(PropType::List) => input.parse().map(Props::List), - Some(PropType::With) => input.parse().map(Props::With), + Some(PropType::List) => input.parse().map(|l| Props::List(Box::new(l))), + Some(PropType::With) => input.parse().map(|w| Props::With(Box::new(w))), None => Ok(Props::None), } } diff --git a/crates/macro/src/html_tree/html_node.rs b/crates/macro/src/html_tree/html_node.rs index f469092034b..e20d47f79f3 100644 --- a/crates/macro/src/html_tree/html_node.rs +++ b/crates/macro/src/html_tree/html_node.rs @@ -17,9 +17,9 @@ impl Parse for HtmlNode { Lit::Str(_) | Lit::Char(_) | Lit::Int(_) | Lit::Float(_) | Lit::Bool(_) => {} _ => return Err(syn::Error::new(lit.span(), "unsupported type")), } - Node::Literal(lit) + Node::Literal(Box::new(lit)) } else { - Node::Expression(input.parse()?) + Node::Expression(Box::new(input.parse()?)) }; Ok(HtmlNode(node)) @@ -56,6 +56,6 @@ impl ToTokens for Node { } enum Node { - Literal(Lit), - Expression(Expr), + Literal(Box), + Expression(Box), } diff --git a/crates/macro/src/html_tree/html_tag/tag_attributes.rs b/crates/macro/src/html_tree/html_tag/tag_attributes.rs index 61ae5581e17..3945bf2f790 100644 --- a/crates/macro/src/html_tree/html_tag/tag_attributes.rs +++ b/crates/macro/src/html_tree/html_tag/tag_attributes.rs @@ -20,7 +20,7 @@ pub struct TagAttributes { pub enum ClassesForm { Tuple(Vec), - Single(Expr), + Single(Box), } lazy_static! { @@ -146,7 +146,7 @@ impl TagAttributes { fn map_classes(class_expr: Expr) -> ClassesForm { match class_expr { Expr::Tuple(ExprTuple { elems, .. }) => ClassesForm::Tuple(elems.into_iter().collect()), - expr => ClassesForm::Single(expr), + expr => ClassesForm::Single(Box::new(expr)), } } } diff --git a/crates/macro/src/html_tree/mod.rs b/crates/macro/src/html_tree/mod.rs index 20102dd53a1..63a1782981a 100644 --- a/crates/macro/src/html_tree/mod.rs +++ b/crates/macro/src/html_tree/mod.rs @@ -31,12 +31,12 @@ pub enum HtmlType { } pub enum HtmlTree { - Block(HtmlBlock), - Component(HtmlComponent), - Iterable(HtmlIterable), - List(HtmlList), - Tag(HtmlTag), - Node(HtmlNode), + Block(Box), + Component(Box), + Iterable(Box), + List(Box), + Tag(Box), + Node(Box), Empty, } @@ -46,9 +46,9 @@ impl Parse for HtmlRoot { let html_root = if HtmlTree::peek(input.cursor()).is_some() { HtmlRoot(input.parse()?) } else if HtmlIterable::peek(input.cursor()).is_some() { - HtmlRoot(HtmlTree::Iterable(input.parse()?)) + HtmlRoot(HtmlTree::Iterable(Box::new(input.parse()?))) } else { - HtmlRoot(HtmlTree::Node(input.parse()?)) + HtmlRoot(HtmlTree::Node(Box::new(input.parse()?))) }; if !input.is_empty() { @@ -76,10 +76,10 @@ impl Parse for HtmlTree { .ok_or_else(|| input.error("expected valid html element"))?; let html_tree = match html_type { HtmlType::Empty => HtmlTree::Empty, - HtmlType::Component => HtmlTree::Component(input.parse()?), - HtmlType::Tag => HtmlTree::Tag(input.parse()?), - HtmlType::Block => HtmlTree::Block(input.parse()?), - HtmlType::List => HtmlTree::List(input.parse()?), + HtmlType::Component => HtmlTree::Component(Box::new(input.parse()?)), + HtmlType::Tag => HtmlTree::Tag(Box::new(input.parse()?)), + HtmlType::Block => HtmlTree::Block(Box::new(input.parse()?)), + HtmlType::List => HtmlTree::List(Box::new(input.parse()?)), }; Ok(html_tree) } diff --git a/examples/dashboard/src/lib.rs b/examples/dashboard/src/lib.rs index 6916363c628..07193048d56 100644 --- a/examples/dashboard/src/lib.rs +++ b/examples/dashboard/src/lib.rs @@ -5,7 +5,6 @@ use serde_derive::{Deserialize, Serialize}; use yew::format::{Json, Nothing, Toml}; use yew::services::fetch::{FetchService, FetchTask, Request, Response}; use yew::services::websocket::{WebSocketService, WebSocketStatus, WebSocketTask}; -use yew::services::Task; use yew::{html, Component, ComponentLink, Html, ShouldRender}; type AsBinary = bool; From 49d4f1045428b67820b41b3d8c95bbadf6e2de5a Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Mon, 10 Feb 2020 15:52:09 +0800 Subject: [PATCH 20/28] Import Task trait in dashboard example --- examples/dashboard/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/dashboard/src/lib.rs b/examples/dashboard/src/lib.rs index 07193048d56..6916363c628 100644 --- a/examples/dashboard/src/lib.rs +++ b/examples/dashboard/src/lib.rs @@ -5,6 +5,7 @@ use serde_derive::{Deserialize, Serialize}; use yew::format::{Json, Nothing, Toml}; use yew::services::fetch::{FetchService, FetchTask, Request, Response}; use yew::services::websocket::{WebSocketService, WebSocketStatus, WebSocketTask}; +use yew::services::Task; use yew::{html, Component, ComponentLink, Html, ShouldRender}; type AsBinary = bool; From e1f676897e9407aa778b31c9da4b58c681beb836 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sun, 16 Feb 2020 11:56:30 +0800 Subject: [PATCH 21/28] Remove duplicate vtag tests --- tests/vtag_test.rs | 418 --------------------------------------------- 1 file changed, 418 deletions(-) delete mode 100644 tests/vtag_test.rs diff --git a/tests/vtag_test.rs b/tests/vtag_test.rs deleted file mode 100644 index 96c75d4d30b..00000000000 --- a/tests/vtag_test.rs +++ /dev/null @@ -1,418 +0,0 @@ -#![recursion_limit = "128"] -#[cfg(feature = "std_web")] -use stdweb::web::{document, IElement}; -#[cfg(feature = "wasm_test")] -use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; -use yew::virtual_dom::vtag::{VTag, HTML_NAMESPACE, SVG_NAMESPACE}; -use yew::virtual_dom::{VDiff, VNode}; -use yew::{html, Component, ComponentLink, Html, ShouldRender}; - -#[cfg(feature = "wasm_test")] -wasm_bindgen_test_configure!(run_in_browser); - -struct Comp; - -impl Component for Comp { - type Message = (); - type Properties = (); - - fn create(_: Self::Properties, _: ComponentLink) -> Self { - Comp - } - - fn update(&mut self, _: Self::Message) -> ShouldRender { - unimplemented!(); - } - - fn view(&self) -> Html { - unimplemented!(); - } -} - -struct CompInt; - -impl Component for CompInt { - type Message = u32; - type Properties = (); - - fn create(_: Self::Properties, _: ComponentLink) -> Self { - CompInt - } - - fn update(&mut self, _: Self::Message) -> ShouldRender { - unimplemented!(); - } - - fn view(&self) -> Html { - unimplemented!(); - } -} - -struct CompBool; - -impl Component for CompBool { - type Message = bool; - type Properties = (); - - fn create(_: Self::Properties, _: ComponentLink) -> Self { - CompBool - } - - fn update(&mut self, _: Self::Message) -> ShouldRender { - unimplemented!(); - } - - fn view(&self) -> Html { - unimplemented!(); - } -} - -#[test] -fn it_compares_tags() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -

    - }; - - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn it_compares_text() { - let a = html! { -
    { "correct" }
    - }; - - let b = html! { -
    { "correct" }
    - }; - - let c = html! { -
    { "incorrect" }
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn it_compares_attributes() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn it_compares_children() { - let a = html! { -
    -

    -
    - }; - - let b = html! { -
    -

    -
    - }; - - let c = html! { -
    - -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn it_compares_classes() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - let d = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - assert_eq!(c, d); -} - -#[test] -fn classes_from_local_variables() { - let a = html! { -
    - }; - - let class_2 = "class-2"; - let b = html! { -
    - }; - - let class_2_fmt = format!("class-{}", 2); - let c = html! { -
    - }; - - assert_eq!(a, b); - assert_eq!(a, c); -} - -#[test] -fn supports_multiple_classes_string() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - assert_ne!(a, b); - - if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert!(vtag.classes.contains("class-1")); - assert!(vtag.classes.contains("class-2")); - assert!(vtag.classes.contains("class-3")); - } else { - panic!("vtag expected"); - } -} - -#[test] -fn supports_multiple_classes_vec() { - let mut classes = vec!["class-1"]; - classes.push("class-2"); - let a = html! { -
    - }; - - if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert!(vtag.classes.contains("class-1")); - assert!(vtag.classes.contains("class-2")); - assert!(!vtag.classes.contains("class-3")); - } else { - panic!("vtag expected"); - } -} - -#[test] -fn filter_empty_string_classes_vec() { - let mut classes = vec![""]; - classes.push("class-2"); - let a = html! {
    }; - let b = html! {
    }; - let c = html! {
    }; - - if let VNode::VTag(vtag) = a { - assert!(vtag.classes.is_empty()); - } else { - panic!("vtag expected"); - } - - if let VNode::VTag(vtag) = b { - assert!(vtag.classes.is_empty()); - } else { - panic!("vtag expected"); - } - - if let VNode::VTag(vtag) = c { - assert!(vtag.classes.is_empty()); - } else { - panic!("vtag expected"); - } -} - -fn assert_vtag(node: &mut VNode) -> &mut VTag { - if let VNode::VTag(vtag) = node { - return vtag; - } - panic!("should be vtag"); -} - -fn assert_namespace(vtag: &VTag, namespace: &'static str) { - assert_eq!( - vtag.reference.as_ref().unwrap().namespace_uri().unwrap(), - namespace - ); -} - -#[test] -fn supports_svg() { - #[cfg(feature = "std_web")] - let document = document(); - #[cfg(feature = "web_sys")] - let document = web_sys::window().unwrap().document().unwrap(); - - let div_el = document.create_element("div").unwrap(); - let namespace = SVG_NAMESPACE; - #[cfg(feature = "web_sys")] - let namespace = Some(namespace); - let svg_el = document.create_element_ns(namespace, "svg").unwrap(); - - let mut g_node = html! { }; - let path_node = html! { }; - let mut svg_node = html! { {path_node} }; - - let svg_tag = assert_vtag(&mut svg_node); - svg_tag.apply(&div_el, None, None); - assert_namespace(svg_tag, SVG_NAMESPACE); - let path_tag = assert_vtag(svg_tag.children.get_mut(0).unwrap()); - assert_namespace(path_tag, SVG_NAMESPACE); - - let g_tag = assert_vtag(&mut g_node); - g_tag.apply(&div_el, None, None); - assert_namespace(g_tag, HTML_NAMESPACE); - g_tag.reference = None; - - g_tag.apply(&svg_el, None, None); - assert_namespace(g_tag, SVG_NAMESPACE); -} - -#[test] -fn keeps_order_of_classes() { - let a = html! { -
    - }; - - if let VNode::VTag(vtag) = a { - println!("{:?}", vtag.classes); - assert_eq!(vtag.classes.to_string(), "class-1 class-2 class-3"); - } -} - -#[test] -fn it_compares_values() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn it_compares_kinds() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn it_compares_checked() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); -} - -#[test] -fn it_allows_aria_attributes() { - let a = html! { -

    - - -

    -

    - }; - if let VNode::VTag(vtag) = a { - assert!(vtag.attributes.contains_key("aria-controls")); - assert_eq!( - vtag.attributes.get("aria-controls"), - Some(&"it-works".into()) - ); - } else { - panic!("vtag expected"); - } -} - -#[test] -fn it_checks_mixed_closing_tags() { - let a = html! {
    }; - let b = html! {
    }; - assert_eq!(a, b); -} - -#[test] -fn it_checks_misleading_gt() { - html! {
    ::default()>
    }; - html! {
    ::default()>
    }; - - html! { }; - html! { }; -} From f6fe1e0a0a72e11d9e8761391a0a8a9d0aac6afa Mon Sep 17 00:00:00 2001 From: Jet Li Date: Sun, 23 Feb 2020 01:12:51 +0800 Subject: [PATCH 22/28] Fix prevent_default() by non-passive (#958) * Fix prevent_default() by non-passive * Fix cargo fmt --- src/html/listener/macros.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/html/listener/macros.rs b/src/html/listener/macros.rs index 957f69036c4..7e1199c1287 100644 --- a/src/html/listener/macros.rs +++ b/src/html/listener/macros.rs @@ -17,7 +17,7 @@ macro_rules! impl_action { use stdweb::web::event::{$type, IEvent}; use stdweb::web::{Element, IEventTarget}; } else if #[cfg(feature = "web_sys")] { - use gloo::events::EventListener; + use gloo::events::{EventListener, EventListenerOptions}; use wasm_bindgen::JsValue; use web_sys::{$type as WebSysType, Element, EventTarget}; } @@ -58,7 +58,16 @@ macro_rules! impl_action { }; cfg_match! { feature = "std_web" => EventListener(Some(element.add_event_listener(listener))), - feature = "web_sys" => EventListener::new(&EventTarget::from(element.clone()), $name, listener), + feature = "web_sys" => ({ + // We should only set passive event listeners for `touchstart` and `touchmove`. + // See here: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners + if $name == "touchstart" || $name == "touchmove" { + EventListener::new(&EventTarget::from(element.clone()), $name, listener) + } else { + let options = EventListenerOptions::enable_prevent_default(); + EventListener::new_with_options(&EventTarget::from(element.clone()), $name, options, listener) + } + }), } } } From e35d5e1bca476ee95b70ca5c2ec02a7533381a61 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 25 Feb 2020 20:29:54 +0100 Subject: [PATCH 23/28] Remove `Option` from most services. --- src/services/fetch/std_web.rs | 35 +++++++++++++++----------------- src/services/fetch/web_sys.rs | 37 +++++++++++----------------------- src/services/keyboard.rs | 5 ++++- src/services/reader/std_web.rs | 8 +------- src/services/reader/web_sys.rs | 16 +++++---------- 5 files changed, 38 insertions(+), 63 deletions(-) diff --git a/src/services/fetch/std_web.rs b/src/services/fetch/std_web.rs index 643f97518df..ad772491077 100644 --- a/src/services/fetch/std_web.rs +++ b/src/services/fetch/std_web.rs @@ -331,7 +331,7 @@ where return error; } }; - if let Ok(_) = Error::try_from(js!( return @{header_map.as_ref()}; )) { + if Error::try_from(js!( return @{header_map.as_ref()}; )).is_ok() { return Err("couldn't build headers"); } @@ -429,29 +429,26 @@ impl Task for FetchTask { false } } - fn cancel(&mut self) { - // Fetch API doesn't support request cancelling in all browsers - // and we should use this workaround with a flag. - // In that case, request not canceled, but callback won't be called. - let handle = self - .0 - .take() - .expect("tried to cancel request fetching twice"); - js! { @(no_return) - var handle = @{handle}; - handle.active = false; - handle.callback.drop(); - if (handle.abortController) { - handle.abortController.abort(); - } - } - } } impl Drop for FetchTask { fn drop(&mut self) { if self.is_active() { - self.cancel(); + // Fetch API doesn't support request cancelling in all browsers + // and we should use this workaround with a flag. + // In that case, request not canceled, but callback won't be called. + let handle = self + .0 + .take() + .expect("tried to cancel request fetching twice"); + js! { @(no_return) + var handle = @{handle}; + handle.active = false; + handle.callback.drop(); + if (handle.abortController) { + handle.abortController.abort(); + } + } } } } diff --git a/src/services/fetch/web_sys.rs b/src/services/fetch/web_sys.rs index 73b0b21f2b5..9dfcabc67d8 100644 --- a/src/services/fetch/web_sys.rs +++ b/src/services/fetch/web_sys.rs @@ -125,9 +125,9 @@ struct Handle { abort_controller: Option, } -/// A handle to control sent requests. Can be canceled with a `Task::cancel` call. +/// A handle to control sent requests. #[must_use] -pub struct FetchTask(Option); +pub struct FetchTask(Handle); impl fmt::Debug for FetchTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -360,10 +360,10 @@ where let data_fetcher = DataFetcher::new(binary, callback, active.clone()); spawn_local(DataFetcher::fetch_data(data_fetcher, promise)); - Ok(FetchTask(Some(Handle { + Ok(FetchTask(Handle { active, abort_controller, - }))) + })) } struct DataFetcher @@ -490,33 +490,20 @@ fn build_request(parts: Parts, body: &JsValue) -> Result { impl Task for FetchTask { fn is_active(&self) -> bool { - if let Some(handle) = &self.0 { - *handle.active.borrow() - } else { - false - } - } - - fn cancel(&mut self) { - // Fetch API doesn't support request cancelling in all browsers - // and we should use this workaround with a flag. - // In that case, request not canceled, but callback won't be called. - let handle = self - .0 - .take() - .expect("tried to cancel request fetching twice"); - - *handle.active.borrow_mut() = false; - if let Some(abort_controller) = handle.abort_controller { - abort_controller.abort(); - } + *self.0.active.borrow() } } impl Drop for FetchTask { fn drop(&mut self) { if self.is_active() { - self.cancel(); + // Fetch API doesn't support request cancelling in all browsers + // and we should use this workaround with a flag. + // In that case, request not canceled, but callback won't be called. + *self.0.active.borrow_mut() = false; + if let Some(abort_controller) = &self.0.abort_controller { + abort_controller.abort(); + } } } } diff --git a/src/services/keyboard.rs b/src/services/keyboard.rs index 9741137e0b4..24194add47d 100644 --- a/src/services/keyboard.rs +++ b/src/services/keyboard.rs @@ -119,7 +119,10 @@ fn register_key_impl( let handle = element.add_event_listener(move |event: E| { callback.emit(event); }); - KeyListenerHandle(Some(handle)) + cfg_match! { + feature = "std_web" => KeyListenerHandle(Some(handle)), + feature = "web_sys" => KeyListenerHandle(handle), + } } #[cfg(feature = "web_sys")] diff --git a/src/services/reader/std_web.rs b/src/services/reader/std_web.rs index 03b72e0fa18..1a14f72bf2c 100644 --- a/src/services/reader/std_web.rs +++ b/src/services/reader/std_web.rs @@ -121,17 +121,11 @@ impl ReaderService { /// A handle to control reading. #[must_use] pub struct ReaderTask { - file_reader: FileReader, + pub(super) file_reader: FileReader, } impl Task for ReaderTask { fn is_active(&self) -> bool { self.file_reader.ready_state() == FileReaderReadyState::Loading } - - fn cancel(&mut self) { - if self.is_active() { - self.file_reader.abort(); - } - } } diff --git a/src/services/reader/web_sys.rs b/src/services/reader/web_sys.rs index 7c712507f7a..cedee52dbe7 100644 --- a/src/services/reader/web_sys.rs +++ b/src/services/reader/web_sys.rs @@ -31,7 +31,7 @@ impl ReaderService { callback.emit(data); } }; - let listener = Some(EventListener::once(&file_reader, "loadend", callback)); + let listener = EventListener::once(&file_reader, "loadend", callback); file_reader.read_as_array_buffer(&file).unwrap(); Ok(ReaderTask { file_reader, @@ -81,7 +81,7 @@ impl ReaderService { callback.emit(None); } }; - let listener = Some(EventListener::new(&file_reader, "loadend", callback)); + let listener = EventListener::new(&file_reader, "loadend", callback); let blob = Blob::new().map_err(|_| anyhow!("Blob constructor is not supported"))?; file_reader.read_as_text(&blob).unwrap(); Ok(ReaderTask { @@ -94,19 +94,13 @@ impl ReaderService { /// A handle to control reading. #[must_use] pub struct ReaderTask { - file_reader: FileReader, - listener: Option, + pub(super) file_reader: FileReader, + #[allow(dead_code)] + listener: EventListener, } impl Task for ReaderTask { fn is_active(&self) -> bool { self.file_reader.ready_state() == FileReader::LOADING } - - fn cancel(&mut self) { - if self.is_active() { - self.file_reader.abort(); - } - self.listener.take(); - } } From 84e2802b58d437c2452adaad7fa49186044b89e5 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 25 Feb 2020 20:51:33 +0100 Subject: [PATCH 24/28] Remove `Option` from resize service. --- src/services/resize.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/services/resize.rs b/src/services/resize.rs index 26e65e2d488..86f4b131f99 100644 --- a/src/services/resize.rs +++ b/src/services/resize.rs @@ -21,7 +21,7 @@ pub struct ResizeService {} /// A handle to the event listener for resize events. #[must_use] pub struct ResizeTask( - #[cfg(feature = "std_web")] Option, + #[cfg(feature = "std_web")] Value, #[cfg(feature = "web_sys")] EventListener, ); @@ -73,13 +73,11 @@ impl ResizeService { callback.emit(dimensions); }; let handle = cfg_match! { - feature = "std_web" => ({ - Some(js! { - var handle = @{callback}; - window.addEventListener("resize", handle); - return handle; - }) - }), + feature = "std_web" => js! { + var handle = @{callback}; + window.addEventListener("resize", handle); + return handle; + }, feature = "web_sys" => EventListener::new(&web_sys::window().unwrap(), "resize", callback), }; ResizeTask(handle) @@ -89,7 +87,7 @@ impl ResizeService { #[cfg(feature = "std_web")] impl Drop for ResizeTask { fn drop(&mut self) { - let handle = self.0.take().expect("Resize task already empty."); + let handle = &self.0; js! { @(no_return) var handle = @{handle}; From 2ce21b79ac2d14adf349ce8d1bd3c81e45032bd9 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 25 Feb 2020 21:39:42 +0100 Subject: [PATCH 25/28] Apply fetch changes. --- src/services/fetch/std_web.rs | 73 ++++++++++++++++++++++++++++++----- src/services/fetch/web_sys.rs | 25 +++++++++++- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/services/fetch/std_web.rs b/src/services/fetch/std_web.rs index ad772491077..228ea8d7978 100644 --- a/src/services/fetch/std_web.rs +++ b/src/services/fetch/std_web.rs @@ -1,5 +1,6 @@ //! `stdweb` implementation for the fetch service. +use super::Referrer; use crate::callback::Callback; use crate::format::{Binary, Format, Text}; use crate::services::Task; @@ -72,9 +73,47 @@ pub enum Redirect { Manual, } +impl Serialize for Referrer { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match *self { + Referrer::SameOriginUrl(ref s) => serializer.serialize_str(s), + Referrer::AboutClient => { + serializer.serialize_unit_variant("Referrer", 0, "about:client") + } + Referrer::Empty => serializer.serialize_unit_variant("Referrer", 1, ""), + } + } +} + +/// Type to set referrer policy for fetch. +#[derive(Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum ReferrerPolicy { + /// `no-referrer` value of referrerPolicy. + NoReferrer, + /// `no-referrer-when-downgrade` value of referrerPolicy. + NoReferrerWhenDowngrade, + /// `same-origin` value of referrerPolicy. + SameOrigin, + /// `origin` value of referrerPolicy. + Origin, + /// `strict-origin` value of referrerPolicy. + StrictOrigin, + /// `origin-when-cross-origin` value of referrerPolicy. + OriginWhenCrossOrigin, + /// `strict-origin-when-cross-origin` value of referrerPolicy. + StrictOriginWhenCrossOrigin, + /// `unsafe-url` value of referrerPolicy. + UnsafeUrl, +} + /// Init options for `fetch()` function call. /// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch #[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] pub struct FetchOptions { /// Cache of a fetch request. #[serde(skip_serializing_if = "Option::is_none")] @@ -88,6 +127,15 @@ pub struct FetchOptions { /// Request mode of a fetch request. #[serde(skip_serializing_if = "Option::is_none")] pub mode: Option, + /// Referrer of a fetch request. + #[serde(skip_serializing_if = "Option::is_none")] + pub referrer: Option, + /// Referrer policy of a fetch request. + #[serde(skip_serializing_if = "Option::is_none")] + pub referrer_policy: Option, + /// Integrity of a fetch request. + #[serde(skip_serializing_if = "Option::is_none")] + pub integrity: Option, } /// Represents errors of a fetch service. @@ -344,7 +392,12 @@ where // Notice that the callback signature must match the call from the javascript // side. There is no static check at this point. let callback = move |success: bool, status: u16, headers: HashMap, data: X| { - let mut response_builder = Response::builder().status(status); + let mut response_builder = Response::builder(); + + if let Ok(status) = StatusCode::from_u16(status) { + response_builder = response_builder.status(status); + } + for (key, values) in headers { response_builder = response_builder.header(key.as_str(), values.as_str()); } @@ -366,12 +419,6 @@ where if (@{binary} && body != null) { body = Uint8Array.from(body); } - var data = { - method: @{method}, - body: body, - headers: @{header_map}, - }; - var request = new Request(@{uri}, data); var callback = @{callback}; var abortController = AbortController ? new AbortController() : null; var handle = { @@ -379,11 +426,19 @@ where callback, abortController, }; - var init = @{Serde(options)} || {}; + var init = { + method: @{method}, + body: body, + headers: @{header_map}, + }; + var opts = @{Serde(options)} || {}; + for (var attrname in opts) { + init[attrname] = opts[attrname]; + } if (abortController && !("signal" in init)) { init.signal = abortController.signal; } - fetch(request, init).then(function(response) { + fetch(@{uri}, init).then(function(response) { var promise = (@{binary}) ? response.arrayBuffer() : response.text(); var status = response.status; var headers = {}; diff --git a/src/services/fetch/web_sys.rs b/src/services/fetch/web_sys.rs index 9dfcabc67d8..3e3ea6850a2 100644 --- a/src/services/fetch/web_sys.rs +++ b/src/services/fetch/web_sys.rs @@ -1,5 +1,6 @@ //! `web-sys` implementation for the fetch service. +use super::Referrer; use crate::callback::Callback; use crate::format::{Binary, Format, Text}; use crate::services::Task; @@ -16,7 +17,7 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen_futures::{spawn_local, JsFuture}; use web_sys::{ - AbortController, DomException, Headers, Request as WebRequest, RequestInit, + AbortController, DomException, Headers, ReferrerPolicy, Request as WebRequest, RequestInit, Response as WebResponse, }; @@ -64,6 +65,12 @@ pub struct FetchOptions { pub redirect: Option, /// Request mode of a fetch request. pub mode: Option, + /// Referrer of a fetch request. + pub referrer: Option, + /// Referrer policy of a fetch request. + pub referrer_policy: Option, + /// Integrity of a fetch request. + pub integrity: Option, } impl Into for FetchOptions { @@ -86,6 +93,22 @@ impl Into for FetchOptions { init.mode(mode); } + if let Some(referrer) = self.referrer { + match referrer { + Referrer::SameOriginUrl(referrer) => init.referrer(&referrer), + Referrer::AboutClient => init.referrer("about:client"), + Referrer::Empty => init.referrer(""), + }; + } + + if let Some(referrer_policy) = self.referrer_policy { + init.referrer_policy(referrer_policy); + } + + if let Some(integrity) = self.integrity { + init.integrity(&integrity); + } + init } } From 8b7ab86dfb07a984bfe4f383b8f53b123030b266 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 25 Feb 2020 21:41:47 +0100 Subject: [PATCH 26/28] Apply reader service changes. --- src/services/reader/std_web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/reader/std_web.rs b/src/services/reader/std_web.rs index 1a14f72bf2c..3181049f301 100644 --- a/src/services/reader/std_web.rs +++ b/src/services/reader/std_web.rs @@ -93,7 +93,7 @@ impl ReaderService { let from = position; let to = cmp::min(position + chunk_size, total_size); position = to; - // TODO Implement `slice` method in `stdweb` + // TODO(#942): Implement `slice` method in `stdweb` let blob: Blob = (js! { return @{file}.slice(@{from as u32}, @{to as u32}); }) From 4790b3339ed2c70e5ab7d7e0d4b6014989ae7348 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 25 Feb 2020 22:19:11 +0100 Subject: [PATCH 27/28] Fix `node_refs` example. --- examples/web_sys/node_refs/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/web_sys/node_refs/src/lib.rs b/examples/web_sys/node_refs/src/lib.rs index 472f52c0fa8..63312b715b4 100644 --- a/examples/web_sys/node_refs/src/lib.rs +++ b/examples/web_sys/node_refs/src/lib.rs @@ -29,7 +29,7 @@ impl Component for Model { } fn mounted(&mut self) -> ShouldRender { - if let Some(input) = self.refs[self.focus_index].try_into::() { + if let Some(input) = self.refs[self.focus_index].cast::() { input.focus().unwrap(); } false @@ -39,7 +39,7 @@ impl Component for Model { match msg { Msg::HoverIndex(index) => self.focus_index = index, } - if let Some(input) = self.refs[self.focus_index].try_into::() { + if let Some(input) = self.refs[self.focus_index].cast::() { input.focus().unwrap(); } true From 42a5e7d867bbefb53e5329ae860c46794f50542f Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Fri, 28 Feb 2020 11:26:52 +0800 Subject: [PATCH 28/28] Remove web-sys travis branch --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b4971cd63b7..5d7763baa69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ branches: - staging - trying - master - - web-sys dist: trusty language: rust