├── .nowignore ├── static ├── logo.png ├── manifest.json └── index.css ├── .gitignore ├── index.js ├── now.json ├── src ├── console.rs ├── fetch.rs ├── types.rs ├── app.rs ├── lib.rs ├── controller.rs ├── view.rs └── view │ └── element.rs ├── package.json ├── readme.md ├── Cargo.toml ├── index.html └── webpack.config.js /.nowignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | target 3 | src 4 | package.json -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ragingwind/wasm-hnpwa/HEAD/static/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | /pkg 5 | node_modules 6 | yarn.lock 7 | dist 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const rust = import('./pkg/wasm_hnpwa.js'); 2 | 3 | document.addEventListener('app', e => { 4 | console.log('event', e); 5 | }); 6 | 7 | rust.then(m => m.run()).catch(console.error); 8 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "wasm-hnpwa", 4 | "alias": ["wasm-hnpwa.now.sh"], 5 | "routes": [ 6 | { "src": "/service-worker.js", "dest": "/dist/service-worker.js" }, 7 | { "src": "/pkg", "dest": "./pkg" }, 8 | { "src": "/(.*)", "dest": "/dist/$1" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/console.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | #[wasm_bindgen] 4 | extern "C" { 5 | #[wasm_bindgen(js_namespace = console)] 6 | pub fn log(s: &str); 7 | 8 | #[wasm_bindgen(js_namespace = console, js_name=log)] 9 | pub fn log_u32(a: u32); 10 | 11 | #[wasm_bindgen(js_namespace = console, js_name=log)] 12 | pub fn log_many(a: &str, b: &str); 13 | } 14 | 15 | #[macro_export] 16 | macro_rules! console_log { 17 | ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) 18 | } 19 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "HNPWA-WASM", 3 | "name": "HNPWA with WASM", 4 | "icons": [ 5 | { 6 | "src": "logo.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "logo.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": "/?source=pwa", 17 | "background_color": "#000000", 18 | "display": "standalone", 19 | "scope": "/", 20 | "theme_color": "#000000" 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-hnpwa", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/ragingwind/wasm-hnpwa.git", 6 | "author": "Jimmy Moon ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "webpack", 10 | "serve": "webpack-dev-server", 11 | "now": "now alias wasm-hnpwa" 12 | }, 13 | "devDependencies": { 14 | "@wasm-tool/wasm-pack-plugin": "0.4.2", 15 | "copy-webpack-plugin": "^5.0.4", 16 | "html-webpack-plugin": "^3.2.0", 17 | "text-encoding": "^0.7.0", 18 | "webpack": "^4.29.4", 19 | "webpack-cli": "^3.1.1", 20 | "webpack-dev-server": "^3.1.0", 21 | "workbox-webpack-plugin": "^4.3.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # wasm-hnpwa 2 | 3 | > HNPWA with WASM, Warning you, it is still om experimental stage, PoC version for WebAssembly, Rust, wasm-pack, and wasm-bindgen. Application logic fully coded by rust in WASM module not HTML and JS also fetching. This is highly inspired by [wasm-bindgen todomvc example](https://github.com/rustwasm/wasm-bindgen/tree/master/examples/todomvc). also brought some of code and design from the example. 4 | 5 | HNPWA with WASM 6 | 7 | # How to build and run 8 | 9 | ```sh 10 | $ yarn build 11 | $ npx http-server ./dist 12 | ``` 13 | 14 | # Deployment 15 | 16 | This application is running powered by [now](https://zeit.co/now) with static build. Therefore, it might be needed to test the app on live. 17 | 18 | ```sh 19 | now 20 | ``` 21 | 22 | # License 23 | 24 | @ [Jimmy Moon](jimmymoon.dev) 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-hnpwa" 3 | version = "0.1.0" 4 | authors = ["Jimmy Moon "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | futures = "0.1.20" 12 | js-sys = "0.3.24" 13 | wasm-bindgen = { version = "0.2.47", features = ["serde-serialize"] } 14 | wasm-bindgen-futures = "0.3.24" 15 | serde = { version = "1.0.80", features = ["derive"] } 16 | serde_derive = "^1.0.59" 17 | console_error_panic_hook = "0.1.5" 18 | lazy_static = "1.3.0" 19 | serde_json = "1.0" 20 | 21 | [dependencies.web-sys] 22 | version = "0.3.4" 23 | features = [ 24 | 'console', 25 | 'CssStyleDeclaration', 26 | 'Document', 27 | 'DomStringMap', 28 | 'DomTokenList', 29 | 'Element', 30 | 'CustomEvent', 31 | 'CustomEventInit', 32 | 'Event', 33 | 'EventTarget', 34 | 'HtmlBodyElement', 35 | 'HtmlElement', 36 | 'HtmlInputElement', 37 | 'HtmlAnchorElement', 38 | 'KeyboardEvent', 39 | 'Location', 40 | 'Node', 41 | 'NodeList', 42 | 'Storage', 43 | 'Headers', 44 | 'Request', 45 | 'RequestInit', 46 | 'RequestMode', 47 | 'Response', 48 | 'Window' 49 | ] -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | HNPWA with WASM 11 | 12 | 13 | 14 |
15 | 26 |
27 |
28 |
29 |
30 |
31 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const { GenerateSW } = require('workbox-webpack-plugin'); 7 | 8 | module.exports = { 9 | entry: './index.js', 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | filename: 'index.js' 13 | }, 14 | plugins: [ 15 | new GenerateSW({ 16 | runtimeCaching: [ 17 | { 18 | urlPattern: /^http[s|]?.*/, 19 | handler: 'StaleWhileRevalidate' 20 | } 21 | ], 22 | clientsClaim: true, 23 | skipWaiting: true 24 | }), 25 | new CopyWebpackPlugin([{ from: 'static', to: 'static' }]), 26 | new HtmlWebpackPlugin({ 27 | template: 'index.html' 28 | }), 29 | new WasmPackPlugin({ 30 | crateDirectory: path.resolve(__dirname, '.') 31 | }), 32 | new webpack.ProvidePlugin({ 33 | TextDecoder: ['text-encoding', 'TextDecoder'], 34 | TextEncoder: ['text-encoding', 'TextEncoder'] 35 | }) 36 | ], 37 | mode: 'development' 38 | }; 39 | -------------------------------------------------------------------------------- /src/fetch.rs: -------------------------------------------------------------------------------- 1 | use futures::{future, Future}; 2 | use js_sys::Promise; 3 | use wasm_bindgen::prelude::*; 4 | use wasm_bindgen::JsCast; 5 | use wasm_bindgen_futures::future_to_promise; 6 | use wasm_bindgen_futures::JsFuture; 7 | use web_sys::{Request, RequestInit, RequestMode, Response}; 8 | 9 | #[wasm_bindgen] 10 | extern "C" { 11 | #[wasm_bindgen(js_namespace = window)] 12 | pub fn fetch(url: &str) -> Promise; 13 | } 14 | 15 | pub struct Fetch {} 16 | 17 | impl Fetch { 18 | pub fn get_json(ep: &String) -> Promise { 19 | let mut opts = RequestInit::new(); 20 | opts.method("GET"); 21 | opts.mode(RequestMode::Cors); 22 | 23 | let request = Request::new_with_str_and_init(ep, &opts).unwrap(); 24 | 25 | request.headers().set("Accept", "application/json").unwrap(); 26 | 27 | let window = web_sys::window().unwrap(); 28 | let request_promise = window.fetch_with_request(&request); 29 | let future = JsFuture::from(request_promise) 30 | .and_then(|resp_value| { 31 | let resp: Response = resp_value.dyn_into().unwrap(); 32 | resp.json() 33 | }) 34 | .and_then(|json_value: Promise| JsFuture::from(json_value)) 35 | .and_then(|json| future::ok(json)); 36 | 37 | future_to_promise(future) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize, Clone)] 4 | pub struct News { 5 | pub id: u64, 6 | pub title: String, 7 | pub points: Option, 8 | pub user: Option, 9 | pub time: u64, 10 | pub time_ago: String, 11 | pub comments_count: u64, 12 | pub r#type: String, 13 | pub url: String, 14 | pub domain: Option, 15 | } 16 | 17 | #[derive(Debug, Serialize, Deserialize, Clone)] 18 | pub struct Item { 19 | pub id: u64, 20 | pub title: Option, 21 | pub points: Option, 22 | pub user: Option, 23 | pub time: u64, 24 | pub time_ago: String, 25 | pub content: String, 26 | pub deleted: Option, 27 | pub dead: Option, 28 | pub r#type: String, 29 | pub url: Option, 30 | pub domain: Option, 31 | pub comments: Vec, 32 | pub level: Option, 33 | pub comments_count: u64, 34 | } 35 | 36 | #[derive(Debug, Serialize, Deserialize, Clone)] 37 | pub struct User { 38 | pub id: String, 39 | pub created: String, 40 | pub created_time: u64, 41 | pub karma: u64, 42 | } 43 | 44 | pub fn get_url(item_type: &str, page: u32) -> String { 45 | format!("https://api.hnpwa.com/v0/{}/{}.json", item_type, page) 46 | } 47 | 48 | pub fn to_static_str(s: String) -> &'static str { 49 | Box::leak(s.into_boxed_str()) 50 | } 51 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::{Controller, ControllerMessage}; 2 | use crate::view::{View, ViewMessage}; 3 | use std::cell::RefCell; 4 | use std::rc::Rc; 5 | 6 | pub enum Message { 7 | Controller(ControllerMessage), 8 | View(ViewMessage), 9 | } 10 | 11 | pub struct App { 12 | controller: Rc>>, 13 | view: Rc>>, 14 | events: RefCell>, 15 | } 16 | 17 | impl App { 18 | pub fn new() -> App { 19 | App { 20 | controller: Rc::new(RefCell::new(None)), 21 | view: Rc::new(RefCell::new(None)), 22 | events: RefCell::new(Vec::new()), 23 | } 24 | } 25 | 26 | pub fn set_controller(&self, controller: Controller) { 27 | if let Ok(mut controller_data) = self.controller.try_borrow_mut() { 28 | *controller_data = Some(controller); 29 | } 30 | } 31 | 32 | pub fn set_view(&self, view: View) { 33 | let mut view_data = self.view.try_borrow_mut().unwrap(); 34 | *view_data = Some(view); 35 | } 36 | 37 | pub fn add_message(&self, message: Message) { 38 | { 39 | let mut events = self.events.try_borrow_mut().unwrap(); 40 | events.push(message); 41 | } 42 | 43 | { 44 | self.run(); 45 | } 46 | } 47 | 48 | fn run(&self) { 49 | self.next_message(); 50 | } 51 | 52 | fn next_message(&self) { 53 | let event = { 54 | if let Ok(mut events) = self.events.try_borrow_mut() { 55 | Some(events.pop()) 56 | } else { 57 | None 58 | } 59 | }; 60 | 61 | if let Some(Some(event)) = event { 62 | match event { 63 | Message::Controller(e) => { 64 | let mut controller = self.controller.try_borrow_mut().unwrap(); 65 | if let Some(ref mut controller) = *controller { 66 | controller.call(e) 67 | } 68 | } 69 | Message::View(e) => { 70 | let mut view = self.view.try_borrow_mut().unwrap(); 71 | if let Some(ref mut view) = *view { 72 | view.call(e) 73 | } 74 | } 75 | } 76 | self.run(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::app::{App, Message}; 4 | use crate::console::*; 5 | use crate::controller::Controller; 6 | use crate::controller::ControllerMessage; 7 | use crate::types::*; 8 | use crate::view::View; 9 | use wasm_bindgen::prelude::*; 10 | use wasm_bindgen::JsCast; 11 | 12 | #[macro_use] 13 | mod console; 14 | mod app; 15 | mod controller; 16 | mod fetch; 17 | mod types; 18 | mod view; 19 | 20 | #[wasm_bindgen] 21 | pub fn app() { 22 | let app = Rc::new(App::new()); 23 | let view = View::new(); 24 | let controller = Controller::new(app.clone()); 25 | 26 | { 27 | app.set_view(view); 28 | app.set_controller(controller); 29 | } 30 | 31 | { 32 | let window = web_sys::window().unwrap(); 33 | let document = window.document().unwrap(); 34 | let location = document.location().unwrap(); 35 | 36 | let href = location.href().unwrap(); 37 | let mut domain: Vec<&str> = href.split("/").collect(); 38 | 39 | if let Some(hash) = domain.pop() { 40 | let mut hashes: Vec<&str> = hash.split("&").collect(); 41 | 42 | if hashes.len() < 2 { 43 | hashes.push("1"); 44 | } 45 | 46 | let hash = match hashes[0] { 47 | "news" => "news", 48 | "newest" => "newest", 49 | "ask" => "ask", 50 | "show" => "show", 51 | "jobs" => "jobs", 52 | "detail" => "detail", 53 | "user" => "user", 54 | "comment" => "comment", 55 | _ => "news", 56 | }; 57 | 58 | app.add_message(Message::Controller(ControllerMessage::ChangePage( 59 | to_static_str(format!("#/{}&{}", hash, hashes[1])), 60 | ))); 61 | } 62 | } 63 | 64 | { 65 | let window = web_sys::window().unwrap(); 66 | let document = window.document().unwrap(); 67 | 68 | let set_page = Closure::wrap(Box::new(move || { 69 | if let Some(location) = document.location() { 70 | if let Ok(hash) = location.hash() { 71 | console_log!("hash change {}", hash); 72 | app.add_message(Message::Controller(ControllerMessage::ChangePage( 73 | to_static_str(hash), 74 | ))); 75 | } 76 | } 77 | }) as Box); 78 | 79 | let et: web_sys::EventTarget = window.into(); 80 | et.add_event_listener_with_callback("hashchange", set_page.as_ref().unchecked_ref()) 81 | .unwrap(); 82 | 83 | set_page.forget(); 84 | } 85 | } 86 | 87 | #[wasm_bindgen] 88 | pub fn run() { 89 | console_error_panic_hook::set_once(); 90 | app() 91 | } 92 | -------------------------------------------------------------------------------- /static/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-size: 12px; 6 | overflow-x: hidden; 7 | width: 100vw; 8 | } 9 | 10 | header { 11 | width: 100vw; 12 | } 13 | 14 | nav { 15 | position: fixed; 16 | display: flex; 17 | top: 0; 18 | height: 48px; 19 | width: 100%; 20 | } 21 | 22 | nav h1 { 23 | padding: 0; 24 | margin: 0; 25 | } 26 | 27 | nav h1 img { 28 | height: 48px; 29 | } 30 | 31 | nav ul { 32 | width: 100%; 33 | margin: 0; 34 | background-color: rgba(0, 0, 0, 1); 35 | padding: 12px 0 0 30px; 36 | display: flex; 37 | justify-content: flex-start; 38 | } 39 | 40 | nav ul li { 41 | list-style: none; 42 | color: rgb(255, 255, 255); 43 | font-size: 1.2em; 44 | line-height: 1.5em; 45 | margin: 0 25px 0 0; 46 | } 47 | 48 | nav ul li a { 49 | cursor: pointer; 50 | color: white; 51 | } 52 | 53 | nav ul li a:visited { 54 | color: white; 55 | } 56 | 57 | section { 58 | margin-top: 6vh; 59 | } 60 | 61 | section ul { 62 | width: 100vw; 63 | padding: 0 0 1em 0; 64 | margin: 0; 65 | } 66 | 67 | section ul li { 68 | list-style: none; 69 | } 70 | 71 | .like_count { 72 | font-size: 1.6em; 73 | width: 1.2em; 74 | } 75 | 76 | .title { 77 | font-size: 1.6em; 78 | } 79 | 80 | .info { 81 | font-size: 1.4em; 82 | } 83 | 84 | footer { 85 | padding: 0 0 2em 0; 86 | } 87 | 88 | footer a { 89 | font-size: 1em; 90 | } 91 | 92 | .item { 93 | display: flex; 94 | flex-direction: row; 95 | align-items: center; 96 | padding: 16px 4px; 97 | border-bottom: 1px solid #efefef; 98 | } 99 | 100 | .points { 101 | display: flex; 102 | font-size: 2em; 103 | font-weight: 800; 104 | justify-content: center; 105 | align-items: center; 106 | height: 3em; 107 | width: 3em; 108 | flex-shrink: 0; 109 | } 110 | 111 | .content { 112 | display: flex; 113 | align-content: center; 114 | flex-direction: column; 115 | font-size: 1.5em; 116 | } 117 | 118 | .info { 119 | margin-top: 0.5em; 120 | color: #aeaeae; 121 | font-size: 1em; 122 | } 123 | 124 | #more { 125 | font-size: 1.5em; 126 | padding: 1em; 127 | } 128 | 129 | section div.item { 130 | display: flex; 131 | flex-direction: column; 132 | width: 100vw; 133 | padding: 0 0 1em 0; 134 | margin: 0; 135 | align-items: flex-start; 136 | padding: 12px 0 0 30px; 137 | } 138 | 139 | .domain { 140 | color: #aeaeae; 141 | font-size: 0.7em; 142 | } 143 | 144 | .user { 145 | display: flex; 146 | flex-direction: column; 147 | width: 100vw; 148 | padding: 0 0 1em 0; 149 | margin: 0; 150 | align-items: flex-start; 151 | padding: 12px 0 0 30px; 152 | } 153 | 154 | .user h2 { 155 | font-size: 3em; 156 | } 157 | 158 | .user .user_info { 159 | font-size: 1em; 160 | } 161 | 162 | .comment { 163 | display: flex; 164 | flex-direction: column; 165 | width: 100vw; 166 | margin: 0; 167 | align-items: flex-start; 168 | } 169 | 170 | .comment .user_info { 171 | width: 100%; 172 | display: flex; 173 | justify-content: flex-start; 174 | padding: 1em 1em 0; 175 | font-size: 1.5em; 176 | } 177 | 178 | .comment .user_id { 179 | font-weight: 800; 180 | margin-right: 15px; 181 | } 182 | 183 | .comment .content p { 184 | font-size: 1em; 185 | padding: 0 1em; 186 | color: rgba(0, 0, 0, 0.5); 187 | } 188 | -------------------------------------------------------------------------------- /src/controller.rs: -------------------------------------------------------------------------------- 1 | use super::console::*; 2 | use crate::app::{App, Message}; 3 | use crate::fetch::*; 4 | use crate::types::*; 5 | use crate::view::ViewMessage; 6 | use std::cell::RefCell; 7 | use std::rc::Rc; 8 | use wasm_bindgen::prelude::*; 9 | 10 | pub struct Controller { 11 | app: RefCell>, 12 | } 13 | 14 | pub enum ControllerMessage { 15 | ChangePage(&'static str), 16 | } 17 | 18 | impl Controller { 19 | pub fn new(app: Rc) -> Controller { 20 | Controller { 21 | app: RefCell::new(app), 22 | } 23 | } 24 | 25 | pub fn call(&mut self, method_name: ControllerMessage) { 26 | use self::ControllerMessage::*; 27 | match method_name { 28 | ChangePage(hash) => self.change_page(hash), 29 | } 30 | } 31 | 32 | fn change_page(&self, hash: &'static str) { 33 | let hash = hash.trim_start_matches("#/"); 34 | let v: Vec<&str> = to_static_str(hash.to_string()).split("&").collect(); 35 | let pathname = v[0]; 36 | 37 | match pathname { 38 | "news" | "newest" | "ask" | "show" | "jobs" => { 39 | self.get_news(pathname, v[1].parse::().unwrap()); 40 | } 41 | "user" => self.get_user(pathname, v[1]), 42 | "comment" => { 43 | self.get_comment(pathname, v[1].parse::().unwrap()); 44 | } 45 | _ => self.get_news("news", 1), 46 | } 47 | } 48 | 49 | pub fn get_comment(&self, pathname: &'static str, index: u32) { 50 | let app = self.app.clone(); 51 | let fetch = move || { 52 | let done = Closure::wrap(Box::new(move |json: JsValue| { 53 | let item: Item = json.into_serde().unwrap(); 54 | 55 | if let Ok(app) = &(app.try_borrow_mut()) { 56 | app.add_message(Message::View(ViewMessage::ShowComment( 57 | item, pathname, index, 58 | ))); 59 | } 60 | }) as Box); 61 | 62 | let endpoint = format!("https://api.hnpwa.com/v0/item/{}.json", index); 63 | Fetch::get_json(&endpoint).then(&done); 64 | done.forget(); 65 | }; 66 | 67 | fetch(); 68 | } 69 | 70 | pub fn get_user(&self, pathname: &'static str, uid: &'static str) { 71 | let app = self.app.clone(); 72 | let fetch = move || { 73 | let done = Closure::wrap(Box::new(move |json: JsValue| { 74 | let user: User = json.into_serde().unwrap(); 75 | 76 | if let Ok(app) = &(app.try_borrow_mut()) { 77 | app.add_message(Message::View(ViewMessage::ShowUser(user, pathname, uid))); 78 | } 79 | }) as Box); 80 | 81 | let endpoint = format!("https://api.hnpwa.com/v0/user/{}.json", uid); 82 | Fetch::get_json(&endpoint).then(&done); 83 | done.forget(); 84 | }; 85 | 86 | fetch(); 87 | } 88 | 89 | pub fn get_news(&self, pathname: &'static str, index: u32) { 90 | let app = self.app.clone(); 91 | let fetch = move || { 92 | let done = Closure::wrap(Box::new(move |json: JsValue| { 93 | let data: Vec = json.into_serde().unwrap(); 94 | 95 | if let Ok(app) = &(app.try_borrow_mut()) { 96 | app.add_message(Message::View(ViewMessage::ShowNews( 97 | data.clone(), 98 | pathname, 99 | index, 100 | ))); 101 | } 102 | }) as Box); 103 | 104 | let endpoint = get_url(pathname, index); 105 | Fetch::get_json(&endpoint).then(&done); 106 | done.forget(); 107 | }; 108 | 109 | fetch(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/view.rs: -------------------------------------------------------------------------------- 1 | pub mod element; 2 | 3 | use super::console::*; 4 | use crate::types::*; 5 | use crate::view::element::Element; 6 | 7 | pub enum ViewMessage { 8 | ShowNews(Vec, &'static str, u32), 9 | ShowUser(User, &'static str, &'static str), 10 | ShowComment(Item, &'static str, u32), 11 | } 12 | 13 | pub struct View {} 14 | 15 | impl View { 16 | pub fn new() -> View { 17 | View {} 18 | } 19 | 20 | fn bind_more(&mut self, pathname: &'static str, index: u32) { 21 | if let Some(mut more) = Element::qs("#more") { 22 | if let Some(a) = more.qs_from("a") { 23 | more.remove_child(a); 24 | } 25 | 26 | let html: String = format!("More...", pathname, index); 27 | more.set_inner_html(html.to_string()); 28 | } 29 | } 30 | 31 | pub fn call(&mut self, method_name: ViewMessage) { 32 | use self::ViewMessage::*; 33 | match method_name { 34 | ShowNews(news, pathname, index) => self.show_news(&news, pathname, index), 35 | ShowUser(user, pathname, uid) => self.show_user(&user, pathname, uid), 36 | ShowComment(item, pathname, index) => self.show_comment(&item, pathname, index), 37 | } 38 | } 39 | 40 | pub fn show_comment(&mut self, item: &Item, _pathname: &'static str, _index: u32) { 41 | if let Some(mut more) = Element::qs("#more") { 42 | if let Some(a) = more.qs_from("a") { 43 | more.remove_child(a); 44 | } 45 | } 46 | 47 | if let Some(mut section) = Element::qs("#content") { 48 | if let Some(div) = section.qs_from("div") { 49 | section.remove_child(div); 50 | } 51 | 52 | if let Some(mut div) = Element::create_element("div") { 53 | section.append_child(&mut div); 54 | 55 | if let Some(mut ul) = Element::create_element("ul") { 56 | div.append_child(&mut ul); 57 | 58 | let mut comments = String::new(); 59 | 60 | for comment in item.comments.iter() { 61 | let user = match &comment.user { 62 | Some(user) => user, 63 | None => "John Doe", 64 | }; 65 | 66 | comments.push_str(&format!( 67 | "
  • 68 | 72 |
    73 |
    {}
    74 |
  • ", 75 | user, comment.time_ago, comment.content 76 | )); 77 | } 78 | ul.set_inner_html(comments.to_string()); 79 | } 80 | } 81 | } 82 | } 83 | 84 | pub fn show_news(&mut self, news: &Vec, pathname: &'static str, index: u32) { 85 | self.bind_more(pathname, if index < 10 { index + 1 } else { index }); 86 | 87 | if let Some(mut section) = Element::qs("#content") { 88 | if let Some(div) = section.qs_from("div") { 89 | section.remove_child(div); 90 | } 91 | 92 | if let Some(mut div) = Element::create_element("div") { 93 | section.append_child(&mut div); 94 | 95 | if let Some(mut ul) = Element::create_element("ul") { 96 | div.append_child(&mut ul); 97 | 98 | let mut items = String::new(); 99 | 100 | for item in news.iter() { 101 | let points = match item.points { 102 | Some(points) => points, 103 | None => 0, 104 | }; 105 | let domain = match &item.domain { 106 | Some(domain) => domain, 107 | None => "", 108 | }; 109 | let user = match &item.user { 110 | Some(user) => user, 111 | None => "John Doe", 112 | }; 113 | 114 | items.push_str(&format!( 115 | "
  • 116 |
    {}
    117 |
    118 |
    119 | {} 120 | {} 121 |
    122 | 123 |
    124 |
  • ", 125 | points, item.url, item.title, domain, user, user, item.id, item.comments_count 126 | )); 127 | } 128 | ul.set_inner_html(items.to_string()); 129 | } 130 | } 131 | } 132 | } 133 | 134 | pub fn show_user(&mut self, user: &User, _pathname: &'static str, _uid: &'static str) { 135 | if let Some(mut more) = Element::qs("#more") { 136 | if let Some(a) = more.qs_from("a") { 137 | more.remove_child(a); 138 | } 139 | } 140 | 141 | if let Some(mut section) = Element::qs("#content") { 142 | if let Some(div) = section.qs_from("div") { 143 | section.remove_child(div); 144 | } 145 | 146 | if let Some(mut div) = Element::create_element("div") { 147 | section.append_child(&mut div); 148 | 149 | if let Some(mut content) = Element::create_element("div") { 150 | div.append_child(&mut content); 151 | let html: String = format!( 152 | "
    153 |

    {}

    154 | 155 |
    ", 156 | user.id, user.created_time, user.karma 157 | ); 158 | 159 | div.set_inner_html(html); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/view/element.rs: -------------------------------------------------------------------------------- 1 | // element mod from 2 | // https://github.com/rustwasm/wasm-bindgen/tree/master/examples/todomvc 3 | 4 | use wasm_bindgen::prelude::*; 5 | use wasm_bindgen::JsCast; 6 | use web_sys::EventTarget; 7 | 8 | pub struct Element { 9 | pub el: Option, 10 | } 11 | 12 | impl From for Element { 13 | fn from(el: web_sys::Element) -> Element { 14 | Element { el: Some(el) } 15 | } 16 | } 17 | 18 | impl From for Element { 19 | fn from(el: web_sys::EventTarget) -> Element { 20 | let el = wasm_bindgen::JsCast::dyn_into::(el); 21 | Element { el: el.ok() } 22 | } 23 | } 24 | 25 | impl From for Option { 26 | fn from(obj: Element) -> Option { 27 | if let Some(el) = obj.el { 28 | Some(el.into()) 29 | } else { 30 | None 31 | } 32 | } 33 | } 34 | 35 | impl From for Option { 36 | fn from(obj: Element) -> Option { 37 | if let Some(el) = obj.el { 38 | Some(el.into()) 39 | } else { 40 | None 41 | } 42 | } 43 | } 44 | 45 | impl Element { 46 | // Create an element from a tag name 47 | pub fn create_element(tag: &str) -> Option { 48 | if let Some(el) = web_sys::window()?.document()?.create_element(tag).ok() { 49 | Some(el.into()) 50 | } else { 51 | None 52 | } 53 | } 54 | 55 | pub fn qs(selector: &str) -> Option { 56 | let body: web_sys::Element = web_sys::window()?.document()?.body()?.into(); 57 | let el = body.query_selector(selector).ok()?; 58 | Some(Element { el }) 59 | } 60 | 61 | /// Add event listener to this node 62 | pub fn add_event_listener(&mut self, event_name: &str, handler: T) 63 | where 64 | T: 'static + FnMut(web_sys::Event), 65 | { 66 | let cb = Closure::wrap(Box::new(handler) as Box); 67 | if let Some(el) = self.el.take() { 68 | let el_et: EventTarget = el.into(); 69 | el_et 70 | .add_event_listener_with_callback(event_name, cb.as_ref().unchecked_ref()) 71 | .unwrap(); 72 | cb.forget(); 73 | if let Ok(el) = el_et.dyn_into::() { 74 | self.el = Some(el); 75 | } 76 | } 77 | } 78 | 79 | pub fn remove_event_listener(&mut self, event_name: &str, handler: T) 80 | where 81 | T: 'static + FnMut(web_sys::Event), 82 | { 83 | let cb = Closure::wrap(Box::new(handler) as Box); 84 | if let Some(el) = self.el.take() { 85 | let el_et: EventTarget = el.into(); 86 | el_et 87 | .remove_event_listener_with_callback(event_name, cb.as_ref().unchecked_ref()) 88 | .unwrap(); 89 | cb.forget(); 90 | if let Ok(el) = el_et.dyn_into::() { 91 | self.el = Some(el); 92 | } 93 | } 94 | } 95 | 96 | pub fn set_href(&mut self, href: &str) { 97 | if let Some(el) = self.el.as_ref() { 98 | if let Some(node) = &el.dyn_ref::() { 99 | node.set_href(href); 100 | } 101 | } 102 | } 103 | 104 | /// Delegate an event to a selector 105 | pub fn delegate( 106 | &mut self, 107 | selector: &'static str, 108 | event: &str, 109 | mut handler: T, 110 | use_capture: bool, 111 | ) where 112 | T: 'static + FnMut(web_sys::Event) -> (), 113 | { 114 | let el = match self.el.take() { 115 | Some(e) => e, 116 | None => return, 117 | }; 118 | if let Some(dyn_el) = &el.dyn_ref::() { 119 | if let Some(window) = web_sys::window() { 120 | if let Some(document) = window.document() { 121 | // TODO document selector to the target element 122 | let tg_el = document; 123 | 124 | let cb = Closure::wrap(Box::new(move |event: web_sys::Event| { 125 | if let Some(target_element) = event.target() { 126 | let dyn_target_el: Option<&web_sys::Node> = 127 | wasm_bindgen::JsCast::dyn_ref(&target_element); 128 | if let Some(target_element) = dyn_target_el { 129 | if let Ok(potential_elements) = tg_el.query_selector_all(selector) { 130 | let mut has_match = false; 131 | for i in 0..potential_elements.length() { 132 | if let Some(el) = potential_elements.get(i) { 133 | if target_element.is_equal_node(Some(&el)) { 134 | has_match = true; 135 | } 136 | break; 137 | } 138 | } 139 | 140 | if has_match { 141 | handler(event); 142 | } 143 | } 144 | } 145 | } 146 | }) as Box); 147 | 148 | dyn_el 149 | .add_event_listener_with_callback_and_bool( 150 | event, 151 | cb.as_ref().unchecked_ref(), 152 | use_capture, 153 | ) 154 | .unwrap(); 155 | cb.forget(); // TODO cycle collect 156 | } 157 | } 158 | } 159 | self.el = Some(el); 160 | } 161 | 162 | /// Find child `Element`s from this node 163 | pub fn qs_from(&mut self, selector: &str) -> Option { 164 | let mut found_el = None; 165 | if let Some(el) = self.el.as_ref() { 166 | found_el = Some(Element { 167 | el: el.query_selector(selector).ok()?, 168 | }); 169 | } 170 | found_el 171 | } 172 | 173 | /// Sets the inner HTML of the `self.el` element 174 | pub fn set_inner_html(&mut self, value: String) { 175 | if let Some(el) = self.el.take() { 176 | el.set_inner_html(&value); 177 | self.el = Some(el); 178 | } 179 | } 180 | 181 | /// Sets the text content of the `self.el` element 182 | pub fn set_text_content(&mut self, value: &str) { 183 | if let Some(el) = self.el.as_ref() { 184 | if let Some(node) = &el.dyn_ref::() { 185 | node.set_text_content(Some(&value)); 186 | } 187 | } 188 | } 189 | 190 | /// Gets the text content of the `self.el` element 191 | pub fn text_content(&mut self) -> Option { 192 | let mut text = None; 193 | if let Some(el) = self.el.as_ref() { 194 | if let Some(node) = &el.dyn_ref::() { 195 | text = node.text_content(); 196 | } 197 | } 198 | text 199 | } 200 | 201 | /// Gets the parent of the `self.el` element 202 | pub fn parent_element(&mut self) -> Option { 203 | let mut parent = None; 204 | if let Some(el) = self.el.as_ref() { 205 | if let Some(node) = &el.dyn_ref::() { 206 | if let Some(parent_node) = node.parent_element() { 207 | parent = Some(parent_node.into()); 208 | } 209 | } 210 | } 211 | parent 212 | } 213 | 214 | /// Gets the parent of the `self.el` element 215 | pub fn append_child(&mut self, child: &mut Element) { 216 | if let Some(el) = self.el.as_ref() { 217 | if let Some(node) = &el.dyn_ref::() { 218 | if let Some(ref child_el) = child.el { 219 | if let Some(child_node) = child_el.dyn_ref::() { 220 | node.append_child(child_node).unwrap(); 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | /// Removes a class list item from the element 228 | /// 229 | /// ``` 230 | /// e.class_list_remove(String::from("clickable")); 231 | /// // removes the class 'clickable' from e.el 232 | /// ``` 233 | // pub fn class_list_remove(&mut self, value: &str) { 234 | // if let Some(el) = self.el.take() { 235 | // el.class_list().remove_1(&value).unwrap(); 236 | // self.el = Some(el); 237 | // } 238 | // } 239 | 240 | // pub fn class_list_add(&mut self, value: &str) { 241 | // if let Some(el) = self.el.take() { 242 | // el.class_list().add_1(&value).unwrap(); 243 | // self.el = Some(el); 244 | // } 245 | // } 246 | 247 | /// Given another `Element` it will remove that child from the DOM from this element 248 | /// Consumes `child` so it can't be used after it's removal. 249 | pub fn remove_child(&mut self, mut child: Element) { 250 | if let Some(child_el) = child.el.take() { 251 | if let Some(el) = self.el.take() { 252 | if let Some(el_node) = el.dyn_ref::() { 253 | let child_node: web_sys::Node = child_el.into(); 254 | el_node.remove_child(&child_node).unwrap(); 255 | } 256 | self.el = Some(el); 257 | } 258 | } 259 | } 260 | 261 | /// Sets the whole class value for `self.el` 262 | pub fn set_class_name(&mut self, class_name: &str) { 263 | if let Some(el) = self.el.take() { 264 | el.set_class_name(&class_name); 265 | self.el = Some(el); 266 | } 267 | } 268 | 269 | /// Sets the visibility for the element in `self.el` 270 | pub fn set_visibility(&mut self, visible: bool) { 271 | if let Some(el) = self.el.take() { 272 | { 273 | let dyn_el: Option<&web_sys::HtmlElement> = wasm_bindgen::JsCast::dyn_ref(&el); 274 | if let Some(el) = dyn_el { 275 | el.set_hidden(!visible); 276 | } 277 | } 278 | self.el = Some(el); 279 | } 280 | } 281 | 282 | pub fn blur(&mut self) { 283 | if let Some(el) = self.el.take() { 284 | { 285 | let dyn_el: Option<&web_sys::HtmlElement> = wasm_bindgen::JsCast::dyn_ref(&el); 286 | if let Some(el) = dyn_el { 287 | // There isn't much we can do with the result here so ignore 288 | el.blur().unwrap(); 289 | } 290 | } 291 | self.el = Some(el); 292 | } 293 | } 294 | 295 | pub fn focus(&mut self) { 296 | if let Some(el) = self.el.take() { 297 | { 298 | let dyn_el: Option<&web_sys::HtmlElement> = wasm_bindgen::JsCast::dyn_ref(&el); 299 | if let Some(el) = dyn_el { 300 | // There isn't much we can do with the result here so ignore 301 | el.focus().unwrap(); 302 | } 303 | } 304 | self.el = Some(el); 305 | } 306 | } 307 | 308 | pub fn dataset_set(&mut self, key: &str, value: &str) { 309 | if let Some(el) = self.el.take() { 310 | { 311 | if let Some(el) = wasm_bindgen::JsCast::dyn_ref::(&el) { 312 | el.dataset().set(key, value).unwrap(); 313 | } 314 | } 315 | self.el = Some(el); 316 | } 317 | } 318 | 319 | pub fn dataset_get(&mut self, key: &str) -> String { 320 | let mut text = String::new(); 321 | if let Some(el) = self.el.take() { 322 | { 323 | if let Some(el) = wasm_bindgen::JsCast::dyn_ref::(&el) { 324 | text = el.dataset().get(key); 325 | } 326 | } 327 | self.el = Some(el); 328 | } 329 | text 330 | } 331 | 332 | /// Sets the value for the element in `self.el` (The element must be an input) 333 | pub fn set_value(&mut self, value: &str) { 334 | if let Some(el) = self.el.take() { 335 | if let Some(el) = wasm_bindgen::JsCast::dyn_ref::(&el) { 336 | el.set_value(&value); 337 | } 338 | self.el = Some(el); 339 | } 340 | } 341 | 342 | /// Gets the value for the element in `self.el` (The element must be an input) 343 | pub fn value(&mut self) -> String { 344 | let mut v = String::new(); 345 | if let Some(el) = self.el.take() { 346 | if let Some(el) = wasm_bindgen::JsCast::dyn_ref::(&el) { 347 | v = el.value(); 348 | } 349 | self.el = Some(el); 350 | } 351 | v 352 | } 353 | 354 | /// Sets the checked state for the element in `self.el` (The element must be an input) 355 | pub fn set_checked(&mut self, checked: bool) { 356 | if let Some(el) = self.el.take() { 357 | if let Some(el) = wasm_bindgen::JsCast::dyn_ref::(&el) { 358 | el.set_checked(checked); 359 | } 360 | self.el = Some(el); 361 | } 362 | } 363 | 364 | /// Gets the checked state for the element in `self.el` (The element must be an input) 365 | pub fn checked(&mut self) -> bool { 366 | let mut checked = false; 367 | if let Some(el) = self.el.take() { 368 | if let Some(el) = wasm_bindgen::JsCast::dyn_ref::(&el) { 369 | checked = el.checked(); 370 | } 371 | self.el = Some(el); 372 | } 373 | checked 374 | } 375 | } 376 | --------------------------------------------------------------------------------