├── .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 |
195 |
202 | ${_.content}
203 |
219 |
220 |
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 |
${_.text}
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 {
97 | fn set(self, data: &mut TemplateData, name: &str) {
98 | let a = JSObject::from(
99 | js!("function(){
100 | return this.storeObject([]);
101 | }")
102 | .invoke_0(),
103 | );
104 | for t in self {
105 | js!("function(o,v){
106 | this.getObject(o).push(this.getObject(v));
107 | }")
108 | .invoke_2(a.handle, t.handle);
109 | }
110 | js!("function(o,n,nlen,v){
111 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = this.getObject(v);
112 | }")
113 | .invoke_4(
114 | data.obj.handle,
115 | name.as_ptr() as u32,
116 | name.len() as u32,
117 | a.handle,
118 | );
119 | }
120 | }
121 |
122 | impl TemplateValue for &Template {
123 | fn set(self, data: &mut TemplateData, name: &str) {
124 | js!("function(o,n,nlen,v){
125 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = this.getObject(v);
126 | }")
127 | .invoke_4(
128 | data.obj.handle,
129 | name.as_ptr() as u32,
130 | name.len() as u32,
131 | self.handle,
132 | );
133 | }
134 | }
135 |
136 | impl TemplateValue for u32 {
137 | fn set(self, data: &mut TemplateData, name: &str) {
138 | js!("function(o,n,nlen,v){
139 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = v;
140 | }")
141 | .invoke_4(
142 | data.obj.handle,
143 | name.as_ptr() as u32,
144 | name.len() as u32,
145 | self,
146 | );
147 | }
148 | }
149 |
150 | impl TemplateValue for bool {
151 | fn set(self, data: &mut TemplateData, name: &str) {
152 | js!("function(o,n,nlen,v){
153 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = v>0;
154 | }")
155 | .invoke_4(
156 | data.obj.handle,
157 | name.as_ptr() as u32,
158 | name.len() as u32,
159 | if self { 1.0 } else { 0.0 },
160 | );
161 | }
162 | }
163 |
164 | impl TemplateValue for i32 {
165 | fn set(self, data: &mut TemplateData, name: &str) {
166 | js!("function(o,n,nlen,v){
167 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = v;
168 | }")
169 | .invoke_4(
170 | data.obj.handle,
171 | name.as_ptr() as u32,
172 | name.len() as u32,
173 | self,
174 | );
175 | }
176 | }
177 |
178 | impl TemplateValue for T
179 | where
180 | T: Sync + FnMut() + 'static + Send,
181 | {
182 | fn set(self, data: &mut TemplateData, name: &str) {
183 | js!("function(o,n,nlen,v){
184 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = this.createCallback(v);
185 | }")
186 | .invoke_4(
187 | data.obj.handle,
188 | name.as_ptr() as u32,
189 | name.len() as u32,
190 | create_callback_0(self),
191 | );
192 | }
193 | }
194 |
195 | impl TemplateValue for KeyEventHandler {
196 | fn set(mut self, data: &mut TemplateData, name: &str) {
197 | let mut f = self.handler.take().unwrap();
198 | js!("function(o,n,nlen,v){
199 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = this.createCallback(v);
200 | }")
201 | .invoke_4(
202 | data.obj.handle,
203 | name.as_ptr() as u32,
204 | name.len() as u32,
205 | create_callback_1(move |v| f(KeyEvent::new(v))),
206 | );
207 | }
208 | }
209 |
210 | impl TemplateValue for MouseEventHandler {
211 | fn set(mut self, data: &mut TemplateData, name: &str) {
212 | let mut f = self.handler.take().unwrap();
213 | js!("function(o,n,nlen,v){
214 | this.getObject(o)[this.readUtf8FromMemory(n,nlen)] = this.createCallback(v);
215 | }")
216 | .invoke_4(
217 | data.obj.handle,
218 | name.as_ptr() as u32,
219 | name.len() as u32,
220 | create_callback_1(move |v| f(MouseEvent::new(v))),
221 | );
222 | }
223 | }
224 |
225 | pub struct TemplateData {
226 | obj: JSObject,
227 | }
228 |
229 | impl TemplateData {
230 | pub fn new() -> TemplateData {
231 | TemplateData {
232 | obj: JSObject::from(js!("function(){return this.storeObject({});}").invoke_0()),
233 | }
234 | }
235 |
236 | pub fn set(&mut self, name: &str, value: impl TemplateValue) {
237 | value.set(self, name);
238 | }
239 | }
240 |
241 | impl Into for &TemplateData {
242 | fn into(self) -> f64 {
243 | self.obj.handle
244 | }
245 | }
246 |
247 | pub struct KeyEventHandler {
248 | handler: Option>,
249 | }
250 |
251 | impl KeyEventHandler {
252 | pub fn new(f: impl Sync + FnMut(KeyEvent) + 'static + Send) -> KeyEventHandler {
253 | KeyEventHandler {
254 | handler: Some(Box::new(f)),
255 | }
256 | }
257 | }
258 |
259 | pub struct KeyEvent {
260 | obj: JSObject,
261 | }
262 |
263 | impl KeyEvent {
264 | pub fn new(o: f64) -> KeyEvent {
265 | KeyEvent {
266 | obj: JSObject::from(o),
267 | }
268 | }
269 |
270 | pub fn key_code(&self) -> usize {
271 | js!("function(o){
272 | return this.getObject(o).keyCode;
273 | }")
274 | .invoke_1(self.obj.handle) as usize
275 | }
276 |
277 | pub fn target(&self) -> JSObject {
278 | let r = js!("function(o){
279 | return this.storeObject(this.getObject(o).target);
280 | }")
281 | .invoke_1(self.obj.handle);
282 | JSObject::from(r)
283 | }
284 | }
285 |
286 | pub struct InputElement {
287 | obj: JSObject,
288 | }
289 |
290 | impl InputElement {
291 | pub fn new(o: f64) -> InputElement {
292 | InputElement {
293 | obj: JSObject::from(o),
294 | }
295 | }
296 |
297 | pub fn from(o: JSObject) -> InputElement {
298 | InputElement { obj: o }
299 | }
300 |
301 | pub fn value(&self) -> Option {
302 | get_property_string(&self.obj, "value")
303 | }
304 |
305 | pub fn set_value(&mut self, s: &str) {
306 | set_property_string(&self.obj, "value", s)
307 | }
308 | }
309 |
310 | pub fn get_property_string(el: impl Into, name: &str) -> Option {
311 | let attr = js!(r#"function(o,strPtr,strLen){
312 | o = this.getObject(o);
313 | const a = o[this.readUtf8FromMemory(strPtr,strLen)];
314 | if(a === null){
315 | return -1;
316 | }
317 | return this.writeCStringToMemory(a);
318 | }"#)
319 | .invoke_3(el.into(), name.as_ptr() as u32, name.len() as u32);
320 | if attr == -1.0 {
321 | return None;
322 | } else {
323 | Some(cstr_to_string(attr as i32))
324 | }
325 | }
326 |
327 | pub fn set_property_string(el: impl Into, name: &str, txt: &str) {
328 | js!(r#"function(o,strPtr,strLen,valPtr,valLen){
329 | o = this.getObject(o);
330 | o[this.readUtf8FromMemory(strPtr,strLen)] = this.readUtf8FromMemory(valPtr,valLen);
331 | }"#)
332 | .invoke_5(
333 | el.into(),
334 | name.as_ptr() as u32,
335 | name.len() as u32,
336 | txt.as_ptr() as u32,
337 | txt.len() as u32,
338 | );
339 | }
340 |
341 | pub struct MouseEventHandler {
342 | handler: Option>,
343 | }
344 |
345 | impl MouseEventHandler {
346 | pub fn new(f: impl Sync + FnMut(MouseEvent) + 'static + Send) -> MouseEventHandler {
347 | MouseEventHandler {
348 | handler: Some(Box::new(f)),
349 | }
350 | }
351 | }
352 |
353 | pub struct MouseEvent {
354 | obj: JSObject,
355 | }
356 |
357 | impl MouseEvent {
358 | pub fn new(o: f64) -> MouseEvent {
359 | MouseEvent {
360 | obj: JSObject::from(o),
361 | }
362 | }
363 | pub fn target(&self) -> JSObject {
364 | let r = js!("function(o){
365 | return this.storeObject(this.getObject(o).target);
366 | }")
367 | .invoke_1(self.obj.handle);
368 | JSObject::from(r)
369 | }
370 | }
371 |
--------------------------------------------------------------------------------