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 ``, `
+ };
+ 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! { };
+ }
+}
diff --git a/src/virtual_dom/vtext.rs b/src/virtual_dom/vtext.rs
index d5c26fb3a03..1a1b11c0845 100644
--- a/src/virtual_dom/vtext.rs
+++ b/src/virtual_dom/vtext.rs
@@ -131,3 +131,43 @@ impl PartialEq for VText {
self.text == other.text
}
}
+
+#[cfg(test)]
+mod test {
+ use crate::{html, Component, ComponentLink, Html, ShouldRender};
+ #[cfg(feature = "wasm_test")]
+ use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
+
+ #[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!();
+ }
+ }
+
+ #[test]
+ fn text_as_root() {
+ html! {
+ "Text Node As Root"
+ };
+
+ html! {
+ { "Text Node As Root" }
+ };
+ }
+}
diff --git a/tests/format_test.rs b/tests/format_test.rs
deleted file mode 100644
index 6892a9ba8f9..00000000000
--- a/tests/format_test.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-use serde::{Deserialize, Serialize};
-#[cfg(feature = "wasm_test")]
-use wasm_bindgen_test::wasm_bindgen_test as test;
-use yew::format::{Binary, Json, Text};
-
-#[test]
-fn json_format() {
- #[derive(Serialize, Deserialize)]
- struct Data {
- value: u8,
- }
-
- let Json(data): Json> = Json::from(Ok(r#"{"value": 123}"#.to_string()));
- let data = data.unwrap();
- assert_eq!(data.value, 123);
-
- let _stored: Text = Json(&data).into();
- let _stored: Binary = Json(&data).into();
-}
diff --git a/tests/vcomp_test.rs b/tests/vcomp_test.rs
deleted file mode 100644
index f3e99470014..00000000000
--- a/tests/vcomp_test.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-#[cfg(feature = "wasm_test")]
-use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
-use yew::macros::Properties;
-use yew::{html, Component, ComponentLink, Html, ShouldRender};
-
-#[cfg(feature = "wasm_test")]
-wasm_bindgen_test_configure!(run_in_browser);
-
-struct Comp;
-
-#[derive(Clone, PartialEq, Properties)]
-struct Props {
- field_1: u32,
- field_2: u32,
-}
-
-impl Component for Comp {
- type Message = ();
- type Properties = Props;
-
- fn create(_: Self::Properties, _: ComponentLink) -> Self {
- Comp
- }
-
- fn update(&mut self, _: Self::Message) -> ShouldRender {
- unimplemented!();
- }
-
- fn view(&self) -> Html {
- unimplemented!();
- }
-}
-
-#[test]
-fn set_properties_to_component() {
- html! {
-
- };
-
- html! {
-
- };
-
- html! {
-
- };
-
- html! {
-
- };
-
- let props = Props {
- field_1: 1,
- field_2: 1,
- };
-
- html! {
-
- };
-}
diff --git a/tests/vlist_test.rs b/tests/vlist_test.rs
deleted file mode 100644
index 02bee958017..00000000000
--- a/tests/vlist_test.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-#[cfg(feature = "wasm_test")]
-use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
-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!();
- }
-}
-
-#[test]
-fn check_fragments() {
- let fragment = html! {
- <>
- >
- };
- html! {
-
- { fragment }
-
- };
-}
diff --git a/tests/vtext_test.rs b/tests/vtext_test.rs
deleted file mode 100644
index 25d85a9baeb..00000000000
--- a/tests/vtext_test.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-#[cfg(feature = "wasm_test")]
-use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
-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!();
- }
-}
-
-#[test]
-fn text_as_root() {
- html! {
- "Text Node As Root"
- };
-
- html! {
- { "Text Node As Root" }
- };
-}
From 047ad2acf44c1c48c215e299240fc15f773798db Mon Sep 17 00:00:00 2001
From: daxpedda
Date: Mon, 27 Jan 2020 02:50:28 +0100
Subject: [PATCH 13/28] `web-sys` fetch service conversion (#867)
* Split implementation.
* Import global.
* Import global.
* Revert split.
* Make fetch available again.
* Revert "Revert split."
This reverts commit 6e3f101dbedde2142f041467a8ae40ef5e3920c5.
* Re-revert split.
* Some polish.
* Move to `wasm_bindgen_futures`.
* Switch to `thiserror`.
* wip
* Update src/services/fetch/web_sys.rs
Co-Authored-By: daxpedda
* Some more polish.
Co-authored-by: Justin Starry
---
Cargo.toml | 6 +-
src/callback.rs | 68 ++++
src/services/fetch.rs | 451 +------------------------
src/services/fetch/std_web.rs | 457 +++++++++++++++++++++++++
src/services/fetch/web_sys.rs | 605 ++++++++++++++++++++++++++++++++++
src/services/mod.rs | 2 -
6 files changed, 1142 insertions(+), 447 deletions(-)
create mode 100644 src/services/fetch/std_web.rs
create mode 100644 src/services/fetch/web_sys.rs
diff --git a/Cargo.toml b/Cargo.toml
index afc07c64a50..3d5df1598d8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,7 @@ bincode = { version = "~1.2.1", optional = true }
cfg-if = "0.1"
cfg-match = "0.2"
console_error_panic_hook = { version = "0.1", optional = true }
+futures = { version = "0.3", optional = true }
gloo = { version = "0.2", optional = true }
http = "0.2"
indexmap = "1.0.2"
@@ -42,6 +43,7 @@ stdweb = { version = "0.4.20", optional = true }
thiserror = "1"
toml = { version = "0.5", optional = true }
wasm-bindgen = { version = "0.2.58", optional = true }
+wasm-bindgen-futures = { version = "0.4", optional = true }
yew-macro = { version = "0.11.1", path = "crates/macro" }
[dependencies.web-sys]
@@ -55,6 +57,7 @@ features = [
"console",
"DedicatedWorkerGlobalScope",
"Document",
+ "DomException",
"DomTokenList",
"DragEvent",
"Element",
@@ -92,6 +95,7 @@ features = [
"WheelEvent",
"Window",
"Worker",
+ "WorkerGlobalScope",
"WorkerOptions",
]
@@ -116,7 +120,7 @@ bincode = "~1.2.1"
[features]
default = []
std_web = ["stdweb"]
-web_sys = ["console_error_panic_hook", "gloo", "js-sys", "web-sys", "wasm-bindgen"]
+web_sys = ["console_error_panic_hook", "futures", "gloo", "js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures"]
doc_test = []
wasm_test = []
services = []
diff --git a/src/callback.rs b/src/callback.rs
index d53810e41e3..c61a293224b 100644
--- a/src/callback.rs
+++ b/src/callback.rs
@@ -64,3 +64,71 @@ impl Callback {
Callback::from(func)
}
}
+
+#[cfg(test)]
+pub(crate) mod test_util {
+ use super::*;
+ use std::cell::RefCell;
+ use std::future::Future;
+ use std::pin::Pin;
+ use std::task::{Context, Poll, Waker};
+
+ struct CallbackHandle {
+ waker: Option,
+ output: Option,
+ }
+
+ impl Default for CallbackHandle {
+ fn default() -> Self {
+ CallbackHandle {
+ waker: None,
+ output: None,
+ }
+ }
+ }
+
+ pub(crate) struct CallbackFuture(Rc>>);
+
+ impl Clone for CallbackFuture {
+ fn clone(&self) -> Self {
+ Self(self.0.clone())
+ }
+ }
+
+ impl Default for CallbackFuture {
+ fn default() -> Self {
+ Self(Rc::default())
+ }
+ }
+
+ impl Into> for CallbackFuture {
+ fn into(self) -> Callback {
+ Callback::from(move |r| self.finish(r))
+ }
+ }
+
+ impl Future for CallbackFuture {
+ type Output = T;
+ fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
+ if let Some(output) = self.ready() {
+ Poll::Ready(output)
+ } else {
+ self.0.borrow_mut().waker = Some(cx.waker().clone());
+ Poll::Pending
+ }
+ }
+ }
+
+ impl CallbackFuture {
+ fn ready(&self) -> Option {
+ self.0.borrow_mut().output.take()
+ }
+
+ fn finish(&self, output: T) {
+ self.0.borrow_mut().output = Some(output);
+ if let Some(waker) = self.0.borrow_mut().waker.take() {
+ waker.wake();
+ }
+ }
+ }
+}
diff --git a/src/services/fetch.rs b/src/services/fetch.rs
index 00f6d90070b..b7acecae288 100644
--- a/src/services/fetch.rs
+++ b/src/services/fetch.rs
@@ -1,448 +1,11 @@
//! Service to send HTTP-request to a server.
-use super::Task;
-use crate::callback::Callback;
-use crate::format::{Binary, Format, Text};
-use serde::Serialize;
-use std::collections::HashMap;
-use std::fmt;
-use stdweb::serde::Serde;
-use stdweb::unstable::{TryFrom, TryInto};
-use stdweb::web::ArrayBuffer;
-use stdweb::{JsSerialize, Value};
-#[allow(unused_imports)]
-use stdweb::{_js_impl, js};
-use thiserror::Error;
-
-pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};
-
-/// Type to set cache for fetch.
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "kebab-case")]
-pub enum Cache {
- /// `default` value of cache.
- #[serde(rename = "default")]
- DefaultCache,
- /// `no-store` value of cache.
- NoStore,
- /// `reload` value of cache.
- Reload,
- /// `no-cache` value of cache.
- NoCache,
- /// `force-cache` value of cache
- ForceCache,
- /// `only-if-cached` value of cache
- OnlyIfCached,
-}
-
-/// Type to set credentials for fetch.
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "kebab-case")]
-pub enum Credentials {
- /// `omit` value of credentials.
- Omit,
- /// `include` value of credentials.
- Include,
- /// `same-origin` value of credentials.
- SameOrigin,
-}
-
-/// Type to set mode for fetch.
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "kebab-case")]
-pub enum Mode {
- /// `same-origin` value of mode.
- SameOrigin,
- /// `no-cors` value of mode.
- NoCors,
- /// `cors` value of mode.
- Cors,
-}
-
-/// Type to set redirect behaviour for fetch.
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "kebab-case")]
-pub enum Redirect {
- /// `follow` value of redirect.
- Follow,
- /// `error` value of redirect.
- Error,
- /// `manual` value of redirect.
- Manual,
-}
-
-/// Init options for `fetch()` function call.
-/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
-#[derive(Serialize, Default, Debug)]
-pub struct FetchOptions {
- /// Cache of a fetch request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cache: Option,
- /// Credentials of a fetch request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub credentials: Option,
- /// Redirect behaviour of a fetch request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub redirect: Option,
- /// Request mode of a fetch request.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub mode: Option,
-}
-
-/// Represents errors of a fetch service.
-#[derive(Debug, Error)]
-enum FetchError {
- #[error("failed response")]
- FailedResponse,
-}
-
-/// A handle to control sent requests. Can be canceled with a `Task::cancel` call.
-#[must_use]
-pub struct FetchTask(Option);
-
-impl fmt::Debug for FetchTask {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.write_str("FetchTask")
- }
-}
-
-/// A service to fetch resources.
-#[derive(Default, Debug)]
-pub struct FetchService {}
-
-impl FetchService {
- /// Creates a new service instance connected to `App` by provided `sender`.
- pub fn new() -> Self {
- Self {}
- }
-
- /// Sends a request to a remote server given a Request object and a callback
- /// fuction to convert a Response object into a loop's message.
- ///
- /// You may use a Request builder to build your request declaratively as on the
- /// following examples:
- ///
- /// ```
- ///# use yew::format::{Nothing, Json};
- ///# use yew::services::fetch::Request;
- ///# use serde_json::json;
- /// let post_request = Request::post("https://my.api/v1/resource")
- /// .header("Content-Type", "application/json")
- /// .body(Json(&json!({"foo": "bar"})))
- /// .expect("Failed to build request.");
- ///
- /// let get_request = Request::get("https://my.api/v1/resource")
- /// .body(Nothing)
- /// .expect("Failed to build request.");
- /// ```
- ///
- /// The callback function can build a loop message by passing or analizing the
- /// response body and metadata.
- ///
- /// ```
- ///# use yew::{Component, ComponentLink, Html, Renderable};
- ///# use yew::services::FetchService;
- ///# use yew::services::fetch::{Response, Request};
- ///# struct Comp;
- ///# impl Component for Comp {
- ///# type Message = Msg;type Properties = ();
- ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()}
- ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
- ///# fn view(&self) -> Html {unimplemented!()}
- ///# }
- ///# enum Msg {
- ///# Noop,
- ///# Error
- ///# }
- ///# fn dont_execute() {
- ///# let link: ComponentLink = unimplemented!();
- ///# let mut fetch_service: FetchService = FetchService::new();
- ///# let post_request: Request> = unimplemented!();
- /// let task = fetch_service.fetch(
- /// post_request,
- /// link.callback(|response: Response>| {
- /// if response.status().is_success() {
- /// Msg::Noop
- /// } else {
- /// Msg::Error
- /// }
- /// }),
- /// );
- ///# }
- /// ```
- ///
- /// For a full example, you can specify that the response must be in the JSON format,
- /// and be a specific serialized data type. If the mesage isn't Json, or isn't the specified
- /// data type, then you will get a message indicating failure.
- ///
- /// ```
- ///# use yew::format::{Json, Nothing, Format};
- ///# use yew::services::FetchService;
- ///# use http::Request;
- ///# use yew::services::fetch::Response;
- ///# use yew::{Component, ComponentLink, Renderable, Html};
- ///# use serde_derive::Deserialize;
- ///# struct Comp;
- ///# impl Component for Comp {
- ///# type Message = Msg;type Properties = ();
- ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()}
- ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
- ///# fn view(&self) -> Html {unimplemented!()}
- ///# }
- ///# enum Msg {
- ///# FetchResourceComplete(Data),
- ///# FetchResourceFailed
- ///# }
- /// #[derive(Deserialize)]
- /// struct Data {
- /// value: String
- /// }
- ///
- ///# fn dont_execute() {
- ///# let link: ComponentLink = unimplemented!();
- /// let get_request = Request::get("/thing").body(Nothing).unwrap();
- /// let callback = link.callback(|response: Response>>| {
- /// if let (meta, Json(Ok(body))) = response.into_parts() {
- /// if meta.status.is_success() {
- /// return Msg::FetchResourceComplete(body);
- /// }
- /// }
- /// Msg::FetchResourceFailed
- /// });
- ///
- /// let task = FetchService::new().fetch(get_request, callback);
- ///# }
- /// ```
- ///
- pub fn fetch(
- &mut self,
- request: Request,
- callback: Callback>,
- ) -> FetchTask
- where
- IN: Into,
- OUT: From,
- {
- fetch_impl::(false, request, None, callback)
- }
-
- /// `fetch` with provided `FetchOptions` object.
- /// Use it if you need to send cookies with a request:
- /// ```
- ///# use yew::format::Nothing;
- ///# use yew::services::fetch::{self, FetchOptions, Credentials};
- ///# use yew::{Renderable, Html, Component, ComponentLink};
- ///# use yew::services::FetchService;
- ///# use http::Response;
- ///# struct Comp;
- ///# impl Component for Comp {
- ///# type Message = Msg;
- ///# type Properties = ();
- ///# fn create(props: Self::Properties, link: ComponentLink) -> Self {unimplemented!()}
- ///# fn update(&mut self, msg: Self::Message) -> bool {unimplemented!()}
- ///# fn view(&self) -> Html {unimplemented!()}
- ///# }
- ///# pub enum Msg {}
- ///# fn dont_execute() {
- ///# let link: ComponentLink = unimplemented!();
- ///# let callback = link.callback(|response: Response>| unimplemented!());
- /// let request = fetch::Request::get("/path/")
- /// .body(Nothing)
- /// .unwrap();
- /// let options = FetchOptions {
- /// credentials: Some(Credentials::SameOrigin),
- /// ..FetchOptions::default()
- /// };
- /// let task = FetchService::new().fetch_with_options(request, options, callback);
- ///# }
- /// ```
- pub fn fetch_with_options(
- &mut self,
- request: Request,
- options: FetchOptions,
- callback: Callback>,
- ) -> FetchTask
- where
- IN: Into,
- OUT: From,
- {
- fetch_impl::(false, request, Some(options), callback)
- }
-
- /// Fetch the data in binary format.
- pub fn fetch_binary(
- &mut self,
- request: Request,
- callback: Callback>,
- ) -> FetchTask
- where
- IN: Into,
- OUT: From,
- {
- fetch_impl::, ArrayBuffer>(true, request, None, callback)
- }
-
- /// Fetch the data in binary format.
- pub fn fetch_binary_with_options(
- &mut self,
- request: Request,
- options: FetchOptions,
- callback: Callback>,
- ) -> FetchTask
- where
- IN: Into,
- OUT: From,
- {
- fetch_impl::, ArrayBuffer>(true, request, Some(options), callback)
- }
-}
-
-fn fetch_impl(
- binary: bool,
- request: Request,
- options: Option,
- callback: Callback>,
-) -> FetchTask
-where
- IN: Into>,
- OUT: From>,
- T: JsSerialize,
- X: TryFrom + Into,
-{
- // Consume request as parts and body.
- let (parts, body) = request.into_parts();
-
- // Map headers into a Js serializable HashMap.
- let header_map: HashMap<&str, &str> = parts
- .headers
- .iter()
- .map(|(k, v)| {
- (
- k.as_str(),
- v.to_str().unwrap_or_else(|_| {
- panic!("Unparsable request header {}: {:?}", k.as_str(), v)
- }),
- )
- })
- .collect();
-
- // Formats URI.
- let uri = format!("{}", parts.uri);
- let method = parts.method.as_str();
- let body = body.into().ok();
-
- // Prepare the response callback.
- // 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);
- for (key, values) in headers {
- response_builder = response_builder.header(key.as_str(), values.as_str());
- }
-
- // Deserialize and wrap response data into a Text object.
- let data = if success {
- Ok(data.into())
- } else {
- Err(FetchError::FailedResponse.into())
- };
- let out = OUT::from(data);
- let response = response_builder.body(out).unwrap();
- callback.emit(response);
- };
-
- #[allow(clippy::too_many_arguments)]
- let handle = js! {
- var body = @{body};
- 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 = {
- active: true,
- callback,
- abortController,
- };
- var init = @{Serde(options)} || {};
- if (abortController && !("signal" in init)) {
- init.signal = abortController.signal;
- }
- fetch(request, init).then(function(response) {
- var promise = (@{binary}) ? response.arrayBuffer() : response.text();
- var status = response.status;
- var headers = {};
- response.headers.forEach(function(value, key) {
- headers[key] = value;
- });
- promise.then(function(data) {
- if (handle.active == true) {
- handle.active = false;
- callback(true, status, headers, data);
- callback.drop();
- }
- }).catch(function(err) {
- if (handle.active == true) {
- handle.active = false;
- callback(false, status, headers, data);
- callback.drop();
- }
- });
- }).catch(function(e) {
- if (handle.active == true) {
- var data = (@{binary}) ? new ArrayBuffer() : "";
- handle.active = false;
- callback(false, 408, {}, data);
- callback.drop();
- }
- });
- return handle;
- };
- FetchTask(Some(handle))
-}
-
-impl Task for FetchTask {
- fn is_active(&self) -> bool {
- if let Some(ref task) = self.0 {
- let result = js! {
- var the_task = @{task};
- return the_task.active &&
- (!the_task.abortController || !the_task.abortController.signal.aborted);
- };
- result.try_into().unwrap_or(false)
- } 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");
- 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();
- }
+cfg_if::cfg_if! {
+ if #[cfg(feature = "std_web")] {
+ mod std_web;
+ pub use std_web::*;
+ } else if #[cfg(feature = "web_sys")] {
+ mod web_sys;
+ pub use self::web_sys::*;
}
}
diff --git a/src/services/fetch/std_web.rs b/src/services/fetch/std_web.rs
new file mode 100644
index 00000000000..643f97518df
--- /dev/null
+++ b/src/services/fetch/std_web.rs
@@ -0,0 +1,457 @@
+//! `stdweb` implementation for the fetch service.
+
+use crate::callback::Callback;
+use crate::format::{Binary, Format, Text};
+use crate::services::Task;
+use serde::Serialize;
+use std::collections::HashMap;
+use std::fmt;
+use stdweb::serde::Serde;
+use stdweb::unstable::{TryFrom, TryInto};
+use stdweb::web::error::Error;
+use stdweb::web::ArrayBuffer;
+use stdweb::{JsSerialize, Value};
+#[allow(unused_imports)]
+use stdweb::{_js_impl, js};
+use thiserror::Error;
+
+pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};
+
+/// Type to set cache for fetch.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub enum Cache {
+ /// `default` value of cache.
+ #[serde(rename = "default")]
+ DefaultCache,
+ /// `no-store` value of cache.
+ NoStore,
+ /// `reload` value of cache.
+ Reload,
+ /// `no-cache` value of cache.
+ NoCache,
+ /// `force-cache` value of cache
+ ForceCache,
+ /// `only-if-cached` value of cache
+ OnlyIfCached,
+}
+
+/// Type to set credentials for fetch.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub enum Credentials {
+ /// `omit` value of credentials.
+ Omit,
+ /// `include` value of credentials.
+ Include,
+ /// `same-origin` value of credentials.
+ SameOrigin,
+}
+
+/// Type to set mode for fetch.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub enum Mode {
+ /// `same-origin` value of mode.
+ SameOrigin,
+ /// `no-cors` value of mode.
+ NoCors,
+ /// `cors` value of mode.
+ Cors,
+}
+
+/// Type to set redirect behaviour for fetch.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub enum Redirect {
+ /// `follow` value of redirect.
+ Follow,
+ /// `error` value of redirect.
+ Error,
+ /// `manual` value of redirect.
+ Manual,
+}
+
+/// Init options for `fetch()` function call.
+/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
+#[derive(Serialize, Default, Debug)]
+pub struct FetchOptions {
+ /// Cache of a fetch request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub cache: Option,
+ /// Credentials of a fetch request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub credentials: Option,
+ /// Redirect behaviour of a fetch request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub redirect: Option,
+ /// Request mode of a fetch request.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mode: Option,
+}
+
+/// Represents errors of a fetch service.
+#[derive(Debug, Error)]
+enum FetchError {
+ #[error("failed response")]
+ FailedResponse,
+}
+
+/// A handle to control sent requests. Can be canceled with a `Task::cancel` call.
+#[must_use]
+pub struct FetchTask(Option);
+
+impl fmt::Debug for FetchTask {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str("FetchTask")
+ }
+}
+
+/// A service to fetch resources.
+#[derive(Default, Debug)]
+pub struct FetchService {}
+
+impl FetchService {
+ /// Creates a new service instance connected to `App` by provided `sender`.
+ pub fn new() -> Self {
+ Self {}
+ }
+
+ /// Sends a request to a remote server given a Request object and a callback
+ /// fuction to convert a Response object into a loop's message.
+ ///
+ /// You may use a Request builder to build your request declaratively as on the
+ /// following examples:
+ ///
+ /// ```
+ ///# use yew::format::{Nothing, Json};
+ ///# use yew::services::fetch::Request;
+ ///# use serde_json::json;
+ /// let post_request = Request::post("https://my.api/v1/resource")
+ /// .header("Content-Type", "application/json")
+ /// .body(Json(&json!({"foo": "bar"})))
+ /// .expect("Failed to build request.");
+ ///
+ /// let get_request = Request::get("https://my.api/v1/resource")
+ /// .body(Nothing)
+ /// .expect("Failed to build request.");
+ /// ```
+ ///
+ /// The callback function can build a loop message by passing or analizing the
+ /// response body and metadata.
+ ///
+ /// ```
+ ///# use yew::{Component, ComponentLink, Html, Renderable};
+ ///# use yew::services::FetchService;
+ ///# use yew::services::fetch::{Response, Request};
+ ///# struct Comp;
+ ///# impl Component for Comp {
+ ///# type Message = Msg;type Properties = ();
+ ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()}
+ ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
+ ///# fn view(&self) -> Html {unimplemented!()}
+ ///# }
+ ///# enum Msg {
+ ///# Noop,
+ ///# Error
+ ///# }
+ ///# fn dont_execute() {
+ ///# let link: ComponentLink = unimplemented!();
+ ///# let mut fetch_service: FetchService = FetchService::new();
+ ///# let post_request: Request> = unimplemented!();
+ /// let task = fetch_service.fetch(
+ /// post_request,
+ /// link.callback(|response: Response>| {
+ /// if response.status().is_success() {
+ /// Msg::Noop
+ /// } else {
+ /// Msg::Error
+ /// }
+ /// }),
+ /// );
+ ///# }
+ /// ```
+ ///
+ /// For a full example, you can specify that the response must be in the JSON format,
+ /// and be a specific serialized data type. If the mesage isn't Json, or isn't the specified
+ /// data type, then you will get a message indicating failure.
+ ///
+ /// ```
+ ///# use yew::format::{Json, Nothing, Format};
+ ///# use yew::services::FetchService;
+ ///# use http::Request;
+ ///# use yew::services::fetch::Response;
+ ///# use yew::{Component, ComponentLink, Renderable, Html};
+ ///# use serde_derive::Deserialize;
+ ///# struct Comp;
+ ///# impl Component for Comp {
+ ///# type Message = Msg;type Properties = ();
+ ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()}
+ ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
+ ///# fn view(&self) -> Html {unimplemented!()}
+ ///# }
+ ///# enum Msg {
+ ///# FetchResourceComplete(Data),
+ ///# FetchResourceFailed
+ ///# }
+ /// #[derive(Deserialize)]
+ /// struct Data {
+ /// value: String
+ /// }
+ ///
+ ///# fn dont_execute() {
+ ///# let link: ComponentLink = unimplemented!();
+ /// let get_request = Request::get("/thing").body(Nothing).unwrap();
+ /// let callback = link.callback(|response: Response>>| {
+ /// if let (meta, Json(Ok(body))) = response.into_parts() {
+ /// if meta.status.is_success() {
+ /// return Msg::FetchResourceComplete(body);
+ /// }
+ /// }
+ /// Msg::FetchResourceFailed
+ /// });
+ ///
+ /// let task = FetchService::new().fetch(get_request, callback);
+ ///# }
+ /// ```
+ ///
+ pub fn fetch(
+ &mut self,
+ request: Request,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::(false, request, None, callback)
+ }
+
+ /// `fetch` with provided `FetchOptions` object.
+ /// Use it if you need to send cookies with a request:
+ /// ```
+ ///# use yew::format::Nothing;
+ ///# use yew::services::fetch::{self, FetchOptions, Credentials};
+ ///# use yew::{Renderable, Html, Component, ComponentLink};
+ ///# use yew::services::FetchService;
+ ///# use http::Response;
+ ///# struct Comp;
+ ///# impl Component for Comp {
+ ///# type Message = Msg;
+ ///# type Properties = ();
+ ///# fn create(props: Self::Properties, link: ComponentLink) -> Self {unimplemented!()}
+ ///# fn update(&mut self, msg: Self::Message) -> bool {unimplemented!()}
+ ///# fn view(&self) -> Html {unimplemented!()}
+ ///# }
+ ///# pub enum Msg {}
+ ///# fn dont_execute() {
+ ///# let link: ComponentLink = unimplemented!();
+ ///# let callback = link.callback(|response: Response>| unimplemented!());
+ /// let request = fetch::Request::get("/path/")
+ /// .body(Nothing)
+ /// .unwrap();
+ /// let options = FetchOptions {
+ /// credentials: Some(Credentials::SameOrigin),
+ /// ..FetchOptions::default()
+ /// };
+ /// let task = FetchService::new().fetch_with_options(request, options, callback);
+ ///# }
+ /// ```
+ pub fn fetch_with_options(
+ &mut self,
+ request: Request,
+ options: FetchOptions,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::(false, request, Some(options), callback)
+ }
+
+ /// Fetch the data in binary format.
+ pub fn fetch_binary(
+ &mut self,
+ request: Request,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::, ArrayBuffer>(true, request, None, callback)
+ }
+
+ /// Fetch the data in binary format.
+ pub fn fetch_binary_with_options(
+ &mut self,
+ request: Request,
+ options: FetchOptions,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::, ArrayBuffer>(true, request, Some(options), callback)
+ }
+}
+
+fn fetch_impl(
+ binary: bool,
+ request: Request,
+ options: Option,
+ callback: Callback>,
+) -> Result
+where
+ IN: Into>,
+ OUT: From>,
+ T: JsSerialize,
+ X: TryFrom + Into,
+{
+ // Consume request as parts and body.
+ let (parts, body) = request.into_parts();
+
+ // Map headers into a Js `Header` to make sure it's supported.
+ let header_list = parts
+ .headers
+ .iter()
+ .map(|(k, v)| {
+ Ok((
+ k.as_str(),
+ v.to_str().map_err(|_| "Unparsable request header")?,
+ ))
+ })
+ .collect::, _>>()?;
+ let header_map = js! {
+ try {
+ return new Headers(@{header_list});
+ } catch(error) {
+ return error;
+ }
+ };
+ if let Ok(_) = Error::try_from(js!( return @{header_map.as_ref()}; )) {
+ return Err("couldn't build headers");
+ }
+
+ // Formats URI.
+ let uri = parts.uri.to_string();
+ let method = parts.method.as_str();
+ let body = body.into().ok();
+
+ // Prepare the response callback.
+ // 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);
+ for (key, values) in headers {
+ response_builder = response_builder.header(key.as_str(), values.as_str());
+ }
+
+ // Deserialize and wrap response data into a Text object.
+ let data = if success {
+ Ok(data.into())
+ } else {
+ Err(FetchError::FailedResponse.into())
+ };
+ let out = OUT::from(data);
+ let response = response_builder.body(out).unwrap();
+ callback.emit(response);
+ };
+
+ #[allow(clippy::too_many_arguments)]
+ let handle = js! {
+ var body = @{body};
+ 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 = {
+ active: true,
+ callback,
+ abortController,
+ };
+ var init = @{Serde(options)} || {};
+ if (abortController && !("signal" in init)) {
+ init.signal = abortController.signal;
+ }
+ fetch(request, init).then(function(response) {
+ var promise = (@{binary}) ? response.arrayBuffer() : response.text();
+ var status = response.status;
+ var headers = {};
+ response.headers.forEach(function(value, key) {
+ headers[key] = value;
+ });
+ promise.then(function(data) {
+ if (handle.active == true) {
+ handle.active = false;
+ callback(true, status, headers, data);
+ callback.drop();
+ }
+ }).catch(function(err) {
+ if (handle.active == true) {
+ handle.active = false;
+ callback(false, status, headers, data);
+ callback.drop();
+ }
+ });
+ }).catch(function(e) {
+ if (handle.active == true) {
+ var data = (@{binary}) ? new ArrayBuffer() : "";
+ handle.active = false;
+ callback(false, 408, {}, data);
+ callback.drop();
+ }
+ });
+ return handle;
+ };
+ Ok(FetchTask(Some(handle)))
+}
+
+impl Task for FetchTask {
+ fn is_active(&self) -> bool {
+ if let Some(ref task) = self.0 {
+ let result = js! {
+ var the_task = @{task};
+ return the_task.active &&
+ (!the_task.abortController || !the_task.abortController.signal.aborted);
+ };
+ result.try_into().unwrap_or(false)
+ } 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");
+ 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();
+ }
+ }
+}
diff --git a/src/services/fetch/web_sys.rs b/src/services/fetch/web_sys.rs
new file mode 100644
index 00000000000..73b0b21f2b5
--- /dev/null
+++ b/src/services/fetch/web_sys.rs
@@ -0,0 +1,605 @@
+//! `web-sys` implementation for the fetch service.
+
+use crate::callback::Callback;
+use crate::format::{Binary, Format, Text};
+use crate::services::Task;
+use anyhow::{anyhow, Error};
+use http::request::Parts;
+use js_sys::{Array, Promise, Uint8Array};
+use std::cell::RefCell;
+use std::fmt;
+use std::iter::FromIterator;
+use std::marker::PhantomData;
+use std::rc::Rc;
+use thiserror::Error as ThisError;
+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,
+ Response as WebResponse,
+};
+
+pub use web_sys::{
+ RequestCache as Cache, RequestCredentials as Credentials, RequestMode as Mode,
+ RequestRedirect as Redirect, Window, WorkerGlobalScope,
+};
+
+pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};
+
+trait JsInterop: Sized {
+ fn from_js(js_value: JsValue) -> Result;
+ fn to_js(self) -> JsValue;
+}
+
+impl JsInterop for Vec {
+ fn from_js(js_value: JsValue) -> Result {
+ Ok(Uint8Array::new(&js_value).to_vec())
+ }
+
+ fn to_js(self) -> JsValue {
+ Uint8Array::from(self.as_slice()).into()
+ }
+}
+
+impl JsInterop for String {
+ fn from_js(js_value: JsValue) -> Result {
+ js_value.as_string().ok_or(FetchError::InternalError)
+ }
+
+ fn to_js(self) -> JsValue {
+ self.into()
+ }
+}
+
+/// Init options for `fetch()` function call.
+/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
+#[derive(Default, Debug)]
+pub struct FetchOptions {
+ /// Cache of a fetch request.
+ pub cache: Option,
+ /// Credentials of a fetch request.
+ pub credentials: Option,
+ /// Redirect behaviour of a fetch request.
+ pub redirect: Option,
+ /// Request mode of a fetch request.
+ pub mode: Option,
+}
+
+impl Into for FetchOptions {
+ fn into(self) -> RequestInit {
+ let mut init = RequestInit::new();
+
+ if let Some(cache) = self.cache {
+ init.cache(cache);
+ }
+
+ if let Some(credentials) = self.credentials {
+ init.credentials(credentials);
+ }
+
+ if let Some(redirect) = self.redirect {
+ init.redirect(redirect);
+ }
+
+ if let Some(mode) = self.mode {
+ init.mode(mode);
+ }
+
+ init
+ }
+}
+
+// convert `headers` to `Iterator- `
+fn header_iter(headers: Headers) -> impl Iterator
- {
+ js_sys::try_iter(&headers)
+ .unwrap()
+ .unwrap()
+ .map(Result::unwrap)
+ .map(|entry| {
+ let entry = Array::from(&entry);
+ let key = entry.get(0);
+ let value = entry.get(1);
+ (key.as_string().unwrap(), value.as_string().unwrap())
+ })
+}
+
+/// Represents errors of a fetch service.
+#[derive(Debug, ThisError)]
+enum FetchError {
+ #[error("canceled")]
+ Canceled,
+ #[error("fetch failed")]
+ FetchFailed,
+ #[error("invalid response")]
+ InvalidResponse,
+ #[error("unexpected error, please report")]
+ InternalError,
+ #[error("network failure")]
+ NetworkFailure,
+}
+
+#[derive(Debug)]
+struct Handle {
+ active: Rc>,
+ abort_controller: Option,
+}
+
+/// A handle to control sent requests. Can be canceled with a `Task::cancel` call.
+#[must_use]
+pub struct FetchTask(Option);
+
+impl fmt::Debug for FetchTask {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str("FetchTask")
+ }
+}
+
+/// A service to fetch resources.
+#[derive(Default, Debug)]
+pub struct FetchService {}
+
+impl FetchService {
+ /// Creates a new service instance connected to `App` by provided `sender`.
+ pub fn new() -> Self {
+ Self {}
+ }
+
+ /// Sends a request to a remote server given a Request object and a callback
+ /// fuction to convert a Response object into a loop's message.
+ ///
+ /// You may use a Request builder to build your request declaratively as on the
+ /// following examples:
+ ///
+ /// ```
+ ///# use yew::format::{Nothing, Json};
+ ///# use yew::services::fetch::Request;
+ ///# use serde_json::json;
+ /// let post_request = Request::post("https://my.api/v1/resource")
+ /// .header("Content-Type", "application/json")
+ /// .body(Json(&json!({"foo": "bar"})))
+ /// .expect("Failed to build request.");
+ ///
+ /// let get_request = Request::get("https://my.api/v1/resource")
+ /// .body(Nothing)
+ /// .expect("Failed to build request.");
+ /// ```
+ ///
+ /// The callback function can build a loop message by passing or analizing the
+ /// response body and metadata.
+ ///
+ /// ```
+ ///# use yew::{Component, ComponentLink, Html, Renderable};
+ ///# use yew::services::FetchService;
+ ///# use yew::services::fetch::{Response, Request};
+ ///# struct Comp;
+ ///# impl Component for Comp {
+ ///# type Message = Msg;type Properties = ();
+ ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()}
+ ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
+ ///# fn view(&self) -> Html {unimplemented!()}
+ ///# }
+ ///# enum Msg {
+ ///# Noop,
+ ///# Error
+ ///# }
+ ///# fn dont_execute() {
+ ///# let link: ComponentLink = unimplemented!();
+ ///# let mut fetch_service: FetchService = FetchService::new();
+ ///# let post_request: Request> = unimplemented!();
+ /// let task = fetch_service.fetch(
+ /// post_request,
+ /// link.callback(|response: Response>| {
+ /// if response.status().is_success() {
+ /// Msg::Noop
+ /// } else {
+ /// Msg::Error
+ /// }
+ /// }),
+ /// );
+ ///# }
+ /// ```
+ ///
+ /// For a full example, you can specify that the response must be in the JSON format,
+ /// and be a specific serialized data type. If the mesage isn't Json, or isn't the specified
+ /// data type, then you will get a message indicating failure.
+ ///
+ /// ```
+ ///# use yew::format::{Json, Nothing, Format};
+ ///# use yew::services::FetchService;
+ ///# use http::Request;
+ ///# use yew::services::fetch::Response;
+ ///# use yew::{Component, ComponentLink, Renderable, Html};
+ ///# use serde_derive::Deserialize;
+ ///# struct Comp;
+ ///# impl Component for Comp {
+ ///# type Message = Msg;type Properties = ();
+ ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()}
+ ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
+ ///# fn view(&self) -> Html {unimplemented!()}
+ ///# }
+ ///# enum Msg {
+ ///# FetchResourceComplete(Data),
+ ///# FetchResourceFailed
+ ///# }
+ /// #[derive(Deserialize)]
+ /// struct Data {
+ /// value: String
+ /// }
+ ///
+ ///# fn dont_execute() {
+ ///# let link: ComponentLink = unimplemented!();
+ /// let get_request = Request::get("/thing").body(Nothing).unwrap();
+ /// let callback = link.callback(|response: Response>>| {
+ /// if let (meta, Json(Ok(body))) = response.into_parts() {
+ /// if meta.status.is_success() {
+ /// return Msg::FetchResourceComplete(body);
+ /// }
+ /// }
+ /// Msg::FetchResourceFailed
+ /// });
+ ///
+ /// let task = FetchService::new().fetch(get_request, callback);
+ ///# }
+ /// ```
+ ///
+ pub fn fetch(
+ &mut self,
+ request: Request,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::(false, request, None, callback)
+ }
+
+ /// `fetch` with provided `FetchOptions` object.
+ /// Use it if you need to send cookies with a request:
+ /// ```
+ ///# use yew::format::Nothing;
+ ///# use yew::services::fetch::{self, FetchOptions, Credentials};
+ ///# use yew::{Renderable, Html, Component, ComponentLink};
+ ///# use yew::services::FetchService;
+ ///# use http::Response;
+ ///# struct Comp;
+ ///# impl Component for Comp {
+ ///# type Message = Msg;
+ ///# type Properties = ();
+ ///# fn create(props: Self::Properties, link: ComponentLink) -> Self {unimplemented!()}
+ ///# fn update(&mut self, msg: Self::Message) -> bool {unimplemented!()}
+ ///# fn view(&self) -> Html {unimplemented!()}
+ ///# }
+ ///# pub enum Msg {}
+ ///# fn dont_execute() {
+ ///# let link: ComponentLink = unimplemented!();
+ ///# let callback = link.callback(|response: Response>| unimplemented!());
+ /// let request = fetch::Request::get("/path/")
+ /// .body(Nothing)
+ /// .unwrap();
+ /// let options = FetchOptions {
+ /// credentials: Some(Credentials::SameOrigin),
+ /// ..FetchOptions::default()
+ /// };
+ /// let task = FetchService::new().fetch_with_options(request, options, callback);
+ ///# }
+ /// ```
+ pub fn fetch_with_options(
+ &mut self,
+ request: Request,
+ options: FetchOptions,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::(false, request, Some(options), callback)
+ }
+
+ /// Fetch the data in binary format.
+ pub fn fetch_binary(
+ &mut self,
+ request: Request,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::>(true, request, None, callback)
+ }
+
+ /// Fetch the data in binary format.
+ pub fn fetch_binary_with_options(
+ &mut self,
+ request: Request,
+ options: FetchOptions,
+ callback: Callback>,
+ ) -> Result
+ where
+ IN: Into,
+ OUT: From,
+ {
+ fetch_impl::>(true, request, Some(options), callback)
+ }
+}
+
+fn fetch_impl(
+ binary: bool,
+ request: Request,
+ options: Option,
+ callback: Callback>,
+) -> Result
+where
+ DATA: JsInterop,
+ IN: Into>,
+ OUT: From>,
+{
+ // Transform http::Request into WebRequest.
+ let (parts, body) = request.into_parts();
+ let body = match body.into() {
+ Ok(b) => b.to_js(),
+ Err(_) => JsValue::NULL,
+ };
+ let request = build_request(parts, &body)?;
+
+ // Transform FetchOptions into RequestInit.
+ let abort_controller = AbortController::new().ok();
+ let mut init = options.map_or_else(RequestInit::new, Into::into);
+ if let Some(abort_controller) = &abort_controller {
+ init.signal(Some(&abort_controller.signal()));
+ }
+
+ // Start fetch
+ let promise = GLOBAL.with(|global| global.fetch_with_request_and_init(&request, &init));
+
+ // Spawn future to resolve fetch
+ let active = Rc::new(RefCell::new(true));
+ let data_fetcher = DataFetcher::new(binary, callback, active.clone());
+ spawn_local(DataFetcher::fetch_data(data_fetcher, promise));
+
+ Ok(FetchTask(Some(Handle {
+ active,
+ abort_controller,
+ })))
+}
+
+struct DataFetcher
+where
+ DATA: JsInterop,
+ OUT: From>,
+{
+ binary: bool,
+ active: Rc>,
+ callback: Callback>,
+ _marker: PhantomData,
+}
+
+impl DataFetcher
+where
+ DATA: JsInterop,
+ OUT: From>,
+{
+ fn new(binary: bool, callback: Callback>, active: Rc>) -> Self {
+ Self {
+ binary,
+ callback,
+ active,
+ _marker: PhantomData::default(),
+ }
+ }
+
+ async fn fetch_data(self, promise: Promise) {
+ let result = self.fetch_data_impl(promise).await;
+ let (data, status, headers) = match result {
+ Ok((data, response)) => (Ok(data), response.status(), Some(response.headers())),
+ Err(err) => (Err(err), 408, None),
+ };
+ self.callback(data, status, headers);
+ }
+
+ async fn fetch_data_impl(&self, promise: Promise) -> Result<(DATA, WebResponse), Error> {
+ let response = self.get_response(promise).await?;
+ let data = self.get_data(&response).await?;
+ Ok((data, response))
+ }
+
+ // Prepare the response callback.
+ // Notice that the callback signature must match the call from the javascript
+ // side. There is no static check at this point.
+ fn callback(&self, data: Result, status: u16, headers: Option) {
+ let mut response_builder = Response::builder().status(status);
+ if let Some(headers) = headers {
+ for (key, value) in header_iter(headers) {
+ response_builder = response_builder.header(key.as_str(), value.as_str());
+ }
+ }
+
+ // Deserialize and wrap response data into a Text or Binary object.
+ let response = response_builder
+ .body(OUT::from(data))
+ .expect("failed to build response, please report");
+ *self.active.borrow_mut() = false;
+ self.callback.emit(response);
+ }
+
+ async fn get_response(&self, fetch_promise: Promise) -> Result {
+ let response = JsFuture::from(fetch_promise).await.map_err(|err| {
+ let dom_exception = DomException::from(err);
+ match dom_exception.code() {
+ DomException::ABORT_ERR => FetchError::Canceled,
+ DomException::NETWORK_ERR => FetchError::NetworkFailure,
+ _ => FetchError::FetchFailed,
+ }
+ })?;
+
+ if *self.active.borrow() {
+ Ok(WebResponse::from(response))
+ } else {
+ Err(FetchError::Canceled)
+ }
+ }
+
+ async fn get_data(&self, response: &WebResponse) -> Result {
+ let data_promise = if self.binary {
+ response.array_buffer()
+ } else {
+ response.text()
+ }
+ .map_err(|_| FetchError::InvalidResponse)?;
+
+ let data_result = JsFuture::from(data_promise).await;
+ if *self.active.borrow() {
+ data_result
+ .map_err(|_| FetchError::InvalidResponse)
+ .and_then(DATA::from_js)
+ } else {
+ Err(FetchError::Canceled)
+ }
+ }
+}
+
+fn build_request(parts: Parts, body: &JsValue) -> Result {
+ // Map headers into a Js `Header` type.
+ let header_list = parts
+ .headers
+ .iter()
+ .map(|(k, v)| {
+ Ok(Array::from_iter(&[
+ JsValue::from_str(k.as_str()),
+ JsValue::from_str(
+ v.to_str()
+ .map_err(|_| anyhow!("Unparsable request header"))?,
+ ),
+ ]))
+ })
+ .collect::>()?;
+
+ let header_map = Headers::new_with_str_sequence_sequence(&header_list)
+ .map_err(|_| anyhow!("couldn't build headers"))?;
+
+ // Formats URI.
+ let uri = parts.uri.to_string();
+ let method = parts.method.as_str();
+ let mut init = RequestInit::new();
+ init.method(method).body(Some(body)).headers(&header_map);
+ WebRequest::new_with_str_and_init(&uri, &init).map_err(|_| anyhow!("failed to build request"))
+}
+
+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();
+ }
+ }
+}
+
+impl Drop for FetchTask {
+ fn drop(&mut self) {
+ if self.is_active() {
+ self.cancel();
+ }
+ }
+}
+
+thread_local! {
+ static GLOBAL: WindowOrWorker = WindowOrWorker::new();
+}
+
+enum WindowOrWorker {
+ Window(Window),
+ Worker(WorkerGlobalScope),
+}
+
+impl WindowOrWorker {
+ fn new() -> Self {
+ #[wasm_bindgen]
+ extern "C" {
+ type Global;
+
+ #[wasm_bindgen(method, getter, js_name = Window)]
+ fn window(this: &Global) -> JsValue;
+
+ #[wasm_bindgen(method, getter, js_name = WorkerGlobalScope)]
+ fn worker(this: &Global) -> JsValue;
+ }
+
+ let global: Global = js_sys::global().unchecked_into();
+
+ if !global.window().is_undefined() {
+ Self::Window(global.unchecked_into())
+ } else if !global.worker().is_undefined() {
+ Self::Worker(global.unchecked_into())
+ } else {
+ panic!("Only supported in a browser or web worker");
+ }
+ }
+}
+
+impl WindowOrWorker {
+ fn fetch_with_request_and_init(&self, input: &WebRequest, init: &RequestInit) -> Promise {
+ match self {
+ Self::Window(window) => window.fetch_with_request_and_init(input, init),
+ Self::Worker(worker) => worker.fetch_with_request_and_init(input, init),
+ }
+ }
+}
+
+#[cfg(test)]
+#[cfg(feature = "wasm_test")]
+mod tests {
+ use super::*;
+ use crate::callback::test_util::CallbackFuture;
+ use crate::format::{Json, Nothing};
+ use serde::Deserialize;
+ use std::collections::HashMap;
+ use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
+
+ wasm_bindgen_test_configure!(run_in_browser);
+
+ #[derive(Deserialize, Debug)]
+ struct HttpBin {
+ headers: HashMap,
+ origin: String,
+ url: String,
+ }
+
+ #[test]
+ async fn fetch_redirect_default() {
+ let request = Request::get("https://httpbin.org/relative-redirect/1")
+ .body(Nothing)
+ .unwrap();
+ let options = FetchOptions::default();
+ let cb_future = CallbackFuture::>>>::default();
+ let callback: Callback<_> = cb_future.clone().into();
+ let _task = FetchService::new()
+ .fetch_with_options(request, options, callback)
+ .expect("failed to fetch");
+ let resp = cb_future.await;
+ assert_eq!(resp.status(), StatusCode::OK);
+ if let Json(Ok(http_bin)) = resp.body() {
+ assert_eq!(http_bin.url, String::from("https://httpbin.org/get"));
+ } else {
+ assert!(false, "unexpected resp: {:#?}", resp);
+ }
+ }
+}
diff --git a/src/services/mod.rs b/src/services/mod.rs
index a3cdbfffca8..30a3968724d 100644
--- a/src/services/mod.rs
+++ b/src/services/mod.rs
@@ -5,7 +5,6 @@
pub mod console;
pub mod dialog;
-#[cfg(feature = "std_web")]
pub mod fetch;
pub mod interval;
pub mod keyboard;
@@ -19,7 +18,6 @@ pub mod websocket;
pub use self::console::ConsoleService;
pub use self::dialog::DialogService;
-#[cfg(feature = "std_web")]
pub use self::fetch::FetchService;
pub use self::interval::IntervalService;
#[cfg(feature = "std_web")]
From e4762ca88d748d5e2b66b56773cc262e1fcd288a Mon Sep 17 00:00:00 2001
From: daxpedda
Date: Mon, 27 Jan 2020 17:38:12 +0100
Subject: [PATCH 14/28] `web-sys` reader service conversion (#868)
* Split reader implementation.
* Revert split.
* Remove leftover files.
* Make reader available again.
* Revert "Revert split."
This reverts commit 8abdc9cf2b014ab61fef2c48d1af8927d9a5a330.
* Revert "Remove leftover files."
This reverts commit 188c6eb9693881c9987061de8deb29c6f4c613a4.
* Re-revert split.
* Polish.
* Forgot some part.
* Some polish.
* Some polish.
---
src/services/mod.rs | 2 -
src/services/reader.rs | 126 +++---------------------------
src/services/reader/std_web.rs | 137 +++++++++++++++++++++++++++++++++
src/services/reader/web_sys.rs | 112 +++++++++++++++++++++++++++
4 files changed, 260 insertions(+), 117 deletions(-)
create mode 100644 src/services/reader/std_web.rs
create mode 100644 src/services/reader/web_sys.rs
diff --git a/src/services/mod.rs b/src/services/mod.rs
index 30a3968724d..350349e2566 100644
--- a/src/services/mod.rs
+++ b/src/services/mod.rs
@@ -8,7 +8,6 @@ pub mod dialog;
pub mod fetch;
pub mod interval;
pub mod keyboard;
-#[cfg(feature = "std_web")]
pub mod reader;
pub mod render;
pub mod resize;
@@ -20,7 +19,6 @@ pub use self::console::ConsoleService;
pub use self::dialog::DialogService;
pub use self::fetch::FetchService;
pub use self::interval::IntervalService;
-#[cfg(feature = "std_web")]
pub use self::reader::ReaderService;
pub use self::render::RenderService;
pub use self::resize::ResizeService;
diff --git a/src/services/reader.rs b/src/services/reader.rs
index c1fd144b098..c57bdc2dad7 100644
--- a/src/services/reader.rs
+++ b/src/services/reader.rs
@@ -1,15 +1,16 @@
//! Service to load files using `FileReader`.
-use super::Task;
-use crate::callback::Callback;
-use std::cmp;
+use crate::services::Task;
use std::fmt;
-use stdweb::unstable::TryInto;
-use stdweb::web::event::LoadEndEvent;
-pub use stdweb::web::{Blob, File, IBlob};
-use stdweb::web::{FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, TypedArray};
-#[allow(unused_imports)]
-use stdweb::{_js_impl, js};
+cfg_if::cfg_if! {
+ if #[cfg(feature = "std_web")] {
+ mod std_web;
+ pub use std_web::*;
+ } else if #[cfg(feature = "web_sys")] {
+ mod web_sys;
+ pub use self::web_sys::*;
+ }
+}
/// Struct that represents data of a file.
#[derive(Clone, Debug)]
@@ -43,119 +44,14 @@ pub enum FileChunk {
#[derive(Default, Debug)]
pub struct ReaderService {}
-impl ReaderService {
- /// Creates a new service instance connected to `App` by provided `sender`.
- pub fn new() -> Self {
- Self {}
- }
-
- /// Reads all bytes from a file and returns them with a callback.
- pub fn read_file(&mut self, file: File, callback: Callback) -> ReaderTask {
- let file_reader = FileReader::new();
- let reader = file_reader.clone();
- let name = file.name();
- file_reader.add_event_listener(move |_event: LoadEndEvent| match reader.result() {
- Some(FileReaderResult::String(_)) => {
- unreachable!();
- }
- Some(FileReaderResult::ArrayBuffer(buffer)) => {
- let array: TypedArray = buffer.into();
- let data = FileData {
- name: name.clone(),
- content: array.to_vec(),
- };
- callback.emit(data);
- }
- None => {}
- });
- file_reader.read_as_array_buffer(&file).unwrap();
- ReaderTask { file_reader }
- }
-
- /// Reads data chunks from a file and returns them with a callback.
- pub fn read_file_by_chunks(
- &mut self,
- file: File,
- callback: Callback,
- chunk_size: usize,
- ) -> ReaderTask {
- let file_reader = FileReader::new();
- let name = file.name();
- let mut position = 0;
- let total_size = file.len() as usize;
- let reader = file_reader.clone();
- file_reader.add_event_listener(move |_event: LoadEndEvent| {
- match reader.result() {
- // This branch is used to start reading
- Some(FileReaderResult::String(_)) => {
- let started = FileChunk::Started { name: name.clone() };
- callback.emit(started);
- }
- // This branch is used to send a chunk value
- Some(FileReaderResult::ArrayBuffer(buffer)) => {
- let array: TypedArray = buffer.into();
- let chunk = FileChunk::DataChunk {
- data: array.to_vec(),
- progress: position as f32 / total_size as f32,
- };
- callback.emit(chunk);
- }
- None => {}
- }
- // Read the next chunk
- if position < total_size {
- let file = &file;
- let from = position;
- let to = cmp::min(position + chunk_size, total_size);
- position = to;
- // TODO Implement `slice` method in `stdweb`
- let blob: Blob = (js! {
- return @{file}.slice(@{from as u32}, @{to as u32});
- })
- .try_into()
- .unwrap();
- reader.read_as_array_buffer(&blob).unwrap();
- } else {
- let finished = FileChunk::Finished;
- callback.emit(finished);
- }
- });
- let blob: Blob = (js! {
- return (new Blob());
- })
- .try_into()
- .unwrap();
- file_reader.read_as_text(&blob).unwrap();
- ReaderTask { file_reader }
- }
-}
-
-/// A handle to control reading.
-#[must_use]
-pub struct ReaderTask {
- file_reader: FileReader,
-}
-
impl fmt::Debug for ReaderTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ReaderTask")
}
}
-impl Task for ReaderTask {
- fn is_active(&self) -> bool {
- self.file_reader.ready_state() == FileReaderReadyState::Loading
- }
-
- fn cancel(&mut self) {
- self.file_reader.abort();
- }
-}
-
impl Drop for ReaderTask {
fn drop(&mut self) {
- if self.is_active() {
- self.cancel();
- }
+ self.cancel();
}
}
diff --git a/src/services/reader/std_web.rs b/src/services/reader/std_web.rs
new file mode 100644
index 00000000000..03b72e0fa18
--- /dev/null
+++ b/src/services/reader/std_web.rs
@@ -0,0 +1,137 @@
+//! `stdweb` implementation for the reader service.
+
+use super::*;
+use crate::callback::Callback;
+use crate::services::Task;
+use std::cmp;
+use stdweb::unstable::{TryFrom, TryInto};
+use stdweb::web::event::LoadEndEvent;
+pub use stdweb::web::{Blob, File, IBlob};
+use stdweb::web::{FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, TypedArray};
+#[allow(unused_imports)]
+use stdweb::{_js_impl, js};
+
+fn new_file_reader() -> Result {
+ let file_reader = js! {
+ try {
+ return new FileReader;
+ } catch(error) {
+ return error;
+ }
+ };
+ FileReader::try_from(js!( return @{file_reader.as_ref()}; ))
+ .map_err(|_| "couldn't aquire file reader")
+}
+
+impl ReaderService {
+ /// Creates a new service instance connected to `App` by provided `sender`.
+ pub fn new() -> Self {
+ Self {}
+ }
+
+ /// Reads all bytes from a file and returns them with a callback.
+ pub fn read_file(
+ &mut self,
+ file: File,
+ callback: Callback,
+ ) -> Result {
+ let file_reader = new_file_reader()?;
+ let reader = file_reader.clone();
+ let name = file.name();
+ file_reader.add_event_listener(move |_event: LoadEndEvent| match reader.result() {
+ Some(FileReaderResult::String(_)) => {
+ unreachable!();
+ }
+ Some(FileReaderResult::ArrayBuffer(buffer)) => {
+ let array: TypedArray = buffer.into();
+ let data = FileData {
+ name: name.clone(),
+ content: array.to_vec(),
+ };
+ callback.emit(data);
+ }
+ None => {}
+ });
+ file_reader.read_as_array_buffer(&file).unwrap();
+ Ok(ReaderTask { file_reader })
+ }
+
+ /// Reads data chunks from a file and returns them with a callback.
+ pub fn read_file_by_chunks(
+ &mut self,
+ file: File,
+ callback: Callback