mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 09:02:37 -05:00
Compare commits
258 Commits
pr/76
...
component-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4360a73392 | ||
|
|
50b0fe157a | ||
|
|
64a5d75ec4 | ||
|
|
baf3cc8712 | ||
|
|
23777ad67b | ||
|
|
08be1ba622 | ||
|
|
605398bcea | ||
|
|
aca2c131d4 | ||
|
|
9d950b97ff | ||
|
|
f6a299ae3c | ||
|
|
1ba602ec47 | ||
|
|
1f3dde5b4a | ||
|
|
a65cd67db3 | ||
|
|
bacd99260b | ||
|
|
2b726f1a88 | ||
|
|
5c45538e9f | ||
|
|
7f696a9ac4 | ||
|
|
bcd6e671f7 | ||
|
|
7a72f127de | ||
|
|
2ff5ec21c8 | ||
|
|
a1f94b609f | ||
|
|
da5034da33 | ||
|
|
0c509970b5 | ||
|
|
d894c4dcf9 | ||
|
|
dc15184781 | ||
|
|
3200068ab3 | ||
|
|
0a9da8d55e | ||
|
|
52ad546710 | ||
|
|
f88d2fa56a | ||
|
|
f63cb02277 | ||
|
|
4b363f9b33 | ||
|
|
7b376b6d3a | ||
|
|
8fbb4abc76 | ||
|
|
d0ff64daaa | ||
|
|
bb97234817 | ||
|
|
19698d86b6 | ||
|
|
21ef96806f | ||
|
|
70e18d2aeb | ||
|
|
5152703f0c | ||
|
|
3d54055573 | ||
|
|
a5b99a3e40 | ||
|
|
101e65b724 | ||
|
|
a3f91604b9 | ||
|
|
f457d8f319 | ||
|
|
58abe55d7b | ||
|
|
634ac17095 | ||
|
|
79faad4aac | ||
|
|
cedc68c341 | ||
|
|
8ec772a129 | ||
|
|
8d671866a3 | ||
|
|
2edc5b3b8b | ||
|
|
be96a230ee | ||
|
|
0f8930b6f2 | ||
|
|
2b5c4abac5 | ||
|
|
db8c393f49 | ||
|
|
f18a7b35f2 | ||
|
|
a2c5855362 | ||
|
|
644d097cb6 | ||
|
|
9c0be9e317 | ||
|
|
5faa2efa2d | ||
|
|
c5a1e9a447 | ||
|
|
e88e131ec3 | ||
|
|
80df7a0dac | ||
|
|
493f05fda1 | ||
|
|
4578622b6f | ||
|
|
c7dd6200e8 | ||
|
|
6e20f31df1 | ||
|
|
5f58db40f0 | ||
|
|
321e11e97a | ||
|
|
c472a1c5ef | ||
|
|
1180eeeadb | ||
|
|
2348bbc5cc | ||
|
|
ee41ea8b1d | ||
|
|
a0ea3cfd7c | ||
|
|
edb0f8c848 | ||
|
|
2b71c07fa9 | ||
|
|
a109e3d51c | ||
|
|
40a842ff1d | ||
|
|
17baec46b7 | ||
|
|
fe5c9c6f0d | ||
|
|
6c22c47bbf | ||
|
|
2d88a113c4 | ||
|
|
b0dd759bcf | ||
|
|
507191e1a4 | ||
|
|
36de06f183 | ||
|
|
b54c0f14e8 | ||
|
|
41c03852e1 | ||
|
|
c3fb9396e1 | ||
|
|
3a9d16ad29 | ||
|
|
c0709b210d | ||
|
|
569fa9b1c6 | ||
|
|
ed24e47c1d | ||
|
|
fdd07aafb7 | ||
|
|
1a0168bf28 | ||
|
|
de524e21b1 | ||
|
|
dbe3daf16a | ||
|
|
3f6eeb319a | ||
|
|
db34565959 | ||
|
|
d5cd2b814e | ||
|
|
2b9ac037e3 | ||
|
|
66ecc2ac25 | ||
|
|
4093f4c2d8 | ||
|
|
a46e92bed8 | ||
|
|
611a1aeb28 | ||
|
|
994debea3f | ||
|
|
5399f54255 | ||
|
|
22668f7999 | ||
|
|
f7b1e732c7 | ||
|
|
93f68e022f | ||
|
|
2b4dc76d95 | ||
|
|
55f70367b5 | ||
|
|
a01b0cbbc6 | ||
|
|
6d329f33eb | ||
|
|
5a863ec411 | ||
|
|
4800600e4f | ||
|
|
a051b1e08c | ||
|
|
4a426be6fb | ||
|
|
d9ab70de0d | ||
|
|
aaac1d37ac | ||
|
|
498b5345d5 | ||
|
|
02a7af2c1e | ||
|
|
e465867b30 | ||
|
|
835c465c34 | ||
|
|
45e2c09e53 | ||
|
|
19d7b8434b | ||
|
|
3ac92dc0fe | ||
|
|
6949750668 | ||
|
|
440719071a | ||
|
|
588ebf51a5 | ||
|
|
3a65ad9a51 | ||
|
|
7a10ffd150 | ||
|
|
a23d80fe27 | ||
|
|
6966ef4b39 | ||
|
|
e0c8b827c4 | ||
|
|
ebef5156a5 | ||
|
|
f3947abdc2 | ||
|
|
701a12ab46 | ||
|
|
fe830e524c | ||
|
|
6ddef3018f | ||
|
|
0cbab3ef87 | ||
|
|
4fda94144b | ||
|
|
b6d902a584 | ||
|
|
102fb9d819 | ||
|
|
545fcce97c | ||
|
|
33424683d0 | ||
|
|
19c3186d3f | ||
|
|
3482f456f8 | ||
|
|
d8a97a81ff | ||
|
|
931e60347d | ||
|
|
2a547936d4 | ||
|
|
2ce7e71748 | ||
|
|
4f205b5368 | ||
|
|
8e624d4942 | ||
|
|
cee32a3f8f | ||
|
|
e827ee93e2 | ||
|
|
6b77b51fa0 | ||
|
|
6564b95342 | ||
|
|
2651bf5fef | ||
|
|
10d19f7fb3 | ||
|
|
00de5d0d88 | ||
|
|
0f0e3da407 | ||
|
|
4a741d772b | ||
|
|
6c521226e3 | ||
|
|
739e7db49d | ||
|
|
7c79cb1b1f | ||
|
|
4f522d135b | ||
|
|
60ecd740f5 | ||
|
|
b707eada86 | ||
|
|
71594daa93 | ||
|
|
89f837d3b6 | ||
|
|
014b5f9453 | ||
|
|
55896d97b8 | ||
|
|
5e532b60b0 | ||
|
|
a3181dea64 | ||
|
|
1f1218bbb7 | ||
|
|
9322cc991b | ||
|
|
d0c6319a72 | ||
|
|
9f1b27ad26 | ||
|
|
986cd2979a | ||
|
|
8b3a8489b6 | ||
|
|
525a31bf3d | ||
|
|
f773f52abc | ||
|
|
257c07325e | ||
|
|
01a1226c53 | ||
|
|
5208616178 | ||
|
|
4e8c1758c3 | ||
|
|
cbcd7e506f | ||
|
|
eff42a196f | ||
|
|
20634e38a1 | ||
|
|
4f3d7dc492 | ||
|
|
6ddc720227 | ||
|
|
8077ae9ead | ||
|
|
75de8a95b6 | ||
|
|
63d06211b9 | ||
|
|
ba199e1acb | ||
|
|
9f4b3c9f26 | ||
|
|
d654a13541 | ||
|
|
63ae4e7dda | ||
|
|
ad880efc0d | ||
|
|
5ff806d35a | ||
|
|
165ec069ba | ||
|
|
be7bce03dc | ||
|
|
1b1182114d | ||
|
|
412693c2c3 | ||
|
|
5c36f0963c | ||
|
|
491f124669 | ||
|
|
43524c0135 | ||
|
|
5562e2d6ee | ||
|
|
bbf2d69b55 | ||
|
|
00b6b39ee0 | ||
|
|
3d88227bac | ||
|
|
d530b28348 | ||
|
|
97a7240e26 | ||
|
|
2ad49a0a7e | ||
|
|
58e0bead02 | ||
|
|
fe41b6c840 | ||
|
|
8e2930141a | ||
|
|
36777c2055 | ||
|
|
7ad8a6bef2 | ||
|
|
e26393a42c | ||
|
|
6e78e85590 | ||
|
|
46b1a96cc7 | ||
|
|
d35fdf71ed | ||
|
|
0473093d0a | ||
|
|
4a187e83f7 | ||
|
|
d6c6ab7939 | ||
|
|
5b64af1fed | ||
|
|
72f20f7413 | ||
|
|
f4e5ef41b2 | ||
|
|
f709b46d29 | ||
|
|
f87ac34656 | ||
|
|
13a1d2efaa | ||
|
|
cae3bb8bbd | ||
|
|
77504de8f1 | ||
|
|
c17c6549cf | ||
|
|
971f75b6c5 | ||
|
|
fc6a3c0eb2 | ||
|
|
cca63e6724 | ||
|
|
becd107290 | ||
|
|
36f86afa02 | ||
|
|
96238c553e | ||
|
|
3c3e87f97c | ||
|
|
fd6c2d3059 | ||
|
|
2173bb8a29 | ||
|
|
19f89633ff | ||
|
|
3885816699 | ||
|
|
21471f809f | ||
|
|
ccb5aeac6d | ||
|
|
04b20ebad4 | ||
|
|
e74e9a3fc9 | ||
|
|
4ba9844852 | ||
|
|
7498282936 | ||
|
|
780c6d2e64 | ||
|
|
796764493b | ||
|
|
b0f64aacba | ||
|
|
7cfd6fa42b | ||
|
|
745317a79b | ||
|
|
777f25e311 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ blob.rs
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.leptos.kdl
|
||||
|
||||
36
Cargo.toml
36
Cargo.toml
@@ -4,10 +4,15 @@ members = [
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_core",
|
||||
"leptos_config",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
|
||||
# libraries
|
||||
"meta",
|
||||
"router",
|
||||
@@ -19,9 +24,13 @@ members = [
|
||||
"examples/counters-stable",
|
||||
"examples/fetch",
|
||||
"examples/hackernews",
|
||||
"examples/hackernews-axum",
|
||||
"examples/parent-child",
|
||||
"examples/router",
|
||||
"examples/todomvc",
|
||||
"examples/todo-app-sqlite",
|
||||
"examples/todo-app-sqlite-axum",
|
||||
"examples/todo-app-cbor",
|
||||
"examples/view-tests",
|
||||
|
||||
# book
|
||||
@@ -37,4 +46,29 @@ lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.metadata.cargo-all-features]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -59,7 +59,8 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
Here are some resources for learning more about Leptos:
|
||||
|
||||
- [Examples](https://github.com/gbj/leptos/tree/main/examples)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/) (in progress)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [Common Bugs](https://github.com/gbj/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
## `nightly` Note
|
||||
@@ -107,6 +108,7 @@ The gold standard for testing raw rendering performance for front-end web framew
|
||||
### Can I use this for native GUI?
|
||||
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
|
||||
- Use signals, derived signals, and memos to create your reactive system
|
||||
- Create GUI widgets
|
||||
- Use event listeners to update signals
|
||||
@@ -131,7 +133,7 @@ There are some practical differences that make a significant difference:
|
||||
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Template node cloning:** Leptos's `view` macro compiles to a static HTML string and a set of instructions of how to assign its reactive values. This means that at runtime, Leptos can clone a `<template>` node rather than calling `document.createElement()` to create DOM nodes. This is a _significantly_ faster way of rendering components.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` *(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)*
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
|
||||
@@ -5,7 +5,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> Element {
|
||||
let (value, set_value) = create_signal(cx, initial);
|
||||
|
||||
@@ -115,7 +115,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::Event| {
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
@@ -222,7 +222,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
let input: Element;
|
||||
let input = NodeRef::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
|
||||
@@ -11,7 +11,7 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
@@ -63,7 +63,7 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
|
||||
63
docs/COMMON_BUGS.md
Normal file
63
docs/COMMON_BUGS.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Leptos Gotchas: Common Bugs
|
||||
|
||||
This document is intended as a running list of common issues, with example code and solutions.
|
||||
|
||||
## Reactivity
|
||||
|
||||
### Avoid writing to a signal from an effect
|
||||
|
||||
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_a) = create_signal(cx, false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if a() > 5 {
|
||||
set_b(true);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
This creates an inefficient chain of updates, and can easily lead to infinite loops in more complex applications.
|
||||
|
||||
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let b = move || a () > 5;
|
||||
```
|
||||
|
||||
## Templates and the DOM
|
||||
|
||||
### `<input value=...>` doesn't update or stops updating
|
||||
|
||||
Many DOM attributes can be updated either by setting an attribute on the DOM node, or by setting an object property directly on it. In general, `setAttribute()` stops working once the property has been set.
|
||||
|
||||
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
// ❌ reactivity doesn't work as expected: typing only updates the default
|
||||
// of each input, so if you start typing in the second input, it won't
|
||||
// update the first one
|
||||
<input value=a on:input=on_input />
|
||||
<input value=a on:input=on_input />
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
// ✅ works as intended by setting the value *property*
|
||||
<input prop:value=a on:input=on_input />
|
||||
<input prop:value=a on:input=on_input />
|
||||
}
|
||||
```
|
||||
@@ -4,3 +4,13 @@ language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "The Leptos Guide"
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.mermaid]
|
||||
command = "mdbook-mermaid"
|
||||
|
||||
[output]
|
||||
|
||||
[output.html]
|
||||
additional-js = ["mermaid.min.js", "mermaid-init.js"]
|
||||
|
||||
1
docs/book/mermaid-init.js
Normal file
1
docs/book/mermaid-init.js
Normal file
@@ -0,0 +1 @@
|
||||
mermaid.initialize({startOnLoad:true});
|
||||
4
docs/book/mermaid.min.js
vendored
Normal file
4
docs/book/mermaid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -4,7 +4,7 @@ fn main() {
|
||||
mount_to_body(|cx| {
|
||||
let name = "gbj";
|
||||
let userid = 0;
|
||||
let _input_element: Element;
|
||||
let _input_element = NodeRef::new(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
run_scope(|cx| {
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// signal
|
||||
let (count, set_count) = create_signal(cx, 1);
|
||||
|
||||
// derived signal
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
// memo
|
||||
let memoized_square = create_memo(cx, move |_| count() * count());
|
||||
|
||||
// effect
|
||||
create_effect(cx, move |_| {
|
||||
println!(
|
||||
"count =\t\t{}\ndouble_count = \t{}",
|
||||
"count =\t\t{} \ndouble_count = \t{}, \nsquare = \t{}",
|
||||
count(),
|
||||
double_count(),
|
||||
memoized_square()
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ let mut a = 0;
|
||||
let mut b = 0;
|
||||
let c = a + b;
|
||||
|
||||
assert_eq!(c, 0); // sanity check
|
||||
|
||||
a = 2;
|
||||
b = 2;
|
||||
|
||||
@@ -25,11 +27,13 @@ But that’s _exactly_ how reactive programming works.
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
run_scope(|cx| {
|
||||
run_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let c = move || a() + b();
|
||||
|
||||
assert_eq!(c(), 0); // yep, still true
|
||||
|
||||
set_a(2);
|
||||
set_b(2);
|
||||
|
||||
@@ -37,8 +41,73 @@ run_scope(|cx| {
|
||||
});
|
||||
```
|
||||
|
||||
Hopefully, this makes some intuitive sense. After all, `c` is a closure. Calling it again causes it to access its values a second time. This isn’t _that_ cool.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
run_scope(create_runtime(), |cx| {
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
let c = move || a() + b();
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
println!("c = {}", c()); // prints "c = 0"
|
||||
});
|
||||
|
||||
set_a(2); // prints "c = 2"
|
||||
set_b(2); // prints "c = 4"
|
||||
});
|
||||
```
|
||||
|
||||
This example’s a little different. [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) defines a “side effect,” a bridge between the reactive system of signals and the outside world. Effects synchronize the reactive system with everything else: the console, the filesystem, an HTTP request, whatever.
|
||||
|
||||
Because the closure `c` is called within the effect and in turns calls the signals `a` and `b`, the effect automatically subscribes to the signals `a` and `b`. This means that whenever `a` or `b` is updated, the effect will re-run, logging the value again.
|
||||
|
||||
You can picture the reactive graph for this system like this:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->C;
|
||||
B-->C;
|
||||
C-->Effect;
|
||||
```
|
||||
|
||||
This is the foundation on which _everything_ else is built.
|
||||
|
||||
## Reactive Primitives
|
||||
|
||||
### Overview
|
||||
|
||||
The reactive system is built on the interaction between these two halves: **signals** and **effects**. When a signal is called inside an effect, the effect automatically subscribes to the signal. When a signal’s value is updated, it automatically notifies all its subscribers, and they re-run.
|
||||
|
||||
The following simple example contains most of the core reactive concepts:
|
||||
|
||||
```rust
|
||||
{{#include ../project/ch04_reactivity/src/main.rs}}
|
||||
```
|
||||
|
||||
This creates a reactive graph like this:
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
count-->double_count;
|
||||
count-->memoized_square;
|
||||
count-->effect;
|
||||
double_count-->effect;
|
||||
memoized_square-->effect;
|
||||
```
|
||||
|
||||
**Signals** are reactive values created using [`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html) or [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html).
|
||||
|
||||
**Derived Signals** computations in ordinary closures that rely on other signals. The computation re-runs whenever you access its value.
|
||||
|
||||
**Memos** are computations that are memoized with [create_memo](https://docs.rs/leptos/latest/leptos/fn.create_memo.html). Memos only re-run when one of their signal dependencies has changed.
|
||||
|
||||
And **effects** (created with [create_effect](<(https://docs.rs/leptos/latest/leptos/fn.create_effect.html)>) synchronize the reactive system with something outside it.
|
||||
|
||||
The rest of this chapter will walk through each of these concepts in more depth.
|
||||
|
||||
### Signals
|
||||
|
||||
A **signal** is a piece of data that may change over time, and notifies other code when it has changed. This is the core primitive of Leptos’s reactive system.
|
||||
@@ -53,7 +122,7 @@ let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
#### `ReadSignal<T>`
|
||||
|
||||
The `ReadSignal` half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
|
||||
The [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
@@ -65,7 +134,7 @@ assert_eq!(value(), 0);
|
||||
|
||||
> Here, a **reactive context** means anywhere within an `Effect`. Leptos’s templating system is built on top of its reactive system, so if you’re reading the signal’s value within the template, the template will automatically subscribe to the signal and update exactly the value that needs to change in the DOM.
|
||||
|
||||
Calling a `ReadSignal` clones the value it contains. If that’s too expensive, use `ReadSignal::with()` to borrow the value and do whatever you need.
|
||||
Calling a `ReadSignal` clones the value it contains. If that’s too expensive, use [`ReadSignal::with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#method.with) to borrow the value and do whatever you need.
|
||||
|
||||
```rust
|
||||
struct MySuperExpensiveStruct {
|
||||
@@ -77,14 +146,12 @@ let (value, set_value) = create_signal(cx, MySuperExpensiveStruct::default());
|
||||
// ❌ this is going to clone the `StructThatsSuperExpensiveToClone` unnecessarily!
|
||||
let lowercased = move || value().a.to_lowercase();
|
||||
// ✅ only use what we need
|
||||
let lowercased = move || value.with(|value| value.to_lowercase());
|
||||
// 🔥 aaaand there's no need to type "value" three times in a row
|
||||
let lowercased = move || value.with(String::to_lowercase);
|
||||
let lowercased = move || value.with(|value: &MySuperExpensiveStruct| value.a.to_lowercase());
|
||||
```
|
||||
|
||||
#### `WriteSignal<T>`
|
||||
|
||||
The `WriteSignal` half of this tuple allows you to update the value of the signal, which will automatically notify anything that’s listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call `WriteSignal::update` instead.
|
||||
The [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) half of this tuple allows you to update the value of the signal, which will automatically notify anything that’s listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call [`WriteSignal::update`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#method.update) instead.
|
||||
|
||||
```rust
|
||||
// often you just want to replace the value
|
||||
@@ -99,3 +166,75 @@ assert_eq!(items(), vec![1]);
|
||||
```
|
||||
|
||||
> Under the hood, `set_value(1)` is just syntactic sugar for `set_value.update(|n| *n = 1)`.
|
||||
|
||||
#### `RwSignal<T>`
|
||||
|
||||
This kind of “read-write segregation,” in which the getter and the setter are stored in separate variables, may be familiar from the tuple-based ”hooks” pattern in libraries like React, Solid, Yew, or Dioxus. It encourages clear contracts between components. For example, if a child component only needs to be able to read a signal, but shouldn’t be able to update it (and therefore trigger changes in other parts of the application), you can pass it only the `ReadSignal`.
|
||||
|
||||
Sometimes, however, you may prefer to keep the getter and setter combined in one variable. For example, it’s awkward and repetitive to store both halves of a signal in another data structure:
|
||||
|
||||
```rust
|
||||
# use leptos::*;
|
||||
|
||||
// pretty repetitive
|
||||
struct AppState {
|
||||
count: ReadSignal<i32>,
|
||||
set_count: WriteSignal<i32>,
|
||||
name: ReadSignal<String>,
|
||||
set_name: WriteSignal<String>
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (name, set_name) = create_signal(cx, "Alice".to_string());
|
||||
provide_context(cx, AppState {
|
||||
count,
|
||||
set_count,
|
||||
name,
|
||||
set_name
|
||||
})
|
||||
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
Or maybe you just like to keep your getters and setters in one place.
|
||||
|
||||
In this case, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) and the [`RwSignal`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html) type. This returns a **R**ead-**w**rite Signal, which has the same [`get`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get), [`with`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.with), [`set`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.set), and [`update`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.update) functions as the `ReadSignal` and `WriteSignal` halves.
|
||||
|
||||
```rust
|
||||
# use leptos::*;
|
||||
|
||||
// better
|
||||
struct AppState {
|
||||
count: RwSignal<i32>,
|
||||
name: RwSignal<String>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) {
|
||||
let count = create_rw_signal(cx, 0);
|
||||
let name = create_rw_signal(cx, "Alice".to_string());
|
||||
provide_context(cx, AppState {
|
||||
count,
|
||||
name,
|
||||
})
|
||||
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
If you still want to hand off read-only access to another part of the app, you can get a `ReadSignal` with [`RwSignal::read_only()`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get).
|
||||
|
||||
### Derived Signals
|
||||
|
||||
(todo)
|
||||
|
||||
### Memos
|
||||
|
||||
(todo)
|
||||
|
||||
### Effects
|
||||
|
||||
(todo)
|
||||
|
||||
@@ -16,15 +16,15 @@ serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "1"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
#counter = { path = "../counter", default-features = false}
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
@@ -34,10 +34,11 @@ ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web"]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
to generate the Webassembly to provide hydration features for the server.
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
to generate the WebAssembly to provide hydration features for the server.
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
|
||||
If for some reason you want to run it as a fully client side app, that can be done with the instructions below.
|
||||
|
||||
@@ -9,7 +9,7 @@ cfg_if! {
|
||||
use crate::counters::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
@@ -10,74 +9,7 @@ cfg_if! {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
|
||||
#[get("{tail:.*}")]
|
||||
async fn render(req: HttpRequest) -> impl Responder {
|
||||
let path = req.path();
|
||||
let path = "http://leptos".to_string() + path;
|
||||
println!("path = {path}");
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").body(format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Isomorphic Counter</title>
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
</body>
|
||||
<script type="module">import init, {{ main }} from './pkg/leptos_counter_isomorphic.js'; init().then(main);</script>
|
||||
</html>"#,
|
||||
run_scope({
|
||||
move |cx| {
|
||||
let integration = ServerIntegration { path: path.clone() };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
|
||||
view! { cx, <Counters/>}
|
||||
}
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/api/{tail:.*}")]
|
||||
async fn handle_server_fns(
|
||||
req: HttpRequest,
|
||||
params: web::Path<String>,
|
||||
body: web::Bytes,
|
||||
) -> impl Responder {
|
||||
let path = params.into_inner();
|
||||
let accept_header = req
|
||||
.headers()
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
|
||||
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||
let body: &[u8] = &body;
|
||||
match server_fn(&body).await {
|
||||
Ok(serialized) => {
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
if let Some("application/json") = accept_header {
|
||||
HttpResponse::Ok().body(serialized)
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
HttpResponse::SeeOther()
|
||||
.insert_header(("Location", "/"))
|
||||
.content_type("application/json")
|
||||
.body(serialized)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("server function error: {e:#?}");
|
||||
HttpResponse::InternalServerError().body(e.to_string())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
|
||||
}
|
||||
}
|
||||
use std::{net::SocketAddr, env};
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
@@ -98,17 +30,20 @@ cfg_if! {
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
crate::counters::register_server_functions();
|
||||
|
||||
HttpServer::new(|| {
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_counter_isomorphic").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(counter_events)
|
||||
.service(handle_server_fns)
|
||||
.service(render)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <Counters/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8081))?
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
This example creates a simple counter in a client side rendered app with Rust and WASM!
|
||||
|
||||
|
||||
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
use leptos::*;
|
||||
|
||||
pub fn simple_counter(cx: Scope) -> web_sys::Element {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
/// A simple counter component.
|
||||
///
|
||||
/// You can document each of the properties passed to a component using the format below.
|
||||
///
|
||||
/// # Props
|
||||
/// - **initial_value** [`i32`] - The value the counter should start at.
|
||||
/// - **step** [`i32`] - The change that should be applied on each step.
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope, initial_value: i32, step: i32) -> web_sys::Element {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<button on:click=move |_| set_value(initial_value)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use counter::simple_counter;
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(simple_counter)
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> })
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ use wasm_bindgen_test::*;
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
use counter::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(counter::simple_counter);
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> });
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
|
||||
10
examples/counters-stable/README.md
Normal file
10
examples/counters-stable/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos Counters Example on Rust Stable
|
||||
|
||||
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -7,6 +7,8 @@ fn main() {
|
||||
mount_to_body(|cx| view! { cx, <Counters/> })
|
||||
}
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -28,12 +30,14 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let mut new_counters = vec![];
|
||||
for next_id in 0..1000 {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(cx, 0);
|
||||
new_counters.push((next_id, signal));
|
||||
}
|
||||
set_counters.update(|counters| counters.extend(new_counters.iter()));
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
@@ -46,7 +50,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
"Add 1000 Counters"
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
|
||||
10
examples/counters/README.md
Normal file
10
examples/counters/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos Counters Example
|
||||
|
||||
This example showcases a basic Leptos app with many counters. It is a good example of how to set up a basic reactive app with signals and effects, and how to interact with browser events.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,6 +1,8 @@
|
||||
use leptos::*;
|
||||
use leptos::{For, ForProps};
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -22,12 +24,14 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
};
|
||||
|
||||
let add_many_counters = move |_| {
|
||||
let mut new_counters = vec![];
|
||||
for next_id in 0..1000 {
|
||||
let next_id = next_counter_id();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(cx, 0);
|
||||
new_counters.push((next_id, signal));
|
||||
}
|
||||
set_counters.update(move |counters| counters.extend(new_counters.iter()));
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
set_counters.update(move |counters| counters.extend(new_counters));
|
||||
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
|
||||
};
|
||||
|
||||
let clear_counters = move |_| {
|
||||
@@ -40,7 +44,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
"Add 1000 Counters"
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
|
||||
@@ -11,7 +11,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
gloo-timers = { version = "0.2", features = ["futures"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
|
||||
10
examples/fetch/README.md
Normal file
10
examples/fetch/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Client Side Fetch
|
||||
|
||||
This example shows how to fetch data from the client in WebAssembly.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -9,6 +10,10 @@ pub struct Cat {
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
// artificial delay
|
||||
// the cat API is too fast to show the transition
|
||||
TimeoutFuture::new(500).await;
|
||||
|
||||
if count > 0 {
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={}",
|
||||
@@ -32,8 +37,9 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
pub fn fetch_example(cx: Scope) -> web_sys::Element {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
view! { cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<label>
|
||||
"How many cats would you like?"
|
||||
@@ -45,16 +51,22 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{move || pending().then(|| view! { cx, <p>"Loading more cats..."</p> })}
|
||||
<div>
|
||||
<Suspense fallback={"Loading (Suspense Fallback)...".to_string()}>
|
||||
// <Transition/> holds the previous value while new async data is being loaded
|
||||
// Switch the <Transition/> to <Suspense/> to fall back to "Loading..." every time
|
||||
<Transition
|
||||
fallback={"Loading (Suspense Fallback)...".to_string()}
|
||||
set_pending
|
||||
>
|
||||
{move || {
|
||||
cats.read().map(|data| match data {
|
||||
Err(_) => view! { cx, <pre>"Error"</pre> },
|
||||
Ok(cats) => view! { cx,
|
||||
Ok(cats) => view! { cx,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
@@ -64,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
|
||||
})
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
8
examples/gtk/README.md
Normal file
8
examples/gtk/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Leptos in a GTK App
|
||||
|
||||
This example creates a basic GTK app that uses Leptos’s reactive primitives.
|
||||
|
||||
## Build and Run
|
||||
|
||||
Unlike the other examples, this has a variety of build prerequisites that are out of scope of this crate. More detail on that can be found [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation.html). The example comes from [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html) and should be
|
||||
runnable with `cargo run` if you have the GTK prerequisites installed.
|
||||
@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
|
||||
|
||||
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
|
||||
fn main() {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
// Create a new application
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
|
||||
51
examples/hackernews-axum/Cargo.toml
Normal file
51
examples/hackernews-axum/Cargo.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "leptos-hackernews-axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:http",
|
||||
"leptos/ssr",
|
||||
"leptos_axum",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
21
examples/hackernews-axum/LICENSE
Normal file
21
examples/hackernews-axum/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
29
examples/hackernews-axum/README.md
Normal file
29
examples/hackernews-axum/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Leptos Hacker News Example with Axum
|
||||
|
||||
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
8
examples/hackernews-axum/index.html
Normal file
8
examples/hackernews-axum/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="css" href="./static/style.css"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
78
examples/hackernews-axum/src/api.rs
Normal file
78
examples/hackernews-axum/src/api.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use leptos::Serializable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
format!("https://node-hnapi.herokuapp.com/{path}")
|
||||
}
|
||||
|
||||
pub fn user(path: &str) -> String {
|
||||
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
let json = gloo_net::http::Request::get(path)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
T::from_json(&json).ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
let json = reqwest::get(path)
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
T::from_json(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct Story {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub points: Option<i32>,
|
||||
pub user: Option<String>,
|
||||
pub time: usize,
|
||||
pub time_ago: String,
|
||||
#[serde(alias = "type")]
|
||||
pub story_type: String,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub domain: String,
|
||||
#[serde(default)]
|
||||
pub comments: Option<Vec<Comment>>,
|
||||
pub comments_count: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct Comment {
|
||||
pub id: usize,
|
||||
pub level: usize,
|
||||
pub user: Option<String>,
|
||||
pub time: usize,
|
||||
pub time_ago: String,
|
||||
pub content: Option<String>,
|
||||
pub comments: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
pub struct User {
|
||||
pub created: usize,
|
||||
pub id: String,
|
||||
pub karma: i32,
|
||||
pub about: Option<String>,
|
||||
}
|
||||
48
examples/hackernews-axum/src/lib.rs
Normal file
48
examples/hackernews-axum/src/lib.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
mod routes;
|
||||
use routes::nav::*;
|
||||
use routes::stories::*;
|
||||
use routes::story::*;
|
||||
use routes::users::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> Element {
|
||||
provide_context(cx, MetaContext::default());
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Stylesheet href="/static/style.css"/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" element=|cx| view! { cx, <User/> }/>
|
||||
<Route path="stories/:id" element=|cx| view! { cx, <Story/> }/>
|
||||
<Route path="*stories" element=|cx| view! { cx, <Stories/> }/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::hydrate(body().unwrap(), move |cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
72
examples/hackernews-axum/src/main.rs
Normal file
72
examples/hackernews-axum/src/main.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
routing::{get},
|
||||
Router,
|
||||
error_handling::HandleError,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use std::net::SocketAddr;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use leptos_hackernews_axum::*;
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
|
||||
log::debug!("serving at {addr}");
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
// These are Tower Services that will serve files from the static and pkg repos.
|
||||
// HandleError is needed as Axum requires services to implement Infallible Errors
|
||||
// because all Errors are converted into Responses
|
||||
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
|
||||
let pkg_service =HandleError::new( ServeDir::new("./pkg"), handle_file_error);
|
||||
|
||||
/// Convert the Errors from ServeDir to a type that implements IntoResponse
|
||||
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("File Not Found: {}", err),
|
||||
)
|
||||
}
|
||||
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
// `GET /` goes to `root`
|
||||
.nest_service("/pkg", pkg_service)
|
||||
.nest_service("/static", static_service)
|
||||
.fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <App/> }));
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use leptos_hackernews_axum::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
4
examples/hackernews-axum/src/routes.rs
Normal file
4
examples/hackernews-axum/src/routes.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod nav;
|
||||
pub mod stories;
|
||||
pub mod story;
|
||||
pub mod users;
|
||||
30
examples/hackernews-axum/src/routes/nav.rs
Normal file
30
examples/hackernews-axum/src/routes/nav.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn Nav(cx: Scope) -> Element {
|
||||
view! { cx,
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<A href="/">
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
<strong>"New"</strong>
|
||||
</A>
|
||||
<A href="/show">
|
||||
<strong>"Show"</strong>
|
||||
</A>
|
||||
<A href="/ask">
|
||||
<strong>"Ask"</strong>
|
||||
</A>
|
||||
<A href="/job">
|
||||
<strong>"Jobs"</strong>
|
||||
</A>
|
||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
||||
"Built with Leptos"
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
}
|
||||
}
|
||||
154
examples/hackernews-axum/src/routes/stories.rs
Normal file
154
examples/hackernews-axum/src/routes/stories.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::api;
|
||||
|
||||
fn category(from: &str) -> &'static str {
|
||||
match from {
|
||||
"new" => "newest",
|
||||
"show" => "show",
|
||||
"ask" => "ask",
|
||||
"job" => "jobs",
|
||||
_ => "news",
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Stories(cx: Scope) -> Element {
|
||||
let query = use_query_map(cx);
|
||||
let params = use_params_map(cx);
|
||||
let page = move || {
|
||||
query
|
||||
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
|
||||
.unwrap_or(1)
|
||||
};
|
||||
let story_type = move || {
|
||||
params
|
||||
.with(|p| p.get("stories").cloned())
|
||||
.unwrap_or_else(|| "top".to_string())
|
||||
};
|
||||
let stories = create_resource(
|
||||
cx,
|
||||
move || (page(), story_type()),
|
||||
move |(page, story_type)| async move {
|
||||
let path = format!("{}?page={}", category(&story_type), page);
|
||||
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
|
||||
},
|
||||
);
|
||||
|
||||
let hide_more_link = move || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div class="news-view">
|
||||
<div class="news-list-nav">
|
||||
<span>
|
||||
{move || if page() > 1 {
|
||||
view! {
|
||||
cx,
|
||||
<a class="page-link"
|
||||
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
||||
attr:aria_label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
}
|
||||
} else {
|
||||
view! {
|
||||
cx,
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
}
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
>
|
||||
"more >"
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
<Suspense fallback=view! { cx, <p>"Loading..."</p> }>
|
||||
{move || match stories.read() {
|
||||
None => None,
|
||||
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }),
|
||||
Some(Some(stories)) => {
|
||||
Some(view! { cx,
|
||||
<ul>
|
||||
<For each=move || stories.clone() key=|story| story.id>{
|
||||
move |cx: Scope, story: &api::Story| {
|
||||
view! { cx,
|
||||
<Story story=story.clone() />
|
||||
}
|
||||
}
|
||||
}</For>
|
||||
</ul>
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Story(cx: Scope, story: api::Story) -> Element {
|
||||
view! { cx,
|
||||
<li class="news-item">
|
||||
<span class="score">{story.points}</span>
|
||||
<span class="title">
|
||||
{if !story.url.starts_with("item?id=") {
|
||||
view! { cx,
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
</span>
|
||||
}
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<span class="meta">
|
||||
{if story.story_type != "job" {
|
||||
view! { cx,
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { cx, <A href=format!("/users/{}", user)>{user.clone()}</A>})}
|
||||
{format!(" {} | ", story.time_ago)}
|
||||
<A href=format!("/stories/{}", story.id)>
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"discuss".into()
|
||||
}}
|
||||
</A>
|
||||
</span>
|
||||
}
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }
|
||||
}}
|
||||
</span>
|
||||
{(story.story_type != "link").then(|| view! { cx,
|
||||
<span>
|
||||
//{" "}
|
||||
<span class="label">{story.story_type}</span>
|
||||
</span>
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
103
examples/hackernews-axum/src/routes/story.rs
Normal file
103
examples/hackernews-axum/src/routes/story.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn Story(cx: Scope) -> Element {
|
||||
let params = use_params_map(cx);
|
||||
let story = create_resource(
|
||||
cx,
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
move |id| async move { api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await },
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
{move || story.read().map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { cx, <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{}", user)>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For each=move || story.comments.clone().unwrap_or_default() key=|comment| comment.id>
|
||||
{move |cx, comment: &api::Comment| view! { cx, <Comment comment=comment.clone() /> }}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Comment(cx: Scope, comment: api::Comment) -> Element {
|
||||
let (open, set_open) = create_signal(cx, true);
|
||||
|
||||
view! { cx,
|
||||
<li class="comment">
|
||||
<div class="by">
|
||||
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
|
||||
{format!(" {}", comment.time_ago)}
|
||||
</div>
|
||||
<div class="text" inner_html=comment.content></div>
|
||||
{(!comment.comments.is_empty()).then(|| {
|
||||
view! { cx,
|
||||
<div>
|
||||
<div class="toggle" class:open=open>
|
||||
<a on:click=move |_| set_open.update(|n| *n = !*n)>
|
||||
{
|
||||
let comments_len = comment.comments.len();
|
||||
move || if open() {
|
||||
"[-]".into()
|
||||
} else {
|
||||
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
|
||||
}
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
{move || open().then({
|
||||
let comments = comment.comments.clone();
|
||||
move || view! { cx,
|
||||
<ul class="comment-children">
|
||||
<For each=move || comments.clone() key=|comment| comment.id>
|
||||
{|cx, comment: &api::Comment| view! { cx, <Comment comment=comment.clone() /> }}
|
||||
</For>
|
||||
</ul>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
fn pluralize(n: usize) -> &'static str {
|
||||
if n == 1 {
|
||||
" reply"
|
||||
} else {
|
||||
" replies"
|
||||
}
|
||||
}
|
||||
39
examples/hackernews-axum/src/routes/users.rs
Normal file
39
examples/hackernews-axum/src/routes/users.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::api::{self, User};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn User(cx: Scope) -> Element {
|
||||
let params = use_params_map(cx);
|
||||
let user = create_resource(
|
||||
cx,
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
move |id| async move { api::fetch_api::<User>(&api::user(&id)).await },
|
||||
);
|
||||
view! { cx,
|
||||
<div class="user-view">
|
||||
{move || user.read().map(|user| match user {
|
||||
None => view! { cx, <h1>"User not found."</h1> },
|
||||
Some(user) => view! { cx,
|
||||
<div>
|
||||
<h1>"User: " {&user.id}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
326
examples/hackernews-axum/static/style.css
Normal file
326
examples/hackernews-axum/static/style.css
Normal file
@@ -0,0 +1,326 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: 15px;
|
||||
background-color: #f2f3f5;
|
||||
margin: 0;
|
||||
padding-top: 55px;
|
||||
color: #34495e;
|
||||
overflow-y: scroll
|
||||
}
|
||||
|
||||
a {
|
||||
color: #34495e;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #335d92;
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
height: 55px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0
|
||||
}
|
||||
|
||||
.header .inner {
|
||||
max-width: 800px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
padding: 15px 5px
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: rgba(255, 255, 255, .8);
|
||||
line-height: 24px;
|
||||
transition: color .15s ease;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-weight: 300;
|
||||
letter-spacing: .075em;
|
||||
margin-right: 1.8em
|
||||
}
|
||||
|
||||
.header a:hover {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.header a.active {
|
||||
color: #fff;
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
.header a:nth-child(6) {
|
||||
margin-right: 0
|
||||
}
|
||||
|
||||
.header .github {
|
||||
color: #fff;
|
||||
font-size: .9em;
|
||||
margin: 0;
|
||||
float: right
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 24px;
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
.view {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-exit-active {
|
||||
transition: all .2s ease
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-exit-active {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
@media (max-width:860px) {
|
||||
.header .inner {
|
||||
padding: 15px 30px
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.header .inner {
|
||||
padding: 15px
|
||||
}
|
||||
|
||||
.header a {
|
||||
margin-right: 1em
|
||||
}
|
||||
|
||||
.header .github {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.news-view {
|
||||
padding-top: 45px
|
||||
}
|
||||
|
||||
.news-list,
|
||||
.news-list-nav {
|
||||
background-color: #fff;
|
||||
border-radius: 2px
|
||||
}
|
||||
|
||||
.news-list-nav {
|
||||
padding: 15px 30px;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
top: 55px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 998;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
.news-list-nav .page-link {
|
||||
margin: 0 1em
|
||||
}
|
||||
|
||||
.news-list-nav .disabled {
|
||||
color: #aaa
|
||||
}
|
||||
|
||||
.news-list {
|
||||
position: absolute;
|
||||
margin: 30px 0;
|
||||
width: 100%;
|
||||
transition: all .5s cubic-bezier(.55, 0, .1, 1)
|
||||
}
|
||||
|
||||
.news-list ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.news-list {
|
||||
margin: 10px 0
|
||||
}
|
||||
}
|
||||
|
||||
.news-item {
|
||||
background-color: #fff;
|
||||
padding: 20px 30px 20px 80px;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
line-height: 20px
|
||||
}
|
||||
|
||||
.news-item .score {
|
||||
color: #335d92;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
margin-top: -10px
|
||||
}
|
||||
|
||||
.news-item .host,
|
||||
.news-item .meta {
|
||||
font-size: .85em;
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.news-item .host a,
|
||||
.news-item .meta a {
|
||||
color: #626262;
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.news-item .host a:hover,
|
||||
.news-item .meta a:hover {
|
||||
color: #335d92
|
||||
}
|
||||
|
||||
.item-view-header {
|
||||
background-color: #fff;
|
||||
padding: 1.8em 2em 1em;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
|
||||
}
|
||||
|
||||
.item-view-header h1 {
|
||||
display: inline;
|
||||
font-size: 1.5em;
|
||||
margin: 0;
|
||||
margin-right: .5em
|
||||
}
|
||||
|
||||
.item-view-header .host,
|
||||
.item-view-header .meta,
|
||||
.item-view-header .meta a {
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.item-view-header .meta a {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.item-view-comments {
|
||||
background-color: #fff;
|
||||
margin-top: 10px;
|
||||
padding: 0 2em .5em
|
||||
}
|
||||
|
||||
.item-view-comments-header {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
padding: 1em 0;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.item-view-comments-header .spinner {
|
||||
display: inline-block;
|
||||
margin: -15px 0
|
||||
}
|
||||
|
||||
.comment-children {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.item-view-header h1 {
|
||||
font-size: 1.25em
|
||||
}
|
||||
}
|
||||
|
||||
.comment-children .comment-children {
|
||||
margin-left: 1.5em
|
||||
}
|
||||
|
||||
.comment {
|
||||
border-top: 1px solid #eee;
|
||||
position: relative
|
||||
}
|
||||
|
||||
.comment .by,
|
||||
.comment .text,
|
||||
.comment .toggle {
|
||||
font-size: .9em;
|
||||
margin: 1em 0
|
||||
}
|
||||
|
||||
.comment .by {
|
||||
color: #626262
|
||||
}
|
||||
|
||||
.comment .by a {
|
||||
color: #626262;
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.comment .text {
|
||||
overflow-wrap: break-word
|
||||
}
|
||||
|
||||
.comment .text a:hover {
|
||||
color: #335d92
|
||||
}
|
||||
|
||||
.comment .text pre {
|
||||
white-space: pre-wrap
|
||||
}
|
||||
|
||||
.comment .toggle {
|
||||
background-color: #fffbf2;
|
||||
padding: .3em .5em;
|
||||
border-radius: 4px
|
||||
}
|
||||
|
||||
.comment .toggle a {
|
||||
color: #626262;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.comment .toggle.open {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
margin-bottom: -.5em
|
||||
}
|
||||
|
||||
.user-view {
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 2em 3em
|
||||
}
|
||||
|
||||
.user-view h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em
|
||||
}
|
||||
|
||||
.user-view .meta {
|
||||
list-style-type: none;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.user-view .label {
|
||||
display: inline-block;
|
||||
min-width: 4em
|
||||
}
|
||||
|
||||
.user-view .about {
|
||||
margin: 1em 0
|
||||
}
|
||||
|
||||
.user-view .links a {
|
||||
text-decoration: underline
|
||||
}
|
||||
@@ -14,11 +14,10 @@ console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos = { version = "0.0.20", default-features = false, features = ["serde"] }
|
||||
leptos_meta = { version = "0.0", default-features = false }
|
||||
leptos_actix = { version = "0.0.2", default-features = false, optional = true }
|
||||
leptos_router = { version = "0.0", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -26,6 +25,7 @@ serde_json = "1"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
# openssl = { version = "0.10", features = ["v110"] }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
@@ -34,11 +34,12 @@ hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web"]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
# Leptos Hacker News Example
|
||||
|
||||
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
|
||||
This example creates a basic clone of the Hacker News site. It showcases Leptos’s ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. It uses Actix as its backend.
|
||||
|
||||
## Client Side Rendering
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CRS bundle
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
to generate the Webassembly to provide hydration features for the server.
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr`
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::Serializable;
|
||||
use leptos::{on_cleanup, Scope, Serializable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
@@ -10,11 +10,15 @@ pub fn user(path: &str) -> String {
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
let abort_controller = web_sys::AbortController::new().ok();
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
|
||||
let json = gloo_net::http::Request::get(path)
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
@@ -22,11 +26,19 @@ where
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
// abort in-flight requests if the Scope is disposed
|
||||
// i.e., if we've navigated away from this page
|
||||
on_cleanup(cx, move || {
|
||||
if let Some(abort_controller) = abort_controller {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
T::from_json(&json).ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
{
|
||||
|
||||
@@ -16,7 +16,8 @@ pub fn App(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Stylesheet href="/static/style.css".into()/>
|
||||
<Stylesheet href="/style.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
@@ -37,7 +38,7 @@ cfg_if! {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::hydrate(body().unwrap(), move |cx| {
|
||||
|
||||
@@ -3,109 +3,39 @@ use leptos::*;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use futures::StreamExt;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_hackernews::*;
|
||||
use std::{net::SocketAddr, env};
|
||||
|
||||
#[get("/static/style.css")]
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
// match every path — our router will handle actual dispatch
|
||||
#[get("{tail:.*}")]
|
||||
async fn render_app(req: HttpRequest) -> impl Responder {
|
||||
let path = req.path();
|
||||
|
||||
let query = req.query_string();
|
||||
let path = if query.is_empty() {
|
||||
"http://leptos".to_string() + path
|
||||
} else {
|
||||
"http://leptos".to_string() + path + "?" + query
|
||||
};
|
||||
|
||||
let app = move |cx| {
|
||||
let integration = ServerIntegration { path: path.clone() };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
|
||||
view! { cx, <App/> }
|
||||
};
|
||||
|
||||
let head = r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, { main } from '/pkg/leptos_hackernews.js'; init().then(main);</script>"#;
|
||||
let tail = "</body></html>";
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
futures::stream::once(async { head.to_string() })
|
||||
.chain(render_to_stream(move |cx| {
|
||||
let app = app(cx);
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>{app}")
|
||||
}))
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
|
||||
)
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
|
||||
log::debug!("serving at {host}:{port}");
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
|
||||
// necessary for proper HTTP/2 streaming
|
||||
|
||||
// load TLS keys
|
||||
// to create a self-signed temporary cert for testing:
|
||||
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
|
||||
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||
// builder
|
||||
// .set_private_key_file("key.pem", SslFiletype::PEM)
|
||||
// .unwrap();
|
||||
// builder.set_certificate_chain_file("cert.pem").unwrap();
|
||||
|
||||
HttpServer::new(|| {
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(css)
|
||||
.service(
|
||||
web::scope("/pkg")
|
||||
.service(Files::new("", "./dist"))
|
||||
.wrap(middleware::Compress::default()),
|
||||
)
|
||||
.service(render_app)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <App/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
// replace .bind with .bind_openssl to use HTTPS
|
||||
//.bind_openssl(&format!("{}:{}", host, port), builder)?
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use leptos_hackernews::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> Element {
|
||||
view! { cx,
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<A href="/">
|
||||
<A href="/" class="home".to_string()>
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
|
||||
@@ -32,11 +32,13 @@ pub fn Stories(cx: Scope) -> Element {
|
||||
move || (page(), story_type()),
|
||||
move |(page, story_type)| async move {
|
||||
let path = format!("{}?page={}", category(&story_type), page);
|
||||
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
|
||||
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
|
||||
},
|
||||
);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link = move || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
@@ -76,7 +78,10 @@ pub fn Stories(cx: Scope) -> Element {
|
||||
</div>
|
||||
<main class="news-list">
|
||||
<div>
|
||||
<Suspense fallback=view! { cx, <p>"Loading..."</p> }>
|
||||
<Transition
|
||||
fallback=view! { cx, <p>"Loading..."</p> }
|
||||
set_pending
|
||||
>
|
||||
{move || match stories.read() {
|
||||
None => None,
|
||||
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }),
|
||||
@@ -94,7 +99,7 @@ pub fn Stories(cx: Scope) -> Element {
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
@@ -8,11 +9,19 @@ pub fn Story(cx: Scope) -> Element {
|
||||
let story = create_resource(
|
||||
cx,
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
move |id| async move { api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await },
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
|
||||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title.clone())).unwrap_or_else(|| "Loading story...".to_string());
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<Meta name="description" content=meta_description/>
|
||||
{move || story.read().map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
|
||||
@@ -8,7 +8,13 @@ pub fn User(cx: Scope) -> Element {
|
||||
let user = create_resource(
|
||||
cx,
|
||||
move || params().get("id").cloned().unwrap_or_default(),
|
||||
move |id| async move { api::fetch_api::<User>(&api::user(&id)).await },
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
api::fetch_api::<User>(cx, &api::user(&id)).await
|
||||
}
|
||||
},
|
||||
);
|
||||
view! { cx,
|
||||
<div class="user-view">
|
||||
|
||||
17
examples/parent-child/README.md
Normal file
17
examples/parent-child/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Parent Child Example
|
||||
|
||||
This example highlights four different ways that child components can communicate with their parent:
|
||||
|
||||
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
|
||||
for the child component to write into and the parent to read
|
||||
2. <ButtonB/>: passing a closure as one of the child component props, for
|
||||
the child component to call
|
||||
3. <ButtonC/>: adding a simple event listener on the child component itself
|
||||
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,5 +1,5 @@
|
||||
use leptos::*;
|
||||
use web_sys::Event;
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
// This highlights four different ways that child components can communicate
|
||||
// with their parent:
|
||||
@@ -71,7 +71,7 @@ pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> Element {
|
||||
#[component]
|
||||
pub fn ButtonB<F>(cx: Scope, on_click: F) -> Element
|
||||
where
|
||||
F: Fn(Event) + 'static,
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
view! {
|
||||
cx,
|
||||
@@ -82,11 +82,11 @@ where
|
||||
</button>
|
||||
}
|
||||
|
||||
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(Event) + 'static
|
||||
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(MouseEvent) + 'static
|
||||
// and save you from typing out the generic
|
||||
// the component macro actually expands to define a
|
||||
//
|
||||
// struct ButtonBProps<F> where F: Fn(Event) + 'static {
|
||||
// struct ButtonBProps<F> where F: Fn(MouseEvent) + 'static {
|
||||
// on_click: F
|
||||
// }
|
||||
//
|
||||
|
||||
@@ -7,12 +7,11 @@ edition = "2021"
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features=["csr"] }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# Leptos Router Example
|
||||
|
||||
This example demonstrates how Leptos' router works
|
||||
This example demonstrates how Leptos’s router for client side routing.
|
||||
|
||||
## Build and Run it
|
||||
|
||||
## Run it
|
||||
```bash
|
||||
trunk serve --open
|
||||
```
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
@@ -49,29 +49,29 @@ pub fn router_example(cx: Scope) -> Element {
|
||||
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> Element {
|
||||
log!("rendering ContactList");
|
||||
let location = use_location(cx);
|
||||
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
|
||||
let contacts = move || {
|
||||
contacts.read().map(|contacts| {
|
||||
// this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
|
||||
contacts
|
||||
.into_iter()
|
||||
.map(|contact| {
|
||||
view! { cx,
|
||||
<li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div class="contact-list">
|
||||
<h1>"Contacts"</h1>
|
||||
<ul>
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>{
|
||||
move || {
|
||||
contacts.read().map(|contacts| view! { cx,
|
||||
<For each=move || contacts.clone() key=|contact| contact.id>
|
||||
{move |cx, contact: &ContactSummary| {
|
||||
let id = contact.id;
|
||||
let name = format!("{} {}", contact.first_name, contact.last_name);
|
||||
view! { cx,
|
||||
<li><A href=id.to_string()><span>{name.clone()}</span></A></li>
|
||||
}
|
||||
}}
|
||||
</For>
|
||||
})
|
||||
}
|
||||
}</Suspense>
|
||||
</ul>
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
|
||||
{move || view! { cx, <ul>{contacts}</ul>}}
|
||||
</Suspense>
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
@@ -79,6 +79,7 @@ pub fn ContactList(cx: Scope) -> Element {
|
||||
|
||||
#[component]
|
||||
pub fn Contact(cx: Scope) -> Element {
|
||||
log!("rendering <Contact/> page");
|
||||
let params = use_params_map(cx);
|
||||
let contact = create_resource(
|
||||
cx,
|
||||
@@ -122,28 +123,30 @@ pub fn Contact(cx: Scope) -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn About(_cx: Scope) -> Vec<Element> {
|
||||
pub fn About(_cx: Scope) -> Element {
|
||||
log!("rendering About page");
|
||||
view! { cx,
|
||||
<>
|
||||
<div>
|
||||
<h1>"About"</h1>
|
||||
<p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</p>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Settings(_cx: Scope) -> Vec<Element> {
|
||||
pub fn Settings(_cx: Scope) -> Element {
|
||||
log!("rendering Settings page");
|
||||
view! { cx,
|
||||
<>
|
||||
<div>
|
||||
<h1>"Settings"</h1>
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>"Name"</legend>
|
||||
<input type="text" name="first_name" placeholder="First"/>
|
||||
<input type="text" name="first_name" placeholder="Last"/>
|
||||
<input type="text" name="last_name" placeholder="Last"/>
|
||||
</fieldset>
|
||||
<pre>"This page is just a placeholder."</pre>
|
||||
</form>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
48
examples/todo-app-cbor/Cargo.toml
Normal file
48
examples/todo-app-cbor/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "todo-app-cbor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
|
||||
anyhow = "1"
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
sqlx = { version = "0.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
21
examples/todo-app-cbor/LICENSE
Normal file
21
examples/todo-app-cbor/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
examples/todo-app-cbor/README.md
Normal file
22
examples/todo-app-cbor/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Leptos Todo App Sqlite with CBOR
|
||||
|
||||
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. It is identical to the todo-app-sqlite example, but utilizes CBOR encoding for one of the server functions
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
BIN
examples/todo-app-cbor/Todos.db
Normal file
BIN
examples/todo-app-cbor/Todos.db
Normal file
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
||||
22
examples/todo-app-cbor/src/lib.rs
Normal file
22
examples/todo-app-cbor/src/lib.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::hydrate(body().unwrap(), |cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/todo-app-cbor/src/main.rs
Normal file
49
examples/todo-app-cbor/src/main.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod todo;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::todo::*;
|
||||
use std::{ net::SocketAddr,env };
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
}
|
||||
}
|
||||
}
|
||||
212
examples/todo-app-cbor/src/todo.rs
Normal file
212
examples/todo-app-cbor/src/todo.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api", "Url")]
|
||||
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
let req =
|
||||
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
|
||||
println!("req.path = {:?}", req.path());
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api", "Cbor")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Stylesheet href="/style.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Tasks"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" element=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos(cx: Scope) -> Element {
|
||||
let add_todo = create_server_multi_action::<AddTodo>(cx);
|
||||
let delete_todo = create_server_action::<DeleteTodo>(cx);
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// track mutations that should lead us to refresh the list
|
||||
let add_changed = add_todo.version;
|
||||
let todo_deleted = delete_todo.version;
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
cx,
|
||||
move || (add_changed(), todo_deleted()),
|
||||
move |_| get_todos(cx),
|
||||
);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<div>
|
||||
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
|
||||
{
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
let existing_todos = {
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
todos
|
||||
.read()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }]
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todo| {
|
||||
let delete_todo = delete_todo.clone();
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo.clone()>
|
||||
<input type="hidden" name="id" value=todo.id/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
<div>{existing_todos}</div>
|
||||
<div>{pending_todos}</div>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
3
examples/todo-app-cbor/style.css
Normal file
3
examples/todo-app-cbor/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
64
examples/todo-app-sqlite-axum/Cargo.toml
Normal file
64
examples/todo-app-sqlite-axum/Cargo.toml
Normal file
@@ -0,0 +1,64 @@
|
||||
[package]
|
||||
name = "todo-app-sqlite-axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8", optional = true }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:http",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"axum",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tokio",
|
||||
"http",
|
||||
"sqlx",
|
||||
"leptos_axum",
|
||||
]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
21
examples/todo-app-sqlite-axum/LICENSE
Normal file
21
examples/todo-app-sqlite-axum/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
examples/todo-app-sqlite-axum/README.md
Normal file
22
examples/todo-app-sqlite-axum/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Leptos Todo App Sqlite with Axum
|
||||
|
||||
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
BIN
examples/todo-app-sqlite-axum/Todos.db
Normal file
BIN
examples/todo-app-sqlite-axum/Todos.db
Normal file
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
||||
22
examples/todo-app-sqlite-axum/src/lib.rs
Normal file
22
examples/todo-app-sqlite-axum/src/lib.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::hydrate(body().unwrap(), |cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
81
examples/todo-app-sqlite-axum/src/main.rs
Normal file
81
examples/todo-app-sqlite-axum/src/main.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
routing::{post},
|
||||
error_handling::HandleError,
|
||||
Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use crate::todo::*;
|
||||
use todo_app_sqlite_axum::*;
|
||||
use http::StatusCode;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::env;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
log::debug!("serving at {addr}");
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
|
||||
// These are Tower Services that will serve files from the static and pkg repos.
|
||||
// HandleError is needed as Axum requires services to implement Infallible Errors
|
||||
// because all Errors are converted into Responses
|
||||
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
|
||||
let pkg_service = HandleError::new( ServeDir::new("./pkg"), handle_file_error);
|
||||
|
||||
/// Convert the Errors from ServeDir to a type that implements IntoResponse
|
||||
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("File Not Found: {}", err),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.nest_service("/pkg", pkg_service)
|
||||
.nest_service("/static", static_service)
|
||||
.fallback(leptos_axum::render_app_to_stream(render_options.clone(), |cx| view! { cx, <TodoApp/> }));
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on {}", &render_options.socket_address);
|
||||
axum::Server::bind(&render_options.socket_address)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use todo_app_sqlite_axum::todo::*;
|
||||
|
||||
pub fn main() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
213
examples/todo-app-sqlite-axum/src/todo.rs
Normal file
213
examples/todo-app-sqlite-axum/src/todo.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos(_cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
|
||||
// let req = use_context::<http::Request<axum::body::BoxBody>>(cx)
|
||||
// .expect("couldn't get HttpRequest from context");
|
||||
// println!("req.path = {:?}", req.uri());
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(_row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Tasks"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" element=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos(cx: Scope) -> Element {
|
||||
let add_todo = create_server_multi_action::<AddTodo>(cx);
|
||||
let delete_todo = create_server_action::<DeleteTodo>(cx);
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// track mutations that should lead us to refresh the list
|
||||
let add_changed = add_todo.version;
|
||||
let todo_deleted = delete_todo.version;
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
cx,
|
||||
move || (add_changed(), todo_deleted()),
|
||||
move |_| get_todos(cx),
|
||||
);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<div>
|
||||
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
|
||||
{
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
let existing_todos = {
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
todos
|
||||
.read()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }]
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todo| {
|
||||
let delete_todo = delete_todo.clone();
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo.clone()>
|
||||
<input type="hidden" name="id" value=todo.id/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
<div>{existing_todos}</div>
|
||||
<div>{pending_todos}</div>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
48
examples/todo-app-sqlite/Cargo.toml
Normal file
48
examples/todo-app-sqlite/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "todo-app-sqlite"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6.2", optional = true }
|
||||
actix-web = { version = "4.2.1", optional = true, features = ["openssl", "macros"] }
|
||||
anyhow = "1.0.66"
|
||||
broadcaster = "1.0.0"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
21
examples/todo-app-sqlite/LICENSE
Normal file
21
examples/todo-app-sqlite/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
examples/todo-app-sqlite/README.md
Normal file
22
examples/todo-app-sqlite/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Leptos Todo App Sqlite
|
||||
|
||||
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
BIN
examples/todo-app-sqlite/Todos.db
Normal file
BIN
examples/todo-app-sqlite/Todos.db
Normal file
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
||||
22
examples/todo-app-sqlite/src/lib.rs
Normal file
22
examples/todo-app-sqlite/src/lib.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
use leptos::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::hydrate(body().unwrap(), |cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
52
examples/todo-app-sqlite/src/main.rs
Normal file
52
examples/todo-app-sqlite/src/main.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod todo;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::todo::*;
|
||||
use std::env;
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
}
|
||||
}
|
||||
}
|
||||
218
examples/todo-app-sqlite/src/todo.rs
Normal file
218
examples/todo-app-sqlite/src/todo.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api")]
|
||||
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
let req =
|
||||
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
|
||||
println!("\ncalling server fn");
|
||||
println!(" req.path = {:?}", req.path());
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(350));
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
println!(" returning todos\n");
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(350));
|
||||
|
||||
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Stylesheet href="/style.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Tasks"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" element=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos(cx: Scope) -> Element {
|
||||
let add_todo = create_server_multi_action::<AddTodo>(cx);
|
||||
let delete_todo = create_server_action::<DeleteTodo>(cx);
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// track mutations that should lead us to refresh the list
|
||||
let add_changed = add_todo.version;
|
||||
let todo_deleted = delete_todo.version;
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
cx,
|
||||
move || (add_changed(), todo_deleted()),
|
||||
move |_| get_todos(cx),
|
||||
);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<div>
|
||||
<Transition fallback=view! {cx, <p>"Loading..."</p> }>
|
||||
{
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
let existing_todos = {
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
todos
|
||||
.read()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }]
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todo| {
|
||||
let delete_todo = delete_todo.clone();
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo.clone()>
|
||||
<input type="hidden" name="id" value=todo.id/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
<div>{existing_todos}</div>
|
||||
<div>{pending_todos}</div>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
3
examples/todo-app-sqlite/style.css
Normal file
3
examples/todo-app-sqlite/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
10
examples/todomvc/README.md
Normal file
10
examples/todomvc/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos TodoMVC
|
||||
|
||||
This is a Leptos implementation of the TodoMVC example common to many frameworks. This is a relatively-simple application but shows off features like interaction between components and state management.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -136,10 +136,10 @@ pub fn TodoMVC(cx: Scope) -> Element {
|
||||
});
|
||||
|
||||
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
|
||||
let add_todo = move |ev: web_sys::Event| {
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
@@ -262,7 +262,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
|
||||
// this will be filled by _ref=input below
|
||||
let input: Element;
|
||||
let input = NodeRef::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
@@ -295,8 +295,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
set_editing(true);
|
||||
|
||||
// guard against the fact that in SSR mode, that ref is actually to a String
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
if let Some(input) = input.dyn_ref::<HtmlInputElement>() {
|
||||
if let Some(input) = input.get().expect("should have loaded input already").dyn_ref::<HtmlInputElement>() {
|
||||
input.focus();
|
||||
}
|
||||
}>
|
||||
@@ -311,7 +310,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
let key_code = ev.key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
|
||||
10
examples/view-tests/README.md
Normal file
10
examples/view-tests/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos View Tests
|
||||
|
||||
This is a collection of mostly internal view tests for Leptos. Feel free to look if curious to see a variety of ways you can build identical views!
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -3,7 +3,103 @@ use leptos::*;
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <TemplateConsumer/> })
|
||||
mount_to_body(|cx| view! { cx, <Tests/> })
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SelfUpdatingEffect(cx: Scope) -> Element {
|
||||
let (a, set_a) = create_signal(cx, false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if !a() {
|
||||
set_a(true);
|
||||
}
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<h1>"Hello " {move || a().to_string()}</h1>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Tests(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
//<div><SelfUpdatingEffect/></div>
|
||||
<div><BlockOrders/></div>
|
||||
//<div><TemplateConsumer/></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn BlockOrders(cx: Scope) -> Element {
|
||||
let a = "A";
|
||||
let b = "B";
|
||||
let c = "C";
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<div>"A"</div>
|
||||
<div>{a}</div>
|
||||
<div><span>"A"</span></div>
|
||||
<div><span>{a}</span></div>
|
||||
<hr/>
|
||||
<div>"A" {b}</div>
|
||||
<div>{a} "B"</div>
|
||||
<div>{a} {b}</div>
|
||||
<div>{"A"} {"B"}</div>
|
||||
<div><span style="color: red">{a}</span> {b}</div>
|
||||
<hr/>
|
||||
<div>{a} "B" {c}</div>
|
||||
<div>"A" {b} "C"</div>
|
||||
<div>{a} {b} "C"</div>
|
||||
<div>{a} {b} {c}</div>
|
||||
<div>"A" {b} {c}</div>
|
||||
<hr/>
|
||||
<div>"A" {b} <span style="color: red">"C"</span></div>
|
||||
<div>"A" {b} <span style="color: red">{c}</span></div>
|
||||
<div>"A" <span style="color: red">"B"</span> "C"</div>
|
||||
<div>"A" <span style="color: red">"B"</span> {c}</div>
|
||||
<div>{a} <span style="color: red">{b}</span> {c}</div>
|
||||
<div>"A" {b} <span style="color: red">{c}</span></div>
|
||||
<div><span style="color: red">"A"</span> {b} {c}</div>
|
||||
<div><span style="color: red">{a}</span> "B" {c}</div>
|
||||
<div><span style="color: red">"A"</span> {b} "C"</div>
|
||||
<hr/>
|
||||
<div><span style="color: red">"A"</span> <span style="color: blue">{b}</span> {c}</div>
|
||||
<div><span style="color: red">{a}</span> "B" <span style="color: blue">{c}</span></div>
|
||||
<div><span style="color: red">"A"</span> {b} <span style="color: blue">"C"</span></div>
|
||||
<hr/>
|
||||
<div><A/></div>
|
||||
<div>"A" <B/></div>
|
||||
<div>{a} <B/></div>
|
||||
<div><A/> "B"</div>
|
||||
<div><A/> {b}</div>
|
||||
<div><A/><B/></div>
|
||||
<hr/>
|
||||
<div><A/> "B" <C/></div>
|
||||
<div><A/> {b} <C/></div>
|
||||
<div><A/> {b} "C"</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn A(cx: Scope) -> Element {
|
||||
view! { cx, <span style="color: red">"A"</span> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn B(cx: Scope) -> Element {
|
||||
view! { cx, <span style="color: red">"B"</span> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn C(cx: Scope) -> Element {
|
||||
view! { cx, <span style="color: red">"C"</span> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -18,8 +114,8 @@ fn TemplateConsumer(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div id="template">
|
||||
<h1>"Template Consumer"</h1>
|
||||
{cloned_tpl}
|
||||
/* <h1>"Template Consumer"</h1>
|
||||
{cloned_tpl} */
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
21
integrations/actix/Cargo.toml
Normal file
21
integrations/actix/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "leptos_actix"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
description = "Actix integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
futures = "0.3"
|
||||
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false, version = "0.0", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_router = { path = "../../router", default-features = false, version = "0.0", features = [
|
||||
"ssr",
|
||||
] }
|
||||
226
integrations/actix/src/lib.rs
Normal file
226
integrations/actix/src/lib.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use actix_web::{web::Bytes, *};
|
||||
use futures::StreamExt;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
|
||||
/// Leptos server function arguments in the body, runs the server function if found,
|
||||
/// and returns the resulting [HttpResponse].
|
||||
///
|
||||
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// fn register_server_functions() {
|
||||
/// // call ServerFn::register() for each of the server functions you've defined
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
/// // make sure you actually register your server functions
|
||||
/// register_server_functions();
|
||||
///
|
||||
/// HttpServer::new(|| {
|
||||
/// App::new()
|
||||
/// // "/api" should match the prefix, if any, declared when defining server functions
|
||||
/// // {tail:.*} passes the remainder of the URL as the server function name
|
||||
/// .route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
/// })
|
||||
/// .bind(("127.0.0.1", 8080))?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn handle_server_fns() -> Route {
|
||||
web::post().to(
|
||||
|req: HttpRequest, params: web::Path<String>, body: web::Bytes| async move {
|
||||
{
|
||||
let path = params.into_inner();
|
||||
let accept_header = req
|
||||
.headers()
|
||||
.get("Accept")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
|
||||
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
|
||||
let body: &[u8] = &body;
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
|
||||
// provide HttpRequest as context in server scope
|
||||
provide_context(cx, req.clone());
|
||||
|
||||
match server_fn(cx, body).await {
|
||||
Ok(serialized) => {
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
let mut res: HttpResponseBuilder;
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header == Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = HttpResponse::Ok()
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = req
|
||||
.headers()
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
res = HttpResponse::SeeOther();
|
||||
res.insert_header(("Location", referer))
|
||||
.content_type("application/json");
|
||||
};
|
||||
match serialized {
|
||||
Payload::Binary(data) => {
|
||||
res.content_type("application/cbor");
|
||||
res.body(Bytes::from(data))
|
||||
}
|
||||
Payload::Url(data) => {
|
||||
res.content_type("application/x-www-form-urlencoded");
|
||||
res.body(data)
|
||||
}
|
||||
Payload::Json(data) => {
|
||||
res.content_type("application/json");
|
||||
res.body(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::BadRequest()
|
||||
.body(format!("Could not find a server function at that route."))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{HttpServer, App};
|
||||
/// use leptos::*;
|
||||
/// use std::{env,net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> Element {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[actix_web::main]
|
||||
/// async fn main() -> std::io::Result<()> {
|
||||
///
|
||||
/// let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
/// HttpServer::new(move || {
|
||||
/// let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_example").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
/// render_options.write_to_file();
|
||||
/// App::new()
|
||||
/// // {tail:.*} passes the remainder of the URL as the route
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }))
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
/// .run()
|
||||
/// .await
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn render_app_to_stream(
|
||||
options: RenderOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
|
||||
) -> Route {
|
||||
web::get().to(move |req: HttpRequest| {
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
async move {
|
||||
let path = req.path();
|
||||
|
||||
let query = req.query_string();
|
||||
let path = if query.is_empty() {
|
||||
"http://leptos".to_string() + path
|
||||
} else {
|
||||
"http://leptos".to_string() + path + "?" + query
|
||||
};
|
||||
|
||||
let app = {
|
||||
let app_fn = app_fn.clone();
|
||||
move |cx| {
|
||||
let integration = ServerIntegration { path: path.clone() };
|
||||
provide_context(cx, RouterIntegrationContext::new(integration));
|
||||
provide_context(cx, MetaContext::new());
|
||||
provide_context(cx, req.clone());
|
||||
|
||||
(app_fn)(cx)
|
||||
}
|
||||
};
|
||||
|
||||
let pkg_path = &options.pkg_path;
|
||||
let socket_ip = &options.socket_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match options.environment {
|
||||
RustEnv::DEV => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
|
||||
ws.onmessage = (ev) => {{
|
||||
console.log(`Reload message: `);
|
||||
if (ev.data === 'reload') window.location.reload();
|
||||
}};
|
||||
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
RustEnv::PROD => "".to_string(),
|
||||
};
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
|
||||
let tail = "</body></html>";
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").streaming(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
// TODO this leaks a runtime once per invocation
|
||||
.chain(render_to_stream(move |cx| {
|
||||
let app = app(cx);
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>{app}")
|
||||
}))
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
24
integrations/axum/Cargo.toml
Normal file
24
integrations/axum/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "leptos_axum"
|
||||
version = "0.0.4"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
description = "Axum integrations for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6"
|
||||
derive_builder = "0.12.0"
|
||||
futures = "0.3"
|
||||
kdl = "4.6.0"
|
||||
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_meta = { path = "../../meta", default-features = false, version = "0.0", features = [
|
||||
"ssr",
|
||||
] }
|
||||
leptos_router = { path = "../../router", default-features = false, version = "0.0", features = [
|
||||
"ssr",
|
||||
] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
286
integrations/axum/src/lib.rs
Normal file
286
integrations/axum/src/lib.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
http::{HeaderMap, HeaderValue, Request, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use futures::{Future, SinkExt, Stream, StreamExt};
|
||||
use leptos::*;
|
||||
use leptos_meta::MetaContext;
|
||||
use leptos_router::*;
|
||||
use std::{io, pin::Pin, sync::Arc};
|
||||
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
|
||||
/// run the server function if found, and return the resulting [Response].
|
||||
///
|
||||
/// This provides an `Arc<[Request<Body>](axum::http::Request)>` [Scope](leptos::Scope).
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
///
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, routing::post, Router};
|
||||
/// use std::net::SocketAddr;
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new()
|
||||
/// .route("/api/*fn_name", post(leptos_axum::handle_server_fns));
|
||||
///
|
||||
/// // run our app with hyper
|
||||
/// // `axum::Server` is a re-export of `hyper::Server`
|
||||
/// axum::Server::bind(&addr)
|
||||
/// .serve(app.into_make_service())
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
pub async fn handle_server_fns(
|
||||
Path(fn_name): Path<String>,
|
||||
headers: HeaderMap<HeaderValue>,
|
||||
body: Bytes,
|
||||
// req: Request<Body>,
|
||||
) -> impl IntoResponse {
|
||||
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
|
||||
let fn_name: String = match fn_name.strip_prefix("/") {
|
||||
Some(path) => path.to_string(),
|
||||
None => fn_name,
|
||||
};
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
std::thread::spawn({
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
async move {
|
||||
let res = if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) {
|
||||
let runtime = create_runtime();
|
||||
let (cx, disposer) = raw_scope_and_disposer(runtime);
|
||||
|
||||
// provide request as context in server scope
|
||||
// provide_context(cx, Arc::new(req));
|
||||
|
||||
match server_fn(cx, body.as_ref()).await {
|
||||
Ok(serialized) => {
|
||||
// clean up the scope, which we only needed to run the server fn
|
||||
disposer.dispose();
|
||||
runtime.dispose();
|
||||
|
||||
// if this is Accept: application/json then send a serialized JSON response
|
||||
let accept_header =
|
||||
headers.get("Accept").and_then(|value| value.to_str().ok());
|
||||
let mut res = Response::builder();
|
||||
|
||||
if accept_header == Some("application/json")
|
||||
|| accept_header
|
||||
== Some("application/x-www-form-urlencoded")
|
||||
|| accept_header == Some("application/cbor")
|
||||
{
|
||||
res = res.status(StatusCode::OK);
|
||||
}
|
||||
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
|
||||
else {
|
||||
let referer = headers
|
||||
.get("Referer")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("/");
|
||||
res = res
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header("Location", referer);
|
||||
}
|
||||
match serialized {
|
||||
Payload::Binary(data) => res
|
||||
.header("Content-Type", "application/cbor")
|
||||
.body(Full::from(data)),
|
||||
Payload::Url(data) => res
|
||||
.header(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded",
|
||||
)
|
||||
.body(Full::from(data)),
|
||||
Payload::Json(data) => res
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Full::from(data)),
|
||||
}
|
||||
}
|
||||
Err(e) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Full::from(e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Full::from(
|
||||
"Could not find a server function at that route.".to_string(),
|
||||
))
|
||||
}
|
||||
.expect("could not build Response");
|
||||
|
||||
_ = tx.send(res);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
rx.await.unwrap()
|
||||
}
|
||||
|
||||
pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||
///
|
||||
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
|
||||
/// rendering it, and includes any meta tags injected using [leptos_meta].
|
||||
///
|
||||
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
|
||||
/// the documentation for that function.
|
||||
///
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use axum::handler::Handler;
|
||||
/// use axum::Router;
|
||||
/// use std::{net::SocketAddr, env};
|
||||
/// use leptos::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyApp(cx: Scope) -> Element {
|
||||
/// view! { cx, <main>"Hello, world!"</main> }
|
||||
/// }
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
|
||||
/// let render_options: RenderOptions = RenderOptions::builder()
|
||||
/// .pkg_path("/pkg/leptos_example")
|
||||
/// .socket_address(addr)
|
||||
/// .reload_port(3001)
|
||||
/// .environment(&env::var("RUST_ENV")).build();
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new()
|
||||
/// .fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }));
|
||||
///
|
||||
/// // run our app with hyper
|
||||
/// // `axum::Server` is a re-export of `hyper::Server`
|
||||
/// axum::Server::bind(&addr)
|
||||
/// .serve(app.into_make_service())
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
pub fn render_app_to_stream(
|
||||
options: RenderOptions,
|
||||
app_fn: impl Fn(leptos::Scope) -> Element + Clone + Send + 'static,
|
||||
) -> impl Fn(
|
||||
Request<Body>,
|
||||
) -> Pin<Box<dyn Future<Output = StreamBody<PinnedHtmlStream>> + Send + 'static>>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ 'static {
|
||||
move |req: Request<Body>| {
|
||||
Box::pin({
|
||||
let options = options.clone();
|
||||
let app_fn = app_fn.clone();
|
||||
async move {
|
||||
// Need to get the path and query string of the Request
|
||||
let path = req.uri();
|
||||
let query = path.query();
|
||||
|
||||
let full_path;
|
||||
if let Some(query) = query {
|
||||
full_path = "http://leptos".to_string() + &path.to_string() + "?" + query
|
||||
} else {
|
||||
full_path = "http://leptos".to_string() + &path.to_string()
|
||||
}
|
||||
|
||||
let pkg_path = &options.pkg_path;
|
||||
let socket_ip = &options.socket_address.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
|
||||
let leptos_autoreload = match options.environment {
|
||||
RustEnv::DEV => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
|
||||
ws.onmessage = (ev) => {{
|
||||
console.log(`Reload message: `);
|
||||
if (ev.data === 'reload') window.location.reload();
|
||||
}};
|
||||
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
|
||||
}})()
|
||||
</script>
|
||||
"#
|
||||
),
|
||||
RustEnv::PROD => "".to_string(),
|
||||
};
|
||||
|
||||
let head = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
let tail = "</body></html>";
|
||||
|
||||
let (mut tx, rx) = futures::channel::mpsc::channel(8);
|
||||
|
||||
std::thread::spawn({
|
||||
let app_fn = app_fn.clone();
|
||||
move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("couldn't spawn runtime")
|
||||
.block_on({
|
||||
let app_fn = app_fn.clone();
|
||||
async move {
|
||||
tokio::task::LocalSet::new()
|
||||
.run_until(async {
|
||||
let mut shell = Box::pin(render_to_stream({
|
||||
let full_path = full_path.clone();
|
||||
move |cx| {
|
||||
let integration = ServerIntegration {
|
||||
path: full_path.clone(),
|
||||
};
|
||||
provide_context(
|
||||
cx,
|
||||
RouterIntegrationContext::new(integration),
|
||||
);
|
||||
provide_context(cx, MetaContext::new());
|
||||
let app = app_fn(cx);
|
||||
let head = use_context::<MetaContext>(cx)
|
||||
.map(|meta| meta.dehydrate())
|
||||
.unwrap_or_default();
|
||||
format!("{head}</head><body>{app}")
|
||||
}
|
||||
}));
|
||||
while let Some(fragment) = shell.next().await {
|
||||
_ = tx.send(fragment).await;
|
||||
}
|
||||
tx.close_channel();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move { head.clone() })
|
||||
.chain(rx)
|
||||
.chain(futures::stream::once(async { tail.to_string() }))
|
||||
.map(|html| Ok(Bytes::from(html)));
|
||||
StreamBody::new(Box::pin(stream) as PinnedHtmlStream)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos"
|
||||
version = "0.0.16"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -9,45 +9,76 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
|
||||
readme = "../README.md"
|
||||
|
||||
[dependencies]
|
||||
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.16" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.16" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.16" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.16" }
|
||||
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.16" }
|
||||
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.20" }
|
||||
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.20" }
|
||||
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.20" }
|
||||
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
|
||||
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.20" }
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4"
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
default = ["csr", "serde", "interning"]
|
||||
csr = [
|
||||
"leptos_core/csr",
|
||||
"leptos_dom/csr",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
"leptos_core/csr",
|
||||
"leptos_dom/csr",
|
||||
"leptos_macro/csr",
|
||||
"leptos_reactive/csr",
|
||||
"leptos_server/csr",
|
||||
]
|
||||
hydrate = [
|
||||
"leptos_core/hydrate",
|
||||
"leptos_dom/hydrate",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
"leptos_core/hydrate",
|
||||
"leptos_dom/hydrate",
|
||||
"leptos_macro/hydrate",
|
||||
"leptos_reactive/hydrate",
|
||||
"leptos_server/hydrate",
|
||||
]
|
||||
ssr = [
|
||||
"leptos_core/ssr",
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
"leptos_core/ssr",
|
||||
"leptos_dom/ssr",
|
||||
"leptos_macro/ssr",
|
||||
"leptos_reactive/ssr",
|
||||
"leptos_server/ssr",
|
||||
]
|
||||
stable = [
|
||||
"leptos_core/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
"leptos_core/stable",
|
||||
"leptos_dom/stable",
|
||||
"leptos_macro/stable",
|
||||
"leptos_reactive/stable",
|
||||
"leptos_server/stable",
|
||||
]
|
||||
serde = ["leptos_reactive/serde"]
|
||||
serde-lite = ["leptos_reactive/serde-lite"]
|
||||
miniserde = ["leptos_reactive/miniserde"]
|
||||
interning = ["leptos_dom/interning"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["stable", "interning"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
12
leptos/build.rs
Normal file
12
leptos/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustc_version::{version, version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
assert!(version().unwrap().major >= 1);
|
||||
|
||||
match version_meta().unwrap().channel {
|
||||
Channel::Stable => {
|
||||
println!("cargo:rustc-cfg=feature=\"stable\"")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,12 @@
|
||||
//! the [examples](https://github.com/gbj/leptos/tree/main/examples):
|
||||
//! - [`counter`](https://github.com/gbj/leptos/tree/main/examples/counter) is the classic
|
||||
//! counter example, showing the basics of client-side rendering and reactive DOM updates
|
||||
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counter) introduces parent-child
|
||||
//! - [`counter-isomorphic`](https://github.com/gbj/leptos/tree/main/examples/counter-isomorphic) is the classic
|
||||
//! counter example run on the server using an isomorphic function, showing the basics of client-side rendering and reactive DOM updates
|
||||
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counters) introduces parent-child
|
||||
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
|
||||
//! - [`counters-stable`](https://github.com/gbj/leptos/tree/main/examples/counters-stable) introduces parent-child
|
||||
//! communication via contexts, and the `<For/>` component for efficient keyed list updates. Unlike counters, this compiles in Rust stable.
|
||||
//! - [`parent-child`](https://github.com/gbj/leptos/tree/main/examples/parent-child) shows four different
|
||||
//! ways a parent component can communicate with a child, including passing a closure, context, and more
|
||||
//! - [`todomvc`](https://github.com/gbj/leptos/tree/main/examples/todomvc) implements the classic to-do
|
||||
@@ -47,6 +51,13 @@
|
||||
//! - [`hackernews`](https://github.com/gbj/leptos/tree/main/examples/hackernews) pulls everything together.
|
||||
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`hackernews-axum`](https://github.com/gbj/leptos/tree/main/examples/hackernews-axum) pulls everything together.
|
||||
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run. This one uses Axum as it's backend.
|
||||
//! - [`todo-app-sqlite`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite) is a simple todo app, showcasing the use of
|
||||
//! functions that run only on the server, but are called from client side function calls
|
||||
//! - [`todo-app-sqlite-axum`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite-axum) is a simple todo app, showcasing the use of
|
||||
//! functions that run only on the server, but are called from client side function calls. Now with Axum backend
|
||||
//!
|
||||
//! (The SPA examples can be run using `trunk serve`. For information about Trunk,
|
||||
//! [see here]((https://trunkrs.dev/)).)
|
||||
@@ -71,12 +82,16 @@
|
||||
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
|
||||
//! of calling signals as functions. If you need to use `stable`, you will need to call `.get()`
|
||||
//! and `.set()` manually.
|
||||
//! - `serde` (*Default*) In SSR/hydrate mode, uses [serde] to serialize resources and send them
|
||||
//! - `serde` (*Default*) In SSR/hydrate mode, uses [serde](https://docs.rs/serde/latest/serde/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite] to serialize resources and send them
|
||||
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `miniserde` In SSR/hydrate mode, uses [miniserde] to serialize resources and send them
|
||||
//! - `miniserde` In SSR/hydrate mode, uses [miniserde](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `interning` (*Default*) When client-side rendering, Leptos uses [`wasm_bindgen::intern`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/fn.intern.html)
|
||||
//! to reduce the cost of copying class names, attribute names, attribute values, and properties through JavaScript to the DOM. This feature
|
||||
//! (included by default) makes DOM updates marginally faster and WASM binary size marginally larger. Disabling the feature makes binary sizes
|
||||
//! marginally smaller at the cost of a small decrease in speed.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
@@ -111,12 +126,22 @@
|
||||
//! ```
|
||||
//!
|
||||
//! Leptos is easy to use with [Trunk](https://trunkrs.dev/) (or with a simple wasm-bindgen setup):
|
||||
//! ```ignore
|
||||
//! ```
|
||||
//! # use leptos::*;
|
||||
//! # if false { // can't run in doctests
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! pub fn main() {
|
||||
//! mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use leptos_config::*;
|
||||
pub use leptos_core::*;
|
||||
pub use leptos_dom;
|
||||
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
fn simple_ssr_test() {
|
||||
use leptos_dom::*;
|
||||
use leptos_macro::view;
|
||||
use leptos_reactive::{create_scope, create_signal};
|
||||
use leptos_reactive::{create_runtime, create_scope, create_signal};
|
||||
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let rendered = view! {
|
||||
cx,
|
||||
@@ -30,7 +30,7 @@ fn ssr_test_with_components() {
|
||||
use leptos_core::Prop;
|
||||
use leptos_dom::*;
|
||||
use leptos_macro::*;
|
||||
use leptos_reactive::{create_scope, create_signal, Scope};
|
||||
use leptos_reactive::{create_runtime, create_scope, create_signal, Scope};
|
||||
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial_value: i32) -> Element {
|
||||
@@ -45,7 +45,7 @@ fn ssr_test_with_components() {
|
||||
}
|
||||
}
|
||||
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<div class="counters">
|
||||
@@ -66,9 +66,9 @@ fn ssr_test_with_components() {
|
||||
fn test_classes() {
|
||||
use leptos_dom::*;
|
||||
use leptos_macro::view;
|
||||
use leptos_reactive::{create_scope, create_signal};
|
||||
use leptos_reactive::{create_runtime, create_scope, create_signal};
|
||||
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let (value, set_value) = create_signal(cx, 5);
|
||||
let rendered = view! {
|
||||
cx,
|
||||
|
||||
11
leptos_config/Cargo.toml
Normal file
11
leptos_config/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "leptos_config"
|
||||
version = "0.0.20"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gbj/leptos"
|
||||
description = "Configuraiton for the Leptos web framework."
|
||||
|
||||
[dependencies]
|
||||
typed-builder = "0.11.0"
|
||||
110
leptos_config/src/lib.rs
Normal file
110
leptos_config/src/lib.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use std::{env::VarError, net::SocketAddr, str::FromStr};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// This struct serves as a convenient place to store details used for rendering.
|
||||
/// It's serialized into a file in the root called `.leptos.kdl` for cargo-leptos
|
||||
/// to watch. It's also used in our actix and axum integrations to generate the
|
||||
/// correct path for WASM, JS, and Websockets. Its goal is to be the single source
|
||||
/// of truth for render options
|
||||
#[derive(TypedBuilder, Clone)]
|
||||
pub struct RenderOptions {
|
||||
/// The path and name of the WASM and JS files generated by wasm-bindgen
|
||||
/// For example, `/pkg/app` might be a valid input if your crate name was `app`.
|
||||
#[builder(setter(into))]
|
||||
pub pkg_path: String,
|
||||
/// Used to control whether the Websocket code for code watching is included.
|
||||
/// I recommend passing in the result of `env::var("RUST_ENV")`
|
||||
#[builder(setter(into), default)]
|
||||
pub environment: RustEnv,
|
||||
/// Provides a way to control the address leptos is served from.
|
||||
/// Using an env variable here would allow you to run the same code in dev and prod
|
||||
/// Defaults to `127.0.0.1:3000`
|
||||
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
|
||||
pub socket_address: SocketAddr,
|
||||
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
|
||||
/// Defaults to `3001`
|
||||
#[builder(default = 3001)]
|
||||
pub reload_port: u32,
|
||||
}
|
||||
|
||||
impl RenderOptions {
|
||||
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings. We do not read from this file
|
||||
/// only write to it, you'll want to change the settings in your main function when you create RenderOptions
|
||||
pub fn write_to_file(&self) {
|
||||
use std::fs;
|
||||
let options = format!(
|
||||
r#"// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
|
||||
RenderOptions {{
|
||||
pkg_path "{}"
|
||||
environment "{:?}"
|
||||
socket_address "{:?}"
|
||||
reload_port {:?}
|
||||
}}
|
||||
"#,
|
||||
self.pkg_path, self.environment, self.socket_address, self.reload_port
|
||||
);
|
||||
fs::write("./.leptos.kdl", options).expect("Unable to write file");
|
||||
}
|
||||
}
|
||||
/// An enum that can be used to define the environment Leptos is running in. Can be passed to RenderOptions.
|
||||
/// Setting this to the PROD variant will not include the websockets code for cargo-leptos' watch.
|
||||
/// Defaults to PROD
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RustEnv {
|
||||
PROD,
|
||||
DEV,
|
||||
}
|
||||
|
||||
impl Default for RustEnv {
|
||||
fn default() -> Self {
|
||||
Self::PROD
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RustEnv {
|
||||
type Err = ();
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let sanitized = input.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Ok(Self::DEV),
|
||||
"development" => Ok(Self::DEV),
|
||||
"prod" => Ok(Self::PROD),
|
||||
"production" => Ok(Self::PROD),
|
||||
_ => Ok(Self::PROD),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RustEnv {
|
||||
fn from(str: &str) -> Self {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_str() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&Result<String, VarError>> for RustEnv {
|
||||
fn from(input: &Result<String, VarError>) -> Self {
|
||||
match input {
|
||||
Ok(str) => {
|
||||
let sanitized = str.to_lowercase();
|
||||
match sanitized.as_ref() {
|
||||
"dev" => Self::DEV,
|
||||
"development" => Self::DEV,
|
||||
"prod" => Self::PROD,
|
||||
"production" => Self::PROD,
|
||||
_ => {
|
||||
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Self::PROD,
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user