├── .gitignore ├── examples ├── counter │ ├── example.wasm │ ├── Makefile │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── lib.rs ├── todo_list │ ├── example.wasm │ ├── Makefile │ ├── Cargo.toml │ ├── index.html │ ├── index.css │ └── src │ │ └── lib.rs └── helloworld │ ├── example.wasm │ ├── Makefile │ ├── src │ └── lib.rs │ ├── Cargo.toml │ └── index.html ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/counter/example.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardanaya/lit-html-rs/HEAD/examples/counter/example.wasm -------------------------------------------------------------------------------- /examples/todo_list/example.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardanaya/lit-html-rs/HEAD/examples/todo_list/example.wasm -------------------------------------------------------------------------------- /examples/helloworld/example.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardanaya/lit-html-rs/HEAD/examples/helloworld/example.wasm -------------------------------------------------------------------------------- /examples/helloworld/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 3 | @cp target/wasm32-unknown-unknown/release/example.wasm . 4 | lint: 5 | @cargo fmt 6 | serve: 7 | @python3 -m http.server -------------------------------------------------------------------------------- /examples/counter/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 3 | @cp target/wasm32-unknown-unknown/release/example.wasm . 4 | lint: 5 | @cargo fmt 6 | serve: build 7 | @python3 -m http.server 8 | -------------------------------------------------------------------------------- /examples/todo_list/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release 3 | @cp target/wasm32-unknown-unknown/release/example.wasm . 4 | lint: 5 | @cargo fmt 6 | serve: build 7 | @python3 -m http.server 8 | -------------------------------------------------------------------------------- /examples/helloworld/src/lib.rs: -------------------------------------------------------------------------------- 1 | use js::*; 2 | use lit_html::*; 3 | 4 | #[no_mangle] 5 | pub fn main() { 6 | let mut data = TemplateData::new(); 7 | data.set("name", "Ferris"); 8 | render(&html!(r#"

Hello ${_.name}

"#, &data), DOM_BODY); 9 | } 10 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.0.0" 4 | authors = ["Richard "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | lit-html = {path="../../"} 11 | js = "0" 12 | 13 | [lib] 14 | crate-type =["cdylib"] 15 | 16 | [profile.release] 17 | lto = true 18 | -------------------------------------------------------------------------------- /examples/helloworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.0.0" 4 | authors = ["Richard "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | lit-html = {path="../../"} 11 | js = "0" 12 | 13 | [lib] 14 | crate-type =["cdylib"] 15 | 16 | [profile.release] 17 | lto = true 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lit-html" 3 | version = "0.1.2" 4 | authors = ["Richard Anaya"] 5 | edition = "2018" 6 | description = "A library for rendering HTML" 7 | license = "MIT OR Apache-2.0" 8 | categories = ["wasm", "no-std"] 9 | repository = "https://github.com/richardanaya/lit-html-rs" 10 | readme = "README.md" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | [dependencies] 14 | js = "0" 15 | web = "0" -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/helloworld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/todo_list/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.0.0" 4 | authors = ["Richard "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | [dependencies] 9 | lit-html = { path = "../../" } 10 | js = "0" 11 | web = "0" 12 | serde = { version = "1", features = ["derive"] } 13 | serde_json = "1" 14 | lazy_static = "1" 15 | globals = "1" 16 | 17 | [lib] 18 | crate-type = ["cdylib"] 19 | 20 | [profile.release] 21 | lto = true 22 | -------------------------------------------------------------------------------- /examples/todo_list/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rust Todo 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/counter/src/lib.rs: -------------------------------------------------------------------------------- 1 | use js::*; 2 | use lit_html::*; 3 | 4 | static mut COUNT: u32 = 0; 5 | 6 | fn counter() -> Template { 7 | let mut data = TemplateData::new(); 8 | data.set("count", unsafe { COUNT }); 9 | data.set("increment", || { 10 | unsafe { COUNT += 1 }; 11 | rerender(); 12 | }); 13 | html!( 14 | r#"The current count is ${_.count} "#, 15 | &data 16 | ) 17 | } 18 | 19 | fn app() -> Template { 20 | let mut data = TemplateData::new(); 21 | data.set("content", &counter()); 22 | html!( 23 | r#"
This is a counter in Rust!
${_.content}
"#, 24 | &data 25 | ) 26 | } 27 | 28 | fn rerender() { 29 | render(&app(), DOM_BODY); 30 | } 31 | 32 | #[no_mangle] 33 | pub fn main() { 34 | rerender(); 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lit-html-rs 2 | 3 | A Rust library for using the HTML template library [lit-html](https://lit-html.polymer-project.org/) created by the Google Polymer project. 4 | 5 | **this library is still in very early stages** 6 | 7 | ```toml 8 | [dependencies] 9 | lit-html = "0" 10 | ``` 11 | 12 | Here's a demo of the iconic [todo list](https://richardanaya.github.io/lit-html-rs/examples/todo_list/). 13 | 14 | # Basics 15 | 16 | `lit-html` works by creating templates that efficiently render to the DOM. When you are building a `TemplateData` object your data is being moved from WebAssembly into an object in JavaScript that can be used by the `lit-html` template. 17 | 18 | You can put the following data on TemplateData: 19 | * strings 20 | * numbers 21 | * booleans 22 | * callbacks functions 23 | 24 | ```rust 25 | use js::*; 26 | use lit_html::*; 27 | 28 | #[no_mangle] 29 | pub fn main() { 30 | let mut data = TemplateData::new(); 31 | data.set("name", "Ferris"); 32 | render(html!(r#"

Hello ${_.name}

"#, &data), DOM_BODY); 33 | } 34 | ``` 35 | 36 | See it working [here](https://richardanaya.github.io/lit-html-rs/examples/helloworld/). 37 | 38 | # Counter 39 | 40 | You can build up complex UI by creating Templates that contain other data bound templates. `lit-html` efficiently manipulates the DOM when data changes. 41 | 42 | ```rust 43 | use js::*; 44 | use lit_html::*; 45 | 46 | static mut COUNT: u32 = 0; 47 | 48 | fn counter() -> Template { 49 | let mut data = TemplateData::new(); 50 | data.set("count", unsafe { COUNT }); 51 | data.set("increment", || { 52 | unsafe { COUNT += 1 }; 53 | rerender(); 54 | }); 55 | html!( 56 | r#"The current count is ${_.count} "#, 57 | data 58 | ) 59 | } 60 | 61 | fn app() -> Template { 62 | let mut data = TemplateData::new(); 63 | data.set("content", counter()); 64 | html!( 65 | r#"
This is a counter in Rust!
${_.content}
"#, 66 | data 67 | ) 68 | } 69 | 70 | fn rerender() { 71 | render(&app(), DOM_BODY); 72 | } 73 | 74 | #[no_mangle] 75 | pub fn main() { 76 | rerender(); 77 | } 78 | ``` 79 | 80 | See it working [here](https://richardanaya.github.io/lit-html-rs/examples/counter/). 81 | 82 | 83 | # License 84 | 85 | This project is licensed under either of 86 | 87 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 88 | http://www.apache.org/licenses/LICENSE-2.0) 89 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 90 | http://opensource.org/licenses/MIT) 91 | 92 | at your option. 93 | 94 | ### Contribution 95 | 96 | Unless you explicitly state otherwise, any contribution intentionally submitted 97 | for inclusion in `js-wasm` by you, as defined in the Apache-2.0 license, shall be 98 | dual licensed as above, without any additional terms or conditions. 99 | 100 | -------------------------------------------------------------------------------- /examples/todo_list/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #111111; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .todoapp input::-webkit-input-placeholder { 53 | font-style: italic; 54 | font-weight: 300; 55 | color: rgba(0, 0, 0, 0.4); 56 | } 57 | 58 | .todoapp input::-moz-placeholder { 59 | font-style: italic; 60 | font-weight: 300; 61 | color: rgba(0, 0, 0, 0.4); 62 | } 63 | 64 | .todoapp input::input-placeholder { 65 | font-style: italic; 66 | font-weight: 300; 67 | color: rgba(0, 0, 0, 0.4); 68 | } 69 | 70 | .todoapp h1 { 71 | position: absolute; 72 | top: -140px; 73 | width: 100%; 74 | font-size: 80px; 75 | font-weight: 200; 76 | text-align: center; 77 | color: #b83f45; 78 | -webkit-text-rendering: optimizeLegibility; 79 | -moz-text-rendering: optimizeLegibility; 80 | text-rendering: optimizeLegibility; 81 | } 82 | 83 | .new-todo, 84 | .edit { 85 | position: relative; 86 | margin: 0; 87 | width: 100%; 88 | font-size: 24px; 89 | font-family: inherit; 90 | font-weight: inherit; 91 | line-height: 1.4em; 92 | color: inherit; 93 | padding: 6px; 94 | border: 1px solid #999; 95 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 96 | box-sizing: border-box; 97 | -webkit-font-smoothing: antialiased; 98 | -moz-osx-font-smoothing: grayscale; 99 | } 100 | 101 | .new-todo { 102 | padding: 16px 16px 16px 60px; 103 | border: none; 104 | background: rgba(0, 0, 0, 0.003); 105 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 106 | } 107 | 108 | .main { 109 | position: relative; 110 | z-index: 2; 111 | border-top: 1px solid #e6e6e6; 112 | } 113 | 114 | .toggle-all { 115 | width: 1px; 116 | height: 1px; 117 | border: none; /* Mobile Safari */ 118 | opacity: 0; 119 | position: absolute; 120 | right: 100%; 121 | bottom: 100%; 122 | } 123 | 124 | .toggle-all + label { 125 | width: 60px; 126 | height: 34px; 127 | font-size: 0; 128 | position: absolute; 129 | top: -52px; 130 | left: -13px; 131 | -webkit-transform: rotate(90deg); 132 | transform: rotate(90deg); 133 | } 134 | 135 | .toggle-all + label:before { 136 | content: '❯'; 137 | font-size: 22px; 138 | color: #e6e6e6; 139 | padding: 10px 27px 10px 27px; 140 | } 141 | 142 | .toggle-all:checked + label:before { 143 | color: #737373; 144 | } 145 | 146 | .todo-list { 147 | margin: 0; 148 | padding: 0; 149 | list-style: none; 150 | } 151 | 152 | .todo-list li { 153 | position: relative; 154 | font-size: 24px; 155 | border-bottom: 1px solid #ededed; 156 | } 157 | 158 | .todo-list li:last-child { 159 | border-bottom: none; 160 | } 161 | 162 | .todo-list li.editing { 163 | border-bottom: none; 164 | padding: 0; 165 | } 166 | 167 | .todo-list li.editing .edit { 168 | display: block; 169 | width: calc(100% - 43px); 170 | padding: 12px 16px; 171 | margin: 0 0 0 43px; 172 | } 173 | 174 | .todo-list li.editing .view { 175 | display: none; 176 | } 177 | 178 | .todo-list li .toggle { 179 | text-align: center; 180 | width: 40px; 181 | /* auto, since non-WebKit browsers doesn't support input styling */ 182 | height: auto; 183 | position: absolute; 184 | top: 0; 185 | bottom: 0; 186 | margin: auto 0; 187 | border: none; /* Mobile Safari */ 188 | -webkit-appearance: none; 189 | appearance: none; 190 | } 191 | 192 | .todo-list li .toggle { 193 | opacity: 0; 194 | } 195 | 196 | .todo-list li .toggle + label { 197 | /* 198 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 199 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 200 | */ 201 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 202 | background-repeat: no-repeat; 203 | background-position: center left; 204 | } 205 | 206 | .todo-list li .toggle.checked + label { 207 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 208 | } 209 | 210 | .todo-list li label { 211 | word-break: break-all; 212 | padding: 15px 15px 15px 60px; 213 | display: block; 214 | line-height: 1.2; 215 | transition: color 0.4s; 216 | font-weight: 400; 217 | color: #4d4d4d; 218 | } 219 | 220 | .todo-list li.completed label { 221 | color: #cdcdcd; 222 | text-decoration: line-through; 223 | } 224 | 225 | .todo-list li .destroy { 226 | display: none; 227 | position: absolute; 228 | top: 0; 229 | right: 10px; 230 | bottom: 0; 231 | width: 40px; 232 | height: 40px; 233 | margin: auto 0; 234 | font-size: 30px; 235 | color: #cc9a9a; 236 | margin-bottom: 11px; 237 | transition: color 0.2s ease-out; 238 | } 239 | 240 | .todo-list li .destroy:hover { 241 | color: #af5b5e; 242 | } 243 | 244 | .todo-list li .destroy:after { 245 | content: '×'; 246 | } 247 | 248 | .todo-list li:hover .destroy { 249 | display: block; 250 | } 251 | 252 | .todo-list li .edit { 253 | display: none; 254 | } 255 | 256 | .todo-list li.editing:last-child { 257 | margin-bottom: -1px; 258 | } 259 | 260 | .footer { 261 | padding: 10px 15px; 262 | height: 20px; 263 | text-align: center; 264 | font-size: 15px; 265 | border-top: 1px solid #e6e6e6; 266 | } 267 | 268 | .footer:before { 269 | content: ''; 270 | position: absolute; 271 | right: 0; 272 | bottom: 0; 273 | left: 0; 274 | height: 50px; 275 | overflow: hidden; 276 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 277 | 0 8px 0 -3px #f6f6f6, 278 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 279 | 0 16px 0 -6px #f6f6f6, 280 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 281 | } 282 | 283 | .todo-count { 284 | float: left; 285 | text-align: left; 286 | } 287 | 288 | .todo-count strong { 289 | font-weight: 300; 290 | } 291 | 292 | .filters { 293 | margin: 0; 294 | padding: 0; 295 | list-style: none; 296 | position: absolute; 297 | right: 0; 298 | left: 0; 299 | } 300 | 301 | .filters li { 302 | display: inline; 303 | } 304 | 305 | .filters li a { 306 | color: inherit; 307 | margin: 3px; 308 | padding: 3px 7px; 309 | text-decoration: none; 310 | border: 1px solid transparent; 311 | border-radius: 3px; 312 | } 313 | 314 | .filters li a:hover { 315 | border-color: rgba(175, 47, 47, 0.1); 316 | } 317 | 318 | .filters li a.selected { 319 | border-color: rgba(175, 47, 47, 0.2); 320 | } 321 | 322 | .clear-completed, 323 | html .clear-completed:active { 324 | float: right; 325 | position: relative; 326 | line-height: 20px; 327 | text-decoration: none; 328 | cursor: pointer; 329 | } 330 | 331 | .clear-completed:hover { 332 | text-decoration: underline; 333 | } 334 | 335 | .info { 336 | margin: 65px auto 0; 337 | color: #4d4d4d; 338 | font-size: 11px; 339 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 340 | text-align: center; 341 | } 342 | 343 | .info p { 344 | line-height: 1; 345 | } 346 | 347 | .info a { 348 | color: inherit; 349 | font-weight: 400; 350 | } 351 | 352 | .info a:hover { 353 | text-decoration: underline; 354 | } 355 | 356 | /* 357 | Hack to remove background from Mobile Safari. 358 | Can't use it globally since it destroys checkboxes in Firefox 359 | */ 360 | @media screen and (-webkit-min-device-pixel-ratio:0) { 361 | .toggle-all, 362 | .todo-list li .toggle { 363 | background: none; 364 | } 365 | 366 | .todo-list li .toggle { 367 | height: 40px; 368 | } 369 | } 370 | 371 | @media (max-width: 430px) { 372 | .footer { 373 | height: 50px; 374 | } 375 | 376 | .filters { 377 | bottom: 10px; 378 | } 379 | } -------------------------------------------------------------------------------- /examples/todo_list/src/lib.rs: -------------------------------------------------------------------------------- 1 | use lit_html::*; 2 | use serde::{Deserialize, Serialize}; 3 | use web::*; 4 | 5 | // DATASTRUCTURES 6 | 7 | // First we need a datatstructure for representing our todo list 8 | // They use serde serialization/deserialization to convert to/from JSON 9 | 10 | #[derive(Serialize, Deserialize)] 11 | pub struct Todo { 12 | pub completed: bool, 13 | pub text: String, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | pub struct TodoList { 18 | pub items: Vec, 19 | } 20 | 21 | // Let's add some helper functions to store/load from the browser's local storage 22 | impl TodoList { 23 | pub fn save(&self) { 24 | match serde_json::to_string(self) { 25 | Ok(s) => local_storage_set_item("todos", &s), 26 | Err(_) => console_error("error saving todos to localstorage"), 27 | }; 28 | } 29 | pub fn load() -> Option { 30 | return match local_storage_get_item("todos") { 31 | Some(s) => match serde_json::from_str(&s) { 32 | Ok(s) => Some(s), 33 | Err(_) => { 34 | console_error("error loading todos from localstorage"); 35 | None 36 | } 37 | }, 38 | None => None, 39 | }; 40 | } 41 | pub fn add(&mut self, txt: &str) { 42 | self.items.push(Todo { 43 | text: txt.to_owned(), 44 | completed: false, 45 | }); 46 | } 47 | } 48 | 49 | // Our initial todo list should have no item 50 | impl Default for TodoList { 51 | fn default() -> Self { 52 | match TodoList::load() { 53 | Some(tl) => tl, 54 | None => TodoList { items: vec![] }, 55 | } 56 | } 57 | } 58 | 59 | // Let's create a structure that represents our app's state 60 | struct AppState { 61 | mode: Mode, 62 | } 63 | 64 | enum Mode { 65 | All, // Show all todos 66 | Active, // Show todos not completed 67 | Completed, // Show todos that are completed 68 | } 69 | 70 | // Our default state for app should show active items only 71 | impl Default for AppState { 72 | fn default() -> Self { 73 | AppState { mode: Mode::Active } 74 | } 75 | } 76 | 77 | // FUNCTIONS 78 | 79 | // lit-html-rs uses functions to generate a tree of html templates that will be rendered to the browsers DOM 80 | 81 | // Okay, let's start our app, the first thing we need to do is render 82 | #[no_mangle] 83 | pub fn main() { 84 | rerender(); 85 | } 86 | 87 | fn rerender() { 88 | // render next chance we get and prevent locks of global mutex 89 | // in theory you could get around doing this by making sure your global 90 | // state won't lock up 91 | set_timeout( 92 | || { 93 | render(&app(), DOM_BODY); 94 | }, 95 | 0, 96 | ); 97 | } 98 | 99 | // Our first top most component is our app 100 | fn app() -> Template { 101 | // Our app uses global state of our todo list and app state 102 | // This basically gets a mutex locked instance of the type 103 | // and instantiates it if it isn't already instantiated 104 | let todo_list = globals::get::(); 105 | let app_state = globals::get::(); 106 | 107 | // lit-html-rs works by create templates and rendering them with data 108 | let mut data = TemplateData::new(); 109 | // how many todos are there to do 110 | data.set( 111 | "num_items_todo", 112 | todo_list 113 | .items 114 | .iter() 115 | .filter(|todo| !todo.completed) 116 | .count() as f64, 117 | ); 118 | // add a handler for whe user hits enter on input 119 | data.set( 120 | "todo_key_down", 121 | KeyEventHandler::new(|e: KeyEvent| { 122 | let mut input = InputElement::from(e.target()); 123 | if e.key_code() == 13 { 124 | let v = input.value(); 125 | if let Some(txt) = v { 126 | input.set_value(""); 127 | let mut todos = globals::get::(); 128 | todos.add(&txt); 129 | todos.save(); 130 | } 131 | } 132 | rerender(); 133 | }), 134 | ); 135 | // add a list of child todo elements (see todo component below) 136 | data.set( 137 | "todo_items", 138 | todo_list 139 | .items 140 | .iter() 141 | .enumerate() 142 | .filter(|(_, todo)| match app_state.mode { 143 | Mode::All => true, 144 | Mode::Completed => todo.completed == true, 145 | Mode::Active => todo.completed == false, 146 | }) 147 | .map(|(pos, todo)| todo_item(pos, todo)) 148 | .collect::>(), 149 | ); 150 | // handle when user clicks button to show all todos 151 | data.set( 152 | "toggle_filter_all", 153 | MouseEventHandler::new(move |_e: MouseEvent| { 154 | let mut app = globals::get::(); 155 | app.mode = Mode::All; 156 | rerender(); 157 | }), 158 | ); 159 | // handle when user clicks button to show only completed todos 160 | data.set( 161 | "toggle_filter_completed", 162 | MouseEventHandler::new(move |_e: MouseEvent| { 163 | let mut app = globals::get::(); 164 | app.mode = Mode::Completed; 165 | rerender(); 166 | }), 167 | ); 168 | // handle when user clicks button to show only non-completed todos 169 | data.set( 170 | "toggle_filter_active", 171 | MouseEventHandler::new(move |_e: MouseEvent| { 172 | let mut app = globals::get::(); 173 | app.mode = Mode::Active; 174 | rerender(); 175 | }), 176 | ); 177 | match app_state.mode { 178 | Mode::All => data.set("all_selected_class", "selected"), 179 | Mode::Active => data.set("active_selected_class", "selected"), 180 | Mode::Completed => data.set("completed_selected_class", "selected"), 181 | } 182 | // render the html with this data 183 | html!( 184 | r##" 185 |
186 |
187 |

todos

188 | 194 |
195 |
196 | 197 | 198 |
    199 | ${_.todo_items} 200 |
201 |
202 | ${_.content} 203 |
204 | ${_.num_items_todo} item left 207 | 218 |
219 |
220 |
221 |

Want to see the source code? Go here

222 |
223 | "##, 224 | &data 225 | ) 226 | } 227 | 228 | // this component renders a todo list item 229 | fn todo_item(pos: usize, todo: &Todo) -> Template { 230 | // again, its just rendering a template 231 | let mut data = TemplateData::new(); 232 | // the todo's text 233 | data.set("text", &*todo.text); 234 | // should the check mark be checked 235 | if todo.completed { 236 | data.set("check_class", "checked"); 237 | } 238 | // add click handler if the click the completed button 239 | data.set( 240 | "toggle_done", 241 | MouseEventHandler::new(move |_e: MouseEvent| { 242 | let mut todos = globals::get::(); 243 | todos.items[pos].completed = !todos.items[pos].completed; 244 | todos.save(); 245 | rerender(); 246 | }), 247 | ); 248 | // add handler for if they delete the item 249 | data.set( 250 | "delete", 251 | MouseEventHandler::new(move |_e: MouseEvent| { 252 | let mut todos = globals::get::(); 253 | todos.items.remove(pos); 254 | todos.save(); 255 | rerender(); 256 | }), 257 | ); 258 | //render it 259 | html!( 260 | r#"
  • 261 |
    262 |
    263 | 264 | 265 |
    266 |
  • "#, 267 | &data 268 | ) 269 | } 270 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | extern crate alloc; 3 | use alloc::boxed::Box; 4 | use alloc::string::String; 5 | use js::*; 6 | 7 | pub fn render(template_result: &Template, dom: R) 8 | where 9 | R: Into, 10 | { 11 | let r = js!("function(template,dom){ 12 | template = this.getObject(template); 13 | dom = this.getObject(dom); 14 | window.LitHtml.render(template,dom); 15 | }"); 16 | r.invoke_2(template_result.handle, dom.into()); 17 | } 18 | 19 | #[macro_export] 20 | macro_rules! html { 21 | ($e:expr,$d:expr) => {{ 22 | JSObject::from(js!(&[ 23 | r#"function(_){ 24 | _ = this.getObject(_); 25 | return this.storeObject(window.LitHtml.html`"#, 26 | $e, 27 | r#"`); 28 | }"# 29 | ] 30 | .concat()) 31 | .invoke_1($d)) 32 | }}; 33 | ($e:expr) => {{ 34 | JSObject::from(js!(&[ 35 | r#"function(_){ 36 | _ = this.getObject(_); 37 | return this.storeObject(window.LitHtml.html`"#, 38 | $e, 39 | r#"`); 40 | }"# 41 | ] 42 | .concat()) 43 | .invoke_0()) 44 | }}; 45 | } 46 | 47 | pub type Template = JSObject; 48 | 49 | pub trait TemplateValue { 50 | fn set(self, data: &mut TemplateData, name: &str); 51 | } 52 | 53 | impl TemplateValue for &str { 54 | fn set(self, data: &mut TemplateData, name: &str) { 55 | js!("function(o,n,nlen,v,vlen){ 56 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = this.readUtf8FromMemory(v,vlen); 57 | }") 58 | .invoke_5( 59 | data.obj.handle, 60 | name.as_ptr() as u32, 61 | name.len() as u32, 62 | self.as_ptr() as u32, 63 | self.len() as u32, 64 | ); 65 | } 66 | } 67 | 68 | impl TemplateValue for f64 { 69 | fn set(self, data: &mut TemplateData, name: &str) { 70 | js!("function(o,n,nlen,v){ 71 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = v; 72 | }") 73 | .invoke_4( 74 | data.obj.handle, 75 | name.as_ptr() as u32, 76 | name.len() as u32, 77 | self, 78 | ); 79 | } 80 | } 81 | 82 | impl TemplateValue for Template { 83 | fn set(self, data: &mut TemplateData, name: &str) { 84 | js!("function(o,n,nlen,v){ 85 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = this.getObject(v); 86 | }") 87 | .invoke_4( 88 | data.obj.handle, 89 | name.as_ptr() as u32, 90 | name.len() as u32, 91 | self.handle, 92 | ); 93 | } 94 | } 95 | 96 | impl TemplateValue for alloc::vec::Vec