Adding in async example

This commit is contained in:
Pauan 2020-03-21 04:00:56 +01:00
parent 5ed4c0ae05
commit 24920fd7af
8 changed files with 387 additions and 0 deletions

5
examples/async/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
/dist/js
/target
/wasm-pack.log
/yarn-error.log

41
examples/async/Cargo.toml Normal file
View File

@ -0,0 +1,41 @@
[package]
name = "async"
version = "0.1.0"
description = "Async example using dominator"
authors = ["Pauan <pauanyu+github@pm.me>"]
categories = ["wasm"]
readme = "README.md"
license = "MIT"
edition = "2018"
[profile.release]
lto = true
[lib]
crate-type = ["cdylib"]
[dependencies]
console_error_panic_hook = "0.1.5"
dominator = "0.5.0"
futures = "0.3.4"
futures-signals = "0.3.0"
wasm-bindgen-futures = "0.4.9"
wasm-bindgen = "0.2.48"
gloo-timers = { version = "0.2.1", features = ["futures"] }
js-sys = "0.3.36"
serde_json = "1.0.10"
serde_derive = "1.0.27"
serde = "1.0.27"
lazy_static = "1.0.0"
[dependencies.web-sys]
version = "0.3.22"
features = [
"console",
"AbortController",
"AbortSignal",
"Headers",
"Response",
"RequestInit",
"Window",
]

12
examples/async/README.md Normal file
View File

@ -0,0 +1,12 @@
## How to install
```sh
yarn install
```
## How to build
```sh
# Builds the project and places it into the `dist` folder.
yarn run build
```

10
examples/async/dist/index.html vendored Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>rust-dominator • Async</title>
</head>
<body>
<script src="js/index.js"></script>
</body>
</html>

View File

@ -0,0 +1,14 @@
{
"private": true,
"author": "Pauan <pauanyu+github@pm.me>",
"name": "async",
"version": "0.1.0",
"scripts": {
"build": "rimraf dist/js && rollup --config"
},
"devDependencies": {
"@wasm-tool/rollup-plugin-rust": "^1.0.0",
"rimraf": "^3.0.2",
"rollup": "^1.31.0"
}
}

View File

@ -0,0 +1,17 @@
import rust from "@wasm-tool/rollup-plugin-rust";
export default {
input: {
index: "./Cargo.toml",
},
output: {
dir: "dist/js",
format: "iife",
sourcemap: true,
},
plugins: [
rust({
serverPath: "js/",
}),
],
};

151
examples/async/src/lib.rs Normal file
View File

@ -0,0 +1,151 @@
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use gloo_timers::future::TimeoutFuture;
use serde_derive::{Serialize, Deserialize};
use futures_signals::signal::{Mutable, not};
use dominator::{html, class, events, clone, with_node, Dom};
use web_sys::HtmlInputElement;
use lazy_static::lazy_static;
use util::*;
mod util;
#[derive(Debug, Serialize, Deserialize)]
struct User {
login: String,
id: u32,
node_id: String,
avatar_url: String,
gravatar_id: String,
url: String,
html_url: String,
followers_url: String,
following_url: String,
gists_url: String,
starred_url: String,
subscriptions_url: String,
repos_url: String,
events_url: String,
received_events_url: String,
#[serde(rename = "type")]
type_: String,
site_admin: bool,
name: Option<String>,
company: Option<String>,
blog: String,
location: Option<String>,
email: Option<String>,
hireable: Option<bool>,
bio: Option<String>,
public_repos: u32,
public_gists: u32,
followers: u32,
following: u32,
created_at: String,
updated_at: String,
}
impl User {
async fn fetch(user: &str) -> Result<Self, JsValue> {
let user = fetch_github(&format!("https://api.github.com/users/{}", user)).await?;
Ok(serde_json::from_str::<Self>(&user).unwrap_throw())
}
}
struct App {
user: Mutable<Option<User>>,
input: Mutable<String>,
loader: AsyncLoader,
}
impl App {
fn new(name: &str, user: Option<User>) -> Rc<Self> {
Rc::new(Self {
user: Mutable::new(user),
input: Mutable::new(name.to_string()),
loader: AsyncLoader::new(),
})
}
fn render(app: Rc<Self>) -> Dom {
lazy_static! {
static ref APP: String = class! {
.style("white-space", "pre")
};
}
html!("div", {
.class(&*APP)
.children(&mut [
html!("input" => HtmlInputElement, {
.property_signal("value", app.input.signal_cloned())
.with_node!(element => {
.event(clone!(app => move |_: events::Input| {
app.input.set(element.value());
}))
})
}),
html!("button", {
.text("Lookup user")
.event(clone!(app => move |_: events::Click| {
let input = app.input.lock_ref();
if *input == "" {
app.user.set(None);
} else {
let input = input.to_string();
app.loader.load(clone!(app => async move {
// Simulate a slow network
TimeoutFuture::new(5_000).await;
let user = User::fetch(&input).await.ok();
app.user.set(user);
}));
}
}))
}),
html!("button", {
.text("Cancel")
.event(clone!(app => move |_: events::Click| {
app.loader.cancel();
}))
}),
html!("div", {
.visible_signal(app.loader.is_loading())
.text("LOADING")
}),
html!("div", {
.visible_signal(not(app.loader.is_loading()))
.text_signal(app.user.signal_ref(|user| format!("{:#?}", user)))
}),
])
})
}
}
#[wasm_bindgen(start)]
pub async fn main_js() -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let user = User::fetch("Pauan").await.ok();
let app = App::new("Pauan", user);
dominator::append_dom(&dominator::body(), App::render(app));
Ok(())
}

137
examples/async/src/util.rs Normal file
View File

@ -0,0 +1,137 @@
use std::future::Future;
use std::sync::atomic::{AtomicUsize, Ordering};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use futures_signals::signal::{Signal, Mutable};
use wasm_bindgen_futures::{JsFuture, spawn_local};
use futures::future::{abortable, AbortHandle};
use js_sys::Error;
use web_sys::{window, Response, RequestInit, Headers, AbortController, AbortSignal};
struct AsyncState {
id: usize,
handle: AbortHandle,
}
impl AsyncState {
fn new(handle: AbortHandle) -> Self {
static ID: AtomicUsize = AtomicUsize::new(0);
let id = ID.fetch_add(1, Ordering::SeqCst);
Self { id, handle }
}
}
pub struct AsyncLoader {
loading: Mutable<Option<AsyncState>>,
}
impl AsyncLoader {
pub fn new() -> Self {
Self {
loading: Mutable::new(None),
}
}
pub fn cancel(&self) {
self.replace(None);
}
fn replace(&self, value: Option<AsyncState>) {
let mut loading = self.loading.lock_mut();
if let Some(state) = loading.as_mut() {
state.handle.abort();
}
*loading = value;
}
pub fn load<F>(&self, fut: F) where F: Future<Output = ()> + 'static {
let (fut, handle) = abortable(fut);
let state = AsyncState::new(handle);
let id = state.id;
self.replace(Some(state));
let loading = self.loading.clone();
spawn_local(async move {
match fut.await {
Ok(()) => {
let mut loading = loading.lock_mut();
if let Some(current_id) = loading.as_ref().map(|x| x.id) {
// If it hasn't been overwritten with a new state...
if current_id == id {
*loading = None;
}
}
},
// It was already cancelled
Err(_) => {},
}
});
}
pub fn is_loading(&self) -> impl Signal<Item = bool> {
self.loading.signal_ref(|x| x.is_some())
}
}
struct Abort {
controller: AbortController,
}
impl Abort {
fn new() -> Result<Self, JsValue> {
Ok(Self {
controller: AbortController::new()?,
})
}
fn signal(&self) -> AbortSignal {
self.controller.signal()
}
}
impl Drop for Abort {
fn drop(&mut self) {
self.controller.abort();
}
}
pub async fn fetch_github(url: &str) -> Result<String, JsValue> {
let abort = Abort::new()?;
let headers = Headers::new()?;
headers.set("Accept", "application/vnd.github.v3+json")?;
let future = window()
.unwrap_throw()
.fetch_with_str_and_init(
url,
RequestInit::new()
.headers(&headers)
.signal(Some(&abort.signal())),
);
let response = JsFuture::from(future)
.await?
.unchecked_into::<Response>();
if !response.ok() {
return Err(Error::new("Fetch failed").into());
}
let value = JsFuture::from(response.text()?)
.await?
.as_string()
.unwrap_throw();
Ok(value)
}