├── .cargo └── config ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── crates ├── plaster-forms │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── fields.rs │ │ ├── fields │ │ ├── big_checkbox.rs │ │ ├── checkbox.rs │ │ ├── file.rs │ │ ├── key_value.rs │ │ ├── select.rs │ │ └── text.rs │ │ └── lib.rs ├── plaster-router-macro │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ └── lib.rs │ └── tests │ │ └── basic_routes.rs └── plaster-router │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── examples ├── README.md ├── build_all.sh ├── counter │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── crm │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── lib.rs │ │ ├── main.rs │ │ └── markdown.rs ├── custom_components │ ├── Cargo.toml │ └── src │ │ ├── barrier.rs │ │ ├── button.rs │ │ ├── counter.rs │ │ ├── lib.rs │ │ └── main.rs ├── dashboard │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── static │ │ ├── data.json │ │ └── data.toml ├── fragments │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── game_of_life │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── static │ │ ├── favicon.ico │ │ ├── index.html │ │ └── styles.css ├── inner_html │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── js_callback │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── static │ │ ├── get-payload-script.js │ │ └── index.html ├── large_table │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── static │ │ ├── index.html │ │ └── styles.css ├── minimal │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── mount_point │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── multi_thread │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── bin │ │ │ ├── main.rs │ │ │ └── native_worker.rs │ │ ├── context.rs │ │ ├── job.rs │ │ ├── lib.rs │ │ └── native_worker.rs │ └── static │ │ ├── bin │ │ └── index.html ├── npm_and_rest │ ├── Cargo.toml │ ├── src │ │ ├── ccxt.rs │ │ ├── gravatar.rs │ │ ├── lib.rs │ │ └── main.rs │ └── static │ │ └── index.html ├── routing │ ├── Cargo.toml │ └── src │ │ ├── b_component.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── router.rs │ │ └── routing.rs ├── server │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── showcase │ ├── Cargo.toml │ ├── src │ │ └── main.rs │ └── static │ │ ├── index.html │ │ └── styles.css ├── textarea │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── timer │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── todomvc │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── static │ │ ├── index.html │ │ └── styles.css └── two_apps │ ├── Cargo.toml │ ├── src │ ├── lib.rs │ └── main.rs │ └── static │ └── index.html ├── src ├── agent.rs ├── app.rs ├── callback.rs ├── components │ ├── mod.rs │ └── select.rs ├── html.rs ├── lib.rs ├── macros.rs ├── prelude.rs ├── scheduler.rs └── virtual_dom │ ├── mod.rs │ ├── vcomp.rs │ ├── vlist.rs │ ├── vnode.rs │ ├── vtag.rs │ └── vtext.rs └── tests ├── vcomp_test.rs ├── vlist_test.rs ├── vtag_test.rs └── vtext_test.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | runner = 'wasm-bindgen-test-runner' 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### Description 4 | 5 | **I'm submitting a ...** (check one with "x") 6 | 7 | 12 | 13 | Put your description here 14 | 15 | #### Expected Results 16 | 17 | 18 | ``` 19 | insert short code snippets here 20 | ``` 21 | 22 | 23 | #### Actual Results 24 | 25 | 26 | ``` 27 | insert short code snippets here 28 | ``` 29 | 30 | 31 | #### Context (Environment) 32 | 33 | 34 | - Rust: vX.X.X 35 | 36 | 37 | - yew: vX.X.X 38 | 39 | 40 | - target: 41 | 42 | 43 | - cargo-web: vX.X.X 44 | 45 | - browser if relevant 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | Cargo.lock 4 | orig.* 5 | /.idea 6 | /cmake-build-debug 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: nightly 3 | 4 | addons: 5 | firefox: latest 6 | chrome: stable 7 | 8 | install: 9 | - rustup target add wasm32-unknown-unknown 10 | # Downloads a `wasm-bindgen` release binary from https://github.com/rustwasm/wasm-bindgen/releases. 11 | # Alternatively, use `wasm-pack` to manage `wasm-bindgen` binaries for you 12 | - curl -OL https://github.com/rustwasm/wasm-bindgen/releases/download/0.2.36/wasm-bindgen-0.2.36-x86_64-unknown-linux-musl.tar.gz 13 | - tar xf wasm-bindgen-0.2.36-x86_64-unknown-linux-musl.tar.gz 14 | - chmod +x wasm-bindgen-0.2.36-x86_64-unknown-linux-musl/wasm-bindgen 15 | # Moves the binaries to a directory that is in your PATH 16 | - mv wasm-bindgen-0.2.36-x86_64-unknown-linux-musl/wasm-bindgen* ~/.cargo/bin 17 | # Install node.js with nvm. 18 | - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash 19 | - source ~/.nvm/nvm.sh 20 | - nvm install v10.5 21 | # Install chromedriver. 22 | - curl --retry 5 -LO https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip 23 | - unzip chromedriver_linux64.zip 24 | # Install geckodriver. 25 | - curl --retry 5 -LO https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz 26 | - tar xf geckodriver-v0.21.0-linux64.tar.gz 27 | 28 | script: 29 | # Test in Chrome. 30 | - CHROMEDRIVER=$(pwd)/chromedriver cargo test --target wasm32-unknown-unknown 31 | # Test in Firefox. 32 | - GECKODRIVER=$(pwd)/geckodriver cargo test --target wasm32-unknown-unknown 33 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plaster" 3 | version = "0.2.5" 4 | authors = ["Carlos Diaz-Padron "] 5 | repository = "https://github.com/carlosdp/plaster" 6 | homepage = "https://github.com/carlosdp/plaster" 7 | documentation = "https://docs.rs/plaster/" 8 | license = "MIT/Apache-2.0" 9 | readme = "README.md" 10 | keywords = ["web", "webasm", "javascript"] 11 | categories = ["gui", "web-programming"] 12 | description = "A wasm-bindgen framework for making client-side single-page apps" 13 | 14 | [dependencies] 15 | log = "0.4" 16 | wasm-bindgen = "0.2" 17 | futures = "0.1" 18 | wasm-bindgen-futures = "0.3" 19 | js-sys = "0.3" 20 | 21 | [dependencies.web-sys] 22 | version = "0.3" 23 | features = [ 24 | "Document", 25 | "DomTokenList", 26 | "Element", 27 | "Event", 28 | "EventTarget", 29 | "FileList", 30 | "HtmlInputElement", 31 | "HtmlSelectElement", 32 | "HtmlTextAreaElement", 33 | "MouseEvent", 34 | "PointerEvent", 35 | "KeyEvent", 36 | "KeyboardEvent", 37 | "MouseScrollEvent", 38 | "FocusEvent", 39 | "DragEvent", 40 | "InputEvent", 41 | "Node", 42 | "Text", 43 | "Window" 44 | ] 45 | 46 | [dev-dependencies] 47 | wasm-bindgen-test = "0.2" 48 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Denis Kolodin 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plaster 2 | **plaster** is a modern Rust framework for creating frontend apps with WebAssembly. 3 | 4 | It was orginally forked from [yew]. 5 | 6 | ## Why the fork? 7 | **yew** is a great framework and I've found a ton of great use from it on my projects. 8 | However, it was built on-top of [stdweb], which is also great, but [wasm-bindgen] 9 | and the associated crates such as [web_sys] are pretty much the "annointed" libraries for low-level 10 | access to Web/JS APIs in WebAssembly. They are designed to more or less match the eventual host-level bindings 11 | to these APIs directly from WebAssembly and are generated from WebIDL definitions, making their upkeep much 12 | easier and reducing the time to access new APIs as they become standardized and available. 13 | 14 | Additionally, **yew** takes an opinionated stance to concurrency and parallelism with its actor model. I'm not 15 | personally a huge fan of actors, and I'd prefer to just use Futures and libraries that build on top of that 16 | primitive, so I'd like the framework to easily support that more idiomatic model. [wasm-bindgen-futures] 17 | makes this nice and easy to do with the browser's built-in Promise support. 18 | 19 | In a nutshell: 20 | 21 | - **yew** is built on **stdweb**, I want to use [wasm-bindgen] and [web_sys]. 22 | - **yew** implements an actor-based concurrency model, I want to use Futures and Promises. 23 | - **yew** implements `Services` to try and provide some higher-level Rust primitives for some commmon JS/Web 24 | patterns. I think this is out of the library's scope and would like to thin it out by removing this concept 25 | and instead making interoperation between Promise-based Futures and Component updates easy. 26 | - **yew** uses a custom macro for JSX-like syntax. I'd like to explore potentially integrating one of the 27 | solutions others are working on for a "common" macro that does this. (This isn't really a reason to fork, 28 | more just a note for the future) 29 | 30 | [yew]: https://github.com/DenisKolodin/yew 31 | [stdweb]: https://github.com/koute/stdweb 32 | [wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen 33 | [wasm-bindgen-futures]: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/futures 34 | [web_sys]: https://github.com/rustwasm/wasm-bindgen/tree/master/crates/web-sys 35 | -------------------------------------------------------------------------------- /crates/plaster-forms/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plaster-forms" 3 | version = "0.1.16" 4 | authors = ["Carlos Diaz-Padron "] 5 | repository = "https://github.com/carlosdp/plaster" 6 | homepage = "https://github.com/carlosdp/plaster" 7 | documentation = "https://docs.rs/plaster-forms/" 8 | license = "MIT/Apache-2.0" 9 | readme = "README.md" 10 | keywords = ["web", "wasm", "javascript", "forms"] 11 | categories = ["gui", "web-programming"] 12 | description = "A form handler for front-end web applications." 13 | edition = "2018" 14 | 15 | [dependencies] 16 | plaster = "0.2" 17 | log = "0.4" 18 | serde = { version = "1.0", optional = true } 19 | serde_derive = { version = "1.0", optional = true } 20 | wasm-bindgen = { version = "0.2", features = ["serde-serialize"], optional = true } 21 | 22 | [dependencies.web-sys] 23 | version = "0.3" 24 | features = [ 25 | "File", 26 | ] 27 | 28 | [dev-dependencies] 29 | wasm-bindgen = "=0.2.40" 30 | 31 | [features] 32 | ionic = [ 33 | "wasm-bindgen", 34 | "web-sys/CustomEvent", 35 | "web-sys/Event", 36 | "web-sys/HtmlInputElement", 37 | "serde", 38 | "serde_derive" 39 | ] 40 | -------------------------------------------------------------------------------- /crates/plaster-forms/README.md: -------------------------------------------------------------------------------- 1 | # plaster-forms 2 | A form helper for Plaster projects. 3 | -------------------------------------------------------------------------------- /crates/plaster-forms/src/fields.rs: -------------------------------------------------------------------------------- 1 | pub mod big_checkbox; 2 | pub mod checkbox; 3 | pub mod file; 4 | pub mod key_value; 5 | pub mod select; 6 | pub mod text; 7 | 8 | use std::sync::Arc; 9 | 10 | #[derive(Clone)] 11 | pub struct ValidationFn { 12 | func: Arc Option>, 13 | } 14 | 15 | impl ValidationFn { 16 | pub fn validate(&self, i: V) -> Option { 17 | (self.func)(i) 18 | } 19 | } 20 | 21 | impl Default for ValidationFn { 22 | fn default() -> ValidationFn { 23 | ValidationFn { 24 | func: Arc::new(|_: V| None), 25 | } 26 | } 27 | } 28 | 29 | impl PartialEq for ValidationFn { 30 | fn eq(&self, _other: &ValidationFn) -> bool { 31 | true 32 | } 33 | } 34 | 35 | impl From for ValidationFn 36 | where 37 | FN: Fn(V) -> Option + 'static, 38 | { 39 | fn from(f: FN) -> ValidationFn { 40 | ValidationFn { func: Arc::new(f) } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/plaster-forms/src/fields/big_checkbox.rs: -------------------------------------------------------------------------------- 1 | use plaster::prelude::*; 2 | 3 | #[derive(Clone, PartialEq, Default)] 4 | pub struct CheckboxOption { 5 | pub key: String, 6 | pub label: String, 7 | pub icon: Option, 8 | pub description: Option, 9 | pub disabled: bool, 10 | } 11 | 12 | /// A big enum checkbox field 13 | pub struct BigCheckbox { 14 | label: String, 15 | value: Vec, 16 | options: Vec, 17 | radio: bool, 18 | on_change: Option>>, 19 | } 20 | 21 | pub enum Msg { 22 | Click(String, bool), 23 | } 24 | 25 | #[derive(Default, Clone, PartialEq)] 26 | pub struct Props { 27 | /// The input label 28 | pub label: String, 29 | /// The controlled value of the input 30 | pub value: Vec, 31 | /// The options for the checkboxes 32 | pub options: Vec, 33 | /// Whether this should be a radio button 34 | pub radio: bool, 35 | /// A callback that is fired when the user changes the input value 36 | pub on_change: Option>>, 37 | } 38 | 39 | impl Component for BigCheckbox { 40 | type Message = Msg; 41 | type Properties = Props; 42 | 43 | fn create(props: Self::Properties, _context: ComponentLink) -> Self { 44 | BigCheckbox { 45 | label: props.label, 46 | value: props.value, 47 | options: props.options, 48 | radio: props.radio, 49 | on_change: props.on_change, 50 | } 51 | } 52 | 53 | fn change(&mut self, props: Self::Properties) -> ShouldRender { 54 | self.label = props.label; 55 | self.value = props.value; 56 | self.options = props.options; 57 | self.radio = props.radio; 58 | self.on_change = props.on_change; 59 | 60 | true 61 | } 62 | 63 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 64 | match msg { 65 | Msg::Click(key, disabled) => { 66 | if !disabled { 67 | if self.radio { 68 | self.value = vec![key]; 69 | } else { 70 | if self.value.contains(&key) { 71 | self.value.retain(|x| x != &key); 72 | } else { 73 | self.value.push(key); 74 | } 75 | } 76 | 77 | if let Some(ref callback) = self.on_change { 78 | callback.emit(self.value.clone()); 79 | } 80 | } 81 | } 82 | }; 83 | 84 | true 85 | } 86 | } 87 | 88 | impl Renderable for BigCheckbox { 89 | fn view(&self) -> Html { 90 | let options = self.options.iter().map(|option| { 91 | let icon = if let Some(ref icon) = option.icon { 92 | html! { 93 | 94 | } 95 | } else { 96 | html!() 97 | }; 98 | 99 | let checked = if self.value.contains(&option.key) { 100 | html!() 101 | } else { 102 | html!() 103 | }; 104 | let class = if option.disabled { 105 | "big-enum-select-item disabled" 106 | } else { 107 | "big-enum-select-item" 108 | }; 109 | let key = option.key.clone(); 110 | let disabled = option.disabled; 111 | 112 | html! { 113 |
114 | {checked} 115 | {icon} 116 |
{&option.label}
117 |
{option.description.as_ref().map(|x| x.as_str()).unwrap_or("")}
118 |
119 | } 120 | }); 121 | 122 | html! { 123 |
124 | {for options} 125 |
126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /crates/plaster-forms/src/fields/checkbox.rs: -------------------------------------------------------------------------------- 1 | use plaster::prelude::*; 2 | 3 | /// An field 4 | pub struct Checkbox { 5 | label: String, 6 | value: bool, 7 | radio: bool, 8 | on_change: Option>, 9 | } 10 | 11 | pub enum Msg { 12 | Click, 13 | } 14 | 15 | #[derive(Default, Clone, PartialEq)] 16 | pub struct Props { 17 | /// The input label 18 | pub label: String, 19 | /// The controlled value of the input 20 | pub value: bool, 21 | /// Whether this should be a radio button 22 | pub radio: bool, 23 | /// A callback that is fired when the user changes the input value 24 | pub on_change: Option>, 25 | } 26 | 27 | impl Component for Checkbox { 28 | type Message = Msg; 29 | type Properties = Props; 30 | 31 | fn create(props: Self::Properties, _context: ComponentLink) -> Self { 32 | Checkbox { 33 | label: props.label, 34 | value: props.value, 35 | radio: props.radio, 36 | on_change: props.on_change, 37 | } 38 | } 39 | 40 | fn change(&mut self, props: Self::Properties) -> ShouldRender { 41 | self.label = props.label; 42 | self.value = props.value; 43 | self.radio = props.radio; 44 | self.on_change = props.on_change; 45 | 46 | true 47 | } 48 | 49 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 50 | match msg { 51 | Msg::Click => { 52 | self.value = !self.value; 53 | 54 | if let Some(ref callback) = self.on_change { 55 | callback.emit(self.value); 56 | } 57 | } 58 | }; 59 | 60 | true 61 | } 62 | } 63 | 64 | impl Renderable for Checkbox { 65 | fn view(&self) -> Html { 66 | let ty = if self.radio { "radio" } else { "checkbox" }; 67 | 68 | #[cfg(not(feature = "ionic"))] 69 | html! { 70 |
71 | 76 |
{&self.label}
77 |
78 | } 79 | 80 | #[cfg(feature = "ionic")] 81 | html! { 82 |
83 | 87 |
{&self.label}
88 |
89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/plaster-forms/src/fields/file.rs: -------------------------------------------------------------------------------- 1 | use plaster::prelude::*; 2 | 3 | /// An field 4 | pub struct File { 5 | label: String, 6 | value: Vec, 7 | class: String, 8 | on_change: Option>>, 9 | } 10 | 11 | pub enum Msg { 12 | Change(ChangeData), 13 | } 14 | 15 | #[derive(Default, Clone, PartialEq)] 16 | pub struct Props { 17 | /// The input label 18 | pub label: String, 19 | /// HTML class 20 | pub class: String, 21 | /// A callback that is fired when the user changes the input value 22 | pub on_change: Option>>, 23 | } 24 | 25 | impl Component for File { 26 | type Message = Msg; 27 | type Properties = Props; 28 | 29 | fn create(props: Self::Properties, _context: ComponentLink) -> Self { 30 | File { 31 | label: props.label, 32 | value: Vec::new(), 33 | class: props.class, 34 | on_change: props.on_change, 35 | } 36 | } 37 | 38 | fn change(&mut self, props: Self::Properties) -> ShouldRender { 39 | let mut updated = false; 40 | 41 | if props.label != self.label { 42 | self.label = props.label; 43 | updated = true; 44 | } 45 | 46 | if props.class != self.class { 47 | self.class = props.class; 48 | updated = true; 49 | } 50 | 51 | self.on_change = props.on_change; 52 | 53 | updated 54 | } 55 | 56 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 57 | match msg { 58 | Msg::Change(data) => { 59 | if let ChangeData::Files(list) = data { 60 | self.value.clear(); 61 | 62 | if let Some(files) = list { 63 | for i in 0..files.length() { 64 | self.value.push(files.get(i).unwrap()); 65 | } 66 | } 67 | 68 | if let Some(ref callback) = self.on_change { 69 | callback.emit(self.value.clone()); 70 | } 71 | } 72 | } 73 | }; 74 | 75 | false 76 | } 77 | } 78 | 79 | impl Renderable for File { 80 | fn view(&self) -> Html { 81 | html! { 82 |
83 | 87 |
88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/plaster-forms/src/fields/key_value.rs: -------------------------------------------------------------------------------- 1 | use crate::fields::text::TextField; 2 | use plaster::prelude::*; 3 | use std::collections::HashMap; 4 | 5 | /// A key/value field 6 | pub struct KeyValue { 7 | label: Option, 8 | value: Vec<(String, String)>, 9 | on_change: Option>>, 10 | } 11 | 12 | pub enum Msg { 13 | ChangeKey(usize, String), 14 | ChangeValue(usize, String), 15 | AddKey, 16 | DeleteKey(usize), 17 | } 18 | 19 | #[derive(Default, Clone, PartialEq)] 20 | pub struct Props { 21 | /// The input label 22 | pub label: Option, 23 | /// The controlled value of the input 24 | pub value: Option>, 25 | /// A callback that is fired when the user changes the input value 26 | pub on_change: Option>>, 27 | } 28 | 29 | impl Component for KeyValue { 30 | type Message = Msg; 31 | type Properties = Props; 32 | 33 | fn create(props: Self::Properties, _context: ComponentLink) -> Self { 34 | let initial_value = props 35 | .value 36 | .map(|h| h.into_iter().collect()) 37 | .unwrap_or(Vec::new()); 38 | 39 | KeyValue { 40 | label: props.label, 41 | value: initial_value, 42 | on_change: props.on_change, 43 | } 44 | } 45 | 46 | fn change(&mut self, props: Self::Properties) -> ShouldRender { 47 | let mut updated = false; 48 | 49 | if props.on_change != self.on_change { 50 | self.on_change = props.on_change; 51 | updated = true; 52 | } 53 | 54 | if let Some(value) = props.value { 55 | let mut existing_keys = Vec::new(); 56 | 57 | self.value = self 58 | .value 59 | .clone() 60 | .into_iter() 61 | .filter_map(|(k, v)| { 62 | if let Some(val) = value.get(&k) { 63 | existing_keys.push(k.clone()); 64 | 65 | if val != &v { 66 | updated = true; 67 | Some((k, val.to_string())) 68 | } else { 69 | Some((k, v)) 70 | } 71 | } else { 72 | None 73 | } 74 | }) 75 | .collect(); 76 | 77 | value.into_iter().for_each(|(k, v)| { 78 | if !existing_keys.contains(&k) { 79 | self.value.push((k, v)); 80 | } 81 | }); 82 | } 83 | 84 | if props.label != self.label { 85 | self.label = props.label; 86 | updated = true; 87 | } 88 | 89 | updated 90 | } 91 | 92 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 93 | match msg { 94 | Msg::ChangeKey(i, value) => { 95 | if self.value.len() > i { 96 | self.value.get_mut(i).unwrap().0 = value; 97 | 98 | if let Some(ref callback) = self.on_change { 99 | callback.emit(self.value.clone().into_iter().collect()); 100 | } 101 | } 102 | } 103 | Msg::ChangeValue(i, value) => { 104 | if self.value.len() > i { 105 | self.value.get_mut(i).unwrap().1 = value; 106 | 107 | if let Some(ref callback) = self.on_change { 108 | callback.emit(self.value.clone().into_iter().collect()); 109 | } 110 | } 111 | } 112 | Msg::AddKey => { 113 | self.value.push((String::new(), String::new())); 114 | } 115 | Msg::DeleteKey(i) => { 116 | if self.value.len() > i { 117 | self.value.remove(i); 118 | 119 | if let Some(ref callback) = self.on_change { 120 | callback.emit(self.value.clone().into_iter().collect()); 121 | } 122 | } 123 | } 124 | }; 125 | 126 | true 127 | } 128 | } 129 | 130 | impl Renderable for KeyValue { 131 | fn view(&self) -> Html { 132 | let label = self 133 | .label 134 | .as_ref() 135 | .map(|l| html! { }) 136 | // todo: render nothing instead 137 | .unwrap_or(html! { }); 138 | 139 | let items = self.value.iter().enumerate().map(|(i, (k, v))| { 140 | html! { 141 |
142 | 146 | 150 | {"(-)"} 154 |
155 | } 156 | }); 157 | 158 | html! { 159 |
160 | {label} 161 | {for items} 162 |
163 | {"+"} 167 |
168 |
169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /crates/plaster-forms/src/fields/text.rs: -------------------------------------------------------------------------------- 1 | use crate::fields::ValidationFn; 2 | use plaster::prelude::*; 3 | 4 | /// An field 5 | pub struct TextField { 6 | label: String, 7 | value: String, 8 | password: bool, 9 | class: String, 10 | validate: ValidationFn, 11 | validation_error: Option, 12 | on_change: Option>, 13 | on_blur: Option>, 14 | } 15 | 16 | pub enum Msg { 17 | Change(InputData), 18 | Blur, 19 | } 20 | 21 | #[derive(Default, Clone, PartialEq)] 22 | pub struct Props { 23 | /// The input label 24 | pub label: String, 25 | /// The controlled value of the input 26 | pub value: Option, 27 | /// Whether or not this is a password field 28 | pub password: bool, 29 | /// HTML class 30 | pub class: String, 31 | /// A function that returns a validation error 32 | pub validate: ValidationFn, 33 | /// A callback that is fired when the user changes the input value 34 | pub on_change: Option>, 35 | /// A callback that is fired when the field loses focus 36 | pub on_blur: Option>, 37 | } 38 | 39 | impl Component for TextField { 40 | type Message = Msg; 41 | type Properties = Props; 42 | 43 | fn create(props: Self::Properties, _context: ComponentLink) -> Self { 44 | TextField { 45 | label: props.label, 46 | value: props.value.unwrap_or(String::new()), 47 | password: props.password, 48 | class: props.class, 49 | validate: props.validate, 50 | validation_error: None, 51 | on_change: props.on_change, 52 | on_blur: props.on_blur, 53 | } 54 | } 55 | 56 | fn change(&mut self, props: Self::Properties) -> ShouldRender { 57 | let mut updated = false; 58 | 59 | if let Some(value) = props.value { 60 | if value != self.value { 61 | self.value = value; 62 | updated = true; 63 | } 64 | } 65 | 66 | if props.label != self.label { 67 | self.label = props.label; 68 | updated = true; 69 | } 70 | 71 | if props.class != self.class { 72 | self.class = props.class; 73 | updated = true; 74 | } 75 | 76 | self.validate = props.validate; 77 | self.on_change = props.on_change; 78 | self.on_blur = props.on_blur; 79 | 80 | updated 81 | } 82 | 83 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 84 | match msg { 85 | Msg::Change(data) => { 86 | if let Some(ref callback) = self.on_change { 87 | callback.emit(data.value.clone()); 88 | } 89 | 90 | self.value = data.value; 91 | 92 | self.validation_error = self.validate.validate(self.value.clone()); 93 | } 94 | Msg::Blur => { 95 | if let Some(ref callback) = self.on_blur { 96 | callback.emit(()); 97 | } 98 | } 99 | }; 100 | 101 | true 102 | } 103 | } 104 | 105 | impl Renderable for TextField { 106 | fn view(&self) -> Html { 107 | let ty = if self.password { "password" } else { "text" }; 108 | 109 | let (class, error) = if let Some(ref err) = self.validation_error { 110 | ( 111 | format!("{} error", &self.class), 112 | html! { 113 |
114 | {err} 115 |
116 | }, 117 | ) 118 | } else { 119 | (self.class.clone(), html!()) 120 | }; 121 | 122 | #[cfg(not(feature = "ionic"))] 123 | { 124 | html! { 125 |
126 | 133 | {error} 134 |
135 | } 136 | } 137 | 138 | #[cfg(feature = "ionic")] 139 | { 140 | use wasm_bindgen::JsCast; 141 | 142 | #[derive(serde_derive::Deserialize)] 143 | struct Detail { 144 | value: String, 145 | } 146 | 147 | html! { 148 | 149 | 159 | {error} 160 | 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /crates/plaster-forms/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate plaster; 3 | 4 | pub mod fields; 5 | 6 | pub mod prelude { 7 | pub use crate::fields::{ 8 | big_checkbox::BigCheckbox, checkbox::Checkbox, file::File, key_value::KeyValue, 9 | select::Select, text::TextField, ValidationFn, 10 | }; 11 | } 12 | 13 | use plaster::prelude::*; 14 | 15 | pub trait Form { 16 | type Value: Clone; 17 | 18 | fn value(&self) -> Self::Value; 19 | } 20 | 21 | #[derive(Clone, Default, PartialEq)] 22 | pub struct TestValue { 23 | name: String, 24 | } 25 | 26 | pub struct TestForm { 27 | value: TestValue, 28 | submit_label: Option, 29 | on_change: Option>, 30 | on_submit: Option>, 31 | } 32 | 33 | #[derive(Clone, Default, PartialEq)] 34 | pub struct TestFormProps { 35 | default_value: Option, 36 | submit_label: Option, 37 | on_change: Option>, 38 | on_submit: Option>, 39 | } 40 | 41 | pub enum TestFormMessage { 42 | UpdateName(String), 43 | Submit, 44 | } 45 | 46 | impl Component for TestForm { 47 | type Message = TestFormMessage; 48 | type Properties = TestFormProps; 49 | 50 | fn create(props: Self::Properties, _: ComponentLink) -> TestForm { 51 | TestForm { 52 | value: props.default_value.unwrap_or(TestValue::default()), 53 | submit_label: props.submit_label, 54 | on_change: props.on_change, 55 | on_submit: props.on_submit, 56 | } 57 | } 58 | 59 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 60 | match msg { 61 | TestFormMessage::UpdateName(value) => { 62 | self.value.name = value; 63 | 64 | if let Some(ref callback) = self.on_change { 65 | callback.emit(self.value.clone()); 66 | } 67 | 68 | true 69 | } 70 | TestFormMessage::Submit => { 71 | if let Some(ref callback) = self.on_submit { 72 | callback.emit(self.value.clone()); 73 | } 74 | 75 | false 76 | } 77 | } 78 | } 79 | } 80 | 81 | impl Renderable for TestForm { 82 | fn view(&self) -> Html { 83 | html! { 84 |
85 | 89 | 90 | 91 | } 92 | } 93 | } 94 | 95 | impl Form for TestForm { 96 | type Value = TestValue; 97 | 98 | fn value(&self) -> Self::Value { 99 | self.value.clone() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /crates/plaster-router-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plaster-router-macro" 3 | version = "0.1.1" 4 | authors = ["Carlos Diaz-Padron "] 5 | repository = "https://github.com/carlosdp/plaster" 6 | homepage = "https://github.com/carlosdp/plaster" 7 | documentation = "https://docs.rs/plaster-router-macro/" 8 | license = "MIT/Apache-2.0" 9 | readme = "README.md" 10 | keywords = ["web", "wasm", "javascript", "router"] 11 | categories = ["gui", "web-programming"] 12 | description = "A custom derive helper for plaster-router" 13 | edition = "2018" 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | syn = { version = "0.15", features = ["full"] } 20 | quote = "0.6" 21 | proc-macro2 = "0.4" 22 | 23 | [dev-dependencies] 24 | plaster = { version = "0.2", path = "../.." } 25 | plaster-router = { version = "0.1", path = "../plaster-router" } 26 | wasm-bindgen = "=0.2.40" 27 | -------------------------------------------------------------------------------- /crates/plaster-router-macro/README.md: -------------------------------------------------------------------------------- 1 | # plaster-router-macro 2 | A custom derive for `plaster-router`. 3 | -------------------------------------------------------------------------------- /crates/plaster-router-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | #[macro_use] 3 | extern crate quote; 4 | 5 | use proc_macro::TokenStream; 6 | 7 | #[proc_macro_derive(Routes, attributes(route))] 8 | pub fn plaster_router(input: TokenStream) -> TokenStream { 9 | match syn::parse2::(input.into()) { 10 | Ok(item) => match item { 11 | syn::Item::Enum(item_enum) => parse_enum(item_enum).into(), 12 | _ => panic!("plaster_router must be used on an enum"), 13 | }, 14 | Err(e) => { 15 | panic!("parse error: {}", e); 16 | } 17 | } 18 | } 19 | 20 | fn parse_enum(item: syn::ItemEnum) -> proc_macro2::TokenStream { 21 | let ident = item.ident; 22 | let routes = item.variants.into_iter().map(|variant| { 23 | if let Some(path) = parse_route_attr(&variant.attrs) { 24 | let mut route = path.as_str(); 25 | if route.len() != 0 && route.as_bytes()[0] == b'/' { 26 | route = &route[1..]; 27 | } 28 | 29 | let route_literal = syn::LitStr::new(route, proc_macro2::Span::call_site()); 30 | let variant_ident = variant.ident; 31 | let mut params = Vec::new(); 32 | 33 | for segment in route.split('/') { 34 | if segment.len() > 0 && segment.as_bytes()[0] == b':' { 35 | params.push(segment[1..].to_string()); 36 | } else if segment.len() > 0 && segment.as_bytes()[0] == b'*' { 37 | params.push(segment[1..].to_string()); 38 | } 39 | } 40 | 41 | if params.len() > 0 { 42 | if let syn::Fields::Named(fields) = variant.fields { 43 | // todo: make this optional 44 | // let field_names: Vec = fields 45 | // .named 46 | // .iter() 47 | // .map(|field| field.ident.as_ref().unwrap().to_string()) 48 | // .collect(); 49 | 50 | // if params.len() != field_names.len() 51 | // || params.difference(&field_names).count() > 0 52 | // { 53 | // panic!("all params must have a field in the variant"); 54 | // } 55 | 56 | let field_idents: Vec<_> = fields 57 | .named 58 | .into_iter() 59 | .map(|field| field.ident.unwrap()) 60 | .collect(); 61 | let params_literal: Vec = params 62 | .iter() 63 | .map(|param| syn::LitStr::new(param, proc_macro2::Span::call_site())) 64 | .collect(); 65 | 66 | quote! { 67 | router.add_route(#route_literal, |params| { 68 | #ident::#variant_ident { 69 | #( 70 | #field_idents: params.find(#params_literal).unwrap().to_string() 71 | ),* 72 | } 73 | }); 74 | } 75 | } else { 76 | panic!("all variants with params must have named fields"); 77 | } 78 | } else { 79 | quote! { 80 | router.add_route(#route_literal, |_| #ident::#variant_ident); 81 | } 82 | } 83 | } else { 84 | panic!("all variants of the enum must have a route attribute"); 85 | } 86 | }); 87 | 88 | quote! { 89 | impl plaster_router::Routes<#ident> for #ident { 90 | fn router(callback: plaster::callback::Callback<()>) -> plaster_router::Router<#ident> { 91 | let mut router = plaster_router::Router::new(callback); 92 | #(#routes)* 93 | router 94 | } 95 | } 96 | } 97 | } 98 | 99 | fn parse_route_attr(attrs: &[syn::Attribute]) -> Option { 100 | attrs.iter().find_map(|attr| { 101 | let meta = attr 102 | .parse_meta() 103 | .expect("could not parse meta for attribute"); 104 | match meta { 105 | syn::Meta::List(list) => { 106 | if list.ident == "route" { 107 | if let Some(route) = list.nested.first() { 108 | if let syn::NestedMeta::Literal(syn::Lit::Str(route)) = route.value() { 109 | Some(route.value()) 110 | } else { 111 | panic!("route spec in route attribute must be a string in quotes"); 112 | } 113 | } else { 114 | panic!("must specify a route spec in route attribute"); 115 | } 116 | } else { 117 | None 118 | } 119 | } 120 | _ => None, 121 | } 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /crates/plaster-router-macro/tests/basic_routes.rs: -------------------------------------------------------------------------------- 1 | use plaster_router_macro::Routes; 2 | 3 | #[test] 4 | fn test_basic_route() { 5 | #[derive(Routes)] 6 | enum BasicRoute { 7 | #[route("/route1")] 8 | Route1, 9 | } 10 | } 11 | 12 | #[test] 13 | fn test_route_with_param() { 14 | #[derive(Routes)] 15 | enum BasicRoute { 16 | #[route("/route1/:param")] 17 | Route1 { param: String }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/plaster-router/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plaster-router" 3 | version = "0.1.5" 4 | authors = ["Carlos Diaz-Padron "] 5 | repository = "https://github.com/carlosdp/plaster" 6 | homepage = "https://github.com/carlosdp/plaster" 7 | documentation = "https://docs.rs/plaster-router/" 8 | license = "MIT/Apache-2.0" 9 | readme = "README.md" 10 | keywords = ["web", "wasm", "javascript", "router"] 11 | categories = ["gui", "web-programming"] 12 | description = "A router for plaster-based frontend web applications" 13 | edition = "2018" 14 | 15 | [dependencies] 16 | plaster = "0.2" 17 | plaster-router-macro = { version = "0.1", path = "../plaster-router-macro" } 18 | route-recognizer = "0.1" 19 | wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } 20 | js-sys = "0.3" 21 | log = "0.4" 22 | serde = "1" 23 | serde_derive = "1" 24 | serde_json = "1" 25 | 26 | [dependencies.web-sys] 27 | version = "0.3" 28 | features = [ 29 | "Event", 30 | "EventTarget", 31 | "History", 32 | "Location", 33 | "Window", 34 | "CustomEvent", 35 | "CustomEventInit" 36 | ] 37 | 38 | [dev-dependencies] 39 | wasm-bindgen-test = "0.2" 40 | 41 | [features] 42 | mobile = [] 43 | -------------------------------------------------------------------------------- /crates/plaster-router/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | ```rust 3 | use plaster_router::{Routes, route_to}; 4 | 5 | #[derive(Routes)] 6 | pub enum MyRoutes { 7 | #[route("/posts")] 8 | Posts, 9 | #[route("/posts/:id")] 10 | Post { id: String }, 11 | } 12 | 13 | pub struct MyComponent { 14 | router: Router, 15 | } 16 | 17 | impl Component for MyComponent { 18 | fn create(_:_, mut context: ComponentLink) -> MyComponent { 19 | let mut router = MyRoutes::router(context.send_back(|| Msg::RouteUpdate)); 20 | 21 | MyComponent { 22 | router: router, 23 | } 24 | } 25 | 26 | fn update(msg: Msg) -> ShouldRender { 27 | match msg { 28 | Msg::RouteUpdate => true, 29 | Msg::RouteTo(route) => { route_to(&route); true }, 30 | } 31 | } 32 | } 33 | 34 | impl Renderable on MyComponent { 35 | fn view(&self) -> Html { 36 | match self.router.resolve() { 37 | Some(MyRoutes::Posts) => html! { 38 | 39 | }, 40 | Some(MyRoutes::Post { id }) => html! { 41 |

{format!("Post {}", id)}

42 | }, 43 | None => html! { 44 |

404 Not Found

45 | } 46 | } 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /crates/plaster-router/src/lib.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Function; 2 | use plaster::callback::Callback; 3 | use route_recognizer::{Params, Router as RecRouter}; 4 | use serde_derive::{Deserialize, Serialize}; 5 | use std::sync::{Arc, Mutex}; 6 | use wasm_bindgen::prelude::*; 7 | use wasm_bindgen::{JsCast, JsValue}; 8 | use web_sys::{window, CustomEvent, CustomEventInit}; 9 | 10 | use log::trace; 11 | pub use plaster_router_macro::Routes; 12 | 13 | pub struct Router { 14 | routes: Vec T>, 15 | index_router: RecRouter, 16 | current_path: Arc>, 17 | listener: Closure, 18 | callback: Callback<()>, 19 | } 20 | 21 | impl Router { 22 | pub fn new(callback: Callback<()>) -> Router { 23 | let win = window().expect("need a window context"); 24 | let path = if cfg!(not(feature = "mobile")) { 25 | win.location().pathname().unwrap_or("/".to_string()) 26 | } else { 27 | "/".to_string() 28 | }; 29 | trace!("initial route: {}", &path); 30 | let current_path = Arc::new(Mutex::new(path)); 31 | let current_path_c = current_path.clone(); 32 | let callback_c = callback.clone(); 33 | 34 | let listener_callback = Closure::wrap(Box::new(move |e: CustomEvent| { 35 | let ev: RouteEvent = e 36 | .detail() 37 | .into_serde() 38 | .expect("could not deserialize route event"); 39 | trace!("route change: {}", &ev.route); 40 | *current_path_c.lock().unwrap() = ev.route; 41 | callback_c.emit(()); 42 | }) as Box); 43 | 44 | let listener_function: &Function = listener_callback.as_ref().unchecked_ref(); 45 | 46 | win.add_event_listener_with_callback("plasterroutechange", listener_function) 47 | .expect("could not attach global event listener"); 48 | 49 | if cfg!(not(feature = "mobile")) { 50 | win.add_event_listener_with_callback("popstate", listener_function) 51 | .expect("could not attach popstate event listener"); 52 | } 53 | 54 | Router { 55 | routes: Vec::new(), 56 | index_router: RecRouter::new(), 57 | current_path: current_path, 58 | listener: listener_callback, 59 | callback: callback, 60 | } 61 | } 62 | 63 | pub fn add_route(&mut self, route: &str, closure: fn(Params) -> T) { 64 | trace!("added route: {}", route); 65 | let index = self.routes.len(); 66 | self.routes.push(closure); 67 | self.index_router.add(route, index); 68 | } 69 | 70 | pub fn navigate(&mut self, path: &str) { 71 | *self.current_path.lock().unwrap() = path.to_string(); 72 | if cfg!(not(feature = "mobile")) { 73 | self.push_state(); 74 | } 75 | self.callback.emit(()); 76 | } 77 | 78 | pub fn resolve(&self) -> Option { 79 | let route_match = self 80 | .index_router 81 | .recognize(&self.current_path.lock().unwrap()) 82 | .ok(); 83 | route_match.map(|m| self.routes.get(m.handler.clone()).unwrap()(m.params)) 84 | } 85 | 86 | pub fn current_route(&self) -> String { 87 | self.current_path.lock().unwrap().clone() 88 | } 89 | 90 | pub fn set_route(&self, path: &str) { 91 | *self.current_path.lock().unwrap() = path.to_string(); 92 | } 93 | 94 | fn push_state(&self) { 95 | match window().expect("need a window context").history() { 96 | Ok(history) => { 97 | history 98 | .push_state_with_url( 99 | &JsValue::NULL, 100 | "", 101 | Some(&self.current_path.lock().unwrap()), 102 | ) 103 | .expect("could not pushState"); 104 | } 105 | Err(_) => (), 106 | } 107 | } 108 | } 109 | 110 | impl Drop for Router { 111 | fn drop(&mut self) { 112 | window() 113 | .expect("need window context") 114 | .remove_event_listener_with_callback( 115 | "plasterroutechange", 116 | self.listener.as_ref().unchecked_ref(), 117 | ) 118 | .expect("could not remove event listener"); 119 | } 120 | } 121 | 122 | pub trait Routes { 123 | fn router(callback: Callback<()>) -> Router; 124 | } 125 | 126 | pub fn route_to(path: &str) { 127 | let win = window().expect("need window context"); 128 | 129 | if cfg!(not(feature = "mobile")) { 130 | win.history() 131 | .expect("history API unavailable") 132 | .push_state_with_url(&JsValue::NULL, "", Some(path)) 133 | .expect("could not pushState"); 134 | } 135 | 136 | let mut init = CustomEventInit::new(); 137 | init.detail( 138 | &JsValue::from_serde(&RouteEvent { 139 | route: path.to_owned(), 140 | }) 141 | .unwrap(), 142 | ); 143 | let event = CustomEvent::new_with_event_init_dict("plasterroutechange", &init) 144 | .expect("could not create CustomEvent"); 145 | win.dispatch_event(&event) 146 | .expect("could not dispatch route change"); 147 | } 148 | 149 | #[derive(Serialize, Deserialize)] 150 | struct RouteEvent { 151 | route: String, 152 | } 153 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Yew Examples 2 | -------------------------------------------------------------------------------- /examples/build_all.sh: -------------------------------------------------------------------------------- 1 | PID=-1 2 | 3 | function ctrl_c() { 4 | echo "** Killing the demo..." 5 | kill $PID 6 | } 7 | 8 | function build() { 9 | for example in */ ; do 10 | if [[ $example == server* ]]; then 11 | continue 12 | fi 13 | echo "Building: $example" 14 | cd $example 15 | cargo update 16 | cargo web build --target wasm32-unknown-unknown 17 | cd .. 18 | done 19 | } 20 | 21 | function run() { 22 | trap ctrl_c INT 23 | for example in */ ; do 24 | if [[ $example == server* ]]; then 25 | continue 26 | fi 27 | echo "Running: $example" 28 | cd $example 29 | cargo web start --target wasm32-unknown-unknown & 30 | PID=$! 31 | wait $PID 32 | cd .. 33 | done 34 | } 35 | 36 | case "$1" in 37 | --help) 38 | echo "Available commands: build, run" 39 | ;; 40 | build) 41 | build 42 | ;; 43 | run) 44 | run 45 | ;; 46 | *) 47 | build 48 | ;; 49 | esac 50 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.1" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | stdweb = "0.4.2" 8 | yew = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/counter/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate stdweb; 2 | #[macro_use] 3 | extern crate yew; 4 | 5 | use stdweb::web::Date; 6 | use yew::prelude::*; 7 | use yew::services::ConsoleService; 8 | 9 | pub struct Model { 10 | console: ConsoleService, 11 | value: i64, 12 | } 13 | 14 | pub enum Msg { 15 | Increment, 16 | Decrement, 17 | Bulk(Vec), 18 | } 19 | 20 | impl Component for Model { 21 | type Message = Msg; 22 | type Properties = (); 23 | 24 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 25 | Model { 26 | console: ConsoleService::new(), 27 | value: 0, 28 | } 29 | } 30 | 31 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 32 | match msg { 33 | Msg::Increment => { 34 | self.value = self.value + 1; 35 | self.console.log("plus one"); 36 | } 37 | Msg::Decrement => { 38 | self.value = self.value - 1; 39 | self.console.log("minus one"); 40 | } 41 | Msg::Bulk(list) => for msg in list { 42 | self.update(msg); 43 | self.console.log("Bulk action"); 44 | }, 45 | } 46 | true 47 | } 48 | } 49 | 50 | impl Renderable for Model { 51 | fn view(&self) -> Html { 52 | html! { 53 |
54 | 59 |

{ self.value }

60 |

{ Date::new().to_string() }

61 |
62 | } 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate counter; 3 | 4 | use yew::prelude::*; 5 | use counter::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/crm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crm" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | serde = "1" 8 | serde_derive = "1" 9 | yew = { path = "../.." } 10 | pulldown-cmark = "0.1.2" 11 | -------------------------------------------------------------------------------- /examples/crm/README.md: -------------------------------------------------------------------------------- 1 | ## Yew CRM Demo 2 | 3 | The main goals of this demo example to show you how to: 4 | 5 | * Add multiple screens with Yew (scenes) 6 | * Use storage service 7 | -------------------------------------------------------------------------------- /examples/crm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | #[macro_use] 4 | extern crate yew; 5 | 6 | extern crate pulldown_cmark; 7 | 8 | mod markdown; 9 | 10 | use yew::prelude::*; 11 | use yew::format::Json; 12 | use yew::services::{DialogService, StorageService}; 13 | use yew::services::storage::Area; 14 | 15 | const KEY: &'static str = "yew.crm.database"; 16 | 17 | #[derive(Serialize, Deserialize)] 18 | struct Database { 19 | clients: Vec, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug)] 23 | pub struct Client { 24 | first_name: String, 25 | last_name: String, 26 | description: String 27 | } 28 | 29 | impl Client { 30 | fn empty() -> Self { 31 | Client { 32 | first_name: "".into(), 33 | last_name: "".into(), 34 | description: "".into() 35 | } 36 | } 37 | } 38 | 39 | #[derive(Debug)] 40 | pub enum Scene { 41 | ClientsList, 42 | NewClientForm(Client), 43 | Settings, 44 | } 45 | 46 | pub struct Model { 47 | storage: StorageService, 48 | dialog: DialogService, 49 | database: Database, 50 | scene: Scene, 51 | } 52 | 53 | #[derive(Debug)] 54 | pub enum Msg { 55 | SwitchTo(Scene), 56 | AddNew, 57 | UpdateFirstName(String), 58 | UpdateLastName(String), 59 | UpdateDescription(String), 60 | Clear, 61 | } 62 | 63 | impl Component for Model { 64 | type Message = Msg; 65 | type Properties = (); 66 | 67 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 68 | let mut storage = StorageService::new(Area::Local); 69 | let Json(database) = storage.restore(KEY); 70 | let database = database.unwrap_or_else(|_| Database { 71 | clients: Vec::new(), 72 | }); 73 | Model { 74 | storage, 75 | dialog: DialogService::new(), 76 | database, 77 | scene: Scene::ClientsList, 78 | } 79 | } 80 | 81 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 82 | let mut new_scene = None; 83 | match self.scene { 84 | Scene::ClientsList => { 85 | match msg { 86 | Msg::SwitchTo(Scene::NewClientForm(client)) => { 87 | new_scene = Some(Scene::NewClientForm(client)); 88 | } 89 | Msg::SwitchTo(Scene::Settings) => { 90 | new_scene = Some(Scene::Settings); 91 | } 92 | unexpected => { 93 | panic!("Unexpected message when clients list shown: {:?}", unexpected); 94 | } 95 | } 96 | } 97 | Scene::NewClientForm(ref mut client) => { 98 | match msg { 99 | Msg::UpdateFirstName(val) => { 100 | println!("Input: {}", val); 101 | client.first_name = val; 102 | } 103 | Msg::UpdateLastName(val) => { 104 | println!("Input: {}", val); 105 | client.last_name = val; 106 | } 107 | Msg::UpdateDescription(val) => { 108 | println!("Input: {}", val); 109 | client.description = val; 110 | } 111 | Msg::AddNew => { 112 | let mut new_client = Client::empty(); 113 | ::std::mem::swap(client, &mut new_client); 114 | self.database.clients.push(new_client); 115 | self.storage.store(KEY, Json(&self.database)); 116 | } 117 | Msg::SwitchTo(Scene::ClientsList) => { 118 | new_scene = Some(Scene::ClientsList); 119 | } 120 | unexpected => { 121 | panic!("Unexpected message during new client editing: {:?}", unexpected); 122 | } 123 | } 124 | } 125 | Scene::Settings => { 126 | match msg { 127 | Msg::Clear => { 128 | let ok = { 129 | self.dialog.confirm("Do you really want to clear the data?") 130 | }; 131 | if ok { 132 | self.database.clients.clear(); 133 | self.storage.remove(KEY); 134 | } 135 | } 136 | Msg::SwitchTo(Scene::ClientsList) => { 137 | new_scene = Some(Scene::ClientsList); 138 | } 139 | unexpected => { 140 | panic!("Unexpected message for settings scene: {:?}", unexpected); 141 | } 142 | } 143 | } 144 | } 145 | if let Some(new_scene) = new_scene.take() { 146 | self.scene = new_scene; 147 | } 148 | true 149 | } 150 | } 151 | 152 | impl Renderable for Model { 153 | fn view(&self) -> Html { 154 | match self.scene { 155 | Scene::ClientsList => html! { 156 |
157 |
158 | { for self.database.clients.iter().map(Renderable::view) } 159 |
160 | 161 | 162 |
163 | }, 164 | Scene::NewClientForm(ref client) => html! { 165 |
166 |
167 | { client.view_first_name_input() } 168 | { client.view_last_name_input() } 169 | { client.view_description_textarea() } 170 |
171 | 173 | 174 |
175 | }, 176 | Scene::Settings => html! { 177 |
178 | 179 | 180 |
181 | }, 182 | } 183 | } 184 | } 185 | 186 | impl Renderable for Client { 187 | fn view(&self) -> Html { 188 | html! { 189 |
190 |

{ format!("First Name: {}", self.first_name) }

191 |

{ format!("Last Name: {}", self.last_name) }

192 |

{ "Description:"}

193 | {markdown::render_markdown(&self.description)} 194 |
195 | } 196 | } 197 | } 198 | 199 | impl Client { 200 | fn view_first_name_input(&self) -> Html { 201 | html! { 202 | 207 | } 208 | } 209 | 210 | fn view_last_name_input(&self) -> Html { 211 | html! { 212 | 217 | } 218 | } 219 | fn view_description_textarea(&self) -> Html { 220 | html! { 221 | 71 | 74 | 77 |

{ 78 | nbsp(self.debugged_payload.as_ref()) 79 | }

80 | 81 | } 82 | } 83 | } 84 | 85 | fn nbsp(string: T) -> String 86 | where 87 | String: From, 88 | { 89 | String::from(string).replace(' ', "\u{00a0}") 90 | } 91 | 92 | fn get_payload() -> String { 93 | (js! { return window.get_payload() }).into_string().unwrap() 94 | } 95 | 96 | fn get_payload_later(payload_callback: Callback) { 97 | let callback = move |payload: String| payload_callback.emit(payload); 98 | js! { 99 | // Note: The semi-colons appear to be strictly necessary here due to 100 | // how the interpolation is implemented 101 | var callback = @{callback}; 102 | window.get_payload_later(function(payload) { 103 | callback(payload); 104 | callback.drop(); 105 | }); 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /examples/js_callback/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate js_callback; 3 | 4 | use yew::prelude::*; 5 | use js_callback::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/js_callback/static/get-payload-script.js: -------------------------------------------------------------------------------- 1 | function get_payload() { 2 | return (new Date()).toString() 3 | } 4 | 5 | function get_payload_later(callback) { 6 | setTimeout(() => { 7 | callback(get_payload()) 8 | }, 1000) 9 | } 10 | -------------------------------------------------------------------------------- /examples/js_callback/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | (Asynchronous) callback from JavaScript 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/large_table/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "large_table" 3 | version = "0.1.0" 4 | authors = ["qthree "] 5 | 6 | [dependencies] 7 | yew = { path = "../.." } 8 | -------------------------------------------------------------------------------- /examples/large_table/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This demo originally created by https://github.com/qthree 2 | //! Source: https://github.com/qthree/yew_table100x100_test 3 | 4 | #[macro_use] 5 | extern crate yew; 6 | 7 | use yew::prelude::*; 8 | 9 | pub struct Model { 10 | selected: Option<(u32, u32)> 11 | } 12 | 13 | pub enum Msg { 14 | Select(u32, u32), 15 | } 16 | 17 | impl Component for Model { 18 | type Message = Msg; 19 | type Properties = (); 20 | 21 | fn create(_: (), _: ComponentLink) -> Self { 22 | Model { 23 | selected: None 24 | } 25 | } 26 | 27 | // Some details omitted. Explore the examples to get more. 28 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 29 | match msg { 30 | Msg::Select(x, y) => { 31 | self.selected = Some((x, y)); 32 | } 33 | } 34 | true 35 | } 36 | } 37 | 38 | fn square_class(this: (u32, u32), selected: Option<(u32, u32)>) -> &'static str { 39 | match selected { 40 | Some(xy) if xy == this => {"square_green"}, 41 | _ => {"square_red"} 42 | } 43 | } 44 | 45 | fn view_square(selected: Option<(u32, u32)>, row: u32, column: u32) -> Html { 46 | html! { 47 | 51 | 52 | } 53 | } 54 | 55 | fn view_row(selected: Option<(u32, u32)>, row: u32) -> Html { 56 | html! { 57 | 58 | {for (0..99).map(|column| { 59 | view_square(selected, row, column) 60 | })} 61 | 62 | } 63 | } 64 | 65 | impl Renderable for Model { 66 | fn view(&self) -> Html { 67 | html! { 68 | 69 | {for (0..99).map(|row| { 70 | view_row(self.selected, row) 71 | })} 72 |
73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/large_table/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate large_table; 3 | 4 | use yew::prelude::*; 5 | use large_table::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/large_table/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Table100x100 Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/large_table/static/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | text-align: center; 6 | } 7 | 8 | body { 9 | width: 1000px; 10 | height: 1000px; 11 | } 12 | 13 | tr { 14 | height: 10px; 15 | } 16 | td { 17 | width: 10px; 18 | } 19 | 20 | .square_red { 21 | background-color: #aaaaff 22 | } 23 | 24 | .square_green{ 25 | background-color: #aa5555 26 | } 27 | -------------------------------------------------------------------------------- /examples/minimal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minimal" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | plaster = { path = "../.." } 9 | log = "0.4" 10 | console_log = "0.1" 11 | wasm-bindgen = "0.2" 12 | -------------------------------------------------------------------------------- /examples/minimal/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate plaster; 3 | 4 | use plaster::prelude::*; 5 | 6 | pub struct Model {} 7 | 8 | pub enum Msg { 9 | Click, 10 | } 11 | 12 | impl Component for Model { 13 | type Message = Msg; 14 | type Properties = (); 15 | 16 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 17 | Model {} 18 | } 19 | 20 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 21 | match msg { 22 | Msg::Click => {} 23 | } 24 | true 25 | } 26 | } 27 | 28 | impl Renderable for Model { 29 | fn view(&self) -> Html { 30 | html! { 31 |
32 | 33 |
34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/minimal/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate minimal; 2 | extern crate plaster; 3 | #[macro_use] 4 | extern crate log; 5 | extern crate console_log; 6 | extern crate wasm_bindgen; 7 | 8 | use minimal::Model; 9 | use plaster::prelude::*; 10 | use wasm_bindgen::prelude::*; 11 | 12 | #[wasm_bindgen(start)] 13 | pub fn start() { 14 | console_log::init(); 15 | info!("Starting..."); 16 | App::::new().mount_to_body(); 17 | } 18 | 19 | fn main() {} 20 | -------------------------------------------------------------------------------- /examples/mount_point/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mount_point" 3 | version = "0.1.0" 4 | authors = ["Ben Berman "] 5 | 6 | [dependencies] 7 | stdweb = "0.4" 8 | yew = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/mount_point/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate yew; 3 | 4 | use yew::prelude::*; 5 | 6 | pub struct Model { 7 | name: String, 8 | } 9 | 10 | pub enum Msg { 11 | UpdateName(String), 12 | } 13 | 14 | impl Component for Model { 15 | type Message = Msg; 16 | type Properties = (); 17 | 18 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 19 | Model { 20 | name: "Reversed".to_owned(), 21 | } 22 | } 23 | 24 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 25 | match msg { 26 | Msg::UpdateName(new_name) => { 27 | self.name = new_name; 28 | } 29 | } 30 | true 31 | } 32 | } 33 | 34 | impl Renderable for Model { 35 | fn view(&self) -> Html { 36 | html! { 37 |
38 | 39 |

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

40 |
41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/mount_point/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate stdweb; 3 | extern crate yew; 4 | extern crate mount_point; 5 | 6 | use yew::prelude::*; 7 | use stdweb::web::{IElement, INode, IParentNode, document}; 8 | use mount_point::Model; 9 | 10 | fn main() { 11 | yew::initialize(); 12 | let body = document().query_selector("body").unwrap().unwrap(); 13 | 14 | // This canvas won't be overwritten by yew! 15 | let canvas = document().create_element("canvas").unwrap(); 16 | body.append_child(&canvas); 17 | 18 | js! { 19 | const canvas = document.querySelector("canvas"); 20 | canvas.width = 100; 21 | canvas.height = 100; 22 | const ctx = canvas.getContext("2d"); 23 | ctx.fillStyle = "green"; 24 | ctx.fillRect(10, 10, 50, 50); 25 | }; 26 | 27 | let mount_class = "mount-point"; 28 | let mount_point = document().create_element("div").unwrap(); 29 | mount_point.class_list().add(mount_class).unwrap(); 30 | body.append_child(&mount_point); 31 | 32 | App::::new().mount(mount_point); 33 | yew::run_loop(); 34 | } 35 | -------------------------------------------------------------------------------- /examples/multi_thread/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multi_thread" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | log = "0.4" 8 | web_logger = "0.1" 9 | serde = "1.0" 10 | serde_derive = "1.0" 11 | yew = { path = "../.." } 12 | -------------------------------------------------------------------------------- /examples/multi_thread/README.md: -------------------------------------------------------------------------------- 1 | ### multi_thread 2 | 3 | You should compile a worker which have to be spawned in a separate thread: 4 | 5 | ```sh 6 | cargo web build --bin native_worker --target wasm32-unknown-unknown 7 | ``` 8 | -------------------------------------------------------------------------------- /examples/multi_thread/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | extern crate web_logger; 2 | extern crate yew; 3 | extern crate multi_thread; 4 | 5 | use yew::prelude::*; 6 | use multi_thread::Model; 7 | 8 | fn main() { 9 | web_logger::init(); 10 | yew::initialize(); 11 | App::::new().mount_to_body(); 12 | yew::run_loop(); 13 | } 14 | -------------------------------------------------------------------------------- /examples/multi_thread/src/bin/native_worker.rs: -------------------------------------------------------------------------------- 1 | 2 | extern crate web_logger; 3 | extern crate yew; 4 | extern crate multi_thread; 5 | 6 | use yew::prelude::*; 7 | use multi_thread::native_worker; 8 | 9 | fn main() { 10 | web_logger::init(); 11 | yew::initialize(); 12 | native_worker::Worker::register(); 13 | yew::run_loop(); 14 | } 15 | -------------------------------------------------------------------------------- /examples/multi_thread/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use yew::prelude::worker::*; 3 | // TODO use yew::services::{IntervalService, FetchService, Task}; 4 | use yew::services::Task; 5 | use yew::services::interval::IntervalService; 6 | use yew::services::fetch::FetchService; 7 | 8 | #[derive(Serialize, Deserialize, Debug)] 9 | pub enum Request { 10 | GetDataFromServer, 11 | } 12 | 13 | impl Transferable for Request { } 14 | 15 | #[derive(Serialize, Deserialize, Debug)] 16 | pub enum Response { 17 | DataFetched, 18 | } 19 | 20 | impl Transferable for Response { } 21 | 22 | pub enum Msg { 23 | Updating, 24 | } 25 | 26 | pub struct Worker { 27 | link: AgentLink, 28 | interval: IntervalService, 29 | task: Box, 30 | fetch: FetchService, 31 | } 32 | 33 | impl Agent for Worker { 34 | type Reach = Context; 35 | type Message = Msg; 36 | type Input = Request; 37 | type Output = Response; 38 | 39 | fn create(link: AgentLink) -> Self { 40 | let mut interval = IntervalService::new(); 41 | let duration = Duration::from_secs(3); 42 | let callback = link.send_back(|_| Msg::Updating); 43 | let task = interval.spawn(duration, callback); 44 | Worker { 45 | link, 46 | interval, 47 | task: Box::new(task), 48 | fetch: FetchService::new(), 49 | } 50 | } 51 | 52 | fn update(&mut self, msg: Self::Message) { 53 | match msg { 54 | Msg::Updating => { 55 | info!("Tick..."); 56 | } 57 | } 58 | } 59 | 60 | fn handle(&mut self, msg: Self::Input, who: HandlerId) { 61 | info!("Request: {:?}", msg); 62 | match msg { 63 | Request::GetDataFromServer => { 64 | self.link.response(who, Response::DataFetched); 65 | } 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /examples/multi_thread/src/job.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use yew::prelude::worker::*; 3 | // TODO use yew::services::{IntervalService, FetchService, Task}; 4 | use yew::services::Task; 5 | use yew::services::interval::IntervalService; 6 | use yew::services::fetch::FetchService; 7 | 8 | #[derive(Serialize, Deserialize, Debug)] 9 | pub enum Request { 10 | GetDataFromServer, 11 | } 12 | 13 | impl Transferable for Request { } 14 | 15 | #[derive(Serialize, Deserialize, Debug)] 16 | pub enum Response { 17 | DataFetched, 18 | } 19 | 20 | impl Transferable for Response { } 21 | 22 | pub enum Msg { 23 | Updating, 24 | } 25 | 26 | pub struct Worker { 27 | link: AgentLink, 28 | interval: IntervalService, 29 | task: Box, 30 | fetch: FetchService, 31 | } 32 | 33 | impl Agent for Worker { 34 | type Reach = Job; 35 | type Message = Msg; 36 | type Input = Request; 37 | type Output = Response; 38 | 39 | fn create(link: AgentLink) -> Self { 40 | let mut interval = IntervalService::new(); 41 | let duration = Duration::from_secs(3); 42 | let callback = link.send_back(|_| Msg::Updating); 43 | let task = interval.spawn(duration, callback); 44 | Worker { 45 | link, 46 | interval, 47 | task: Box::new(task), 48 | fetch: FetchService::new(), 49 | } 50 | } 51 | 52 | fn update(&mut self, msg: Self::Message) { 53 | match msg { 54 | Msg::Updating => { 55 | info!("Tick..."); 56 | } 57 | } 58 | } 59 | 60 | fn handle(&mut self, msg: Self::Input, who: HandlerId) { 61 | info!("Request: {:?}", msg); 62 | match msg { 63 | Request::GetDataFromServer => { 64 | self.link.response(who, Response::DataFetched); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/multi_thread/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | #[macro_use] 6 | extern crate yew; 7 | 8 | pub mod native_worker; 9 | pub mod job; 10 | pub mod context; 11 | 12 | use yew::prelude::*; 13 | 14 | pub struct Model { 15 | worker: Box>, 16 | job: Box>, 17 | context: Box>, 18 | context_2: Box>, 19 | } 20 | 21 | pub enum Msg { 22 | SendToWorker, 23 | SendToJob, 24 | SendToContext, 25 | DataReceived, 26 | } 27 | 28 | impl Component for Model { 29 | type Message = Msg; 30 | type Properties = (); 31 | 32 | fn create(_: Self::Properties, mut link: ComponentLink) -> Self { 33 | let callback = link.send_back(|_| Msg::DataReceived); 34 | let worker = native_worker::Worker::bridge(callback); 35 | 36 | let callback = link.send_back(|_| Msg::DataReceived); 37 | let job = job::Worker::bridge(callback); 38 | 39 | let callback = link.send_back(|_| Msg::DataReceived); 40 | let context = context::Worker::bridge(callback); 41 | 42 | let callback = link.send_back(|_| Msg::DataReceived); 43 | let context_2 = context::Worker::bridge(callback); 44 | 45 | Model { worker, job, context, context_2 } 46 | } 47 | 48 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 49 | match msg { 50 | Msg::SendToWorker => { 51 | self.worker.send(native_worker::Request::GetDataFromServer); 52 | } 53 | Msg::SendToJob => { 54 | self.job.send(job::Request::GetDataFromServer); 55 | } 56 | Msg::SendToContext => { 57 | self.context.send(context::Request::GetDataFromServer); 58 | self.context_2.send(context::Request::GetDataFromServer); 59 | } 60 | Msg::DataReceived => { 61 | info!("DataReceived"); 62 | } 63 | } 64 | true 65 | } 66 | } 67 | 68 | impl Renderable for Model { 69 | fn view(&self) -> Html { 70 | html! { 71 |
72 | 77 |
78 | } 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /examples/multi_thread/src/native_worker.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use yew::prelude::worker::*; 3 | // TODO use yew::services::{IntervalService, FetchService, Task}; 4 | use yew::services::Task; 5 | use yew::services::interval::IntervalService; 6 | use yew::services::fetch::FetchService; 7 | 8 | #[derive(Serialize, Deserialize, Debug)] 9 | pub enum Request { 10 | GetDataFromServer, 11 | } 12 | 13 | impl Transferable for Request { } 14 | 15 | #[derive(Serialize, Deserialize, Debug)] 16 | pub enum Response { 17 | DataFetched, 18 | } 19 | 20 | impl Transferable for Response { } 21 | 22 | pub enum Msg { 23 | Updating, 24 | } 25 | 26 | pub struct Worker { 27 | link: AgentLink, 28 | interval: IntervalService, 29 | task: Box, 30 | fetch: FetchService, 31 | } 32 | 33 | impl Agent for Worker { 34 | type Reach = Public; 35 | type Message = Msg; 36 | type Input = Request; 37 | type Output = Response; 38 | 39 | fn create(link: AgentLink) -> Self { 40 | let mut interval = IntervalService::new(); 41 | let duration = Duration::from_secs(3); 42 | let callback = link.send_back(|_| Msg::Updating); 43 | let task = interval.spawn(duration, callback); 44 | Worker { 45 | link, 46 | interval, 47 | task: Box::new(task), 48 | fetch: FetchService::new(), 49 | } 50 | } 51 | 52 | fn update(&mut self, msg: Self::Message) { 53 | match msg { 54 | Msg::Updating => { 55 | info!("Tick..."); 56 | } 57 | } 58 | } 59 | 60 | fn handle(&mut self, msg: Self::Input, who: HandlerId) { 61 | info!("Request: {:?}", msg); 62 | match msg { 63 | Request::GetDataFromServer => { 64 | self.link.response(who, Response::DataFetched); 65 | } 66 | } 67 | } 68 | 69 | fn name_of_resource() -> &'static str { "bin/native_worker.js" } 70 | } 71 | -------------------------------------------------------------------------------- /examples/multi_thread/static/bin: -------------------------------------------------------------------------------- 1 | ../target/wasm32-unknown-unknown/release/ -------------------------------------------------------------------------------- /examples/multi_thread/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yew • Multi-Thread 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/npm_and_rest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "npm_and_rest" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | failure = "0.1" 8 | serde = "1" 9 | serde_derive = "1" 10 | stdweb = "0.4" 11 | yew = { path = "../.." } 12 | -------------------------------------------------------------------------------- /examples/npm_and_rest/src/ccxt.rs: -------------------------------------------------------------------------------- 1 | use stdweb::Value; 2 | use stdweb::unstable::TryInto; 3 | 4 | #[derive(Default)] 5 | pub struct CcxtService(Option); 6 | 7 | impl CcxtService { 8 | pub fn new() -> Self { 9 | let lib = js! { 10 | return ccxt; 11 | }; 12 | CcxtService(Some(lib)) 13 | } 14 | 15 | pub fn exchanges(&mut self) -> Vec { 16 | let lib = self.0.as_ref().expect("ccxt library object lost"); 17 | let v: Value = js! { 18 | var ccxt = @{lib}; 19 | console.log(ccxt.exchanges); 20 | return ccxt.exchanges; 21 | }; 22 | let v: Vec = v.try_into().expect("can't extract exchanges"); 23 | v 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/npm_and_rest/src/gravatar.rs: -------------------------------------------------------------------------------- 1 | use failure::Error; 2 | use yew::callback::Callback; 3 | use yew::format::{Json, Nothing}; 4 | use yew::services::fetch::{FetchService, FetchTask, Request, Response}; 5 | 6 | #[derive(Deserialize, Debug)] 7 | pub struct Profile { 8 | entry: Vec, 9 | } 10 | 11 | #[derive(Deserialize, Debug)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct Entry { 14 | id: String, 15 | hash: String, 16 | request_hash: String, 17 | profile_url: String, 18 | preferred_username: String, 19 | } 20 | 21 | #[derive(Default)] 22 | pub struct GravatarService { 23 | web: FetchService, 24 | } 25 | 26 | impl GravatarService { 27 | pub fn new() -> Self { 28 | Self { 29 | web: FetchService::new(), 30 | } 31 | } 32 | 33 | pub fn profile(&mut self, hash: &str, callback: Callback>) -> FetchTask { 34 | let url = format!("https://en.gravatar.com/{}.json", hash); 35 | let handler = move |response: Response>>| { 36 | let (meta, Json(data)) = response.into_parts(); 37 | if meta.status.is_success() { 38 | callback.emit(data) 39 | } else { 40 | // format_err! is a macro in crate `failure` 41 | callback.emit(Err(format_err!( 42 | "{}: error getting profile https://gravatar.com/", 43 | meta.status 44 | ))) 45 | } 46 | }; 47 | let request = Request::get(url.as_str()).body(Nothing).unwrap(); 48 | self.web.fetch(request, handler.into()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/npm_and_rest/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate failure; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | #[macro_use] 6 | extern crate stdweb; 7 | #[macro_use] 8 | extern crate yew; 9 | 10 | // Own services implementation 11 | pub mod gravatar; 12 | pub mod ccxt; 13 | 14 | use failure::Error; 15 | use yew::prelude::*; 16 | use yew::services::fetch::FetchTask; 17 | 18 | use gravatar::{GravatarService, Profile}; 19 | use ccxt::CcxtService; 20 | 21 | pub struct Model { 22 | gravatar: GravatarService, 23 | ccxt: CcxtService, 24 | callback: Callback>, 25 | profile: Option, 26 | exchanges: Vec, 27 | task: Option, 28 | } 29 | 30 | pub enum Msg { 31 | Gravatar, 32 | GravatarReady(Result), 33 | Exchanges, 34 | } 35 | 36 | impl Component for Model { 37 | type Message = Msg; 38 | type Properties = (); 39 | 40 | fn create(_: Self::Properties, mut link: ComponentLink) -> Self { 41 | Model { 42 | gravatar: GravatarService::new(), 43 | ccxt: CcxtService::new(), 44 | callback: link.send_back(Msg::GravatarReady), 45 | profile: None, 46 | exchanges: Vec::new(), 47 | task: None, 48 | } 49 | } 50 | 51 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 52 | match msg { 53 | Msg::Gravatar => { 54 | let task = self.gravatar.profile("205e460b479e2e5b48aec07710c08d50", self.callback.clone()); 55 | self.task = Some(task); 56 | } 57 | Msg::GravatarReady(Ok(profile)) => { 58 | self.profile = Some(profile); 59 | } 60 | Msg::GravatarReady(Err(_)) => { 61 | // Can't load gravatar profile 62 | } 63 | Msg::Exchanges => { 64 | self.exchanges = self.ccxt.exchanges(); 65 | } 66 | } 67 | true 68 | } 69 | } 70 | 71 | impl Renderable for Model { 72 | fn view(&self) -> Html { 73 | let view_exchange = |exchange| html! { 74 |
  • { exchange }
  • 75 | }; 76 | html! { 77 |
    78 | 79 | 80 |
      81 | { for self.exchanges.iter().map(view_exchange) } 82 |
    83 |
    84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/npm_and_rest/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate npm_and_rest; 3 | 4 | use yew::prelude::*; 5 | use npm_and_rest::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/npm_and_rest/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yew • npm and REST 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/routing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "routing" 3 | version = "0.1.0" 4 | authors = ["Henry Zimmerman "] 5 | 6 | [dependencies] 7 | log = "0.4" 8 | web_logger = "0.1" 9 | serde = "1.0" 10 | serde_derive = "1.0" 11 | yew = { path = "../.." } 12 | stdweb = "0.4" 13 | -------------------------------------------------------------------------------- /examples/routing/src/b_component.rs: -------------------------------------------------------------------------------- 1 | 2 | use router; 3 | use router::Route; 4 | use yew::prelude::*; 5 | use std::usize; 6 | 7 | 8 | pub struct BModel { 9 | number: Option, 10 | sub_path: Option, 11 | router: Box>> 12 | } 13 | 14 | pub enum Msg { 15 | Navigate(Vec), // Navigate after performing other actions 16 | Increment, 17 | Decrement, 18 | UpdateSubpath(String), 19 | HandleRoute(Route<()>) 20 | } 21 | 22 | 23 | impl Component for BModel { 24 | type Message = Msg; 25 | type Properties = (); 26 | 27 | fn create(_: Self::Properties, mut link: ComponentLink) -> Self { 28 | 29 | let callback = link.send_back(|route: Route<()>| Msg::HandleRoute(route)); 30 | let mut router = router::Router::bridge(callback); 31 | 32 | router.send(router::Request::GetCurrentRoute); 33 | 34 | BModel { 35 | number: None, 36 | sub_path: None, 37 | router 38 | } 39 | } 40 | 41 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 42 | match msg { 43 | Msg::Navigate(msgs) => { 44 | // Perform the wrapped action first 45 | for msg in msgs{ 46 | self.update(msg); 47 | } 48 | 49 | // The path dictating that this component be instantiated must be provided 50 | let mut path_segments = vec!["b".into()]; 51 | if let Some(ref sub_path) = self.sub_path { 52 | path_segments.push(sub_path.clone()) 53 | } 54 | 55 | let fragment: Option = self.number.map(|x: usize | x.to_string()); 56 | 57 | let route = router::Route { 58 | path_segments, 59 | query: None, 60 | fragment, 61 | state: (), 62 | }; 63 | 64 | self.router.send(router::Request::ChangeRoute(route)); 65 | false 66 | } 67 | Msg::HandleRoute(route) => { 68 | info!("Routing: {}", route.to_route_string()); 69 | // Instead of each component selecting which parts of the path are important to it, 70 | // it is also possible to match on the `route.to_route_string().as_str()` once 71 | // and create enum variants representing the different children and pass them as props. 72 | self.sub_path = route.path_segments.get(1).map(String::clone); 73 | self.number = route.fragment.and_then(|x| usize::from_str_radix(&x, 10).ok()); 74 | 75 | true 76 | } 77 | Msg::Increment => { 78 | let n = if let Some(number) = self.number{ 79 | number + 1 80 | } else { 81 | 1 82 | }; 83 | self.number = Some(n); 84 | true 85 | } 86 | Msg::Decrement => { 87 | let n: usize = if let Some(number) = self.number{ 88 | if number > 0 { 89 | number - 1 90 | } else { 91 | number 92 | } 93 | } else { 94 | 0 95 | }; 96 | self.number = Some(n); 97 | true 98 | } 99 | Msg::UpdateSubpath(path) => { 100 | self.sub_path = Some(path); 101 | true 102 | } 103 | } 104 | } 105 | 106 | fn change(&mut self, _props: Self::Properties) -> ShouldRender { 107 | // Apparently change MUST be implemented in this case, even though no props were changed 108 | true 109 | } 110 | } 111 | impl Renderable for BModel { 112 | fn view(&self) -> Html { 113 | html! { 114 |
    115 |
    116 | { self.display_number() } 117 | 118 | 119 |
    120 | 121 | { self.display_subpath_input() } 122 | 123 |
    124 | } 125 | } 126 | } 127 | 128 | impl BModel { 129 | fn display_number(&self) -> String { 130 | if let Some(number) = self.number { 131 | format!("Number: {}", number) 132 | } else { 133 | format!("Number: None") 134 | } 135 | } 136 | fn display_subpath_input(&self) -> Html { 137 | let sub_path = self.sub_path.clone(); 138 | html! { 139 | 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /examples/routing/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | extern crate serde; 6 | #[macro_use] 7 | extern crate yew; 8 | extern crate stdweb; 9 | 10 | mod router; 11 | mod routing; 12 | mod b_component; 13 | use b_component::BModel; 14 | 15 | use router::Route; 16 | use yew::prelude::*; 17 | 18 | 19 | pub enum Child { 20 | A, 21 | B, 22 | PathNotFound(String) 23 | } 24 | 25 | pub struct Model { 26 | child: Child, 27 | router: Box>> 28 | } 29 | 30 | pub enum Msg { 31 | NavigateTo(Child), 32 | HandleRoute(Route<()>) 33 | } 34 | 35 | impl Component for Model { 36 | type Message = Msg; 37 | type Properties = (); 38 | 39 | fn create(_: Self::Properties, mut link: ComponentLink) -> Self { 40 | 41 | let callback = link.send_back(|route: Route<()>| Msg::HandleRoute(route)); 42 | let mut router = router::Router::bridge(callback); 43 | 44 | // TODO Not sure if this is technically correct. This should be sent _after_ the component has been created. 45 | // I think the `Component` trait should have a hook called `on_mount()` 46 | // that is called after the component has been attached to the vdom. 47 | // It seems like this only works because the JS engine decides to activate the 48 | // router worker logic after the mounting has finished. 49 | router.send(router::Request::GetCurrentRoute); 50 | 51 | Model { 52 | child: Child::A, // This should be quickly overwritten by the actual route. 53 | router 54 | } 55 | } 56 | 57 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 58 | match msg { 59 | Msg::NavigateTo(child) => { 60 | 61 | let path_segments = match child { 62 | Child::A => vec!["a".into()], 63 | Child::B => vec!["b".into()], 64 | Child::PathNotFound(_) => vec!["path_not_found".into()] 65 | }; 66 | 67 | let route = router::Route { 68 | path_segments, 69 | query: None, 70 | fragment: None, 71 | state: (), 72 | }; 73 | 74 | self.router.send(router::Request::ChangeRoute(route)); 75 | false 76 | } 77 | Msg::HandleRoute(route) => { 78 | info!("Routing: {}", route.to_route_string()); 79 | // Instead of each component selecting which parts of the path are important to it, 80 | // it is also possible to match on the `route.to_route_string().as_str()` once 81 | // and create enum variants representing the different children and pass them as props. 82 | self.child = if let Some(first_segment) = route.path_segments.get(0) { 83 | match first_segment.as_str() { 84 | "a" => Child::A, 85 | "b" => Child::B, 86 | other => Child::PathNotFound(other.into()) 87 | } 88 | } else { 89 | Child::PathNotFound("path_not_found".into()) 90 | }; 91 | 92 | true 93 | } 94 | } 95 | } 96 | } 97 | 98 | impl Renderable for Model { 99 | fn view(&self) -> Html { 100 | html! { 101 |
    102 | 106 |
    107 | {self.child.view()} 108 |
    109 |
    110 | } 111 | } 112 | } 113 | 114 | impl Renderable for Child { 115 | fn view(&self) -> Html { 116 | match *self { 117 | Child::A => html! { 118 | <> 119 | {"This corresponds to route 'a'"} 120 | 121 | }, 122 | Child::B => html! { 123 | <> 124 | {"This corresponds to route 'b'"} 125 | 126 | 127 | }, 128 | Child::PathNotFound(ref path) => html! { 129 | <> 130 | {format!("Invalid path: '{}'", path)} 131 | 132 | } 133 | } 134 | } 135 | } 136 | 137 | -------------------------------------------------------------------------------- /examples/routing/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate routing; 3 | 4 | use yew::prelude::*; 5 | use routing::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } -------------------------------------------------------------------------------- /examples/routing/src/router.rs: -------------------------------------------------------------------------------- 1 | //! Agent that exposes a usable routing interface to components. 2 | 3 | use routing::RouteService; 4 | 5 | use yew::prelude::worker::*; 6 | 7 | use std::collections::HashSet; 8 | 9 | use stdweb::Value; 10 | use stdweb::JsSerialize; 11 | use stdweb::unstable::TryFrom; 12 | 13 | use serde::Serialize; 14 | use serde::Deserialize; 15 | 16 | use std::fmt::Debug; 17 | 18 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize) ] 19 | pub struct Route { 20 | pub path_segments: Vec, 21 | pub query: Option, 22 | pub fragment: Option, 23 | pub state: T 24 | } 25 | 26 | impl Route 27 | where T: JsSerialize + Clone + TryFrom + Default +'static 28 | { 29 | pub fn to_route_string(&self) -> String { 30 | let path = self.path_segments.join("/"); 31 | let mut path = format!("/{}", path); // add the leading '/' 32 | if let Some(ref query) = self.query { 33 | path = format!("{}?{}", path, query); 34 | } 35 | if let Some(ref fragment) = self.fragment { 36 | path = format!("{}#{}", path, fragment) 37 | } 38 | path 39 | } 40 | 41 | pub fn current_route(route_service: &RouteService) -> Self 42 | { 43 | let path = route_service.get_path(); // guaranteed to always start with a '/' 44 | let mut path_segments: Vec = path.split("/").map(String::from).collect(); 45 | path_segments.remove(0); // remove empty string that is split from the first '/' 46 | 47 | let mut query: String = route_service.get_query(); // The first character will be a '?' 48 | let query: Option = if query.len() > 1 { 49 | query.remove(0); 50 | Some(query) 51 | } else { 52 | None 53 | }; 54 | 55 | let mut fragment: String = route_service.get_fragment(); // The first character will be a '#' 56 | let fragment: Option = if fragment.len() > 1 { 57 | fragment.remove(0); 58 | Some(fragment) 59 | } else { 60 | None 61 | }; 62 | 63 | 64 | Route { 65 | path_segments, 66 | query, 67 | fragment, 68 | state: T::default() 69 | } 70 | } 71 | } 72 | 73 | pub enum Msg 74 | where T: JsSerialize + Clone + Debug + TryFrom + 'static 75 | { 76 | BrowserNavigationRouteChanged((String, T)), 77 | } 78 | 79 | 80 | 81 | impl Transferable for Route 82 | where for <'de> T: Serialize + Deserialize<'de> 83 | {} 84 | 85 | #[derive(Serialize, Deserialize, Debug)] 86 | pub enum Request { 87 | /// Changes the route using a RouteInfo struct and alerts connected components to the route change. 88 | ChangeRoute(Route), 89 | /// Changes the route using a RouteInfo struct, but does not alert connected components to the route change. 90 | ChangeRouteNoBroadcast(Route), 91 | GetCurrentRoute 92 | } 93 | 94 | impl Transferable for Request 95 | where for <'de> T: Serialize + Deserialize<'de> 96 | {} 97 | 98 | /// The Router worker holds on to the RouteService singleton and mediates access to it. 99 | pub struct Router 100 | where for <'de> T: JsSerialize + Clone + Debug + TryFrom + Default + Serialize + Deserialize<'de> + 'static 101 | { 102 | link: AgentLink>, 103 | route_service: RouteService, 104 | /// A list of all entities connected to the router. 105 | /// When a route changes, either initiated by the browser or by the app, 106 | /// the route change will be broadcast to all listening entities. 107 | subscribers: HashSet, 108 | } 109 | 110 | impl Agent for Router 111 | where for <'de> T: JsSerialize + Clone + Debug + TryFrom + Default + Serialize + Deserialize<'de> + 'static 112 | { 113 | type Reach = Context; 114 | type Message = Msg; 115 | type Input = Request; 116 | type Output = Route; 117 | 118 | fn create(link: AgentLink) -> Self { 119 | let callback = link.send_back(|route_changed: (String, T)| Msg::BrowserNavigationRouteChanged(route_changed)); 120 | let mut route_service = RouteService::new(); 121 | route_service.register_callback(callback); 122 | 123 | Router { 124 | link, 125 | route_service, 126 | subscribers: HashSet::new(), 127 | } 128 | } 129 | 130 | fn update(&mut self, msg: Self::Message) { 131 | match msg { 132 | Msg::BrowserNavigationRouteChanged((_route_string, state)) => { 133 | info!("Browser navigated"); 134 | let mut route = Route::current_route(&self.route_service); 135 | route.state = state; 136 | for sub in self.subscribers.iter() { 137 | self.link.response(*sub, route.clone()); 138 | } 139 | } 140 | } 141 | } 142 | 143 | fn handle(&mut self, msg: Self::Input, who: HandlerId) { 144 | info!("Request: {:?}", msg); 145 | match msg { 146 | Request::ChangeRoute(route) => { 147 | let route_string: String = route.to_route_string(); 148 | // set the route 149 | self.route_service.set_route(&route_string, route.state); 150 | // get the new route. This will contain a default state object 151 | let route = Route::current_route(&self.route_service); 152 | // broadcast it to all listening components 153 | for sub in self.subscribers.iter() { 154 | self.link.response(*sub, route.clone()); 155 | } 156 | } 157 | Request::ChangeRouteNoBroadcast(route) => { 158 | let route_string: String = route.to_route_string(); 159 | self.route_service.set_route(&route_string, route.state); 160 | } 161 | Request::GetCurrentRoute => { 162 | let route = Route::current_route(&self.route_service); 163 | self.link.response(who, route.clone()); 164 | } 165 | } 166 | } 167 | 168 | fn connected(&mut self, id: HandlerId) { 169 | self.subscribers.insert(id); 170 | } 171 | fn disconnected(&mut self, id: HandlerId) { 172 | self.subscribers.remove(&id); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /examples/routing/src/routing.rs: -------------------------------------------------------------------------------- 1 | //! Service to handle routing. 2 | 3 | use stdweb::web::History; 4 | use stdweb::web::Location; 5 | use stdweb::web::window; 6 | use stdweb::Value; 7 | use stdweb::web::EventListenerHandle; 8 | use stdweb::web::event::PopStateEvent; 9 | use stdweb::web::IEventTarget; 10 | use stdweb::JsSerialize; 11 | use stdweb::unstable::TryFrom; 12 | use yew::callback::Callback; 13 | 14 | use std::marker::PhantomData; 15 | 16 | 17 | /// A service that facilitates manipulation of the browser's URL bar and responding to browser 18 | /// 'forward' and 'back' events. 19 | /// 20 | /// The `T` determines what route state can be stored in the route service. 21 | pub struct RouteService { 22 | history: History, 23 | location: Location, 24 | event_listener: Option, 25 | phantom_data: PhantomData 26 | } 27 | 28 | 29 | impl RouteService 30 | where T: JsSerialize + Clone + TryFrom + 'static 31 | { 32 | /// Creates the route service. 33 | pub fn new() -> RouteService { 34 | let location = window().location().expect("browser does not support location API"); 35 | RouteService { 36 | history: window().history(), 37 | location, 38 | event_listener: None, 39 | phantom_data: PhantomData 40 | } 41 | } 42 | 43 | /// Registers a callback to the route service. 44 | /// Callbacks will be called when the History API experiences a change such as 45 | /// popping a state off of its stack when the forward or back buttons are pressed. 46 | pub fn register_callback(&mut self, callback: Callback<(String, T)>) { 47 | self.event_listener = Some(window().add_event_listener( 48 | move |event: PopStateEvent| { 49 | let state_value: Value = event.state(); 50 | 51 | if let Ok(state) = T::try_from(state_value) { 52 | let location: Location = window().location().unwrap(); 53 | let route: String = Self::get_route_from_location(&location); 54 | 55 | callback.emit((route.clone(), state.clone())) 56 | } else { 57 | eprintln!("Nothing farther back in history, not calling routing callback."); 58 | } 59 | }, 60 | )); 61 | } 62 | 63 | 64 | /// Sets the browser's url bar to contain the provided route, 65 | /// and creates a history entry that can be navigated via the forward and back buttons. 66 | /// The route should be a relative path that starts with a '/'. 67 | /// A state object be stored with the url. 68 | pub fn set_route(&mut self, route: &str, state: T) { 69 | 70 | self.history.push_state( 71 | state, 72 | "", 73 | Some(route), 74 | ); 75 | } 76 | 77 | fn get_route_from_location(location: &Location) -> String { 78 | let path = location.pathname().unwrap(); 79 | let query = location.search().unwrap(); 80 | let fragment = location.hash().unwrap(); 81 | format!("{path}{query}{fragment}", 82 | path=path, 83 | query=query, 84 | fragment=fragment) 85 | } 86 | 87 | /// Gets the concatenated path, query, and fragment strings 88 | pub fn get_route(&self) -> String { 89 | Self::get_route_from_location(&self.location) 90 | } 91 | 92 | /// Gets the path name of the current url. 93 | pub fn get_path(&self) -> String { 94 | self.location.pathname().unwrap() 95 | } 96 | 97 | /// Gets the query string of the current url. 98 | pub fn get_query(&self) -> String { 99 | self.location.search().unwrap() 100 | } 101 | 102 | /// Gets the fragment of the current url. 103 | pub fn get_fragment(&self) -> String { 104 | self.location.hash().unwrap() 105 | } 106 | } -------------------------------------------------------------------------------- /examples/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | tungstenite = "0.5.2" 8 | -------------------------------------------------------------------------------- /examples/server/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate tungstenite; 2 | 3 | use std::net::TcpListener; 4 | use std::thread::spawn; 5 | use tungstenite::server::accept; 6 | 7 | fn main() { 8 | let server = TcpListener::bind("127.0.0.1:9001").unwrap(); 9 | for stream in server.incoming() { 10 | spawn (move || { 11 | let mut websocket = accept(stream.unwrap()).unwrap(); 12 | loop { 13 | let msg = websocket.read_message().unwrap(); 14 | println!("Received: {}", msg); 15 | if msg.is_binary() || msg.is_text() { 16 | websocket.write_message(msg).unwrap(); 17 | } 18 | } 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/showcase/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "showcase" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin ", "Limira"] 5 | 6 | [dependencies] 7 | log = "0.4" 8 | web_logger = "0.1" 9 | strum = "0.9" 10 | strum_macros = "0.9" 11 | yew = { path = "../.." } 12 | counter = { path = "../counter" } 13 | crm = { path = "../crm" } 14 | custom_components = { path = "../custom_components" } 15 | dashboard = { path = "../dashboard" } 16 | fragments = { path = "../fragments" } 17 | game_of_life = { path = "../game_of_life" } 18 | inner_html = { path = "../inner_html" } 19 | large_table = { path = "../large_table" } 20 | mount_point = { path = "../mount_point" } 21 | npm_and_rest = { path = "../npm_and_rest" } 22 | routing = { path = "../routing" } 23 | textarea = { path = "../textarea" } 24 | timer = { path = "../timer" } 25 | todomvc = { path = "../todomvc" } 26 | two_apps = { path = "../two_apps" } 27 | -------------------------------------------------------------------------------- /examples/showcase/src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit="128"] 2 | 3 | #[macro_use] 4 | extern crate log; 5 | extern crate web_logger; 6 | extern crate strum; 7 | #[macro_use] 8 | extern crate strum_macros; 9 | #[macro_use] 10 | extern crate yew; 11 | extern crate counter; 12 | extern crate crm; 13 | extern crate custom_components; 14 | extern crate dashboard; 15 | extern crate fragments; 16 | extern crate game_of_life; 17 | extern crate inner_html; 18 | extern crate large_table; 19 | extern crate mount_point; 20 | extern crate npm_and_rest; 21 | extern crate routing; 22 | extern crate textarea; 23 | extern crate timer; 24 | extern crate todomvc; 25 | extern crate two_apps; 26 | 27 | use strum::IntoEnumIterator; 28 | use yew::components::Select; 29 | use yew::prelude::*; 30 | use counter::Model as Counter; 31 | use crm::Model as Crm; 32 | use custom_components::Model as CustomComponents; 33 | use dashboard::Model as Dashboard; 34 | use fragments::Model as Fragments; 35 | use game_of_life::Model as GameOfLife; 36 | use inner_html::Model as InnerHtml; 37 | use large_table::Model as LargeTable; 38 | use mount_point::Model as MountPoint; 39 | use npm_and_rest::Model as NpmAndRest; 40 | use routing::Model as Routing; 41 | use textarea::Model as Textarea; 42 | use timer::Model as Timer; 43 | use todomvc::Model as Todomvc; 44 | use two_apps::Model as TwoApps; 45 | 46 | #[derive(Clone, Debug, Display, EnumString, EnumIter, PartialEq)] 47 | enum Scene { 48 | Counter, 49 | Crm, 50 | CustomComponents, 51 | Dashboard, 52 | Fragments, 53 | GameOfLife, 54 | InnerHtml, 55 | LargeTable, 56 | MountPoint, 57 | NpmAndRest, 58 | Routing, 59 | Textarea, 60 | Timer, 61 | Todomvc, 62 | TwoApps, 63 | } 64 | 65 | struct Model { 66 | scene: Option, 67 | } 68 | 69 | enum Msg { 70 | SwitchTo(Scene), 71 | } 72 | 73 | impl Component for Model { 74 | type Message = Msg; 75 | type Properties = (); 76 | 77 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 78 | Self { 79 | scene: None, 80 | } 81 | } 82 | 83 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 84 | match msg { 85 | Msg::SwitchTo(scene) => { 86 | self.scene = Some(scene); 87 | true 88 | } 89 | } 90 | } 91 | } 92 | 93 | impl Renderable for Model { 94 | fn view(&self) -> Html { 95 | html! { 96 |
    97 |
    98 |

    { "Yew showcase" }

    99 | : 100 | selected=self.scene.clone(), 101 | options=Scene::iter().collect::>(), 102 | onchange=Msg::SwitchTo, 103 | /> 104 |
    105 |
    106 | { self.view_scene() } 107 |
    108 |
    109 | } 110 | } 111 | } 112 | 113 | impl Model { 114 | fn view_scene(&self) -> Html { 115 | if let Some(scene) = self.scene.as_ref() { 116 | match scene { 117 | Scene::Counter => { 118 | html! { 119 | 120 | } 121 | } 122 | Scene::Crm => { 123 | html! { 124 | 125 | } 126 | } 127 | Scene::CustomComponents => { 128 | html! { 129 | 130 | } 131 | } 132 | Scene::Dashboard => { 133 | html! { 134 | 135 | } 136 | } 137 | Scene::Fragments => { 138 | html! { 139 | 140 | } 141 | } 142 | Scene::GameOfLife => { 143 | html! { 144 | 145 | } 146 | } 147 | Scene::InnerHtml => { 148 | html! { 149 | 150 | } 151 | } 152 | Scene::LargeTable => { 153 | html! { 154 | 155 | } 156 | } 157 | Scene::MountPoint => { 158 | html! { 159 | 160 | } 161 | } 162 | Scene::NpmAndRest => { 163 | html! { 164 | 165 | } 166 | } 167 | Scene::Routing => { 168 | html! { 169 | 170 | } 171 | } 172 | Scene::Textarea => { 173 | html! { 174 | 175 | } 176 | } 177 | Scene::Timer => { 178 | html! { 179 | 180 | } 181 | } 182 | Scene::Todomvc => { 183 | html! { 184 | 185 | } 186 | } 187 | Scene::TwoApps => { 188 | html! { 189 | 190 | } 191 | } 192 | } 193 | } else { 194 | html! { 195 |

    { "Select the scene, please." }

    196 | } 197 | } 198 | } 199 | } 200 | 201 | fn main() { 202 | web_logger::init(); 203 | trace!("Initializing yew..."); 204 | yew::initialize(); 205 | trace!("Creating an application instance..."); 206 | let app: App = App::new(); 207 | trace!("Mount the App to the body of the page..."); 208 | app.mount_to_body(); 209 | trace!("Run"); 210 | yew::run_loop(); 211 | } 212 | 213 | -------------------------------------------------------------------------------- /examples/showcase/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yew • Showcase 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/showcase/static/styles.css: -------------------------------------------------------------------------------- 1 | html, body, #fullscreen { 2 | width: 100%; 3 | } 4 | #left_pane { 5 | position: absolute; 6 | width: 20%; 7 | } 8 | 9 | #right_pane { 10 | position: absolute; 11 | width: 80%; 12 | left: 20%; 13 | } -------------------------------------------------------------------------------- /examples/textarea/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "textarea" 3 | version = "0.1.0" 4 | authors = ["Andrew Straw "] 5 | 6 | [dependencies] 7 | yew = { path = "../.." } 8 | -------------------------------------------------------------------------------- /examples/textarea/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate yew; 3 | 4 | use yew::prelude::*; 5 | 6 | pub struct Model { 7 | value: String, 8 | } 9 | 10 | pub enum Msg { 11 | GotInput(String), 12 | Clicked, 13 | } 14 | 15 | impl Component for Model { 16 | type Message = Msg; 17 | type Properties = (); 18 | 19 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 20 | Model { 21 | value: "".into(), 22 | } 23 | } 24 | 25 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 26 | match msg { 27 | Msg::GotInput(new_value) => { 28 | self.value = new_value; 29 | } 30 | Msg::Clicked => { 31 | self.value = "blah blah blah".to_string(); 32 | } 33 | } 34 | true 35 | } 36 | } 37 | 38 | impl Renderable for Model { 39 | fn view(&self) -> Html { 40 | html! { 41 |
    42 |
    43 | 48 | 49 |
    50 |
    51 | {&self.value} 52 |
    53 |
    54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/textarea/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate textarea; 3 | 4 | use yew::prelude::*; 5 | use textarea::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/timer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "timer" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | yew = { path = "../.." } 8 | -------------------------------------------------------------------------------- /examples/timer/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate yew; 3 | 4 | use std::time::Duration; 5 | use yew::prelude::*; 6 | use yew::services::{ConsoleService, IntervalService, TimeoutService, Task}; 7 | 8 | pub struct Model { 9 | timeout: TimeoutService, 10 | interval: IntervalService, 11 | console: ConsoleService, 12 | callback_tick: Callback<()>, 13 | callback_done: Callback<()>, 14 | job: Option>, 15 | messages: Vec<&'static str>, 16 | _standalone: Box, 17 | } 18 | 19 | pub enum Msg { 20 | StartTimeout, 21 | StartInterval, 22 | Cancel, 23 | Done, 24 | Tick, 25 | } 26 | 27 | impl Component for Model { 28 | type Message = Msg; 29 | type Properties = (); 30 | 31 | fn create(_: Self::Properties, mut link: ComponentLink) -> Self { 32 | // This callback doesn't send any message to a scope 33 | let callback = |_| { 34 | println!("Example of a standalone callback."); 35 | }; 36 | let mut interval = IntervalService::new(); 37 | let handle = interval.spawn(Duration::from_secs(10), callback.into()); 38 | 39 | Model { 40 | timeout: TimeoutService::new(), 41 | interval, 42 | console: ConsoleService::new(), 43 | callback_tick: link.send_back(|_| Msg::Tick), 44 | callback_done: link.send_back(|_| Msg::Done), 45 | job: None, 46 | messages: Vec::new(), 47 | _standalone: Box::new(handle), 48 | } 49 | } 50 | 51 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 52 | match msg { 53 | Msg::StartTimeout => { 54 | { 55 | let handle = self.timeout.spawn(Duration::from_secs(3), self.callback_done.clone()); 56 | self.job = Some(Box::new(handle)); 57 | } 58 | self.messages.clear(); 59 | self.console.clear(); 60 | self.messages.push("Timer started!"); 61 | self.console.time_named("Timer"); 62 | } 63 | Msg::StartInterval => { 64 | { 65 | let handle = self.interval.spawn(Duration::from_secs(1), self.callback_tick.clone()); 66 | self.job = Some(Box::new(handle)); 67 | } 68 | self.messages.clear(); 69 | self.console.clear(); 70 | self.messages.push("Interval started!"); 71 | self.console.log("Interval started!"); 72 | } 73 | Msg::Cancel => { 74 | if let Some(mut task) = self.job.take() { 75 | task.cancel(); 76 | } 77 | self.messages.push("Canceled!"); 78 | self.console.warn("Canceled!"); 79 | self.console.assert(self.job.is_none(), "Job still exists!"); 80 | } 81 | Msg::Done => { 82 | self.messages.push("Done!"); 83 | self.console.group(); 84 | self.console.info("Done!"); 85 | self.console.time_named_end("Timer"); 86 | self.console.group_end(); 87 | self.job = None; 88 | } 89 | Msg::Tick => { 90 | self.messages.push("Tick..."); 91 | self.console.count_named("Tick"); 92 | } 93 | } 94 | true 95 | } 96 | } 97 | 98 | impl Renderable for Model { 99 | fn view(&self) -> Html { 100 | let view_message = |message| { 101 | html! {

    { message }

    } 102 | }; 103 | let has_job = self.job.is_some(); 104 | html! { 105 |
    106 | 107 | 108 | 109 |
    110 | { for self.messages.iter().map(view_message) } 111 |
    112 |
    113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/timer/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate timer; 3 | 4 | use yew::prelude::*; 5 | use timer::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } 12 | -------------------------------------------------------------------------------- /examples/todomvc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todomvc" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | strum = "0.9" 8 | strum_macros = "0.9" 9 | serde = "1" 10 | serde_derive = "1" 11 | yew = { path = "../.." } 12 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | ## Yew TodoMVC Demo 2 | 3 | This it an implementationt of [TodoMVC](http://todomvc.com/) app. 4 | 5 | Unlike other implementations, this stores the full state of the model in the storage, 6 | including: all entries, entered text and choosed filter. 7 | -------------------------------------------------------------------------------- /examples/todomvc/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate yew; 2 | extern crate todomvc; 3 | 4 | use yew::prelude::*; 5 | use todomvc::Model; 6 | 7 | fn main() { 8 | yew::initialize(); 9 | App::::new().mount_to_body(); 10 | yew::run_loop(); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /examples/todomvc/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yew • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/two_apps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "two_apps" 3 | version = "0.1.0" 4 | authors = ["Denis Kolodin "] 5 | 6 | [dependencies] 7 | stdweb = "0.4" 8 | yew = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/two_apps/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// This example demonstrates low-level usage of scopes. 2 | 3 | #[macro_use] 4 | extern crate yew; 5 | 6 | use yew::html::*; 7 | 8 | pub struct Model { 9 | scope: Option>, 10 | selector: &'static str, 11 | title: String, 12 | } 13 | 14 | pub enum Msg { 15 | SetScope(Scope), 16 | SendToOpposite(String), 17 | SetTitle(String), 18 | } 19 | 20 | impl Component for Model { 21 | type Message = Msg; 22 | type Properties = (); 23 | 24 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 25 | Model { 26 | scope: None, 27 | selector: "", 28 | title: "Nothing".into(), 29 | } 30 | } 31 | 32 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 33 | match msg { 34 | Msg::SetScope(scope) => { 35 | self.scope = Some(scope); 36 | } 37 | Msg::SendToOpposite(title) => { 38 | self.scope.as_mut().unwrap().send_message(Msg::SetTitle(title)); 39 | } 40 | Msg::SetTitle(title) => { 41 | match title.as_ref() { 42 | "Ping" => { 43 | self.scope.as_mut().unwrap().send_message(Msg::SetTitle("Pong".into())); 44 | } 45 | "Pong" => { 46 | self.scope.as_mut().unwrap().send_message(Msg::SetTitle("Pong Done".into())); 47 | } 48 | "Pong Done" => { 49 | self.scope.as_mut().unwrap().send_message(Msg::SetTitle("Ping Done".into())); 50 | } 51 | _ => { 52 | } 53 | } 54 | self.title = title; 55 | } 56 | } 57 | true 58 | } 59 | } 60 | 61 | impl Renderable for Model { 62 | fn view(&self) -> Html { 63 | html! { 64 |
    65 |

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

    66 | 67 | 68 | 69 | 70 |
    71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/two_apps/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate stdweb; 2 | extern crate yew; 3 | extern crate two_apps; 4 | 5 | use stdweb::web::{IParentNode, document}; 6 | use yew::prelude::*; 7 | use yew::html::Scope; 8 | use two_apps::{Model, Msg}; 9 | 10 | fn mount_app(selector: &'static str, app: App) -> Scope { 11 | let element = document().query_selector(selector).unwrap().unwrap(); 12 | app.mount(element) 13 | } 14 | 15 | fn main() { 16 | yew::initialize(); 17 | let first_app = App::new(); 18 | let second_app = App::new(); 19 | let mut to_first = mount_app(".first-app", first_app); 20 | let mut to_second = mount_app(".second-app", second_app); 21 | to_first.send_message(Msg::SetScope(to_second.clone())); 22 | to_second.send_message(Msg::SetScope(to_first.clone())); 23 | yew::run_loop(); 24 | } 25 | -------------------------------------------------------------------------------- /examples/two_apps/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yew • Two Apps 6 | 7 | 8 | 9 |
    10 |
    11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | //! This module contains `App` sctruct which used to bootstrap 2 | //! a component in an isolated scope. 3 | 4 | use html::{Component, Renderable, Scope}; 5 | use web_sys::{window, Element}; 6 | 7 | /// An application instance. 8 | pub struct App { 9 | /// `Scope` holder 10 | scope: Scope, 11 | } 12 | 13 | impl App 14 | where 15 | COMP: Component + Renderable, 16 | { 17 | /// Creates a new `App` with a component in a context. 18 | pub fn new() -> Self { 19 | let scope = Scope::new(); 20 | App { scope } 21 | } 22 | 23 | /// Alias to `mount("body", ...)`. 24 | pub fn mount_to_body(self) -> Scope { 25 | // Bootstrap the component for `Window` environment only (not for `Worker`) 26 | let element = window() 27 | .expect("context needs a window") 28 | .document() 29 | .expect("window needs a document") 30 | .query_selector("body") 31 | .expect("can't get body node for rendering") 32 | .expect("can't unwrap body node"); 33 | self.mount(element, None) 34 | } 35 | 36 | /// Alias to `mount()` that allows using a selector 37 | pub fn mount_to_selector(self, selector: &str) -> Scope { 38 | let element = window() 39 | .expect("context needs a window") 40 | .document() 41 | .expect("window needs a document") 42 | .query_selector(selector) 43 | .expect("can't get node for rendering") 44 | .expect("can't unwrap body node"); 45 | self.mount(element, None) 46 | } 47 | 48 | /// Alias to `mount()` that allows passing in initial props 49 | pub fn mount_with_props(self, element: Element, props: COMP::Properties) -> Scope { 50 | self.mount(element, Some(props)) 51 | } 52 | 53 | /// The main entrypoint of a yew program. It works similar as `program` 54 | /// function in Elm. You should provide an initial model, `update` function 55 | /// which will update the state of the model and a `view` function which 56 | /// will render the model to a virtual DOM tree. 57 | pub fn mount(self, element: Element, props: Option) -> Scope { 58 | clear_element(&element); 59 | self.scope.mount_in_place(element, None, None, props) 60 | } 61 | } 62 | 63 | /// Removes anything from the given element. 64 | fn clear_element(element: &Element) { 65 | while let Some(child) = element.last_child() { 66 | element.remove_child(&child).expect("can't remove a child"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/callback.rs: -------------------------------------------------------------------------------- 1 | //! This module contains structs to interact with `Scope`s. 2 | 3 | use std::rc::Rc; 4 | 5 | /// Universal callback wrapper. 6 | /// 11 | /// `Rc` wrapper used to make it clonable. 12 | #[must_use] 13 | pub struct Callback(Rc); 14 | 15 | impl From for Callback { 16 | fn from(func: F) -> Self { 17 | Callback(Rc::new(func)) 18 | } 19 | } 20 | 21 | impl Clone for Callback { 22 | fn clone(&self) -> Self { 23 | Callback(self.0.clone()) 24 | } 25 | } 26 | 27 | impl PartialEq for Callback { 28 | fn eq(&self, other: &Callback) -> bool { 29 | Rc::ptr_eq(&self.0, &other.0) 30 | } 31 | } 32 | 33 | impl Callback { 34 | /// This method calls the actual callback. 35 | pub fn emit(&self, value: IN) { 36 | (self.0)(value); 37 | } 38 | } 39 | 40 | impl Callback { 41 | /// Changes input type of the callback to another. 42 | /// Works like common `map` method but in an opposite direction. 43 | pub fn reform(self, func: F) -> Callback 44 | where 45 | F: Fn(T) -> IN + 'static, 46 | { 47 | let func = move |input| { 48 | let output = func(input); 49 | self.clone().emit(output); 50 | }; 51 | Callback::from(func) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains useful components. 2 | //! At this moment it includes typed `Select` only. 3 | 4 | pub mod select; 5 | 6 | pub use self::select::Select; 7 | -------------------------------------------------------------------------------- /src/components/select.rs: -------------------------------------------------------------------------------- 1 | //! This module contains implementation of `Select` component. 2 | //! You can use it instead ` { 106 | let value = elem.selected_index(); 107 | Msg::Selected(if value < 0 { 108 | None 109 | } else { 110 | Some(value as usize) 111 | }) 112 | } 113 | _ => { 114 | unreachable!(); 115 | } 116 | } 117 | },> 118 | 121 | { for self.props.options.iter().map(view_option) } 122 | 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Plaster Framework - API Documentation 2 | //! 3 | //! Plaster is a framework for web-client apps created with 4 | //! a modern Rust-to-Wasm compilation feature. 5 | //! This framework was highly inspired by 6 | //! [Elm](http://elm-lang.org/) and [React](https://reactjs.org/). 7 | //! Forked originally from [Yew](https://github.com/DenisKolodin/yew). 8 | //! 9 | //! Minimal example: 10 | //! 11 | //! ```rust 12 | //! #[macro_use] 13 | //! extern crate plaster; 14 | //! use plaster::prelude::*; 15 | //! 16 | //! struct Model { 17 | //! value: i64, 18 | //! } 19 | //! 20 | //! enum Msg { 21 | //! DoIt, 22 | //! } 23 | //! 24 | //! impl Component for Model { 25 | //! type Message = Msg; 26 | //! type Properties = (); 27 | //! fn create(_: Self::Properties, _: ComponentLink) -> Self { 28 | //! Self { 29 | //! value: 0, 30 | //! } 31 | //! } 32 | //! 33 | //! fn update(&mut self, msg: Self::Message) -> ShouldRender { 34 | //! match msg { 35 | //! Msg::DoIt => self.value = self.value + 1 36 | //! } 37 | //! true 38 | //! } 39 | //! } 40 | //! 41 | //! impl Renderable for Model { 42 | //! fn view(&self) -> Html { 43 | //! html! { 44 | //!
    45 | //! 46 | //!

    { self.value }

    47 | //!
    48 | //! } 49 | //! } 50 | //! } 51 | //! 52 | //! #[wasm_bindgen(start)] 53 | //! fn main() { 54 | //! App::::new().mount_to_body(); 55 | //! } 56 | //! ``` 57 | //! 58 | 59 | #![deny( 60 | missing_docs, 61 | bare_trait_objects, 62 | anonymous_parameters, 63 | elided_lifetimes_in_paths 64 | )] 65 | #![recursion_limit = "512"] 66 | 67 | #[macro_use] 68 | extern crate log; 69 | extern crate futures; 70 | extern crate js_sys; 71 | extern crate wasm_bindgen; 72 | extern crate wasm_bindgen_futures; 73 | extern crate web_sys; 74 | 75 | #[macro_use] 76 | pub mod macros; 77 | // todo: figure out what to do with this 78 | // pub mod agent; 79 | pub mod app; 80 | pub mod callback; 81 | pub mod components; 82 | pub mod html; 83 | pub mod prelude; 84 | pub mod scheduler; 85 | pub mod virtual_dom; 86 | 87 | use std::cell::RefCell; 88 | use std::rc::Rc; 89 | 90 | type Shared = Rc>; 91 | 92 | struct Hidden; 93 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! The Yew Prelude 2 | //! 3 | //! The purpose of this module is to alleviate imports of many common types: 4 | //! 5 | //! ``` 6 | //! # #![allow(unused_imports)] 7 | //! use yew::prelude::*; 8 | //! ``` 9 | pub use html::{ 10 | ChangeData, Component, ComponentLink, Href, Html, InputData, Renderable, ShouldRender, 11 | }; 12 | 13 | pub use app::App; 14 | 15 | pub use callback::Callback; 16 | 17 | pub use web_sys::{ 18 | DragEvent, Event, FocusEvent, InputEvent, KeyEvent, KeyboardEvent, MouseEvent, 19 | MouseScrollEvent, Node as HtmlNode, PointerEvent, 20 | }; 21 | 22 | // todo: figure out what to do with this 23 | // pub use agent::{Bridge, Bridged, Threaded}; 24 | 25 | // /// Prelude module for creating worker. 26 | // pub mod worker { 27 | // pub use agent::{ 28 | // Agent, AgentLink, Bridge, Bridged, Context, Global, HandlerId, Job, Private, Public, 29 | // Transferable, 30 | // }; 31 | // } 32 | -------------------------------------------------------------------------------- /src/scheduler.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a scheduler. 2 | 3 | use std::cell::RefCell; 4 | use std::collections::VecDeque; 5 | use std::rc::Rc; 6 | use std::sync::atomic::{AtomicBool, Ordering}; 7 | use Shared; 8 | 9 | thread_local! { 10 | static SCHEDULER: Rc = 11 | Rc::new(Scheduler::new()); 12 | } 13 | 14 | pub(crate) fn scheduler() -> Rc { 15 | SCHEDULER.with(Rc::clone) 16 | } 17 | 18 | /// A routine which could be run. 19 | pub(crate) trait Runnable { 20 | /// Runs a routine with a context instance. 21 | fn run(&mut self); 22 | } 23 | 24 | /// This is a global scheduler suitable to schedule and run any tasks. 25 | pub(crate) struct Scheduler { 26 | lock: Rc, 27 | sequence: Shared>>, 28 | } 29 | 30 | impl Clone for Scheduler { 31 | fn clone(&self) -> Self { 32 | Scheduler { 33 | lock: self.lock.clone(), 34 | sequence: self.sequence.clone(), 35 | } 36 | } 37 | } 38 | 39 | impl Scheduler { 40 | /// Creates a new scheduler with a context. 41 | fn new() -> Self { 42 | let sequence = VecDeque::new(); 43 | Scheduler { 44 | lock: Rc::new(AtomicBool::new(false)), 45 | sequence: Rc::new(RefCell::new(sequence)), 46 | } 47 | } 48 | 49 | pub(crate) fn put_and_try_run(&self, runnable: Box) { 50 | self.sequence.borrow_mut().push_back(runnable); 51 | if self.lock.compare_and_swap(false, true, Ordering::Relaxed) == false { 52 | loop { 53 | let do_next = self.sequence.borrow_mut().pop_front(); 54 | if let Some(mut runnable) = do_next { 55 | runnable.run(); 56 | } else { 57 | break; 58 | } 59 | } 60 | self.lock.store(false, Ordering::Relaxed); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/virtual_dom/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the implementation of reactive virtual dom concept. 2 | 3 | pub mod vcomp; 4 | pub mod vlist; 5 | pub mod vnode; 6 | pub mod vtag; 7 | pub mod vtext; 8 | 9 | use std::collections::{HashMap, HashSet}; 10 | use std::fmt; 11 | use web_sys::{Element, Node}; 12 | 13 | pub use self::vcomp::VComp; 14 | pub use self::vlist::VList; 15 | pub use self::vnode::VNode; 16 | pub use self::vtag::VTag; 17 | pub use self::vtext::VText; 18 | use html::{Component, EventListenerHandle, Scope}; 19 | 20 | /// `Listener` trait is an universal implementation of an event listener 21 | /// which helps to bind Rust-listener to JS-listener (DOM). 22 | pub trait Listener { 23 | /// Returns standard name of DOM's event. 24 | fn kind(&self) -> &'static str; 25 | /// Attaches listener to the element and uses scope instance to send 26 | /// prepaired event back to the yew main loop. 27 | fn attach(&mut self, element: &Element, scope: Scope) -> EventListenerHandle; 28 | } 29 | 30 | impl fmt::Debug for dyn Listener { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | write!(f, "Listener {{ kind: {} }}", self.kind()) 33 | } 34 | } 35 | 36 | /// A list of event listeners. 37 | type Listeners = Vec>>; 38 | 39 | /// A map of attributes. 40 | type Attributes = HashMap; 41 | 42 | /// A set of classes. 43 | type Classes = HashSet; 44 | 45 | /// Patch for DOM node modification. 46 | enum Patch { 47 | Add(ID, T), 48 | Replace(ID, T), 49 | Remove(ID), 50 | } 51 | 52 | /// Reform of a node. 53 | enum Reform { 54 | /// Don't create a NEW reference (js Node). 55 | /// 56 | /// The reference _may still be mutated_. 57 | Keep, 58 | 59 | /// Create a new reference (js Node). 60 | /// 61 | /// The optional `Node` is used to insert the 62 | /// new node in the correct slot of the parent. 63 | /// 64 | /// If it does not exist, a `precursor` must be 65 | /// speccified (see `VDiff::apply()`). 66 | Before(Option), 67 | } 68 | 69 | // TODO What about to implement `VDiff` for `Element`? 70 | // In makes possible to include ANY element into the tree. 71 | // `Ace` editor embedding for example? 72 | 73 | /// This trait provides features to update a tree by other tree comparsion. 74 | pub trait VDiff { 75 | /// The component which this instance put into. 76 | type Component: Component; 77 | 78 | /// Remove itself from parent and return the next sibling. 79 | fn detach(&mut self, parent: &Node) -> Option; 80 | 81 | /// Scoped diff apply to other tree. 82 | /// 83 | /// Virtual rendering for the node. It uses parent node and existing children (virtual and DOM) 84 | /// to check the difference and apply patches to the actual DOM represenatation. 85 | /// 86 | /// Parameters: 87 | /// - `parent`: the parent node in the DOM. 88 | /// - `precursor`: the "previous node" in a list of nodes, used to efficiently 89 | /// find where to put the node. 90 | /// - `ancestor`: the node that this node will be replacing in the DOM. 91 | /// This method will _always_ remove the `ancestor` from the `parent`. 92 | /// - `env`: the `Env`. 93 | /// 94 | /// ### Internal Behavior Notice: 95 | /// 96 | /// Note that these modify the DOM by modifying the reference that _already_ exists 97 | /// on the `ancestor`. If `self.reference` exists (which it _shouldn't_) this method 98 | /// will panic. 99 | /// 100 | /// The exception to this is obviously `VRef` which simply uses the inner `Node` directly 101 | /// (always removes the `Node` that exists). 102 | fn apply( 103 | &mut self, 104 | parent: &Node, 105 | precursor: Option<&Node>, 106 | ancestor: Option>, 107 | scope: &Scope, 108 | ) -> Option; 109 | } 110 | -------------------------------------------------------------------------------- /src/virtual_dom/vlist.rs: -------------------------------------------------------------------------------- 1 | //! This module contains fragments implementation. 2 | use super::{VDiff, VNode, VText}; 3 | use html::{Component, Scope}; 4 | use std::iter::FromIterator; 5 | use web_sys::Node; 6 | 7 | /// This struct represents a fragment of the Virtual DOM tree. 8 | pub struct VList { 9 | /// The list of children nodes. Which also could have own children. 10 | pub childs: Vec>, 11 | } 12 | 13 | impl From>> for VList { 14 | fn from(other: Vec>) -> Self { 15 | VList { childs: other } 16 | } 17 | } 18 | 19 | impl FromIterator> for VList { 20 | fn from_iter>>(iter: I) -> Self { 21 | let mut list = VList::new(); 22 | 23 | for c in iter { 24 | list.add_child(c); 25 | } 26 | 27 | list 28 | } 29 | } 30 | 31 | impl VList { 32 | /// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM). 33 | pub fn new() -> Self { 34 | VList { childs: Vec::new() } 35 | } 36 | 37 | /// Add `VNode` child. 38 | pub fn add_child(&mut self, child: VNode) { 39 | self.childs.push(child); 40 | } 41 | } 42 | 43 | impl VDiff for VList { 44 | type Component = COMP; 45 | 46 | fn detach(&mut self, parent: &Node) -> Option { 47 | let mut last_sibling = None; 48 | for mut child in self.childs.drain(..) { 49 | last_sibling = child.detach(parent); 50 | } 51 | last_sibling 52 | } 53 | 54 | fn apply( 55 | &mut self, 56 | parent: &Node, 57 | precursor: Option<&Node>, 58 | ancestor: Option>, 59 | env: &Scope, 60 | ) -> Option { 61 | // Reuse precursor, because fragment reuse parent 62 | let mut precursor = precursor.map(|node| node.to_owned()); 63 | let mut rights = { 64 | match ancestor { 65 | // If element matched this type 66 | Some(VNode::VList(mut vlist)) => { 67 | // Previously rendered items 68 | vlist.childs.drain(..).map(Some).collect::>() 69 | } 70 | Some(mut vnode) => { 71 | // Use the current node as a single fragment list 72 | // and let the `apply` of `VNode` to handle it. 73 | vec![Some(vnode)] 74 | } 75 | None => Vec::new(), 76 | } 77 | }; 78 | // Collect elements of an ancestor if exists or use an empty vec 79 | // TODO DRY?! 80 | if self.childs.is_empty() { 81 | // Fixes: https://github.com/DenisKolodin/yew/issues/294 82 | // Without a placeholder the next element becomes first 83 | // and corrupts the order of rendering 84 | // We use empty text element to stake out a place 85 | let placeholder = VText::new("".into()); 86 | self.childs.push(placeholder.into()); 87 | } 88 | let mut lefts = self.childs.iter_mut().map(Some).collect::>(); 89 | // Process children 90 | let diff = lefts.len() as i32 - rights.len() as i32; 91 | if diff > 0 { 92 | for _ in 0..diff { 93 | rights.push(None); 94 | } 95 | } else if diff < 0 { 96 | for _ in 0..-diff { 97 | lefts.push(None); 98 | } 99 | } 100 | for pair in lefts.into_iter().zip(rights) { 101 | match pair { 102 | (Some(left), right) => { 103 | precursor = left.apply(parent, precursor.as_ref(), right, &env); 104 | } 105 | (None, Some(mut right)) => { 106 | right.detach(parent); 107 | } 108 | (None, None) => { 109 | panic!("redundant iterations during diff"); 110 | } 111 | } 112 | } 113 | precursor 114 | } 115 | } 116 | 117 | impl std::fmt::Debug for VList { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | write!(f, "VList [ {:?} ]", self.childs) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/virtual_dom/vnode.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the implementation of abstract virtual node. 2 | 3 | use super::{VComp, VDiff, VList, VTag, VText}; 4 | use html::{Component, Renderable, Scope}; 5 | use std::cmp::PartialEq; 6 | use std::fmt; 7 | use web_sys::Node; 8 | 9 | /// Bind virtual element to a DOM reference. 10 | pub enum VNode { 11 | /// A bind between `VTag` and `Element`. 12 | VTag(VTag), 13 | /// A bind between `VText` and `TextNode`. 14 | VText(VText), 15 | /// A bind between `VComp` and `Element`. 16 | VComp(VComp), 17 | /// A holder for a list of other nodes. 18 | VList(VList), 19 | /// A holder for any `Node` (necessary for replacing node). 20 | VRef(Node), 21 | } 22 | 23 | impl VDiff for VNode { 24 | type Component = COMP; 25 | 26 | /// Remove VNode from parent. 27 | fn detach(&mut self, parent: &Node) -> Option { 28 | match *self { 29 | VNode::VTag(ref mut vtag) => vtag.detach(parent), 30 | VNode::VText(ref mut vtext) => vtext.detach(parent), 31 | VNode::VComp(ref mut vcomp) => vcomp.detach(parent), 32 | VNode::VList(ref mut vlist) => vlist.detach(parent), 33 | VNode::VRef(ref node) => { 34 | let sibling = node.next_sibling(); 35 | parent 36 | .remove_child(node) 37 | .expect("can't remove node by VRef"); 38 | sibling 39 | } 40 | } 41 | } 42 | 43 | fn apply( 44 | &mut self, 45 | parent: &Node, 46 | precursor: Option<&Node>, 47 | ancestor: Option>, 48 | env: &Scope, 49 | ) -> Option { 50 | match *self { 51 | VNode::VTag(ref mut vtag) => vtag.apply(parent, precursor, ancestor, env), 52 | VNode::VText(ref mut vtext) => vtext.apply(parent, precursor, ancestor, env), 53 | VNode::VComp(ref mut vcomp) => vcomp.apply(parent, precursor, ancestor, env), 54 | VNode::VList(ref mut vlist) => vlist.apply(parent, precursor, ancestor, env), 55 | VNode::VRef(ref mut node) => { 56 | let sibling = match ancestor { 57 | Some(mut n) => n.detach(parent), 58 | None => None, 59 | }; 60 | if let Some(sibling) = sibling { 61 | parent 62 | .insert_before(node, Some(&sibling)) 63 | .expect("can't insert element before sibling"); 64 | } else { 65 | parent 66 | .append_child(node) 67 | .expect("could not append child to node"); 68 | } 69 | 70 | Some(node.to_owned()) 71 | } 72 | } 73 | } 74 | } 75 | 76 | impl From> for VNode { 77 | fn from(vtext: VText) -> Self { 78 | VNode::VText(vtext) 79 | } 80 | } 81 | 82 | impl From> for VNode { 83 | fn from(vlist: VList) -> Self { 84 | VNode::VList(vlist) 85 | } 86 | } 87 | 88 | impl From> for VNode { 89 | fn from(vtag: VTag) -> Self { 90 | VNode::VTag(vtag) 91 | } 92 | } 93 | 94 | impl From> for VNode { 95 | fn from(vcomp: VComp) -> Self { 96 | VNode::VComp(vcomp) 97 | } 98 | } 99 | 100 | impl From for VNode { 101 | fn from(value: T) -> Self { 102 | VNode::VText(VText::new(value.to_string())) 103 | } 104 | } 105 | 106 | impl<'a, COMP: Component> From<&'a dyn Renderable> for VNode { 107 | fn from(value: &'a dyn Renderable) -> Self { 108 | value.view() 109 | } 110 | } 111 | 112 | impl fmt::Debug for VNode { 113 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | match *self { 115 | VNode::VTag(ref vtag) => vtag.fmt(f), 116 | VNode::VText(ref vtext) => vtext.fmt(f), 117 | VNode::VComp(_) => "Component<>".fmt(f), 118 | VNode::VList(ref vlist) => vlist.fmt(f), 119 | VNode::VRef(_) => "NodeReference<>".fmt(f), 120 | } 121 | } 122 | } 123 | 124 | impl PartialEq for VNode { 125 | fn eq(&self, other: &VNode) -> bool { 126 | match *self { 127 | VNode::VTag(ref vtag_a) => match *other { 128 | VNode::VTag(ref vtag_b) => vtag_a == vtag_b, 129 | _ => false, 130 | }, 131 | VNode::VText(ref vtext_a) => match *other { 132 | VNode::VText(ref vtext_b) => vtext_a == vtext_b, 133 | _ => false, 134 | }, 135 | _ => { 136 | // TODO Implement it 137 | false 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/virtual_dom/vtext.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the implementation of a virtual text node `VText`. 2 | 3 | use super::{Reform, VDiff, VNode}; 4 | use html::{Component, Scope}; 5 | use std::cmp::PartialEq; 6 | use std::fmt; 7 | use std::marker::PhantomData; 8 | use web_sys::{window, Node, Text}; 9 | 10 | /// A type for a virtual 11 | /// [`TextNode`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode) 12 | /// represenation. 13 | pub struct VText { 14 | /// Contains a text of the node. 15 | pub text: String, 16 | /// A reference to the `TextNode`. 17 | pub reference: Option, 18 | _comp: PhantomData, 19 | } 20 | 21 | impl VText { 22 | /// Creates new virtual text node with a content. 23 | pub fn new(text: String) -> Self { 24 | VText { 25 | text, 26 | reference: None, 27 | _comp: PhantomData, 28 | } 29 | } 30 | } 31 | 32 | impl VDiff for VText { 33 | type Component = COMP; 34 | 35 | /// Remove VTag from parent. 36 | fn detach(&mut self, parent: &Node) -> Option { 37 | let node = self 38 | .reference 39 | .take() 40 | .expect("tried to remove not rendered VText from DOM"); 41 | let sibling = node.next_sibling(); 42 | if parent.remove_child(&node).is_err() { 43 | warn!("Node not found to remove VText"); 44 | } 45 | sibling 46 | } 47 | 48 | /// Renders virtual node over existent `TextNode`, but 49 | /// only if value of text had changed. 50 | /// Parameter `precursor` is necesssary for `VTag` and `VList` which 51 | /// has children and renders them. 52 | fn apply( 53 | &mut self, 54 | parent: &Node, 55 | _: Option<&Node>, 56 | opposite: Option>, 57 | _: &Scope, 58 | ) -> Option { 59 | assert!( 60 | self.reference.is_none(), 61 | "reference is ignored so must not be set" 62 | ); 63 | let reform = { 64 | match opposite { 65 | // If element matched this type 66 | Some(VNode::VText(mut vtext)) => { 67 | self.reference = vtext.reference.take(); 68 | if self.text != vtext.text { 69 | if let Some(ref element) = self.reference { 70 | element.set_node_value(Some(&self.text)); 71 | } 72 | } 73 | Reform::Keep 74 | } 75 | Some(mut vnode) => { 76 | let node = vnode.detach(parent); 77 | Reform::Before(node) 78 | } 79 | None => Reform::Before(None), 80 | } 81 | }; 82 | match reform { 83 | Reform::Keep => {} 84 | Reform::Before(node) => { 85 | let element = window() 86 | .expect("context needs a window") 87 | .document() 88 | .expect("window needs a document") 89 | .create_text_node(&self.text); 90 | if let Some(sibling) = node { 91 | parent 92 | .insert_before(&element, Some(&sibling)) 93 | .expect("can't insert text before sibling"); 94 | } else { 95 | parent 96 | .append_child(&element) 97 | .expect("could not append child to node"); 98 | } 99 | self.reference = Some(element); 100 | } 101 | } 102 | self.reference.as_ref().map(|t| t.to_owned().into()) 103 | } 104 | } 105 | 106 | impl fmt::Debug for VText { 107 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 | write!(f, "VText {{ text: {} }}", self.text) 109 | } 110 | } 111 | 112 | impl PartialEq for VText { 113 | fn eq(&self, other: &VText) -> bool { 114 | self.text == other.text 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/vcomp_test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate plaster; 3 | #[macro_use] 4 | extern crate wasm_bindgen_test; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | use plaster::prelude::*; 9 | use plaster::virtual_dom::VNode; 10 | 11 | struct Comp; 12 | 13 | #[derive(PartialEq, Clone)] 14 | struct Props { 15 | field_1: u32, 16 | field_2: u32, 17 | } 18 | 19 | impl Default for Props { 20 | fn default() -> Self { 21 | Props { 22 | field_1: 0, 23 | field_2: 0, 24 | } 25 | } 26 | } 27 | 28 | impl Component for Comp { 29 | type Message = (); 30 | type Properties = Props; 31 | 32 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 33 | Comp 34 | } 35 | 36 | fn update(&mut self, _: Self::Message) -> ShouldRender { 37 | unimplemented!(); 38 | } 39 | } 40 | 41 | impl Renderable for Comp { 42 | fn view(&self) -> Html { 43 | unimplemented!(); 44 | } 45 | } 46 | 47 | #[wasm_bindgen_test] 48 | fn set_properties_to_component() { 49 | let _: VNode = html! { 50 | 51 | }; 52 | 53 | let _: VNode = html! { 54 | 55 | }; 56 | 57 | let _: VNode = html! { 58 | 59 | }; 60 | 61 | let _: VNode = html! { 62 | 63 | }; 64 | 65 | let props = Props { 66 | field_1: 1, 67 | field_2: 1, 68 | }; 69 | 70 | let _: VNode = html! { 71 | 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /tests/vlist_test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate plaster; 3 | #[macro_use] 4 | extern crate wasm_bindgen_test; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | use plaster::prelude::*; 9 | use plaster::virtual_dom::VNode; 10 | 11 | struct Comp; 12 | 13 | impl Component for Comp { 14 | type Message = (); 15 | type Properties = (); 16 | 17 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 18 | Comp 19 | } 20 | 21 | fn update(&mut self, _: Self::Message) -> ShouldRender { 22 | unimplemented!(); 23 | } 24 | } 25 | 26 | impl Renderable for Comp { 27 | fn view(&self) -> Html { 28 | unimplemented!(); 29 | } 30 | } 31 | 32 | #[wasm_bindgen_test] 33 | fn check_fragments() { 34 | let fragment: VNode = html! { 35 | <> 36 | 37 | }; 38 | let _: VNode = html! { 39 |
    40 | { fragment } 41 |
    42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /tests/vtag_test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate plaster; 3 | #[macro_use] 4 | extern crate wasm_bindgen_test; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | use plaster::prelude::*; 9 | use plaster::virtual_dom::VNode; 10 | 11 | struct Comp; 12 | 13 | impl Component for Comp { 14 | type Message = (); 15 | type Properties = (); 16 | 17 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 18 | Comp 19 | } 20 | 21 | fn update(&mut self, _: Self::Message) -> ShouldRender { 22 | unimplemented!(); 23 | } 24 | } 25 | 26 | impl Renderable for Comp { 27 | fn view(&self) -> Html { 28 | unimplemented!(); 29 | } 30 | } 31 | 32 | #[wasm_bindgen_test] 33 | fn it_compares_tags() { 34 | let a: VNode = html! { 35 |
    36 | }; 37 | 38 | let b: VNode = html! { 39 |
    40 | }; 41 | 42 | let c: VNode = html! { 43 |

    44 | }; 45 | 46 | assert_eq!(a, b); 47 | assert_ne!(a, c); 48 | } 49 | 50 | #[test] 51 | fn it_compares_text() { 52 | let a: VNode = html! { 53 |
    { "correct" }
    54 | }; 55 | 56 | let b: VNode = html! { 57 |
    { "correct" }
    58 | }; 59 | 60 | let c: VNode = html! { 61 |
    { "incorrect" }
    62 | }; 63 | 64 | assert_eq!(a, b); 65 | assert_ne!(a, c); 66 | } 67 | 68 | #[test] 69 | fn it_compares_attributes() { 70 | let a: VNode = html! { 71 |
    72 | }; 73 | 74 | let b: VNode = html! { 75 |
    76 | }; 77 | 78 | let c: VNode = html! { 79 |
    80 | }; 81 | 82 | assert_eq!(a, b); 83 | assert_ne!(a, c); 84 | } 85 | 86 | #[test] 87 | fn it_compares_children() { 88 | let a: VNode = html! { 89 |
    90 |

    91 |
    92 | }; 93 | 94 | let b: VNode = html! { 95 |
    96 |

    97 |
    98 | }; 99 | 100 | let c: VNode = html! { 101 |
    102 | 103 |
    104 | }; 105 | 106 | assert_eq!(a, b); 107 | assert_ne!(a, c); 108 | } 109 | 110 | #[test] 111 | fn it_compares_classes() { 112 | let a: VNode = html! { 113 |
    114 | }; 115 | 116 | let b: VNode = html! { 117 |
    118 | }; 119 | 120 | let c: VNode = html! { 121 |
    122 | }; 123 | 124 | let d: VNode = html! { 125 |
    126 | }; 127 | 128 | assert_eq!(a, b); 129 | assert_ne!(a, c); 130 | assert_eq!(c, d); 131 | } 132 | 133 | #[test] 134 | fn classes_from_local_variables() { 135 | let a: VNode = html! { 136 |
    137 | }; 138 | 139 | let class_2 = "class-2"; 140 | let b: VNode = html! { 141 |
    142 | }; 143 | 144 | let class_2_fmt = format!("class-{}", 2); 145 | let c: VNode = html! { 146 |
    147 | }; 148 | 149 | assert_eq!(a, b); 150 | assert_eq!(a, c); 151 | } 152 | 153 | #[test] 154 | fn supports_multiple_classes_string() { 155 | let a: VNode = html! { 156 |
    157 | }; 158 | 159 | let b: VNode = html! { 160 |
    161 | }; 162 | 163 | assert_eq!(a, b); 164 | 165 | if let VNode::VTag(vtag) = a { 166 | println!("{:?}", vtag.classes); 167 | assert!(vtag.classes.contains("class-1")); 168 | assert!(vtag.classes.contains("class-2")); 169 | assert!(vtag.classes.contains("class-3")); 170 | } else { 171 | panic!("vtag expected"); 172 | } 173 | } 174 | 175 | #[test] 176 | fn it_compares_values() { 177 | let a: VNode = html! { 178 | 179 | }; 180 | 181 | let b: VNode = html! { 182 | 183 | }; 184 | 185 | let c: VNode = html! { 186 | 187 | }; 188 | 189 | assert_eq!(a, b); 190 | assert_ne!(a, c); 191 | } 192 | 193 | #[test] 194 | fn it_compares_kinds() { 195 | let a: VNode = html! { 196 | 197 | }; 198 | 199 | let b: VNode = html! { 200 | 201 | }; 202 | 203 | let c: VNode = html! { 204 | 205 | }; 206 | 207 | assert_eq!(a, b); 208 | assert_ne!(a, c); 209 | } 210 | 211 | #[test] 212 | fn it_compares_checked() { 213 | let a: VNode = html! { 214 | 215 | }; 216 | 217 | let b: VNode = html! { 218 | 219 | }; 220 | 221 | let c: VNode = html! { 222 | 223 | }; 224 | 225 | assert_eq!(a, b); 226 | assert_ne!(a, c); 227 | } 228 | 229 | #[test] 230 | fn it_allows_aria_attributes() { 231 | let a: VNode = html! { 232 |

    233 | 241 | 249 |

    250 |

    251 | }; 252 | if let VNode::VTag(vtag) = a { 253 | assert!(vtag.attributes.contains_key("aria-controls")); 254 | assert_eq!( 255 | vtag.attributes.get("aria-controls"), 256 | Some(&"it-works".into()) 257 | ); 258 | } else { 259 | panic!("vtag expected"); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /tests/vtext_test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate plaster; 3 | #[macro_use] 4 | extern crate wasm_bindgen_test; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | use plaster::prelude::*; 9 | use plaster::virtual_dom::VNode; 10 | 11 | struct Comp; 12 | 13 | impl Component for Comp { 14 | type Message = (); 15 | type Properties = (); 16 | 17 | fn create(_: Self::Properties, _: ComponentLink) -> Self { 18 | Comp 19 | } 20 | 21 | fn update(&mut self, _: Self::Message) -> ShouldRender { 22 | unimplemented!(); 23 | } 24 | } 25 | 26 | impl Renderable for Comp { 27 | fn view(&self) -> Html { 28 | unimplemented!(); 29 | } 30 | } 31 | 32 | #[wasm_bindgen_test] 33 | fn text_as_root() { 34 | let _: VNode = html! { 35 | { "Text Node As Root" } 36 | }; 37 | } 38 | --------------------------------------------------------------------------------