mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 16:54:41 -05:00
Compare commits
392 Commits
remove-mut
...
component-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c8b640855 | ||
|
|
621976c92c | ||
|
|
b2d7ad2afd | ||
|
|
73b21487b9 | ||
|
|
0b448daf3a | ||
|
|
c01dba5138 | ||
|
|
1929f2d8b2 | ||
|
|
50b0fe157a | ||
|
|
dc7f44933c | ||
|
|
64a5d75ec4 | ||
|
|
b56dde9a6d | ||
|
|
74ec8925dc | ||
|
|
baf3cc8712 | ||
|
|
23777ad67b | ||
|
|
08be1ba622 | ||
|
|
605398bcea | ||
|
|
aca2c131d4 | ||
|
|
3d10bbb0c6 | ||
|
|
8d325fce5c | ||
|
|
7e457ee202 | ||
|
|
bb282189c3 | ||
|
|
2694d2e93c | ||
|
|
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 | ||
|
|
a94e739725 | ||
|
|
780c6d2e64 | ||
|
|
067b52f731 | ||
|
|
40ed424116 | ||
|
|
9c59564e16 | ||
|
|
11f375fdaa | ||
|
|
bf301c2266 | ||
|
|
107f7c05c6 | ||
|
|
e9c1846470 | ||
|
|
796764493b | ||
|
|
b0f64aacba | ||
|
|
7cfd6fa42b | ||
|
|
745317a79b | ||
|
|
777f25e311 | ||
|
|
b9e0255016 | ||
|
|
74b2889e8a | ||
|
|
482c84dc73 | ||
|
|
8c0385d94c | ||
|
|
1c81337024 | ||
|
|
992983efd9 | ||
|
|
d8c2cab64d | ||
|
|
0f2715290c | ||
|
|
22eaa92355 | ||
|
|
f8de0fff81 | ||
|
|
d9f07111e0 | ||
|
|
d4da7e0c25 | ||
|
|
876aa0f0f4 | ||
|
|
05f635f4ac | ||
|
|
ba3156c878 | ||
|
|
e24fb3b294 | ||
|
|
c347e85de7 | ||
|
|
a298bc73dd | ||
|
|
2c894f6a1d | ||
|
|
50cc2f5eac | ||
|
|
aacc2fc902 | ||
|
|
48e934cd40 | ||
|
|
8d67aa1ff0 | ||
|
|
0920cc0cef | ||
|
|
12fc1ca7a1 | ||
|
|
a22dc69729 | ||
|
|
f5ae5f4fff | ||
|
|
8042a7002b | ||
|
|
c7826e0bc9 | ||
|
|
19ac14cf62 | ||
|
|
8f88b50d34 | ||
|
|
281b303c80 | ||
|
|
8315cb2dd7 | ||
|
|
3fe1c6ccda | ||
|
|
7c8ffa9314 | ||
|
|
0b5657564d | ||
|
|
0f89e64eda | ||
|
|
33bbfa6f75 | ||
|
|
7832b59cdd | ||
|
|
ebf4d1308b | ||
|
|
0e093d2c23 | ||
|
|
f093a24d1a | ||
|
|
07e6b361e1 | ||
|
|
183745f319 | ||
|
|
9a59c371fd | ||
|
|
75354517bf | ||
|
|
01807ea514 | ||
|
|
e19dd0a226 | ||
|
|
8e20ab28e0 | ||
|
|
3dfcf99a4c | ||
|
|
1e04442f97 | ||
|
|
7799479364 | ||
|
|
c23bc0ef90 | ||
|
|
e3c1291942 | ||
|
|
3d0a8f574e | ||
|
|
3730427789 | ||
|
|
f94be99246 | ||
|
|
7b7ff492fc | ||
|
|
ba158bbd0f | ||
|
|
7352151744 | ||
|
|
3ad2129a4c | ||
|
|
4e4b513c1b | ||
|
|
21d73463b0 | ||
|
|
d5554082f9 | ||
|
|
8ff7b4c11b | ||
|
|
0e313b3938 | ||
|
|
92f4ea5888 | ||
|
|
acd20a24ac | ||
|
|
63a2199405 | ||
|
|
9da3c66683 | ||
|
|
6b82a37dea | ||
|
|
9edd8a3c74 | ||
|
|
33fdc3eae1 | ||
|
|
10e01bf989 | ||
|
|
49820ccba6 | ||
|
|
36be004ef2 | ||
|
|
b9ca0b11a2 | ||
|
|
296e27cd4a | ||
|
|
fd3443b129 | ||
|
|
aa3dd356c1 | ||
|
|
35ca30fbab | ||
|
|
132f0839c6 | ||
|
|
e9c1799a11 | ||
|
|
4577313cca | ||
|
|
f75d49fe4c | ||
|
|
6a38375c66 | ||
|
|
f9f4fb0fef | ||
|
|
42cd3f1d69 | ||
|
|
ade2eda26d | ||
|
|
680b6ecc20 | ||
|
|
4e9d0354c6 | ||
|
|
cccd7068f9 | ||
|
|
8f56a52615 | ||
|
|
6c04e91088 | ||
|
|
9ef350c2d6 | ||
|
|
f559d47714 | ||
|
|
2483616d0d | ||
|
|
2595ffe10e | ||
|
|
221cdf2685 | ||
|
|
1cb278f520 | ||
|
|
5cfd44474d | ||
|
|
bd652ec542 | ||
|
|
d8852f909e | ||
|
|
e16cc4fc4a | ||
|
|
d5e3661bcf | ||
|
|
8873ddc40a | ||
|
|
b7e2e983f0 | ||
|
|
3701f65693 | ||
|
|
a5712d3e17 | ||
|
|
4fba035f19 | ||
|
|
47fad9a042 | ||
|
|
c8545f47cb |
51
.github/workflows/test.yml
vendored
Normal file
51
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make ci
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ blob.rs
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.leptos.kdl
|
||||
|
||||
60
Cargo.toml
60
Cargo.toml
@@ -4,39 +4,71 @@ members = [
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_core",
|
||||
"leptos_config",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
|
||||
# libraries
|
||||
"meta",
|
||||
"router",
|
||||
|
||||
# examples
|
||||
"examples/counter",
|
||||
"examples/counter-isomorphic/client",
|
||||
"examples/counter-isomorphic/server",
|
||||
"examples/counter-isomorphic/counter",
|
||||
"examples/counter-isomorphic",
|
||||
"examples/counters",
|
||||
"examples/counters-stable",
|
||||
"examples/fetch",
|
||||
"examples/hackernews/hackernews-app",
|
||||
"examples/hackernews/hackernews-client",
|
||||
"examples/hackernews/hackernews-server",
|
||||
"examples/hackernews",
|
||||
"examples/hackernews-axum",
|
||||
"examples/parent-child",
|
||||
"examples/router",
|
||||
"examples/todomvc",
|
||||
"examples/todomvc-ssr/todomvc-ssr-client",
|
||||
"examples/todomvc-ssr/todomvc-ssr-server",
|
||||
]
|
||||
exclude = [
|
||||
"benchmarks",
|
||||
# not gonna lie, this is because my arm64 mac fails when linking a GTK binary
|
||||
"examples/gtk",
|
||||
]
|
||||
"examples/todo-app-sqlite",
|
||||
"examples/todo-app-sqlite-axum",
|
||||
"examples/todo-app-cbor",
|
||||
"examples/view-tests",
|
||||
|
||||
# book
|
||||
"docs/book/project/ch02_getting_started",
|
||||
"docs/book/project/ch03_building_ui",
|
||||
"docs/book/project/ch04_reactivity",
|
||||
]
|
||||
exclude = ["benchmarks"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.metadata.cargo-all-features]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
21
README.md
21
README.md
@@ -48,18 +48,30 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
## What does that mean?
|
||||
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
|
||||
- **Isomorphic**: The same application code and business logic are compiled to run on the client and server, with seamless integration. You can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and let Leptos manage the data loading without the need to manually create APIs to consume.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. Whenever possible, we use Web essentials (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
|
||||
- **Fine-grained reactivity**: The entire framework is build from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
|
||||
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The best way to get started with a Leptos project right now is to use the [`cargo-leptos`](https://github.com/akesson/cargo-leptos) build tool and our [starter template](https://github.com/leptos-rs/start).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
cargo leptos new --git https://github.com/leptos-rs/start
|
||||
cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
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 +119,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 +144,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, enforces 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);`
|
||||
- **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
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "ch01_getting_started"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.0", features = ["csr"] }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
@@ -1,5 +0,0 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
7
docs/book/project/ch02_getting_started/Cargo.toml
Normal file
7
docs/book/project/ch02_getting_started/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ch02_getting_started"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
@@ -4,7 +4,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Leptos • Todos</title>
|
||||
|
||||
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
|
||||
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
5
docs/book/project/ch02_getting_started/src/main.rs
Normal file
5
docs/book/project/ch02_getting_started/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
7
docs/book/project/ch03_building_ui/Cargo.toml
Normal file
7
docs/book/project/ch03_building_ui/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ch03_building_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
14
docs/book/project/ch03_building_ui/index.html
Normal file
14
docs/book/project/ch03_building_ui/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Leptos • Todos</title>
|
||||
|
||||
<!-- This custom link tag with `data-trunk` tells Trunk to insert code here to load our Rust/Wasm code -->
|
||||
<!-- `data-wasm-opt=z` tells the compiler to optimize for binary size in a release build -->
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" />
|
||||
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
39
docs/book/project/ch03_building_ui/src/main.rs
Normal file
39
docs/book/project/ch03_building_ui/src/main.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|cx| {
|
||||
let name = "gbj";
|
||||
let userid = 0;
|
||||
let _input_element = NodeRef::new(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<main>
|
||||
<h1>"My Tasks"</h1> // text nodes are wrapped in quotation marks
|
||||
<h2>"by " {name}</h2>
|
||||
<input
|
||||
type="text" // attributes work just like they do in HTML
|
||||
name="new-todo"
|
||||
prop:value="todo" // `prop:` lets you set a property on a DOM node
|
||||
value="initial" // side note: the DOM `value` attribute only sets *initial* value
|
||||
// this is very important when working with forms!
|
||||
_ref=_input_element // `_ref` stores tis element in a variable
|
||||
/>
|
||||
<ul data-user=userid> // attributes can take expressions as values
|
||||
<li class="todo my-todo" // here we set the `class` attribute
|
||||
class:completed=true // `class:` also lets you toggle individual classes
|
||||
on:click=|_| todo!() // `on:` adds an event listener
|
||||
>
|
||||
"Buy milk."
|
||||
</li>
|
||||
<li class="todo my-todo" class:completed=false>
|
||||
"???"
|
||||
</li>
|
||||
<li class="todo my-todo" class:completed=false>
|
||||
"Profit!!!"
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
}
|
||||
})
|
||||
}
|
||||
7
docs/book/project/ch04_reactivity/Cargo.toml
Normal file
7
docs/book/project/ch04_reactivity/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "ch04_reactivity"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
28
docs/book/project/ch04_reactivity/src/main.rs
Normal file
28
docs/book/project/ch04_reactivity/src/main.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
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{}, \nsquare = \t{}",
|
||||
count(),
|
||||
double_count(),
|
||||
memoized_square()
|
||||
);
|
||||
});
|
||||
|
||||
set_count(1);
|
||||
set_count(2);
|
||||
set_count(3);
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
# Getting Started
|
||||
|
||||
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch01_getting_started).
|
||||
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch02_getting_started).
|
||||
|
||||
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/gbj/leptos/tree/main/examples) do.
|
||||
The easiest way to get started using Leptos is to use [Trunk](https://trunkrs.dev/), as many of our [examples](https://github.com/gbj/leptos/tree/main/examples) do. (Trunk is a simple build tool that includes a dev server.)
|
||||
|
||||
If you don’t already have it installed, you can install Trunk by running
|
||||
|
||||
@@ -19,19 +19,19 @@ cargo init leptos-todo
|
||||
Add `leptos` as a dependency to your `Cargo.toml` with the `csr` featured enabled. (That stands for “client-side rendering.” We’ll talk more about Leptos’s support for server-side rendering and hydration later.)
|
||||
|
||||
```toml
|
||||
leptos = { version = "0.0", features = ["csr"] }
|
||||
leptos = "0.0"
|
||||
```
|
||||
|
||||
You’ll want to set up a basic `index.html` with the following content:
|
||||
|
||||
```html
|
||||
{{#include ../project/ch01_getting_started/index.html}}
|
||||
{{#include ../project/ch02_getting_started/index.html}}
|
||||
```
|
||||
|
||||
Let’s start with a very simple `main.rs`
|
||||
|
||||
```rust
|
||||
{{#include ../project/ch01_getting_started/src/main.rs}}
|
||||
{{#include ../project/ch02_getting_started/src/main.rs}}
|
||||
```
|
||||
|
||||
Now run `trunk serve --open`. Trunk should automatically compile your app and open it in your default browser.
|
||||
Now run `trunk serve --open`. Trunk should automatically compile your app and open it in your default browser. If you make edits to `main.rs`, Trunk will recompile your source code and live-reload the page.
|
||||
49
docs/book/src/03_building_ui.md
Normal file
49
docs/book/src/03_building_ui.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Templating: Building User Interfaces
|
||||
|
||||
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch03_building_ui).
|
||||
|
||||
## RSX and the `view!` macro
|
||||
|
||||
Okay, that “Hello, world!” was a little boring. We’re going to be building a todo app, so let’s look at something a little more complicated.
|
||||
|
||||
As you noticed in the first example, Leptos lets you describe your user interface with a declarative `view!` macro. It looks something like this:
|
||||
|
||||
```
|
||||
view! {
|
||||
cx, // this is the "reactive scope": more on that in the next chapter
|
||||
<p>"..."</p> // this is some HTML-ish stuff
|
||||
}
|
||||
```
|
||||
|
||||
The “HTML-ish stuff” is what we call “RSX”: XML in Rust. (You may recognize the similarity to JSX, which is the mixed JavaScript/XML syntax used by frameworks like React.)
|
||||
|
||||
Here’s a more in-depth example:
|
||||
|
||||
```rust
|
||||
{{#include ../project/ch03_building_ui/src/main.rs}}
|
||||
```
|
||||
|
||||
You’ll probably notice a few things right away:
|
||||
|
||||
1. Elements without children need to be explicit closed with a `/` (`<input/>`, not `<input>`)
|
||||
2. Text nodes are formatted as strings, i.e., wrapped in quotation marks (`"My Tasks"`)
|
||||
3. Dynamic blocks can be inserted as children of elements, if wrapped in curly braces (`<h2>"by " {name}</h2>`)
|
||||
4. Attributes can be given Rust expressions as values. This could be a string literal as in HTML (`<input type="text" .../>)` or a variable or block (`data-user=userid` or `on:click=move |_| { ... }`)
|
||||
5. Unlike in HTML, whitespace is ignored and should be manually added (it’s `<h2>"by " {name}</h2>`, not `<h2>"by" {name}</h2>`; the space between `"by"` and `{name}` is ignored.)
|
||||
6. Normal attributes work exactly like you'd think they would.
|
||||
7. There are also special, prefixed attributes.
|
||||
|
||||
- `class:` lets you make targeted updates to a single class
|
||||
- `on:` lets you add an event listener
|
||||
- `prop:` lets you set a property on a DOM element
|
||||
- `_ref` stores the DOM element you’re creating in a variable
|
||||
|
||||
> You can find more information in the [reference docs for the `view!` macro](https://docs.rs/leptos/0.0.15/leptos/macro.view.html).
|
||||
|
||||
## But, wait...
|
||||
|
||||
This example shows some parts of the Leptos templating syntax. But it’s completely static.
|
||||
|
||||
How do you actually make the user interface interactive?
|
||||
|
||||
In the next chapter, we’ll talk about “fine-grained reactivity,” which is the core of the Leptos framework.
|
||||
240
docs/book/src/04_reactivity.md
Normal file
240
docs/book/src/04_reactivity.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Reactivity
|
||||
|
||||
## What is reactivity?
|
||||
|
||||
A few months ago, I completely baffled a friend by trying to explain what I was working on. “You have two variables, right? Call them `a` and `b`. And then you have a third variable, `c`. And when you update `a` or `b`, the value of `c` just _automatically changes_. And it changes _on the screen_! Automatically!”
|
||||
|
||||
“Isn’t that just... how computers work?” she asked me, puzzled. If your programming experience is limited to something like spreadsheets, it’s a reasonable enough assumption. This is, after all, how math works.
|
||||
|
||||
But you know this isn't how ordinary imperative programming works.
|
||||
|
||||
```rust,should_panic
|
||||
let mut a = 0;
|
||||
let mut b = 0;
|
||||
let c = a + b;
|
||||
|
||||
assert_eq!(c, 0); // sanity check
|
||||
|
||||
a = 2;
|
||||
b = 2;
|
||||
|
||||
// now c = 4, right?
|
||||
assert_eq!(c, 4); // nope. we all know this is wrong!
|
||||
```
|
||||
|
||||
But that’s _exactly_ how reactive programming works.
|
||||
|
||||
```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();
|
||||
|
||||
assert_eq!(c(), 0); // yep, still true
|
||||
|
||||
set_a(2);
|
||||
set_b(2);
|
||||
|
||||
assert_eq!(c(), 4); // ohhhhh yeah.
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Creating a signal is very simple. You call `create_signal`, passing in the reactive scope and the default value, and receive a tuple containing a `ReadSignal` and a `WriteSignal`.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
```
|
||||
|
||||
> If you’ve used signals in Sycamore or Solid, observables in MobX or Knockout, or a similar primitive in reactive library, you probably have a pretty good idea of how signals work in Leptos. If you’re familiar with React, Yew, or Dioxus, you may recognize a similar pattern to their `use_state` hooks.
|
||||
|
||||
#### `ReadSignal<T>`
|
||||
|
||||
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);
|
||||
|
||||
// calling value() with return the current value of the signal,
|
||||
// and automatically track changes if you're in a reactive context
|
||||
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()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#method.with) to borrow the value and do whatever you need.
|
||||
|
||||
```rust
|
||||
struct MySuperExpensiveStruct {
|
||||
a: String,
|
||||
b: StructThatsSuperExpensiveToClone
|
||||
}
|
||||
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: &MySuperExpensiveStruct| value.a.to_lowercase());
|
||||
```
|
||||
|
||||
#### `WriteSignal<T>`
|
||||
|
||||
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
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
set_value(1);
|
||||
assert_eq!(value(), 1);
|
||||
|
||||
// sometimes you want to mutate something in place, like a Vec. Just call update()
|
||||
let (items, set_items) = create_signal(cx, vec![0]);
|
||||
set_items.update(|items: &mut Vec<i32>| items.push(1));
|
||||
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)
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Getting Started](./getting_started.md)
|
||||
- [Templating: Building User Interfaces](./building_ui.md)
|
||||
- [Reactivity: Making Things Interactive](./reactivity.md)
|
||||
- [Introduction](./01_introduction.md)
|
||||
- [Getting Started](./02_getting_started.md)
|
||||
- [Templating: Building User Interfaces](./03_building_ui.md)
|
||||
- [Reactivity: Making Things Interactive](./04_reactivity.md)
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# Templating: Building User Interfaces
|
||||
|
||||
## Views
|
||||
|
||||
Leptos uses a simple `view` macro to create the user interface. If you’re familiar with JSX, then
|
||||
|
||||
## Components
|
||||
|
||||
**Components** are the basic building blocks of your application. Each component is simply a function that creates DOM nodes and sets up the reactive system that will update them. The component function runs exactly once per instance of the component.
|
||||
|
||||
The `component` macro annotates a function as a component, allowing you to use it within other components.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
fn Button(cx: Scope, text: &'static str) -> Element {
|
||||
view! { cx,
|
||||
<button>{text}</button>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn BoringButtons(cx: Scope) -> Element {
|
||||
view! { cx,
|
||||
<div>
|
||||
<Button text="These"/>
|
||||
<Button text="Do"/>
|
||||
<Button text="Nothing"/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Views
|
||||
|
||||
Leptos uses a simple `view` macro to create the user interface. It’s much like HTML, with the following differences:
|
||||
|
||||
1. Text within elements follows the rules of normal Rust strings (i.e., quotation marks or other string syntax)
|
||||
|
||||
```rust
|
||||
view! { cx, <p>"Hello, world!"</p> }
|
||||
```
|
||||
|
||||
2. Values can be inserted between curly braces. Reactive values
|
||||
|
||||
```rust
|
||||
view! { cx, <p id={non_reactive_variable}>{move || value()}</p> }
|
||||
```
|
||||
@@ -1,62 +0,0 @@
|
||||
# Reactivity
|
||||
|
||||
## 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.
|
||||
|
||||
Creating a signal is very simple. You call `create_signal`, passing in the reactive scope and the default value, and receive a tuple containing a `ReadSignal` and a `WriteSignal`.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
```
|
||||
|
||||
> If you’ve used signals in Sycamore or Solid, observables in MobX or Knockout, or a similar primitive in reactive library, you probably have a pretty good idea of how signals work in Leptos. If you’re familiar with React, Yew, or Dioxus, you may recognize a similar pattern to their `use_state` hooks.
|
||||
|
||||
### `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.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// calling value() with return the current value of the signal,
|
||||
// and automatically track changes if you're in a reactive context
|
||||
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.
|
||||
|
||||
```rust
|
||||
struct MySuperExpensiveStruct {
|
||||
a: String,
|
||||
b: StructThatsSuperExpensiveToClone
|
||||
}
|
||||
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);
|
||||
```
|
||||
|
||||
### `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.
|
||||
|
||||
```rust
|
||||
// often you just want to replace the value
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
set_value(1);
|
||||
assert_eq!(value(), 1);
|
||||
|
||||
// sometimes you want to mutate something in place, like a Vec. Just call update()
|
||||
let (items, set_items) = create_signal(cx, vec![0]);
|
||||
set_items.update(|items: &mut Vec<i32>| items.push(1));
|
||||
assert_eq!(items(), vec![1]);
|
||||
```
|
||||
|
||||
> Under the hood, `set_value(1)` is just syntactic sugar for `set_value.update(|n| *n = 1)`.
|
||||
44
examples/counter-isomorphic/Cargo.toml
Normal file
44
examples/counter-isomorphic/Cargo.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
[package]
|
||||
name = "leptos-counter-isomorphic"
|
||||
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"] }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
lazy_static = "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" }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
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", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
21
examples/counter-isomorphic/LICENSE
Normal file
21
examples/counter-isomorphic/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.
|
||||
21
examples/counter-isomorphic/README.md
Normal file
21
examples/counter-isomorphic/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Leptos Counter Isomorphic Example
|
||||
|
||||
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
|
||||
|
||||
```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.
|
||||
|
||||
```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!
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "counter-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2"
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["hydrate", "serde"] }
|
||||
counter-isomorphic = { path = "../counter", default-features = false, features = ["hydrate"] }
|
||||
log = "0.4"
|
||||
wasm-bindgen = "0.2"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
wasm-pack build --target=web --release
|
||||
@@ -1,14 +0,0 @@
|
||||
use counter_isomorphic::*;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
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, <Counters/> }
|
||||
});
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "counter-isomorphic"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["serde"] }
|
||||
leptos_router = { path = "../../../router", default-features = false }
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
futures = "0.3"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
lazy_static = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_router/hydrate"]
|
||||
ssr = ["leptos/ssr", "leptos_router/ssr"]
|
||||
@@ -1,8 +0,0 @@
|
||||
pub use counter_isomorphic::*;
|
||||
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, <Counter/> });
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "counter-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-files = "0.6"
|
||||
actix-web = { version = "4" }
|
||||
futures = "0.3"
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["ssr", "serde"] }
|
||||
leptos_router = { path = "../../../router", default-features = false, features = ["ssr"] }
|
||||
counter-isomorphic = { path = "../counter", default-features = false, features = ["ssr"] }
|
||||
lazy_static = "1"
|
||||
@@ -1,107 +0,0 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use counter_isomorphic::*;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[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/counter_client.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("{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."))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use futures::StreamExt;
|
||||
|
||||
let stream =
|
||||
futures::stream::once(async { counter_isomorphic::get_server_count().await.unwrap_or(0) })
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
counter_isomorphic::register_server_functions();
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "../client/pkg"))
|
||||
.service(counter_events)
|
||||
.service(handle_server_fns)
|
||||
.service(render)
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8081))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use broadcaster::BroadcastChannel;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
GetServerCount::register();
|
||||
@@ -25,13 +21,13 @@ static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
|
||||
#[server(GetServerCount)]
|
||||
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
|
||||
#[server(GetServerCount, "/api")]
|
||||
pub async fn get_server_count() -> Result<i32, ServerFnError> {
|
||||
Ok(COUNT.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[server(AdjustServerCount)]
|
||||
#[server(AdjustServerCount, "/api")]
|
||||
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
|
||||
let new = COUNT.load(Ordering::Relaxed) + delta;
|
||||
COUNT.store(new, Ordering::Relaxed);
|
||||
@@ -40,13 +36,12 @@ pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerF
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
#[server(ClearServerCount)]
|
||||
#[server(ClearServerCount, "/api")]
|
||||
pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
COUNT.store(0, Ordering::Relaxed);
|
||||
_ = COUNT_CHANNEL.send(&0).await;
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> Element {
|
||||
view! {
|
||||
@@ -175,13 +170,13 @@ pub fn FormCounter(cx: Scope) -> Element {
|
||||
// by including them as input values with the same name
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="-1"/>
|
||||
<input type="hidden" name="msg" value="\"form value down\""/>
|
||||
<input type="hidden" name="msg" value="form value down"/>
|
||||
<input type="submit" value="-1"/>
|
||||
</ActionForm>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<ActionForm action=adjust2>
|
||||
<input type="hidden" name="delta" value="1"/>
|
||||
<input type="hidden" name="msg" value="\"form value up\""/>
|
||||
<input type="hidden" name="msg" value="form value up"/>
|
||||
<input type="submit" value="+1"/>
|
||||
</ActionForm>
|
||||
</div>
|
||||
22
examples/counter-isomorphic/src/lib.rs
Normal file
22
examples/counter-isomorphic/src/lib.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod counters;
|
||||
|
||||
// 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::counters::*;
|
||||
|
||||
#[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, <Counters/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
62
examples/counter-isomorphic/src/main.rs
Normal file
62
examples/counter-isomorphic/src/main.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod counters;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
use std::{net::SocketAddr, env};
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
use futures::StreamExt;
|
||||
|
||||
let stream =
|
||||
futures::stream::once(async { crate::counters::get_server_count().await.unwrap_or(0) })
|
||||
.chain(COUNT_CHANNEL.clone())
|
||||
.map(|value| {
|
||||
Ok(web::Bytes::from(format!(
|
||||
"event: message\ndata: {value}\n\n"
|
||||
))) as Result<web::Bytes>
|
||||
});
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("Content-Type", "text/event-stream"))
|
||||
.streaming(stream)
|
||||
}
|
||||
|
||||
#[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(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)
|
||||
.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(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
else {
|
||||
use leptos_counter_isomorphic::counters::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| view! { cx, <Counter/> });
|
||||
}
|
||||
}
|
||||
}
|
||||
7
examples/counter/README.md
Normal file
7
examples/counter/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Leptos Counter Example
|
||||
|
||||
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 {
|
||||
/// 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, 0);
|
||||
|
||||
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.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=3 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>
|
||||
@@ -1,19 +1,13 @@
|
||||
// This is essentially a port of a Solid Hacker News demo
|
||||
// https://github.com/solidjs/solid-hackernews
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
mod api;
|
||||
mod nav;
|
||||
mod stories;
|
||||
mod story;
|
||||
mod users;
|
||||
use nav::*;
|
||||
use stories::*;
|
||||
use story::*;
|
||||
use users::*;
|
||||
mod routes;
|
||||
use routes::nav::*;
|
||||
use routes::stories::*;
|
||||
use routes::story::*;
|
||||
use routes::users::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> Element {
|
||||
@@ -22,7 +16,7 @@ pub fn App(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Stylesheet href="/static/style.css".into()/>
|
||||
<Stylesheet href="/static/style.css"/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<main>
|
||||
@@ -36,3 +30,19 @@ pub fn App(cx: Scope) -> Element {
|
||||
</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;
|
||||
45
examples/hackernews/Cargo.toml
Normal file
45
examples/hackernews/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "leptos-hackernews"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
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"] }
|
||||
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"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
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", "leptos_actix"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
21
examples/hackernews/LICENSE
Normal file
21
examples/hackernews/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/README.md
Normal file
29
examples/hackernews/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Leptos Hacker News Example
|
||||
|
||||
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 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!
|
||||
@@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "hackernews-app"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
console_log = "0.2"
|
||||
leptos = { path = "../../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { path = "../../../meta", default-features = false }
|
||||
leptos_router = { path = "../../../router", default-features = false }
|
||||
log = "0.4"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr"]
|
||||
@@ -1,11 +0,0 @@
|
||||
use hackernews_app::*;
|
||||
use leptos::*;
|
||||
|
||||
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/> }
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "hackernews-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
hackernews-app = { path = "../hackernews-app", default-features = false, features = ["hydrate"] }
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["hydrate", "serde"] }
|
||||
log = "0.4"
|
||||
@@ -1 +0,0 @@
|
||||
wasm-pack build --target=web --release
|
||||
@@ -1,12 +0,0 @@
|
||||
use hackernews_app::*;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::hydrate(body().unwrap(), move |cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
key.pem
|
||||
cert.pem
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "hackernews-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-files = "0.6"
|
||||
actix-web = { version = "4", features = ["openssl", "macros"] }
|
||||
futures = "0.3"
|
||||
leptos = { path = "../../../leptos", default-features = false, features = ["ssr", "serde"] }
|
||||
leptos_router = { path = "../../../router", default-features = false, features = ["ssr"] }
|
||||
leptos_meta = { path = "../../../meta", default-features = false, features = ["ssr"] }
|
||||
log = "0.4"
|
||||
hackernews-app = { path = "../hackernews-app", default-features = false, features = ["ssr"] }
|
||||
openssl = { version = "0.10", features = ["v110"] }
|
||||
simple_logger = "2"
|
||||
serde_json = "1.0.85"
|
||||
@@ -1,111 +0,0 @@
|
||||
use actix_files::{Files, NamedFile};
|
||||
use actix_web::*;
|
||||
use futures::StreamExt;
|
||||
use hackernews_app::*;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
|
||||
|
||||
#[get("/static/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
NamedFile::open_async("../hackernews-app/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 accepts_type = req.headers().get("Accept").map(|h| h.to_str());
|
||||
match accepts_type {
|
||||
// if asks for JSON, send the loader function JSON or 404
|
||||
Some(Ok("application/json")) => {
|
||||
let json = loader_to_json(app).await;
|
||||
|
||||
let res = if let Some(json) = json {
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(json)
|
||||
} else {
|
||||
HttpResponse::NotFound().body(())
|
||||
};
|
||||
|
||||
res
|
||||
}
|
||||
// otherwise, send HTML
|
||||
_ => {
|
||||
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/hackernews_client.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_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}");
|
||||
|
||||
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(|| {
|
||||
App::new()
|
||||
.service(css)
|
||||
.service(
|
||||
web::scope("/pkg")
|
||||
.service(Files::new("", "../hackernews-client/pkg"))
|
||||
.wrap(middleware::Compress::default()),
|
||||
)
|
||||
.service(render_app)
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
// replace .bind with .bind_openssl to use HTTPS
|
||||
//.bind_openssl(&format!("{}:{}", host, port), builder)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
90
examples/hackernews/src/api.rs
Normal file
90
examples/hackernews/src/api.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use leptos::{on_cleanup, Scope, 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>(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}"))
|
||||
.ok()?
|
||||
.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>(cx: Scope, 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>,
|
||||
}
|
||||
49
examples/hackernews/src/lib.rs
Normal file
49
examples/hackernews/src/lib.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
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="/style.css"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<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/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
41
examples/hackernews/src/main.rs
Normal file
41
examples/hackernews/src/main.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use leptos_hackernews::*;
|
||||
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 addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
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)
|
||||
.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(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
}
|
||||
}
|
||||
}
|
||||
4
examples/hackernews/src/routes.rs
Normal file
4
examples/hackernews/src/routes.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod nav;
|
||||
pub mod stories;
|
||||
pub mod story;
|
||||
pub mod users;
|
||||
30
examples/hackernews/src/routes/nav.rs
Normal file
30
examples/hackernews/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="/" class="home".to_string()>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
159
examples/hackernews/src/routes/stories.rs
Normal file
159
examples/hackernews/src/routes/stories.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
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>>(cx, &api::story(&path)).await
|
||||
},
|
||||
);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move || pending() || 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>
|
||||
<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> }),
|
||||
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>
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
112
examples/hackernews/src/routes/story.rs
Normal file
112
examples/hackernews/src/routes/story.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
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 {
|
||||
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,
|
||||
<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"
|
||||
}
|
||||
}
|
||||
45
examples/hackernews/src/routes/users.rs
Normal file
45
examples/hackernews/src/routes/users.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
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 {
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
api::fetch_api::<User>(cx, &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/style.css
Normal file
326
examples/hackernews/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
|
||||
}
|
||||
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,11 +7,11 @@ edition = "2021"
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router" }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
|
||||
11
examples/router/README.md
Normal file
11
examples/router/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Leptos Router Example
|
||||
|
||||
This example demonstrates how Leptos’s router for client side routing.
|
||||
|
||||
## Build and Run it
|
||||
|
||||
```bash
|
||||
trunk serve --open
|
||||
```
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -2,11 +2,13 @@ mod api;
|
||||
|
||||
use api::{Contact, ContactSummary};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
use crate::api::{get_contact, get_contacts};
|
||||
|
||||
pub fn router_example(cx: Scope) -> Element {
|
||||
provide_context(cx, MetaContext::default());
|
||||
view! { cx,
|
||||
<div id="root">
|
||||
<Router>
|
||||
@@ -47,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>
|
||||
}
|
||||
@@ -77,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,
|
||||
@@ -120,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>
|
||||
}
|
||||
}
|
||||
|
||||
10
examples/tailwind/.gitignore
vendored
Normal file
10
examples/tailwind/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
92
examples/tailwind/Cargo.toml
Normal file
92
examples/tailwind/Cargo.toml
Normal file
@@ -0,0 +1,92 @@
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
[dependencies]
|
||||
leptos = { git = "https://github.com/gbj/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_meta = { git = "https://github.com/gbj/leptos", default-features = false }
|
||||
leptos_router = { git = "https://github.com/gbj/leptos", default-features = false }
|
||||
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
log = "0.4"
|
||||
cfg-if = "1.0"
|
||||
|
||||
# dependecies for client (enable when csr or hydrate set)
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
console_log = { version = "0.2", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# dependecies for server (enable when ssr set)
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", features = ["macros"], optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
simple_logger = { version = "4.0", optional = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[features]
|
||||
leptos_autoreload = []
|
||||
default = ["csr"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"leptos_meta/hydrate",
|
||||
"leptos_router/hydrate",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:console_log",
|
||||
"dep:console_error_panic_hook",
|
||||
]
|
||||
csr = [
|
||||
"leptos/csr",
|
||||
"leptos_meta/csr",
|
||||
"leptos_router/csr",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:console_log",
|
||||
"dep:console_error_panic_hook",
|
||||
]
|
||||
ssr = [
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:reqwest",
|
||||
"dep:actix-web",
|
||||
"dep:actix-files",
|
||||
"dep:futures",
|
||||
"dep:simple_logger",
|
||||
"dep:serde_json",
|
||||
]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# Path, relative to root, to generat rust code to
|
||||
gen_file = "src/server/generated.rs"
|
||||
# Path to the source index.html file
|
||||
index_file = "index.html"
|
||||
# [Optional] Files in the asset_dir will be copied to the target/site directory
|
||||
assets_dir = "assets"
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end_test_cmd = "npx playwright test"
|
||||
# On which port to serve the client side rendered site (when using --csr option)
|
||||
csr_port = 3000
|
||||
# The port to use for automatic reload monitoring
|
||||
reload_port = 3001
|
||||
|
||||
[package.metadata.leptos.style]
|
||||
# This points to the TailwindCSS output file
|
||||
file = "style/output.css"
|
||||
# A https://browsersl.ist query
|
||||
browserquery = "defaults"
|
||||
21
examples/tailwind/LICENSE
Normal file
21
examples/tailwind/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 henrik
|
||||
|
||||
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.
|
||||
74
examples/tailwind/README.md
Normal file
74
examples/tailwind/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Leptos Starter Template
|
||||
|
||||
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/gbj/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
|
||||
|
||||
If you don't have `cargo-leptos` installed you can install it with
|
||||
|
||||
`cargo install --locked cargo-leptos`
|
||||
|
||||
Then run
|
||||
|
||||
`npx tailwindcss -i ./input.css -o ./style/output.scss --watch`
|
||||
|
||||
and
|
||||
|
||||
`cargo leptos watch`
|
||||
|
||||
in this directory.
|
||||
|
||||
You can begin editing your app at `src/app/mod.rs`.
|
||||
|
||||
## Installing Tailwind
|
||||
|
||||
You can install Tailwind using `npm`:
|
||||
|
||||
```bash
|
||||
npm install -D tailwindcss
|
||||
```
|
||||
|
||||
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
|
||||
|
||||
## Setting up with VS Code and Additional Tools
|
||||
|
||||
If you're using VS Code, add the following to your `settings.json`
|
||||
|
||||
```json
|
||||
"emmet.includeLanguages": {
|
||||
"rust": "html",
|
||||
"*.rs": "html"
|
||||
},
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"rust": "html",
|
||||
"*.rs": "html"
|
||||
},
|
||||
"files.associations": {
|
||||
"*.rs": "rust"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"other": "on",
|
||||
"comments": "on",
|
||||
"strings": true
|
||||
},
|
||||
"css.validate": false,
|
||||
```
|
||||
|
||||
|
||||
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
|
||||
|
||||
Install "VS Browser" extension, a browser at the right window.
|
||||
Allow vscode Ports forward: 3000, 3001.
|
||||
|
||||
|
||||
## Notes about Tooling
|
||||
|
||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||
|
||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
|
||||
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||
5. `npm install -g sass` - install `dart-sass` (should be optional in future
|
||||
|
||||
## Attribution
|
||||
|
||||
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/gbj/leptos/discussions/125).
|
||||
BIN
examples/tailwind/assets/favicon.ico
Normal file
BIN
examples/tailwind/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
74
examples/tailwind/end2end/package-lock.json
generated
Normal file
74
examples/tailwind/end2end/package-lock.json
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "end2end",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
|
||||
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
|
||||
"dev": true
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.28.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
|
||||
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user