Compare commits
1120 Commits
explicit-s
...
each-leak
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
702ff3d691 | ||
|
|
7f88a660dd | ||
|
|
ddd66270d3 | ||
|
|
66d4870c21 | ||
|
|
765006158a | ||
|
|
8a1adaefaf | ||
|
|
086326324e | ||
|
|
e59ee6329e | ||
|
|
a2b31a51d9 | ||
|
|
b0a98d8b4f | ||
|
|
6931d3904b | ||
|
|
e380097a9e | ||
|
|
44c18da324 | ||
|
|
256cf0c59b | ||
|
|
0765e51db8 | ||
|
|
45d4ebccd8 | ||
|
|
352601aa42 | ||
|
|
7f77910e91 | ||
|
|
76aeb573bf | ||
|
|
e0bf8f5b6d | ||
|
|
5ace580edb | ||
|
|
5d612d9740 | ||
|
|
eacff684ef | ||
|
|
4034aa9c11 | ||
|
|
45275ff8d4 | ||
|
|
3ff5089bf4 | ||
|
|
c28297fe93 | ||
|
|
6d0d70cd17 | ||
|
|
c4e693e01e | ||
|
|
2be4e8d959 | ||
|
|
fec4ff4381 | ||
|
|
25c313aeb5 | ||
|
|
0dbcc323ba | ||
|
|
6b683f9ab6 | ||
|
|
aae4d4445e | ||
|
|
bb9df8937d | ||
|
|
05277f03b6 | ||
|
|
f698f8badd | ||
|
|
98f51fec8a | ||
|
|
65465cad78 | ||
|
|
ddee545e7e | ||
|
|
cbfb724af2 | ||
|
|
0953007f47 | ||
|
|
53f7677258 | ||
|
|
6373fd42fb | ||
|
|
e1bcf77b03 | ||
|
|
b0762bbfb5 | ||
|
|
63a7a4dec1 | ||
|
|
1f6a326268 | ||
|
|
0efc39db8b | ||
|
|
cbf2f73e95 | ||
|
|
160f336303 | ||
|
|
e2b1365e46 | ||
|
|
45eee12b18 | ||
|
|
e2cdbc746f | ||
|
|
48cf8d9382 | ||
|
|
42e50327a6 | ||
|
|
ea0e2ce363 | ||
|
|
465cbc36be | ||
|
|
62061f90ea | ||
|
|
9a231ddef0 | ||
|
|
ce6a093f9f | ||
|
|
f07fa0e0be | ||
|
|
43ad91512a | ||
|
|
116d23f2c3 | ||
|
|
2ecb345a79 | ||
|
|
f55f833426 | ||
|
|
7101a2f55e | ||
|
|
f8b76387ec | ||
|
|
11fc51577b | ||
|
|
ae1ca969ef | ||
|
|
895f9d8487 | ||
|
|
1e45b182a0 | ||
|
|
4c26dc597d | ||
|
|
2863d49a1c | ||
|
|
087eb18c8b | ||
|
|
c7c672717c | ||
|
|
c69cc02f30 | ||
|
|
9eb81f00f9 | ||
|
|
72fe3d45f0 | ||
|
|
7802d941bd | ||
|
|
f10784f686 | ||
|
|
35197691c0 | ||
|
|
4afbef87f6 | ||
|
|
218485e3be | ||
|
|
8d60a191eb | ||
|
|
1ba01a46af | ||
|
|
fdece25051 | ||
|
|
590056e047 | ||
|
|
817bb1628e | ||
|
|
f911cdd56f | ||
|
|
76a9c719a3 | ||
|
|
395336a8c0 | ||
|
|
b84906e6dc | ||
|
|
1563d237d0 | ||
|
|
b861f84e40 | ||
|
|
62812af5b2 | ||
|
|
f300e7fd41 | ||
|
|
44974fcf69 | ||
|
|
815c2e6dc2 | ||
|
|
2fc20d8312 | ||
|
|
679692e202 | ||
|
|
be1343fa88 | ||
|
|
fc7199f188 | ||
|
|
154e42f3f4 | ||
|
|
4c24795ffd | ||
|
|
f2e7b00d5a | ||
|
|
0b36b68846 | ||
|
|
f24bad4bf2 | ||
|
|
a2ea1d8483 | ||
|
|
9b0fb63632 | ||
|
|
2febaf6b99 | ||
|
|
6c8e8e9ce7 | ||
|
|
7aa0181192 | ||
|
|
8496bd59ce | ||
|
|
fd6e63796e | ||
|
|
39cddfc82d | ||
|
|
d1333a3402 | ||
|
|
7f9919e2d5 | ||
|
|
fc2d6ef19d | ||
|
|
a5531b1a7c | ||
|
|
81ab77e8ea | ||
|
|
23bd399239 | ||
|
|
3e04318082 | ||
|
|
2d88524354 | ||
|
|
ecb784e422 | ||
|
|
69e02bfce2 | ||
|
|
a75abb9e04 | ||
|
|
bf1ef1b7c2 | ||
|
|
7fb7bb90f8 | ||
|
|
a22a693de7 | ||
|
|
cbb1e4c9d2 | ||
|
|
dbccf525ac | ||
|
|
ed6d6ae4b0 | ||
|
|
89ee88d75e | ||
|
|
9ea604f516 | ||
|
|
b5ab7b107a | ||
|
|
18eecd9606 | ||
|
|
a49dfd3f8e | ||
|
|
7075f58451 | ||
|
|
bcabdddce5 | ||
|
|
726393c446 | ||
|
|
c336eb8769 | ||
|
|
0f5f0de410 | ||
|
|
3d769c9f21 | ||
|
|
9ac0f0a579 | ||
|
|
a385f502b6 | ||
|
|
603eead12d | ||
|
|
8a73f3b879 | ||
|
|
5fc8907b85 | ||
|
|
678990194f | ||
|
|
a964e89d1a | ||
|
|
285092a467 | ||
|
|
c1c74ead0f | ||
|
|
e4c9109278 | ||
|
|
64add54de6 | ||
|
|
ac343427e7 | ||
|
|
452e397048 | ||
|
|
ad3ac5ad3c | ||
|
|
db63eda2f5 | ||
|
|
6cbdc57f7a | ||
|
|
3215e44c9a | ||
|
|
b54a60213b | ||
|
|
ebeb1d69d1 | ||
|
|
cadb04b076 | ||
|
|
490b7a1596 | ||
|
|
f4d781e739 | ||
|
|
ebe5bf4600 | ||
|
|
d62046dc6f | ||
|
|
c7abb57168 | ||
|
|
bc0cd5d0ba | ||
|
|
7df67444f9 | ||
|
|
40155e91ea | ||
|
|
5c062fa6f1 | ||
|
|
3517820afd | ||
|
|
300cc4f54c | ||
|
|
586e9be99a | ||
|
|
6ed86d0ee9 | ||
|
|
1fe93fd588 | ||
|
|
2723871a80 | ||
|
|
70d92c7f42 | ||
|
|
e96d4b0687 | ||
|
|
ce0910caca | ||
|
|
81a937277d | ||
|
|
355e711964 | ||
|
|
27ec506fd5 | ||
|
|
79c76ae4cb | ||
|
|
e416815591 | ||
|
|
81bdd6788f | ||
|
|
f7d5567a35 | ||
|
|
ae0a243cc0 | ||
|
|
7893ff8b55 | ||
|
|
6130e708ce | ||
|
|
d049d2f36b | ||
|
|
61ca6465df | ||
|
|
10a833d763 | ||
|
|
17982e8ac5 | ||
|
|
6cfb6227f5 | ||
|
|
159852b8d7 | ||
|
|
6f95713b59 | ||
|
|
e17afd4559 | ||
|
|
e0aa1e245b | ||
|
|
7951a6e9cf | ||
|
|
1f39299303 | ||
|
|
2be4610233 | ||
|
|
af254e9b61 | ||
|
|
4aae8a5088 | ||
|
|
7ff044cef6 | ||
|
|
11122b575e | ||
|
|
a62ee4031b | ||
|
|
19b43607a1 | ||
|
|
884297706a | ||
|
|
fb4c208609 | ||
|
|
5701e74efb | ||
|
|
2afe8e202a | ||
|
|
08ec473304 | ||
|
|
3ae0880db4 | ||
|
|
9180aaad7e | ||
|
|
89be6bc68e | ||
|
|
56426170b0 | ||
|
|
0571ea4103 | ||
|
|
cdf709fb09 | ||
|
|
7eaa36812d | ||
|
|
2ef36c65fd | ||
|
|
a16540ccc5 | ||
|
|
613c7b32a1 | ||
|
|
8f2a731c9f | ||
|
|
1621b86d8f | ||
|
|
b1ac17995d | ||
|
|
6471af8b89 | ||
|
|
abf54b832e | ||
|
|
91e839c71a | ||
|
|
b944b17e6d | ||
|
|
54c1abb4b7 | ||
|
|
c24f33aeb2 | ||
|
|
9e83acfe63 | ||
|
|
46254a18f3 | ||
|
|
7d7a96d9bc | ||
|
|
339c920b19 | ||
|
|
ecc24fa65d | ||
|
|
3f036ee321 | ||
|
|
4e00ec2348 | ||
|
|
9c59b720b7 | ||
|
|
da4340894f | ||
|
|
02f5c3891c | ||
|
|
087e67466f | ||
|
|
1925c5bbe5 | ||
|
|
1613616008 | ||
|
|
8a01880ade | ||
|
|
180ab87ff9 | ||
|
|
e324fb6e76 | ||
|
|
0547b4f846 | ||
|
|
75659ce674 | ||
|
|
190cb162ad | ||
|
|
1f556cefb0 | ||
|
|
6a68ef67f3 | ||
|
|
23bbd90c81 | ||
|
|
27b8553076 | ||
|
|
5bfeb93e3d | ||
|
|
dd9ae1b7b1 | ||
|
|
ad34a5d9c6 | ||
|
|
ace5e7cbba | ||
|
|
c8f0988e53 | ||
|
|
64f0f8879b | ||
|
|
b8cafeb650 | ||
|
|
992b218ffe | ||
|
|
5df89b0d25 | ||
|
|
c050456a47 | ||
|
|
8a8d7cbe1b | ||
|
|
f5f345e623 | ||
|
|
4df3687463 | ||
|
|
f6622448e9 | ||
|
|
78825401c5 | ||
|
|
f2b7ad6244 | ||
|
|
43f107d9bd | ||
|
|
a2612ca1fc | ||
|
|
a000c84e1a | ||
|
|
ee647cba1c | ||
|
|
1377b823e2 | ||
|
|
4d21f5ac63 | ||
|
|
1ec603ee58 | ||
|
|
c56806713e | ||
|
|
2f6aa6753d | ||
|
|
2544687acd | ||
|
|
3d25e86c23 | ||
|
|
28ec3a6cda | ||
|
|
8b92a561a3 | ||
|
|
e490c0423f | ||
|
|
b6579a040a | ||
|
|
01e024b726 | ||
|
|
6603c44ce2 | ||
|
|
977f11b180 | ||
|
|
fb34b29ccf | ||
|
|
6576d8eda1 | ||
|
|
6b729f9131 | ||
|
|
dc60c35b58 | ||
|
|
32ec9cc57e | ||
|
|
35601d8284 | ||
|
|
49bc7d2a27 | ||
|
|
aa7c7367dc | ||
|
|
70808c5262 | ||
|
|
67503a108d | ||
|
|
ef52a01838 | ||
|
|
4cacfe98d8 | ||
|
|
52e653316e | ||
|
|
bf5b6ca9c2 | ||
|
|
8875939a27 | ||
|
|
b9a83277d9 | ||
|
|
dad84b5867 | ||
|
|
1f29d29947 | ||
|
|
1b8175e2fa | ||
|
|
dbe3ec015c | ||
|
|
492fa6c6d3 | ||
|
|
343e8c8abe | ||
|
|
656d20cb65 | ||
|
|
a0a66b75dd | ||
|
|
085ba3506c | ||
|
|
63f3780eda | ||
|
|
c41cf879d1 | ||
|
|
b34f2070d3 | ||
|
|
7fa21defa6 | ||
|
|
bdd9abc04d | ||
|
|
1d25134213 | ||
|
|
de73622949 | ||
|
|
5d3cfc6483 | ||
|
|
6fbbd09000 | ||
|
|
f2842cf14e | ||
|
|
a6b6864bc5 | ||
|
|
063b946cd4 | ||
|
|
5a2c9ea345 | ||
|
|
808d87598b | ||
|
|
0956c48b1e | ||
|
|
8915e2615b | ||
|
|
7f47134058 | ||
|
|
af7b93fa1e | ||
|
|
ed940f577a | ||
|
|
916f30a07b | ||
|
|
e01c565de1 | ||
|
|
dffe195cdc | ||
|
|
a5e2587555 | ||
|
|
af8889fab2 | ||
|
|
267c1cfc34 | ||
|
|
3498378e60 | ||
|
|
f8c680d14d | ||
|
|
b852e459a9 | ||
|
|
681f10ec8d | ||
|
|
1d480791a1 | ||
|
|
7acc309f66 | ||
|
|
9527de15ed | ||
|
|
aeb25a715a | ||
|
|
46e91a538c | ||
|
|
1fe526c99c | ||
|
|
6b05918807 | ||
|
|
05d2eb8ce0 | ||
|
|
e12c2d9769 | ||
|
|
825245b65f | ||
|
|
ef067f18e1 | ||
|
|
844dc21efd | ||
|
|
1a00e99a24 | ||
|
|
6c5bcf30ba | ||
|
|
be8ffe935d | ||
|
|
0b80bba4ec | ||
|
|
9cc38988d8 | ||
|
|
0d92a5dec8 | ||
|
|
677e4f2540 | ||
|
|
0029e1d8f7 | ||
|
|
635aa5c681 | ||
|
|
f5c4c9448c | ||
|
|
63b1837315 | ||
|
|
bc43a9d329 | ||
|
|
1850c28d3a | ||
|
|
319a058e63 | ||
|
|
678e49268f | ||
|
|
6df4a6f120 | ||
|
|
73c6bbb225 | ||
|
|
fa57085946 | ||
|
|
aef589cd24 | ||
|
|
05ffd8c989 | ||
|
|
1125a5f7cb | ||
|
|
dfba1d9656 | ||
|
|
96418ed684 | ||
|
|
b010233bb4 | ||
|
|
7e28f56f01 | ||
|
|
dd35c31db1 | ||
|
|
c9ac4ed2b5 | ||
|
|
7631ce3b09 | ||
|
|
a5988c59ee | ||
|
|
9ba807f79b | ||
|
|
9d8627b337 | ||
|
|
64bf01c59e | ||
|
|
ed023c8970 | ||
|
|
13bdef22bd | ||
|
|
6f49a6c12a | ||
|
|
7db292779b | ||
|
|
e2496e01d0 | ||
|
|
d5bda04306 | ||
|
|
9165242744 | ||
|
|
927fe0949f | ||
|
|
459216a30e | ||
|
|
c7fa041469 | ||
|
|
cab7360bef | ||
|
|
159ec4a7bd | ||
|
|
ae40f3134a | ||
|
|
3c080e0564 | ||
|
|
e8c1bf5055 | ||
|
|
2a4a5f75c9 | ||
|
|
91b65654d6 | ||
|
|
dcca6e4e17 | ||
|
|
4550545e4f | ||
|
|
af1a4492e8 | ||
|
|
6b1b4463a0 | ||
|
|
632267c13a | ||
|
|
a349707e1f | ||
|
|
84fa6cd3a8 | ||
|
|
05468d3307 | ||
|
|
0da88f39cd | ||
|
|
5dffb0a803 | ||
|
|
e2a5c2d78f | ||
|
|
ca679ec496 | ||
|
|
10282857fe | ||
|
|
263d5b1d89 | ||
|
|
6a4cbbf266 | ||
|
|
8d14972808 | ||
|
|
441eb1697e | ||
|
|
64e6eedb4d | ||
|
|
78d965cc91 | ||
|
|
28dce925b0 | ||
|
|
a2943c4649 | ||
|
|
d4b5b958f3 | ||
|
|
9537cafe25 | ||
|
|
95dd252c14 | ||
|
|
755ceb7d75 | ||
|
|
d5b74dacc8 | ||
|
|
411fc51ea2 | ||
|
|
e714cac0ec | ||
|
|
ab0b687943 | ||
|
|
7f21ee97a8 | ||
|
|
0ed56d382d | ||
|
|
a47cac6e3c | ||
|
|
edbd3612b3 | ||
|
|
e2517c99b8 | ||
|
|
2b01bf99b4 | ||
|
|
bd5bd71a21 | ||
|
|
60187961a0 | ||
|
|
1344f113c5 | ||
|
|
96bbb86346 | ||
|
|
7478315970 | ||
|
|
b894444b8d | ||
|
|
9f8bcd6fb1 | ||
|
|
d82781abbd | ||
|
|
f8c4cac6d3 | ||
|
|
4264b15aab | ||
|
|
168f9d3a45 | ||
|
|
9663555195 | ||
|
|
e92176029c | ||
|
|
fe820c48c8 | ||
|
|
87cd4b8f00 | ||
|
|
3a5e3aea99 | ||
|
|
9ba06cd604 | ||
|
|
e8eb55ca5c | ||
|
|
7946df8bfc | ||
|
|
827b787c91 | ||
|
|
795270447b | ||
|
|
01c00eee6b | ||
|
|
f45d33db73 | ||
|
|
60e2f34456 | ||
|
|
7ec82c8df3 | ||
|
|
d5f8d3a9b7 | ||
|
|
2a1b531bd2 | ||
|
|
f5c476cfd5 | ||
|
|
26b28be436 | ||
|
|
60f0bf23fd | ||
|
|
442dc1e041 | ||
|
|
c438b46eb1 | ||
|
|
2a399f05ac | ||
|
|
6e1bc42879 | ||
|
|
7ad94cc520 | ||
|
|
c3a7ef0357 | ||
|
|
8ee521787e | ||
|
|
04c85d6eb0 | ||
|
|
71d278927b | ||
|
|
cc1d15989e | ||
|
|
2c614722f4 | ||
|
|
98d151f5fb | ||
|
|
5a9a681d8a | ||
|
|
8eaa0b0c15 | ||
|
|
c3fbf13ef3 | ||
|
|
54f666c957 | ||
|
|
b318449ee7 | ||
|
|
59c291a1e5 | ||
|
|
6c7b20ce77 | ||
|
|
9a00f7f492 | ||
|
|
26e90d1959 | ||
|
|
1f1d675d17 | ||
|
|
9bde885b9d | ||
|
|
383f8a409d | ||
|
|
b98bacdcab | ||
|
|
d3d71875da | ||
|
|
e06946e5a4 | ||
|
|
c485a391ee | ||
|
|
cc48ff72ad | ||
|
|
b2cf953c07 | ||
|
|
f8af065c0e | ||
|
|
333f60cfb7 | ||
|
|
cd9fe66fbb | ||
|
|
3bb9e93c69 | ||
|
|
f9474def96 | ||
|
|
d7dba85f2d | ||
|
|
94af8f26ca | ||
|
|
19dabb6b6a | ||
|
|
15b5f7545a | ||
|
|
9399fc7b4f | ||
|
|
5d8d5d9910 | ||
|
|
e6a1255140 | ||
|
|
528a9d7a6f | ||
|
|
f28da0770f | ||
|
|
0145b01da5 | ||
|
|
725ea8a01e | ||
|
|
70f6297277 | ||
|
|
e595d35c8b | ||
|
|
c44693e0a4 | ||
|
|
b86e7f33dc | ||
|
|
a603531409 | ||
|
|
f47fad3ed5 | ||
|
|
1cb03914ab | ||
|
|
67c5eda099 | ||
|
|
63e70db736 | ||
|
|
8acbc579e0 | ||
|
|
28bb3f81aa | ||
|
|
e8424138ce | ||
|
|
4b1fce4c9c | ||
|
|
fd2a2bd5f4 | ||
|
|
f09ded454d | ||
|
|
a2f85feb57 | ||
|
|
d64ca366fc | ||
|
|
63f680f37d | ||
|
|
50ba796f49 | ||
|
|
f3b62bcf88 | ||
|
|
57c72c038c | ||
|
|
4340fbfc78 | ||
|
|
4e1753fc71 | ||
|
|
f30310a64a | ||
|
|
e3c4e9f6a4 | ||
|
|
494deef9b6 | ||
|
|
0da8d0113c | ||
|
|
dac69b9802 | ||
|
|
3c1e1e12d2 | ||
|
|
0e179d0cb5 | ||
|
|
55b27f7aec | ||
|
|
4467d060b6 | ||
|
|
4dd5768a66 | ||
|
|
a3f090c4df | ||
|
|
5729655657 | ||
|
|
f2ed521de8 | ||
|
|
f8f0d9fae0 | ||
|
|
e23c05a1df | ||
|
|
be94c1b846 | ||
|
|
b3c4c77dee | ||
|
|
8b81425b21 | ||
|
|
04e3e7a9a6 | ||
|
|
ab2d554dc3 | ||
|
|
c712cc8937 | ||
|
|
6077966cd7 | ||
|
|
3179b2a9e5 | ||
|
|
a68d276c90 | ||
|
|
bf3bba3794 | ||
|
|
17eb571ef3 | ||
|
|
ebd7080149 | ||
|
|
f1a148caf8 | ||
|
|
a5351dd33d | ||
|
|
a15b3dd882 | ||
|
|
d42b79b261 | ||
|
|
6cd136ec9b | ||
|
|
85b72f5b68 | ||
|
|
2bd0c38304 | ||
|
|
9dc30da3e9 | ||
|
|
3436cf7fbf | ||
|
|
38ef93d862 | ||
|
|
0e437cac68 | ||
|
|
98e3f5a155 | ||
|
|
a55ce8f752 | ||
|
|
469a65ad7a | ||
|
|
8a8c00455e | ||
|
|
2048e89109 | ||
|
|
5540bb8e8c | ||
|
|
86df770dad | ||
|
|
535bd69b2a | ||
|
|
1b0200390b | ||
|
|
e05778726b | ||
|
|
587a85baaf | ||
|
|
ff0d058a3e | ||
|
|
623bb7cb3f | ||
|
|
fc062e6829 | ||
|
|
479c11e3f8 | ||
|
|
bf9587c349 | ||
|
|
b3da8a5dba | ||
|
|
d3f2cae07a | ||
|
|
8c4dcbeddc | ||
|
|
a4747596fa | ||
|
|
af68da0a9a | ||
|
|
48e1d6cfab | ||
|
|
a4740d6c06 | ||
|
|
ae506fced6 | ||
|
|
86394105dd | ||
|
|
7028dd8b3d | ||
|
|
8092bf1962 | ||
|
|
2d97790ab9 | ||
|
|
ef846e7b88 | ||
|
|
d78ee8c3c9 | ||
|
|
d1ece97575 | ||
|
|
c24958bec4 | ||
|
|
8e1c165427 | ||
|
|
50c9c38b7d | ||
|
|
232776f9a6 | ||
|
|
629ac01484 | ||
|
|
f20c74fa98 | ||
|
|
2499755a9e | ||
|
|
f17f651986 | ||
|
|
c1d6ff51a6 | ||
|
|
4839bfbb29 | ||
|
|
391fe89542 | ||
|
|
f54ffab888 | ||
|
|
7ee0c01594 | ||
|
|
6ce90fa49d | ||
|
|
c96965ab64 | ||
|
|
5def2a72bc | ||
|
|
a648f084c6 | ||
|
|
590ec40e0c | ||
|
|
6354b79588 | ||
|
|
d95dc1858c | ||
|
|
558b13dc0e | ||
|
|
43bbd2f33e | ||
|
|
833eee6639 | ||
|
|
feb7961bd0 | ||
|
|
8aa05f8f3d | ||
|
|
fa87bc6f19 | ||
|
|
0f31e924a6 | ||
|
|
c89930aed0 | ||
|
|
3d160ed152 | ||
|
|
8037915294 | ||
|
|
b62568b8a4 | ||
|
|
a5d7563a67 | ||
|
|
fbacfc787c | ||
|
|
0bf52c95bb | ||
|
|
89aa02af19 | ||
|
|
21af940c61 | ||
|
|
46c939ba28 | ||
|
|
d158c34d24 | ||
|
|
42eef284e6 | ||
|
|
6cf5d0a403 | ||
|
|
79ac501302 | ||
|
|
fdf17af7ab | ||
|
|
ac16d34985 | ||
|
|
aa9f8f24b0 | ||
|
|
d2ba8f5d46 | ||
|
|
7c25cd9200 | ||
|
|
34b4917837 | ||
|
|
ac489e7523 | ||
|
|
0909f60e55 | ||
|
|
5ec76682a7 | ||
|
|
428999fd14 | ||
|
|
ce84632c39 | ||
|
|
351389c2bf | ||
|
|
532f5c5b83 | ||
|
|
0d314224c9 | ||
|
|
b4897f7a61 | ||
|
|
49e93278b5 | ||
|
|
cd59bf5a10 | ||
|
|
66ac7d2a9d | ||
|
|
ae82395100 | ||
|
|
e57b6e0ccf | ||
|
|
2f218e7428 | ||
|
|
f2e9d6f4c3 | ||
|
|
ee379bb405 | ||
|
|
d5fbeb9474 | ||
|
|
7ca131c5b8 | ||
|
|
79712ac4ef | ||
|
|
065d6b3c19 | ||
|
|
9a2035f1e1 | ||
|
|
df8e50e85a | ||
|
|
58748af63b | ||
|
|
36099c19c3 | ||
|
|
8be33ccd7c | ||
|
|
d6920847ca | ||
|
|
f2553c117b | ||
|
|
c63c8728a7 | ||
|
|
557bd25e2c | ||
|
|
5ee8b20770 | ||
|
|
da4b46e359 | ||
|
|
51b0ec3204 | ||
|
|
c103c8f05b | ||
|
|
83c8f8b0cb | ||
|
|
eccbc424a2 | ||
|
|
9d370776d2 | ||
|
|
fc8921445e | ||
|
|
c635adb426 | ||
|
|
a845bf12e6 | ||
|
|
eb573bf242 | ||
|
|
e8aaa77160 | ||
|
|
c0a407b0cd | ||
|
|
d79d4c4f86 | ||
|
|
3195ab4ffc | ||
|
|
80287f7a61 | ||
|
|
218faed341 | ||
|
|
d071a7c1e0 | ||
|
|
234e1cda4e | ||
|
|
e140549755 | ||
|
|
df41e4dbc2 | ||
|
|
9c6aaed0a8 | ||
|
|
eaf4bbb068 | ||
|
|
186e2454b0 | ||
|
|
6fa15a5584 | ||
|
|
aa71e1f66c | ||
|
|
ae16c2f96d | ||
|
|
4e8ad641ec | ||
|
|
64c29b1787 | ||
|
|
df517ea9bb | ||
|
|
2d289dd2b6 | ||
|
|
702a785ca0 | ||
|
|
abc117b9bf | ||
|
|
4823e0eb8d | ||
|
|
d65fde1ed4 | ||
|
|
6cfd2ba04e | ||
|
|
cd178c5c85 | ||
|
|
5a44f9eb4b | ||
|
|
688b0a6b73 | ||
|
|
ef84d77e12 | ||
|
|
924b632fd3 | ||
|
|
2658e158df | ||
|
|
21274c08bf | ||
|
|
b83cf4dc51 | ||
|
|
1826d4bab2 | ||
|
|
e6e71cb8e6 | ||
|
|
01252b841d | ||
|
|
c6d30a710a | ||
|
|
fefca82d16 | ||
|
|
a253d224ac | ||
|
|
04f331d444 | ||
|
|
2fb3515e62 | ||
|
|
7a8b08d149 | ||
|
|
0bc29b5f26 | ||
|
|
f9bda65dbe | ||
|
|
a591d54a22 | ||
|
|
3e508b16f3 | ||
|
|
5603fac109 | ||
|
|
8adf9108c5 | ||
|
|
610e38a967 | ||
|
|
34697b74a0 | ||
|
|
0a8b516182 | ||
|
|
5405dcd09d | ||
|
|
1aaacbaf5b | ||
|
|
ce9aec2520 | ||
|
|
422233eecf | ||
|
|
3b99d2d4fd | ||
|
|
9eadac9f2c | ||
|
|
416e1a617b | ||
|
|
1b0aa4d903 | ||
|
|
01013b00e5 | ||
|
|
3ef64bd372 | ||
|
|
c463579faa | ||
|
|
67de5685bb | ||
|
|
8a85d4261a | ||
|
|
218c4d3c90 | ||
|
|
90849cc6e3 | ||
|
|
5f95776a08 | ||
|
|
adee33a08e | ||
|
|
e3e0460371 | ||
|
|
6e4448dae6 | ||
|
|
fee0f5490b | ||
|
|
e1836af6bd | ||
|
|
b37a36a003 | ||
|
|
e865f609ee | ||
|
|
621122adf7 | ||
|
|
f6acecd3ad | ||
|
|
f17c7fdb90 | ||
|
|
21178b1682 | ||
|
|
1c8b640855 | ||
|
|
9f97497e48 | ||
|
|
70f2b3b4d3 | ||
|
|
181a15cf66 | ||
|
|
a7a35857bb | ||
|
|
caa919b257 | ||
|
|
4c123884de | ||
|
|
5e06eb1a99 | ||
|
|
cc0bf20c9d | ||
|
|
5aab8e40c2 | ||
|
|
eeaccfc815 | ||
|
|
cac1187346 | ||
|
|
4134d2f924 | ||
|
|
621976c92c | ||
|
|
39e809f686 | ||
|
|
b2d7ad2afd | ||
|
|
73b21487b9 | ||
|
|
896812c8d6 | ||
|
|
286c95136f | ||
|
|
5ca169ac06 | ||
|
|
8efb28826f | ||
|
|
10799c33b7 | ||
|
|
657de9df33 | ||
|
|
069fc88042 | ||
|
|
92335989b7 | ||
|
|
58d2ce113d | ||
|
|
e499c88288 | ||
|
|
fea462c90a | ||
|
|
a75cbee133 | ||
|
|
99ff73c721 | ||
|
|
2c9eff3659 | ||
|
|
ce5355d73f | ||
|
|
582cd7d729 | ||
|
|
e2ef293d19 | ||
|
|
b06a4ba805 | ||
|
|
6b6c54e8ff | ||
|
|
8a0e56aff5 | ||
|
|
1804a65857 | ||
|
|
035f929d3b | ||
|
|
0b11a8dda6 | ||
|
|
5881fb9064 | ||
|
|
9dbbb26100 | ||
|
|
9c0d813697 | ||
|
|
4a1d16b641 | ||
|
|
c4eeb4f39f | ||
|
|
37ab7b34f9 | ||
|
|
fcae17eab7 | ||
|
|
c4cc3e944b | ||
|
|
c481e465b0 | ||
|
|
073bd759b0 | ||
|
|
88435af844 | ||
|
|
ff21f38626 | ||
|
|
9bc8492245 | ||
|
|
2389cec5f7 | ||
|
|
86d5f4c2e4 | ||
|
|
811449d664 | ||
|
|
109863c59a | ||
|
|
b9e8777bb1 | ||
|
|
d44c7f121b | ||
|
|
9b1c4e42bd | ||
|
|
ff026ea953 | ||
|
|
9eb1f2fdf8 | ||
|
|
06538ba021 | ||
|
|
14c6dcf902 | ||
|
|
a15dedb82d | ||
|
|
43ffa1bcd7 | ||
|
|
d30de9abce | ||
|
|
f459253cdb | ||
|
|
a53f7ccdb7 | ||
|
|
b7bd4fc69c | ||
|
|
9d43eb5503 | ||
|
|
335c040702 | ||
|
|
a4e5bf03d8 | ||
|
|
90de653a60 | ||
|
|
359feebccf | ||
|
|
6c29d85b8d | ||
|
|
315407b866 | ||
|
|
d22fa3c5e7 | ||
|
|
6d24af7748 | ||
|
|
18bd2162cf | ||
|
|
74ea50a293 | ||
|
|
4a9f906571 | ||
|
|
4b970b3f1c | ||
|
|
6db086c0a6 | ||
|
|
a7da8232a7 | ||
|
|
a5f868915a | ||
|
|
237bacc2da | ||
|
|
e09f5665ca | ||
|
|
11720302a2 | ||
|
|
e097ec84c2 | ||
|
|
bcb130deca | ||
|
|
c4af033f2c | ||
|
|
5bdd150347 | ||
|
|
f9cc57acb9 | ||
|
|
000d796149 | ||
|
|
a7c16c9b09 | ||
|
|
ad01d69540 | ||
|
|
7959f5b324 | ||
|
|
46e77d72ea | ||
|
|
1b8db4d4f4 | ||
|
|
7264d902c6 | ||
|
|
8017b416fb | ||
|
|
dd16890021 | ||
|
|
a17651fe02 | ||
|
|
ba34a9644e | ||
|
|
094492f076 | ||
|
|
6d5994f72e | ||
|
|
641a064200 | ||
|
|
c83f29be1b | ||
|
|
bd3cc0b5ec | ||
|
|
0b448daf3a | ||
|
|
f6eceaeaf9 | ||
|
|
9a114eb595 | ||
|
|
c01dba5138 | ||
|
|
1929f2d8b2 | ||
|
|
50b0fe157a | ||
|
|
dc7f44933c | ||
|
|
64a5d75ec4 | ||
|
|
b56dde9a6d | ||
|
|
74ec8925dc | ||
|
|
baf3cc8712 | ||
|
|
23777ad67b | ||
|
|
1ee9c2432b | ||
|
|
b0c27c48f6 | ||
|
|
c74a8274f2 | ||
|
|
2ceb7f8934 | ||
|
|
16af593277 | ||
|
|
51c8d85528 | ||
|
|
4ffc53a1dc | ||
|
|
b7eeba77a0 | ||
|
|
a8fb2720b9 | ||
|
|
a50f1c58f7 | ||
|
|
6528aadb90 | ||
|
|
dcd9842fca | ||
|
|
5a976525c0 | ||
|
|
b8559d4335 | ||
|
|
eb40f9f7c7 | ||
|
|
08be1ba622 | ||
|
|
605398bcea | ||
|
|
d0fa9b89bf | ||
|
|
fbfd1a4f60 | ||
|
|
21716ea59d | ||
|
|
e641108ed3 | ||
|
|
c3b4945c7e | ||
|
|
95c535290d | ||
|
|
6af0b1a8ed | ||
|
|
9928abf36d | ||
|
|
f0257222f8 | ||
|
|
135317b7b8 | ||
|
|
6b24135070 | ||
|
|
e08808c2ab | ||
|
|
aca2c131d4 | ||
|
|
a932f72a4f | ||
|
|
75befde788 | ||
|
|
3d10bbb0c6 | ||
|
|
8d325fce5c | ||
|
|
adac34790e | ||
|
|
838d0d27c9 | ||
|
|
d29d29c1d4 | ||
|
|
61e206c227 | ||
|
|
70ae60d4d5 | ||
|
|
7e457ee202 | ||
|
|
bb282189c3 | ||
|
|
2694d2e93c | ||
|
|
56457bc3ad | ||
|
|
45395fe580 | ||
|
|
3a90ed6c21 | ||
|
|
55b691e1b0 | ||
|
|
c9b57ffa85 | ||
|
|
c239716170 | ||
|
|
46ef1bcf5d | ||
|
|
5ac06251d4 | ||
|
|
395cfe6bf1 | ||
|
|
9d950b97ff | ||
|
|
f6a299ae3c | ||
|
|
963ff85a5f | ||
|
|
dcbdbc8925 | ||
|
|
1ba602ec47 | ||
|
|
c290d1e9d6 | ||
|
|
1f3dde5b4a | ||
|
|
a65cd67db3 | ||
|
|
bacd99260b | ||
|
|
51e5c6ba62 | ||
|
|
2b726f1a88 | ||
|
|
ee470b37bb | ||
|
|
74c716e0f6 | ||
|
|
00c25b5605 | ||
|
|
280f7a7735 | ||
|
|
f02405b649 | ||
|
|
d6ef65daf6 | ||
|
|
98414ac192 | ||
|
|
8b7728096a | ||
|
|
aec289e384 | ||
|
|
432eda8d6d | ||
|
|
5c45538e9f | ||
|
|
7f696a9ac4 | ||
|
|
bcd6e671f7 | ||
|
|
7a72f127de | ||
|
|
2ff5ec21c8 | ||
|
|
a1f94b609f | ||
|
|
da5034da33 | ||
|
|
0c509970b5 | ||
|
|
d894c4dcf9 | ||
|
|
aa10ab861a | ||
|
|
870d8f3542 | ||
|
|
166df8d4c4 | ||
|
|
666269539d | ||
|
|
0230b1ffa5 | ||
|
|
3dd789f0c2 | ||
|
|
dbb0fca1cb | ||
|
|
b8bd3bef13 | ||
|
|
59f753cebb | ||
|
|
d5f91e67a5 | ||
|
|
1b854ed787 | ||
|
|
13f8006162 | ||
|
|
b3d813d0c1 | ||
|
|
1fbe8bd790 | ||
|
|
eba8af3b38 | ||
|
|
589f2585cc | ||
|
|
b69119f11f | ||
|
|
dc15184781 | ||
|
|
f893dca39b | ||
|
|
64722604b5 | ||
|
|
b6a90f154f | ||
|
|
2ef8032110 | ||
|
|
3992febbfc | ||
|
|
982cec9507 | ||
|
|
4a5b3b3cc5 | ||
|
|
18ff334f70 | ||
|
|
d2b4ae30d1 | ||
|
|
abed797027 | ||
|
|
a4ca863d42 | ||
|
|
6737413103 | ||
|
|
33c3851d5b | ||
|
|
7c298272d3 | ||
|
|
71a2a24d09 | ||
|
|
cffdc56062 | ||
|
|
c29c15e1b7 | ||
|
|
3200068ab3 | ||
|
|
0a9da8d55e | ||
|
|
52ad546710 | ||
|
|
f88d2fa56a | ||
|
|
fe417d50af | ||
|
|
cc538f8427 | ||
|
|
f63cb02277 | ||
|
|
07db7ae62b | ||
|
|
4b363f9b33 | ||
|
|
09d2a56672 | ||
|
|
8ea7e20dfb | ||
|
|
02c982c80f | ||
|
|
a5b157f14f | ||
|
|
5d439ceee8 | ||
|
|
282ece2fab | ||
|
|
205abd4cbc | ||
|
|
fbb372e618 | ||
|
|
b4679ea688 | ||
|
|
d2f68d8a0a | ||
|
|
ebe872ca57 | ||
|
|
bce1ed8d67 | ||
|
|
a87ffd6d5c | ||
|
|
c0d2dde53a | ||
|
|
cd93ecb5ab | ||
|
|
4beee7f924 | ||
|
|
234260a784 | ||
|
|
e82d4d492b | ||
|
|
aa0271a493 | ||
|
|
29db252411 | ||
|
|
aa5caaf4f1 | ||
|
|
718b5f8ae1 | ||
|
|
48e4e01e24 | ||
|
|
7948a1914a | ||
|
|
1fa023ee08 | ||
|
|
cc1c264548 | ||
|
|
11960efb1b | ||
|
|
16706233c7 | ||
|
|
7b376b6d3a | ||
|
|
8fbb4abc76 | ||
|
|
d0ff64daaa | ||
|
|
bb97234817 | ||
|
|
19698d86b6 | ||
|
|
21ef96806f | ||
|
|
c49ed49580 | ||
|
|
a0e0194475 | ||
|
|
d266943a8f | ||
|
|
86acd8a461 | ||
|
|
786b7abcb7 | ||
|
|
05adbda4ca | ||
|
|
6db59d526f | ||
|
|
0149ce1be1 | ||
|
|
b058e68e4e | ||
|
|
0ef0417b5a | ||
|
|
649378ffbe | ||
|
|
060f8d7a6a | ||
|
|
b8c125cd14 | ||
|
|
33afcf6b17 | ||
|
|
bbb188d2f6 | ||
|
|
d8f8673ad3 | ||
|
|
a73c71842a | ||
|
|
d59ce5aebf | ||
|
|
5b14aa98e9 | ||
|
|
f78400a955 | ||
|
|
488856fdcc | ||
|
|
42659e20cd | ||
|
|
54b7b780c8 | ||
|
|
a638c3d39a | ||
|
|
ebb50cff6c | ||
|
|
9424c293d7 | ||
|
|
0b72c5550b | ||
|
|
698527ddf6 | ||
|
|
af067361a9 | ||
|
|
04aa1585fa | ||
|
|
9e84a2c273 | ||
|
|
9050572c68 | ||
|
|
40c6081256 | ||
|
|
484e6796c0 | ||
|
|
fdef43c2fc | ||
|
|
90854e38e6 | ||
|
|
8fac1c5b3a | ||
|
|
6439964ef6 | ||
|
|
5b612d8084 | ||
|
|
b2d9bc4aa8 | ||
|
|
f615dae87c | ||
|
|
79058e1535 | ||
|
|
a51c12d152 | ||
|
|
8999a24ec3 | ||
|
|
da1916e35a | ||
|
|
b1987648cf | ||
|
|
001323c058 | ||
|
|
55633560e7 | ||
|
|
be60713b13 | ||
|
|
000a4bf62d | ||
|
|
68938054ca | ||
|
|
368b96424d | ||
|
|
c5da652ac1 | ||
|
|
38f71a3cc9 | ||
|
|
2d21146665 | ||
|
|
24b1fc01ca | ||
|
|
fbfdb9fd15 | ||
|
|
54074409ab | ||
|
|
e738c5c41f | ||
|
|
d3ec86ab18 | ||
|
|
ce5b8f95e7 | ||
|
|
6bb20aed15 | ||
|
|
9b3c9eb90b |
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: gbj
|
||||
17
.github/workflows/test.yml
vendored
@@ -21,13 +21,14 @@ jobs:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
@@ -35,16 +36,10 @@ jobs:
|
||||
- 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 Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make ci
|
||||
|
||||
1
.gitignore
vendored
@@ -6,3 +6,4 @@ blob.rs
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
51
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
_This Code of Conduct is based on the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
and the [Bevy Code of Conduct](https://raw.githubusercontent.com/bevyengine/bevy/main/CODE_OF_CONDUCT.md),
|
||||
which are adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)
|
||||
and the [Contributor Covenant](https://www.contributor-covenant.org)._
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
We are a community of people learning and exploring how to build better web applications
|
||||
with Rust. When interacting with one another, please remember that there are no experts and there are
|
||||
no stupid questions. Assume the best in other people's communication, and take a step back if
|
||||
you find yourself getting defensive.
|
||||
|
||||
Please note the following guidelines as well:
|
||||
|
||||
* Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
|
||||
* Please be kind and courteous. There’s no need to be mean or rude.
|
||||
* Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
|
||||
* Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
|
||||
* We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term “harassment” as including the definition in the [Citizen Code of Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md); if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don’t tolerate behavior that excludes people in socially marginalized groups.
|
||||
* Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact the maintainers immediately. Whether you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve got your back.
|
||||
* Do not make casual mention of slavery or indentured servitude and/or false comparisons of one's occupation or situation to slavery. Please consider using or asking about alternate terminology when referring to such metaphors in technology.
|
||||
* Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
|
||||
|
||||
## Moderation
|
||||
|
||||
These are the policies for upholding [our community’s standards of conduct](#our-standards). If you feel that a thread needs moderation, please contact the maintainers.
|
||||
|
||||
1. Remarks that violate the community standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner).
|
||||
2. Remarks that maintainers find inappropriate, whether listed in the code of conduct or not, are also not allowed.
|
||||
3. Maintainers will first respond to such remarks with a warning.
|
||||
4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
|
||||
5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
|
||||
6. Maintainers may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
|
||||
7. If a maintainer bans someone and you think it was unjustified, please take it up with that maintainer, or with a different maintainer, in private. Complaints about bans in-channel are not allowed.
|
||||
8. Maintainers are held to a higher standard than other community members. If a maintainer creates an inappropriate situation, they should expect less leeway than others.
|
||||
|
||||
The enforcement policies in the code of conduct apply to all official venues, including Discord channels, GitHub repositories, and all other forums.
|
||||
65
Cargo.toml
@@ -3,7 +3,7 @@ members = [
|
||||
# core
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_core",
|
||||
"leptos_config",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
@@ -15,29 +15,21 @@ members = [
|
||||
# libraries
|
||||
"meta",
|
||||
"router",
|
||||
|
||||
# examples
|
||||
"examples/counter",
|
||||
"examples/counter-isomorphic",
|
||||
"examples/counters",
|
||||
"examples/counters-stable",
|
||||
"examples/fetch",
|
||||
"examples/hackernews",
|
||||
"examples/hackernews-axum",
|
||||
"examples/parent-child",
|
||||
"examples/router",
|
||||
"examples/todomvc",
|
||||
"examples/todo-app-sqlite",
|
||||
"examples/todo-app-sqlite-axum",
|
||||
"examples/todo-app-cbor",
|
||||
"examples/view-tests",
|
||||
|
||||
# book
|
||||
"docs/book/project/ch02_getting_started",
|
||||
"docs/book/project/ch03_building_ui",
|
||||
"docs/book/project/ch04_reactivity",
|
||||
]
|
||||
exclude = ["benchmarks"]
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.3"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.1.3" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.1.3" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.1.3" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.1.3" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.1.3" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.1.3" }
|
||||
leptos_router = { path = "./router", version = "0.1.3" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.1.3" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
@@ -45,29 +37,4 @@ 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",
|
||||
],
|
||||
]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["build", "test"]
|
||||
dependencies = ["build", "check-examples", "test"]
|
||||
|
||||
[tasks.build]
|
||||
clear = true
|
||||
@@ -19,6 +19,26 @@ command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter" },
|
||||
{ name = "check", path = "examples/counter_isomorphic" },
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
{ name = "check", path = "examples/errors_axum" },
|
||||
{ name = "check", path = "examples/fetch" },
|
||||
{ name = "check", path = "examples/hackernews" },
|
||||
{ name = "check", path = "examples/hackernews_axum" },
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todomvc" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
|
||||
118
README.md
@@ -1,6 +1,7 @@
|
||||
**Please note:** This framework is in active development. I'm keeping it in a cycle of 0.0.x releases at the moment to indicate that it’s not even ready for its 0.1.0. Active work is being done on documentation and features, and APIs should not necessarily be considered stable. At the same time, it is more than a toy project or proof of concept, and I am actively using it for my own application development.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/gbj/leptos/main/docs/logos/logo.svg" alt="Leptos Logo" style="width: 100%; height: auto; display: block; margin: auto;">
|
||||
<picture>
|
||||
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_pref_dark_RGB.svg" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
||||
</picture>
|
||||
|
||||
[](https://crates.io/crates/leptos)
|
||||
[](https://docs.rs/leptos)
|
||||
@@ -12,7 +13,7 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
|
||||
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
@@ -22,13 +23,13 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// this JSX is compiled to an HTML template string for performance
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=increment>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
@@ -51,60 +52,71 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
- **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!_)
|
||||
- **Fine-grained reactivity**: The entire framework is built 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.
|
||||
|
||||
## Learn more
|
||||
|
||||
Here are some resources for learning more about Leptos:
|
||||
|
||||
- [Examples](https://github.com/gbj/leptos/tree/main/examples)
|
||||
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
- [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!)
|
||||
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
Most of the examples assume you’re using `nightly` Rust.
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
|
||||
To set up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you haven’t already)
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
you’ll just call `.get()`, `.set()`, or `.update()` manually. Check out the
|
||||
[`counters-stable` example](https://github.com/gbj/leptos/blob/main/examples/counters-stable/src/main.rs)
|
||||
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
for examples of the correct API.
|
||||
|
||||
## Benchmarks
|
||||
## `cargo-leptos`
|
||||
|
||||
### Server-Side Rendering
|
||||
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our [starter template](https://github.com/leptos-rs/start).
|
||||
|
||||
I’ve created a benchmark comparing Leptos’s HTML rendering on the server to [Tera](https://github.com/Keats/tera), [Yew](https://github.com/yewstack/yew), and [Sycamore](https://github.com/sycamore-rs/sycamore). You can find the benchmark [here](https://github.com/gbj/leptos/tree/main/benchmarks) and run it yourself using `cargo bench`. Leptos renders HTML roughly as fast as Tera, and scales well as templates become larger. It's significantly faster than the server-side HTML rendering done by similar frameworks.
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
cargo leptos new --git https://github.com/leptos-rs/start
|
||||
cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Click to show results</summary>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><td><em>ns/iter</em></td><td>Tera</td><td>Leptos</td><td>Yew</td><td>Sycamore</td></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>3 Counters</td><td align="right">3,454</td><td align="right">5,666</td><td align="right">34,984</td><td align="right">32,412</td></tr>
|
||||
<tr><td>TodoMVC (no todos)</td><td align="right">2,396</td><td align="right">5,561</td><td align="right">38,725</td><td align="right">68,749</td></tr>
|
||||
<tr><td>TodoMVC (1000 todos)</td><td align="right">3,829,447</td><td align="right">3,077,907</td><td align="right">5,125,639</td><td align="right">19,448,900</td></tr>
|
||||
<tr><td><em>Average</em></td><td align="right">1.08</td><td align="right">1.65</td><td align="right">6.25</td><td align="right">9.36</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
|
||||
### Client-Side Rendering
|
||||
|
||||
The gold standard for testing raw rendering performance for front-end web frameworks is the [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark). The official results list Leptos as the fastest Rust/Wasm framework, slightly slower than SolidJS and significantly faster than popular JS frameworks like Svelte, Preact, and React.
|
||||
|
||||
<details>
|
||||
<summary>Click to show results</summary>
|
||||
<img width="913" alt="js-framework-benchmark results" src="https://user-images.githubusercontent.com/286622/198388168-d21e938b-5d59-4000-b373-91b48f1ec4d3.png">
|
||||
</details>
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
## FAQs
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
People usually mean one of three things by this question.
|
||||
|
||||
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
|
||||
|
||||
With 0.1 the APIs are basically settled. We’re adding new features, but we’re very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
Yes, I’m sure there are. You can see from the state of our issue tracker over time that there aren’t that _many_ bugs and they’re usually resolved pretty quickly. But for sure, there may be moments where you encounter something that requires a fix at the framework level, which may not be immediately resolved.
|
||||
|
||||
3. **Am I a consumer or a contributor?**
|
||||
|
||||
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) don’t have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
|
||||
|
||||
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if you’re willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
|
||||
|
||||
### 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:
|
||||
@@ -114,7 +126,7 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
|
||||
- Use event listeners to update signals
|
||||
- Create effects to update the UI
|
||||
|
||||
I've put together a [very simple GTK example](https://github.com/gbj/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
|
||||
I've put together a [very simple GTK example](https://github.com/leptos-rs/leptos/blob/main/examples/gtk/src/main.rs) so you can see what I mean.
|
||||
|
||||
### How is this different from Yew/Dioxus?
|
||||
|
||||
@@ -122,7 +134,7 @@ On the surface level, these libraries may seem similar. Yew is, of course, the m
|
||||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising components re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
|
||||
@@ -132,21 +144,19 @@ There are some practical differences that make a significant difference:
|
||||
|
||||
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
|
||||
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
|
||||
- **Template node cloning:** Leptos's `view` macro compiles to a static HTML string and a set of instructions of how to assign its reactive values. This means that at runtime, Leptos can clone a `<template>` node rather than calling `document.createElement()` to create DOM nodes. This is a _significantly_ faster way of rendering components.
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **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
|
||||
let (count, set_count) = create_signal(cx, 0); // a signal
|
||||
let double_count = move || count() * 2; // a derived signal
|
||||
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
|
||||
// all are accessed by calling them
|
||||
assert_eq!(count(), 0);
|
||||
assert_eq!(double_count(), 0);
|
||||
assert_eq!(memoized_count(), 0);
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0); // a signal
|
||||
let double_count = move || count() * 2; // a derived signal
|
||||
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
|
||||
// all are accessed by calling them
|
||||
assert_eq!(count(), 0);
|
||||
assert_eq!(double_count(), 0);
|
||||
assert_eq!(memoized_count(), 0);
|
||||
// this function can accept any of those signals
|
||||
fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
|
||||
```
|
||||
|
||||
// this function can accept any of those signals
|
||||
fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
|
||||
```
|
||||
|
||||
- **Signals and scopes are `'static`:** Both Leptos and Sycamore ease the pain of moving signals in closures (in particular, event listeners) by making them `Copy`, to avoid the `{ let count = count.clone(); move |_| ... }` that's very familiar in Rust UI code. Sycamore does this by using bump allocation to tie the lifetimes of its signals to its scopes: since references are `Copy`, `&'a Signal<T>` can be moved into a closure. Leptos does this by using arena allocation and passing around indices: types like `ReadSignal<T>`, `WriteSignal<T>`, and `Memo<T>` are actually wrapper for indices into an arena. This means that both scopes and signals are both `Copy` and `'static` in Leptos, which means that they can be moved easily into closures without adding lifetime complexity.
|
||||
- **Signals and scopes are `'static`:** Both Leptos and Sycamore ease the pain of moving signals in closures (in particular, event listeners) by making them `Copy`, to avoid the `{ let count = count.clone(); move |_| ... }` that's very familiar in Rust UI code. Sycamore does this by using bump allocation to tie the lifetimes of its signals to its scopes: since references are `Copy`, `&'a Signal<T>` can be moved into a closure. Leptos does this by using arena allocation and passing around indices: types like `ReadSignal<T>`, `WriteSignal<T>`, and `Memo<T>` are actually wrappers for indices into an arena. This means that both scopes and signals are both `Copy` and `'static` in Leptos, which means that they can be moved easily into closures without adding lifetime complexity.
|
||||
|
||||
@@ -2,12 +2,12 @@ use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> Element {
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial);
|
||||
view! {
|
||||
cx,
|
||||
@@ -28,22 +28,21 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
<Counter initial=2/>
|
||||
<Counter initial=3/>
|
||||
</main>
|
||||
};
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main data-hk=\"0-0\"><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><!--#--><div data-hk=\"0-2-0\"><button>-1</button><span>Value: <!--#-->1<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-3-0\"><button>-1</button><span>Value: <!--#-->2<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-4-0\"><button>-1</button><span>Value: <!--#-->3<!--/-->!</span><button>+1</button></div><!--/--></main>"
|
||||
);
|
||||
"<main id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tera_ssr_bench(b: &mut Bencher) {
|
||||
use tera::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
static TEMPLATE: &str = r#"<main>
|
||||
static TEMPLATE: &str = r#"<main>
|
||||
<h1>Welcome to our benchmark page.</h1>
|
||||
<p>Here's some introductory text.</p>
|
||||
{% for counter in counters %}
|
||||
@@ -55,37 +54,40 @@ fn tera_ssr_bench(b: &mut Bencher) {
|
||||
{% endfor %}
|
||||
</main>"#;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
static ref TERA: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Counter {
|
||||
value: i32
|
||||
}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Counter {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert("counters", &vec![
|
||||
Counter { value: 0 },
|
||||
Counter { value: 1},
|
||||
Counter { value: 2 }
|
||||
]);
|
||||
b.iter(|| {
|
||||
let mut ctx = Context::new();
|
||||
ctx.insert(
|
||||
"counters",
|
||||
&vec![
|
||||
Counter { value: 0 },
|
||||
Counter { value: 1 },
|
||||
Counter { value: 2 },
|
||||
],
|
||||
);
|
||||
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
let _ = TERA.render("template.html", &ctx).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_ssr_bench(b: &mut Bencher) {
|
||||
use sycamore::*;
|
||||
use sycamore::prelude::*;
|
||||
use sycamore::prelude::*;
|
||||
use sycamore::*;
|
||||
|
||||
b.iter(|| {
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
#[derive(Prop)]
|
||||
struct CounterProps {
|
||||
@@ -139,10 +141,10 @@ fn sycamore_ssr_bench(b: &mut Bencher) {
|
||||
|
||||
#[bench]
|
||||
fn yew_ssr_bench(b: &mut Bencher) {
|
||||
use yew::prelude::*;
|
||||
use yew::ServerRenderer;
|
||||
use yew::prelude::*;
|
||||
use yew::ServerRenderer;
|
||||
|
||||
b.iter(|| {
|
||||
b.iter(|| {
|
||||
#[derive(Properties, PartialEq, Eq, Debug)]
|
||||
struct CounterProps {
|
||||
initial: i32
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
pub use leptos::*;
|
||||
use miniserde::*;
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
@@ -97,7 +97,7 @@ const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
#[component]
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
@@ -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();
|
||||
@@ -167,62 +167,64 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus on:keydown=add_todo />
|
||||
</header>
|
||||
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For each=filtered_todos key=|todo| todo.id>
|
||||
{move |cx, todo: &Todo| view! { cx, <Todo todo=todo.clone() /> }}
|
||||
</For>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
|
||||
</header>
|
||||
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<input id="toggle-all" class="toggle-all" type="checkbox"
|
||||
prop:checked={move || todos.with(|t| t.remaining() > 0)}
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
<For
|
||||
each=filtered_todos
|
||||
key=|todo| todo.id
|
||||
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
|
||||
/>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
|
||||
<span class="todo-count">
|
||||
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
|
||||
{move || if todos.with(|t| t.remaining()) == 1 {
|
||||
" item"
|
||||
} else {
|
||||
" items"
|
||||
}}
|
||||
" left"
|
||||
</span>
|
||||
<ul class="filters">
|
||||
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
|
||||
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
|
||||
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
|
||||
</ul>
|
||||
<button
|
||||
class="clear-completed hidden"
|
||||
class:hidden={move || todos.with(|t| t.completed() == 0)}
|
||||
on:click=move |_| set_todos.update(|t| t.clear_completed())
|
||||
>
|
||||
"Clear completed"
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="info">
|
||||
<p>"Double-click to edit a todo"</p>
|
||||
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
|
||||
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
}.into_view(cx)
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
let input = NodeRef::new(cx);
|
||||
//let input = NodeRef::new(cx);
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
@@ -234,12 +236,12 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
let tpl = view! { cx,
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
_ref=input
|
||||
//_ref=input
|
||||
>
|
||||
<div class="view">
|
||||
<input
|
||||
@@ -271,9 +273,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
})
|
||||
}
|
||||
</li>
|
||||
};
|
||||
|
||||
tpl
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -7,15 +7,16 @@ mod yew;
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new(cx)/>
|
||||
};
|
||||
}
|
||||
.into_view(cx)
|
||||
.render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
@@ -56,18 +57,18 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
use self::leptos::*;
|
||||
use ::leptos::*;
|
||||
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<TodoMVC todos=Todos::new_with_1000(cx)/>
|
||||
};
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
|
||||
assert!(rendered.len() > 1);
|
||||
});
|
||||
@@ -108,3 +109,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ This document is intended as a running list of common issues, with example code
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_a) = create_signal(cx, false);
|
||||
let (b, set_b) = create_signal(cx, false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if a() > 5 {
|
||||
|
||||
1
docs/book/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
book
|
||||
14
docs/book/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
This project contains the core of a new introductory guide to Leptos.
|
||||
|
||||
It is built using `mdbook`. You can view a local copy by installing `mdbook`
|
||||
|
||||
```bash
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
and run the book with
|
||||
```
|
||||
mdbook serve
|
||||
```
|
||||
|
||||
It should be available at `http://localhost:3000`.
|
||||
@@ -1,16 +0,0 @@
|
||||
[book]
|
||||
authors = ["Greg Johnston"]
|
||||
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 +0,0 @@
|
||||
mermaid.initialize({startOnLoad:true});
|
||||
4
docs/book/mermaid.min.js
vendored
@@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "ch02_getting_started"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
@@ -1,14 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,5 +0,0 @@
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "ch03_building_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
@@ -1,14 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,39 +0,0 @@
|
||||
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>
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "ch04_reactivity"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
@@ -1,28 +0,0 @@
|
||||
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,10 +1,19 @@
|
||||
# Introduction
|
||||
|
||||
This book is intended as an introduction to the [Leptos](https://github.com/gbj/leptos) Web framework. Together, we’ll build a simple todo app—first as a client-side app, then as a full-stack app.
|
||||
This book is intended as an introduction to the [Leptos](https://github.com/leptos-rs/leptos) Web framework.
|
||||
It will walk through the fundamental concepts you need to build applications,
|
||||
beginning with a simple application rendered in the browser, and building toward a
|
||||
full-stack application with server-side rendering and hydration.
|
||||
|
||||
The guide doesn’t assume you know anything about fine-grained reactivity or the details of modern Web frameworks. It does assume you are familiar with the Rust programming language, HTML, CSS, and the DOM and other Web APIs.
|
||||
The guide doesn’t assume you know anything about fine-grained reactivity or the
|
||||
details of modern Web frameworks. It does assume you are familiar with the Rust
|
||||
programming language, HTML, CSS, and the DOM and basic Web APIs.
|
||||
|
||||
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript) and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities to other frameworks like React (JavaScript), Yew (Rust), and Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to understand Leptos.
|
||||
Leptos is most similar to frameworks like [Solid](https://www.solidjs.com) (JavaScript)
|
||||
and [Sycamore](https://sycamore-rs.netlify.app/) (Rust). There are some similarities
|
||||
to other frameworks like React (JavaScript), Svelte (JavaScript), Yew (Rust), and
|
||||
Dioxus (Rust), so knowledge of one of those frameworks may also make it easier to
|
||||
understand Leptos.
|
||||
|
||||
You can find more detailed docs for each part of the API at [Docs.rs](https://docs.rs/leptos/latest/leptos/).
|
||||
|
||||
|
||||
@@ -1,37 +1,48 @@
|
||||
# Getting Started
|
||||
|
||||
> The code for this chapter can be found [here](https://github.com/gbj/leptos/tree/main/docs/book/project/ch02_getting_started).
|
||||
There are two basic paths to getting started with Leptos:
|
||||
1. Client-side rendering with [Trunk](https://trunkrs.dev/)
|
||||
2. Full-stack rendering with [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos)
|
||||
|
||||
For the early examples, it will be easiest to begin with Trunk. We’ll introduce
|
||||
`cargo-leptos` a little later in this series.
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
cargo install --lock trunk
|
||||
cargo install trunk
|
||||
```
|
||||
|
||||
Create a basic Rust binary project
|
||||
|
||||
```bash
|
||||
cargo init leptos-todo
|
||||
cargo init leptos-tutorial
|
||||
```
|
||||
|
||||
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 = "0.0"
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
```bash
|
||||
cargo add leptos
|
||||
```
|
||||
|
||||
You’ll want to set up a basic `index.html` with the following content:
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
```html
|
||||
{{#include ../project/ch02_getting_started/index.html}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Let’s start with a very simple `main.rs`
|
||||
And add a simple “Hello, world!” to your `main.rs`
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
```rust
|
||||
{{#include ../project/ch02_getting_started/src/main.rs}}
|
||||
fn main() {
|
||||
mount_to_body(|_cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,240 +0,0 @@
|
||||
# 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)
|
||||
@@ -2,5 +2,40 @@
|
||||
|
||||
- [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)
|
||||
- [Building User Interfaces](./view/README.md)
|
||||
- [A Basic Component](./view/01_basic_component.md)
|
||||
- [Dynamic Attributes](./view/02_dynamic_attributes.md)
|
||||
- [Components and Props](./view/03_components.md)
|
||||
- [Iteration](./view/04_iteration.md)
|
||||
- [Forms and Inputs](./view/05_forms.md)
|
||||
- [Control Flow](./view/06_control_flow.md)
|
||||
- [Error Handling](./view/07_errors.md)
|
||||
- [Parent-Child Communication](./view/08_parent_child.md)
|
||||
- [Passing Children to Components]()
|
||||
- [Interlude: Reactivity and Functions]()
|
||||
- [Testing]()
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Async]()
|
||||
- [Resource]()
|
||||
- [Suspense]()
|
||||
- [Transition]()
|
||||
- [State Management]()
|
||||
- [Interlude: Advanced Reactivity]()
|
||||
- [Router]()
|
||||
- [Fundamentals]()
|
||||
- [defining `<Routes/>`]()
|
||||
- [`<A/>`]()
|
||||
- [`<Form/>`]()
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
- [Models of SSR]()
|
||||
- [`cargo-leptos`]()
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Headers]()
|
||||
- [Cookies]()
|
||||
- [Server Functions]()
|
||||
- [Actions]()
|
||||
- [Forms]()
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly]()
|
||||
|
||||
143
docs/book/src/view/01_basic_component.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# A Basic Component
|
||||
|
||||
That “Hello, world!” was a *very* simple example. Let’s move on to something a
|
||||
little more like an ordinary app.
|
||||
|
||||
First, let’s edit the `main` function so that, instead of rendering the whole
|
||||
app, it just renders an `<App/>` component. Components are the basic unit of
|
||||
composition and design in most web frameworks, and Leptos is no exception.
|
||||
Conceptually, they are similar to HTML elements: they represent a section of the
|
||||
DOM, with self-contained, defined behavior. Unlike HTML elements, they are in
|
||||
`PascalCase`, so most Leptos applications will start with something like an
|
||||
`<App/>` component.
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
leptos::mount_to_body(|cx| view! { cx, <App/> })
|
||||
}
|
||||
```
|
||||
|
||||
Now let’s define our `<App/>` component itself. Because it’s relatively simple,
|
||||
I’ll give you the whole thing up front, then walk through it line by line.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
"Click me"
|
||||
{move || count.get()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## The Component Signature
|
||||
```rust
|
||||
#[component]
|
||||
```
|
||||
|
||||
Like all component definitions, this begins with the [`#[component]`](https://docs.rs/leptos/latest/leptos/attr.component.html) macro. `#[component]` annotates a function so it can be
|
||||
used as a component in your Leptos application. We’ll see some of the other features of
|
||||
this macro in a couple chapters.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> impl IntoView
|
||||
```
|
||||
|
||||
Every component is a function with the following characteristics
|
||||
1. It takes a reactive [`Scope`](https://docs.rs/leptos/latest/leptos/struct.Scope.html)
|
||||
as its first argument. This `Scope` is our entrypoint into the reactive system.
|
||||
By convention, it’s usually named `cx`.
|
||||
2. You can include other arguments, which will be available as component “props.”
|
||||
3. Component functions return `impl IntoView`, which is an opaque type that includes
|
||||
anything you could return from a Leptos `view`.
|
||||
|
||||
## The Component Body
|
||||
The body of the component function is a set-up function that runs once, not a
|
||||
render function that re-runs multiple times. You’ll typically use it to create a
|
||||
few reactive variables, define any side effects that run in response to those values
|
||||
changing, and describe the user interface.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
```
|
||||
[`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html)
|
||||
creates a signal, the basic unit of reactive change and state management in Leptos.
|
||||
This returns a `(getter, setter)` tuple. To access the current value, you’ll
|
||||
use `count.get()` (or, on `nightly` Rust, the shorthand `count()`). To set the
|
||||
current value, you’ll call `set_count.set(...)` (or `set_count(...)`).
|
||||
|
||||
> `.get()` clones the value and `.set()` overwrites it. In many cases, it’s more
|
||||
efficient to use `.with()` or `.update()`; check out the docs for [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) and [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) if you’d like to learn more about those trade-offs at this point.
|
||||
|
||||
## The View
|
||||
|
||||
Leptos defines user interfaces using a JSX-like format via the [`view`](https://docs.rs/leptos/latest/leptos/macro.view.html) macro.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<button
|
||||
// define an event listener with on:
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
>
|
||||
// text nodes are wrapped in quotation marks
|
||||
"Click me: "
|
||||
// blocks can include Rust code
|
||||
{move || count.get()}
|
||||
</button>
|
||||
}
|
||||
```
|
||||
|
||||
This should mostly be easy to understand: it looks like HTML, with a special
|
||||
`on:click` to define a `click` event listener, a text node that’s formatted like
|
||||
a Rust string, and then...
|
||||
```rust
|
||||
{move || count.get()}
|
||||
```
|
||||
whatever that is.
|
||||
|
||||
People sometimes joke that they use more closures in their first Leptos application
|
||||
than they’ve ever used in their lives. And fair enough. Basically, passing a function
|
||||
into the view tells the framework: “Hey, this is something that might change.”
|
||||
|
||||
When we click the button and call `set_count`, the `count` signal is updated. This
|
||||
`move || count.get()` closure, whose value depends on the value of `count`, re-runs,
|
||||
and the framework makes a targeted update to that one specific text node, touching
|
||||
nothing else in your application. This is what allows for extremely efficient updates
|
||||
to the DOM.
|
||||
|
||||
Now, if you have Clippy on—or if you have a particularly sharp eye—you might notice
|
||||
that this closure is redundant, at least if you’re in `nightly` Rust. If you’re using
|
||||
Leptos with `nightly` Rust, signals are already functions, so the closure is unnecessary.
|
||||
As a result, you can write a simpler view:
|
||||
```rust
|
||||
view! { cx,
|
||||
<button /* ... */>
|
||||
"Click me: "
|
||||
// identical to {move || count.get()}
|
||||
{count}
|
||||
</button>
|
||||
}
|
||||
```
|
||||
|
||||
Remember—and this is *very important*—only functions are reactive. This means that
|
||||
`{count}` and `{count()}` do very different things in your view. `{count}` passes
|
||||
in a function, telling the framework to update the view every time `count` changes.
|
||||
`{count()}` access the value of `count` once, and passes an `i32` into the view,
|
||||
rendering it once, unreactively. You can see the difference in the CodeSandbox below!
|
||||
|
||||
> Throughout this tutorial, we’ll use CodeSandbox to show interactive examples. To
|
||||
show the browser in the sandbox, you may need to click `Add DevTools >
|
||||
Other Previews > 8080.` Hover over any of the variables to show Rust-Analyzer details
|
||||
and docs for what’s going on. Feel free to fork the examples to play with them yourself!
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-3d74p3?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A31%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A31%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
104
docs/book/src/view/02_dynamic_attributes.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# `view`: Dynamic Attributes and Classes
|
||||
|
||||
So far we’ve seen how to use the `view` macro to create event listeners and to
|
||||
create dynamic text by passing a function (such as a signal) into the view.
|
||||
|
||||
But of course there are other things you might want to update in your user interface.
|
||||
In this section, we’ll look at how to update attributes and classes dynamically,
|
||||
and we’ll introduce the concept of a **derived signal**.
|
||||
|
||||
Let’s start with a simple component that should be familiar: click a button to
|
||||
increment a counter.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count.update(|n| *n += 1);
|
||||
}
|
||||
```
|
||||
|
||||
So far, this is just the example from the last chapter.
|
||||
|
||||
## Dynamic Classes
|
||||
|
||||
Now let’s say I’d like to update the list of CSS classes on this element dynamically.
|
||||
For example, let’s say I want to add the class `red` when the count is odd. I can
|
||||
do this using the `class:` syntax.
|
||||
```rust
|
||||
class:red=move || count() & 1 == 1
|
||||
```
|
||||
`class:` attributes take
|
||||
1. the class name, following the colon (`red`)
|
||||
2. a value, which can be a `bool` or a function that returns a `bool`
|
||||
|
||||
When the value is `true`, the class is added. When the value is `false`, the class
|
||||
is removed. And if the value is a function that accesses a signal, the class will
|
||||
reactively update when the signal changes.
|
||||
|
||||
Now every time I click the button, the text should toggle between red and black as
|
||||
the number switches between even and odd.
|
||||
|
||||
## Dynamic Attributes
|
||||
|
||||
The same applies to plain attributes. Passing a plain string or primitive value to
|
||||
an attribute gives it a static value. Passing a function (including a signal) to
|
||||
an attribute causes it to update its value reactively. Let’s add another element
|
||||
to our view:
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
// signals are functions, so this <=> `move || count.get()`
|
||||
value=count
|
||||
/>
|
||||
```
|
||||
|
||||
Now every time we set the count, not only will the `class` of the `<button>` be
|
||||
toggled, but the `value` of the `<progress>` bar will increase, which means that
|
||||
our progress bar will move forward.
|
||||
|
||||
## Derived Signals
|
||||
|
||||
Let’s go one layer deeper, just for fun.
|
||||
|
||||
You already know that we create reactive interfaces just by passing functions into
|
||||
the `view`. This means that we can easily change our progress bar. For example,
|
||||
suppose we want it to move twice as fast:
|
||||
```rust
|
||||
<progress
|
||||
max="50"
|
||||
value=move || count() * 2
|
||||
/>
|
||||
```
|
||||
|
||||
But imagine we want to reuse that calculation in more than one place. You can do this
|
||||
using a **derived signal**: a closure that accesses a signal.
|
||||
```rust
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
/* insert the rest of the view */
|
||||
<progress
|
||||
max="50"
|
||||
// we use it once here
|
||||
value=double_count
|
||||
/>
|
||||
<p>
|
||||
"Double Count: "
|
||||
// and again here
|
||||
{double_count}
|
||||
</p>
|
||||
```
|
||||
|
||||
Derived signals let you create reactive computed values that can be used in multiple
|
||||
places in your application with minimal overhead.
|
||||
|
||||
> Note: Using a derived signal like this means that the calculation runs once per
|
||||
signal change per place we access `double_count`; in other words, twice. This is a
|
||||
very cheap calculation, so that’s fine. We’ll look at memos in a later chapter, which
|
||||
are designed to solve this problem for expensive calculations.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/2-dynamic-attribute-pqyvzl?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
317
docs/book/src/view/03_components.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# Components and Props
|
||||
|
||||
So far, we’ve been building our whole application in a single component. This
|
||||
is fine for really tiny examples, but in any real application you’ll need to
|
||||
break the user interface out into multiple components, so you can break your
|
||||
interface down into smaller, reusable, composable chunks.
|
||||
|
||||
Let’s take our progress bar example. Imagine that you want two progress bars
|
||||
instead of one: one that advances one tick per click, one that advances two ticks
|
||||
per click.
|
||||
|
||||
You _could_ do this by just creating two `<progress>` elements:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! {
|
||||
<progress
|
||||
max="50"
|
||||
value=progress
|
||||
/>
|
||||
<progress
|
||||
max="50"
|
||||
value=double_count
|
||||
/>
|
||||
```
|
||||
|
||||
But of course, this doesn’t scale very well. If you want to add a third progress
|
||||
bar, you need to add this code another time. And if you want to edit anything
|
||||
about it, you need to edit it in triplicate.
|
||||
|
||||
Instead, let’s create a `<ProgressBar/>` component.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
// hmm... where will we get this from?
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There’s just one problem: `progress` is not defined. Where should it come from?
|
||||
When we were defining everything manually, we just used the local variable names.
|
||||
Now we need some way to pass an argument into the component.
|
||||
|
||||
## Component Props
|
||||
|
||||
We do this using component properties, or “props.” If you’ve used another frontend
|
||||
framework, this is probably a familiar idea. Basically, properties are to components
|
||||
as attributes are to HTML elements: they let you pass additional information into
|
||||
the component.
|
||||
|
||||
In Leptos, you define props by giving additional arguments to the component function.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
<progress
|
||||
max="50"
|
||||
// now this works
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now we can use our component in the main `<App/>` component’s view.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
view! { cx,
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
// now we use our component!
|
||||
<ProgressBar progress=count/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using a component in the view looks a lot like using an HTML element. You’ll
|
||||
notice that you can easily tell the difference between an element and a component
|
||||
because components always have `PascalCase` names. You pass the `progress` prop
|
||||
in as if it were an HTML element attribute. Simple.
|
||||
|
||||
> ### Important Note
|
||||
> For every `Component`, Leptos generates a corresponding `ComponentProps` type. This
|
||||
is what allows us to have named props, when Rust does not have named function parameters.
|
||||
If you’re defining a component in one module and importing it into another, make
|
||||
sure you include this `ComponentProps` type:
|
||||
>
|
||||
> `use progress_bar::{ProgressBar, ProgressBarProps};`
|
||||
|
||||
### Reactive and Static Props
|
||||
|
||||
You’ll notice that throughout this example, `progress` takes a reactive
|
||||
`ReadSignal<i32>`, and not a plain `i32`. This is **very important**.
|
||||
|
||||
Component props have no special meaning attached to them. A component is simply
|
||||
a function that runs once to set up the user interface. The only way to tell the
|
||||
interface to respond to changing is to pass it a signal type. So if you have a
|
||||
component property that will change over time, like our `progress`, it should
|
||||
be a signal.
|
||||
|
||||
### `optional` Props
|
||||
|
||||
Right now the `max` setting is hard-coded. Let’s take that as a prop too. But
|
||||
let’s add a catch: let’s make this prop optional by annotating the particular
|
||||
argument to the component function with `#[prop(optional)]`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
// mark this prop optional
|
||||
// you can specify it or not when you use <ProgressBar/>
|
||||
#[prop(optional)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, we can use `<ProgressBar max=50 value=count/>`, or we can omit `max`
|
||||
to use the default value (i.e., `<ProgressBar value=count/>`). The default value
|
||||
on an `optional` is its `Default::default()` value, which for a `u16` is going to
|
||||
be `0`. In the case of a progress bar, a max value of `0` is not very useful.
|
||||
|
||||
So let’s give it a particular default value instead.
|
||||
|
||||
### `default` props
|
||||
|
||||
You can specify a default value other than `Default::default()` pretty simply
|
||||
with `#[prop(default = ...)`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: ReadSignal<i32>
|
||||
) -> impl IntoView {
|
||||
view! { cx,
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generic Props
|
||||
|
||||
This is great. But we began with two counters, one driven by `count`, and one by
|
||||
the derived signal `double_count`. Let’s recreate that by using `double_count`
|
||||
as the `progress` prop on another `<ProgressBar/>`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
<ProgressBar progress=count/>
|
||||
// add a second progress bar
|
||||
<ProgressBar progress=double_count/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hm... this won’t compile. It should be pretty easy to understand why: we’ve declared
|
||||
that the `progress` prop takes `ReadSignal<i32>`, and `double_count` is not
|
||||
`ReadSignal<i32>`. As rust-analyzer will tell you, its type is `|| -> i32`, i.e.,
|
||||
it’s a closure that returns an `i32`.
|
||||
|
||||
There are a couple ways to handle this. One would be to say: “Well, I know that
|
||||
a `ReadSignal` is a function, and I know that a closure is a function; maybe I
|
||||
could just take any function?” If you’re savvy, you may know that both these
|
||||
implement the trait `Fn() -> i32`. So you could use a generic component:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar<F>(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
progress: F
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> i32
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a perfectly reasonable way to write this component: `progress` now takes
|
||||
any value that implements this `Fn()` trait.
|
||||
|
||||
> Note that generic component props _cannot_ be specified inline (as `<F: Fn() -> i32>`)
|
||||
or as `progress: impl Fn() -> i32`, in part because they’re actually used to generate
|
||||
a `struct ProgressBarProps`, and struct fields cannot be `impl` types.
|
||||
|
||||
### `into` Props
|
||||
|
||||
There’s one more way we could implement this, and it would be to use `#[prop(into)]`.
|
||||
This attribute automatically calls `.into()` on the values you pass as proprs,
|
||||
which allows you to pass props of different values easily.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
is a enumerated type that represents any kind of readable reactive signal. It can
|
||||
be useful when defining APIs for components you’ll want to reuse while passing
|
||||
different sorts of signals. The [`MaybeSignal`](https://docs.rs/leptos/latest/leptos/enum.MaybeSignal.html) type is useful when you want to be able to take either a static or
|
||||
reactive value.
|
||||
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>
|
||||
) -> impl IntoView
|
||||
{
|
||||
view! { cx,
|
||||
<progress
|
||||
max=max
|
||||
value=progress
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let double_count = move || count() * 2;
|
||||
|
||||
view! { cx,
|
||||
<button on:click=move |_| { set_count.update(|n| *n += 1); }>
|
||||
"Click me"
|
||||
</button>
|
||||
// .into() converts `ReadSignal` to `Signal`
|
||||
<ProgressBar progress=count/>
|
||||
// use `Signal::derive()` to wrap a derived signal
|
||||
<ProgressBar progress=Signal::derive(cx, double_count)/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documenting Components
|
||||
|
||||
This is one of the least essential but most important sections of this book.
|
||||
It’s not strictly necessary to document your components and their props. It may
|
||||
be very important, depending on the size of your team and your app. But it’s very
|
||||
easy, and bears immediate fruit.
|
||||
|
||||
To document a component and its props, you can simply add doc comments on the
|
||||
component function, and each one of the props:
|
||||
|
||||
```rust
|
||||
/// Shows progress toward a goal.
|
||||
#[component]
|
||||
fn ProgressBar(
|
||||
cx: Scope,
|
||||
/// The maximum value of the progress bar.
|
||||
#[prop(default = 100)]
|
||||
max: u16,
|
||||
/// How much progress should be displayed.
|
||||
#[prop(into)]
|
||||
progress: Signal<i32>,
|
||||
) -> impl IntoView {
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
That’s all you need to do. These behave like ordinary Rust doc comments, except
|
||||
that you can document individual component props, which can’t be done with Rust
|
||||
function arguments.
|
||||
|
||||
This will automatically generate documentation for your component, its `Props`
|
||||
type, and each of the fields used to add props. It can be a little hard to
|
||||
understand how powerful this is until you hover over the component name or props
|
||||
and see the power of the `#[component]` macro combined with rust-analyzer here.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/3-components-50t2e7?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A7%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A7%7D%5D" width="100%" height="1000px"></iframe>
|
||||
88
docs/book/src/view/04_iteration.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Iteration
|
||||
|
||||
Whether you’re listing todos, displaying a table, or showing product images,
|
||||
iterating over a list of items is a common task in web applications. Reconciling
|
||||
the differences between changing sets of items can also be one of the trickiest
|
||||
tasks for a framework to handle well.
|
||||
|
||||
Leptos supports to two different patterns for iterating over items:
|
||||
1. For static views: `Vec<_>`
|
||||
2. For dynamic lists: `<For/>`
|
||||
|
||||
## Static Views with `Vec<_>`
|
||||
|
||||
Sometimes you need to show an item repeatedly, but the list you’re drawing from
|
||||
does not often change. In this case, it’s important to know that you can insert
|
||||
any `Vec<IV> where IV: IntoView` into your view. In other views, if you can render
|
||||
`T`, you can render `Vec<T>`.
|
||||
|
||||
```rust
|
||||
let values = vec![0, 1, 2];
|
||||
view! { cx,
|
||||
// this will just render "012"
|
||||
<p>{values.clone()}</p>
|
||||
// or we can wrap them in <li>
|
||||
<ul>
|
||||
{values.into_iter()
|
||||
.map(|n| view! { cx, <li>{n}</li>})
|
||||
.collect::<Vec<_>>()}
|
||||
</ul>
|
||||
}
|
||||
```
|
||||
|
||||
The fact that the _list_ is static doesn’t mean the interface needs to be static.
|
||||
You can render dynamic items as part of a static list.
|
||||
|
||||
```rust
|
||||
// create a list of N signals
|
||||
let counters = (1..=length).map(|idx| create_signal(cx, idx));
|
||||
|
||||
// each item manages a reactive view
|
||||
// but the list itself will never change
|
||||
let counter_buttons = counters
|
||||
.map(|(count, set_count)| {
|
||||
view! { cx,
|
||||
<li>
|
||||
<button
|
||||
on:click=move |_| set_count.update(|n| *n += 1)
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
view! { cx,
|
||||
<ul>{counter_buttons}</ul>
|
||||
}
|
||||
```
|
||||
|
||||
You _can_ render a `Fn() -> Vec<_>` reactively as well. But note that every time
|
||||
it changes, this will rerender every item in the list. This is quite inefficient!
|
||||
Fortunately, there’s a better way.
|
||||
|
||||
## Dynamic Rendering with the `<For/>` Component
|
||||
|
||||
The [`<For/>`](https://docs.rs/leptos/latest/leptos/fn.For.html) component is a
|
||||
keyed dynamic list. It takes three props:
|
||||
- `each`: a function (such as a signal) that returns the items `T` to be iterated over
|
||||
- `key`: a key function that takes `&T` and returns a stable, unique key or ID
|
||||
- `view`: renders each `T` into a view
|
||||
|
||||
`key` is, well, the key. You can add, remove, and move items within the list. As
|
||||
long as each item’s key is stable over time, the framework does not need to rerender
|
||||
any of the items, unless they are new additions, and it can very efficiently add,
|
||||
remove, and move items as they change. This allows for extremely efficient updates
|
||||
to the list as it changes, with minimal additional work.
|
||||
|
||||
Creating a good `key` can be a little tricky. You generally do _not_ want to use
|
||||
an index for this purpose, as it is not stable—if you remove or move items, their
|
||||
indices change.
|
||||
|
||||
But it’s a great idea to do something like generating a unique ID for each row as
|
||||
it is generated, and using that as an ID for the key function.
|
||||
|
||||
Check out the `<DynamicList/>` component below for an example.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/4-iteration-sglt1o?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A6%2C%22endLineNumber%22%3A55%2C%22startColumn%22%3A5%2C%22startLineNumber%22%3A31%7D%5D" width="100%" height="100px"></iframe>
|
||||
107
docs/book/src/view/05_forms.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Forms and Inputs
|
||||
|
||||
Forms and form inputs are an important part of interactive apps. There are two
|
||||
basic patterns for interacting with inputs in Leptos, which you may recognize
|
||||
if you’re familiar with React, SolidJS, or a similar framework: using **controlled**
|
||||
or **uncontrolled** inputs.
|
||||
|
||||
## Controlled Inputs
|
||||
|
||||
In a "controlled input," the framework controls the state of the input
|
||||
element. On every `input` event, it updates a local signal that holds the current
|
||||
state, which in turn updates the `value` prop of the input.
|
||||
|
||||
There are two important things to remember:
|
||||
1. The `input` event fires on (almost) every change to the element, while the
|
||||
`change` event fires (more or less) when you unfocus the input. You probably
|
||||
want `on:input`, but we give you the freedom to choose.
|
||||
2. The `value` *attribute* only sets the initial value of the input, i.e., it
|
||||
only updates the input up to the point that you begin typing. The `value`
|
||||
*property* continues updating the input after that. You usually want to set
|
||||
`prop:value` for this reason.
|
||||
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Controlled".to_string());
|
||||
|
||||
view! { cx,
|
||||
<input type="text"
|
||||
on:input=move |ev| {
|
||||
// event_target_value is a Leptos helper function
|
||||
// it functions the same way as event.target.value
|
||||
// in JavaScript, but smooths out some of the typecasting
|
||||
// necessary to make this work in Rust
|
||||
set_name(event_target_value(&ev));
|
||||
}
|
||||
|
||||
// the `prop:` syntax lets you update a DOM property,
|
||||
// rather than an attribute.
|
||||
prop:value=name
|
||||
/>
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
```
|
||||
|
||||
## Uncontrolled Inputs
|
||||
|
||||
In an "uncontrolled input," the browser controls the state of the input element.
|
||||
Rather than continuously updating a signal to hold its value, we use a
|
||||
[`NodeRef`](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to access
|
||||
the input once when we want to get its value.
|
||||
|
||||
In this example, we only notify the framework when the `<form>` fires a `submit`
|
||||
event.
|
||||
|
||||
```rust
|
||||
let (name, set_name) = create_signal(cx, "Uncontrolled".to_string());
|
||||
|
||||
let input_element: NodeRef<HtmlElement<Input>> = NodeRef::new(cx);
|
||||
```
|
||||
`NodeRef` is a kind of reactive smart pointer: we can use it to access the
|
||||
underlying DOM node. Its value will be set when the element is rendered.
|
||||
|
||||
```rust
|
||||
let on_submit = move |ev: SubmitEvent| {
|
||||
// stop the page from reloading!
|
||||
ev.prevent_default();
|
||||
|
||||
// here, we'll extract the value from the input
|
||||
let value = input_element()
|
||||
// event handlers can only fire after the view
|
||||
// is mounted to the DOM, so the `NodeRef` will be `Some`
|
||||
.expect("<input> to exist")
|
||||
// `NodeRef` implements `Deref` for the DOM element type
|
||||
// this means we can call`HtmlInputElement::value()`
|
||||
// to get the current value of the input
|
||||
.value();
|
||||
set_name(value);
|
||||
};
|
||||
```
|
||||
Our `on_submit` handler will access the input’s value and use it to call `set_name`.
|
||||
To access the DOM node stored in the `NodeRef`, we can simply call it as a function
|
||||
(or using `.get()`). This will return `Option<web_sys::HtmlInputElement>`, but we
|
||||
know it will already have been filled when we rendered the view, so it’s safe to
|
||||
unwrap here.
|
||||
|
||||
We can then call `.value()` to get the value out of the input, because `NodeRef`
|
||||
gives us access to a correctly-typed HTML element.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<form on:submit=on_submit>
|
||||
<input type="text"
|
||||
value=name
|
||||
node_ref=input_element
|
||||
/>
|
||||
<input type="submit" value="Submit"/>
|
||||
</form>
|
||||
<p>"Name is: " {name}</p>
|
||||
}
|
||||
```
|
||||
The view should be pretty self-explanatory by now. Note two things:
|
||||
1. Unlike in the controlled input example, we use `value` (not `prop:value`).
|
||||
This is because we’re just setting the initial value of the input, and letting
|
||||
the browser control its state. (We could use `prop:value` instead.)
|
||||
2. We use `node_ref` to fill the `NodeRef`. (Older examples sometimes use `_ref`.
|
||||
They are the same thing, but `node_ref` has better rust-analyzer support.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/5-form-inputs-ih9m62?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A12%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A12%7D%5D" width="100%" height="1000px"></iframe>
|
||||
283
docs/book/src/view/06_control_flow.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# Control Flow
|
||||
|
||||
In most applications, you sometimes need to make a decision: Should I render this
|
||||
part of the view, or not? Should I render `<ButtonA/>` or `<WidgetB/>`? This is
|
||||
**control flow**.
|
||||
|
||||
## A Few Tips
|
||||
|
||||
When thinking about how to do this with Leptos, it’s important to remember a few
|
||||
things:
|
||||
|
||||
1. Rust is an expression-oriented language: control-flow expressions like
|
||||
`if x() { y } else { z }` and `match x() { ... }` return their values. This
|
||||
makes them very useful for declarative user interfaces.
|
||||
2. For any `T` that implements `IntoView`—in other words, for any type that Leptos
|
||||
knows how to render—`Option<T>` and `Result<T, impl Error>` _also_ implement
|
||||
`IntoView`. And just as `Fn() -> T` renders a reactive `T`, `Fn() -> Option<T>`
|
||||
and `Fn() -> Result<T, impl Error>` are reactive.
|
||||
3. Rust has lots of handy helpers like [Option::map](https://doc.rust-lang.org/std/option/enum.Option.html#method.map),
|
||||
[Option::and_then](https://doc.rust-lang.org/std/option/enum.Option.html#method.and_then),
|
||||
[Option::ok_or](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or),
|
||||
[Result::map](https://doc.rust-lang.org/std/result/enum.Result.html#method.map),
|
||||
[Result::ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok), and
|
||||
[bool::then](https://doc.rust-lang.org/std/primitive.bool.html#method.then) that
|
||||
allow you to convert, in a declarative way, between a few different standard types,
|
||||
all of which can be rendered. Spending time in the `Option` and `Result` docs in particular
|
||||
is one of the best ways to level up your Rust game.
|
||||
4. And always remember: to be reactive, values must be functions. You’ll see me constantly
|
||||
wrap things in a `move ||` closure, below. This is to ensure that they actually re-run
|
||||
when the signal they depend on changes, keeping the UI reactive.
|
||||
|
||||
## So What?
|
||||
|
||||
To connect the dots a little: this means that you can actually implement most of
|
||||
your control flow with native Rust code, without any control-flow components or
|
||||
special knowledge.
|
||||
|
||||
For example, let’s start with a simple signal and derived signal:
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
let is_odd = move || value() & 1 == 1;
|
||||
```
|
||||
|
||||
> If you don’t recognize what’s going on with `is_odd`, don’t worry about it
|
||||
> too much. It’s just a simple way to test whether an integer is odd by doing a
|
||||
> bitwise `AND` with `1`.
|
||||
|
||||
We can use these signals and ordinary Rust to build most control flow.
|
||||
|
||||
### `if` statements
|
||||
|
||||
Let’s say I want to render some text if the number is odd, and some other text
|
||||
if it’s even. Well, how about this?
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<p>
|
||||
{move || if is_odd() {
|
||||
"Odd"
|
||||
} else {
|
||||
"Even"
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
```
|
||||
|
||||
An `if` expression returns its value, and a `&str` implements `IntoView`, so a
|
||||
`Fn() -> &str` implements `IntoView`, so this... just works!
|
||||
|
||||
### `Option<T>`
|
||||
|
||||
Let’s say we want to render some text if it’s odd, and nothing if it’s even.
|
||||
|
||||
```rust
|
||||
let message = move || {
|
||||
if is_odd() {
|
||||
Some("Ding ding ding!")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
This works fine. We can make it a little shorter if we’d like, using `bool::then()`.
|
||||
|
||||
```rust
|
||||
let message = move || is_odd().then(|| "Ding ding ding!");
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
You could even inline this if you’d like, although personally I sometimes like the
|
||||
better `cargo fmt` and `rust-analyzer` support I get by pulling things out of the `view`.
|
||||
|
||||
### `match` statements
|
||||
|
||||
We’re still just writing ordinary Rust code, right? So you have all the power of Rust’s
|
||||
pattern matching at your disposal.
|
||||
|
||||
```rust
|
||||
let message = move || {
|
||||
match value() {
|
||||
0 => "Zero",
|
||||
1 => "One",
|
||||
n if is_odd() => "Odd",
|
||||
_ => "Even"
|
||||
}
|
||||
};
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
And why not? YOLO, right?
|
||||
|
||||
## Preventing Over-Rendering
|
||||
|
||||
Not so YOLO.
|
||||
|
||||
Everything we’ve just done is basically fine. But there’s one thing you should remember
|
||||
and try to be careful with. Each one of the control-flow functions we’ve created so far
|
||||
is basically a derived signal: it will rerun every time the value changes. In the examples
|
||||
above, where the value switches from even to odd on every change, this is fine.
|
||||
|
||||
But consider the following example:
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
let message = move || if value() > 5 {
|
||||
"Big"
|
||||
} else {
|
||||
"Small"
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<p>{message}</p>
|
||||
}
|
||||
```
|
||||
|
||||
This _works_, for sure. But if you added a log, you might be surprised
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
log!("{}: rendering Big", value());
|
||||
"Big"
|
||||
} else {
|
||||
log!("{}: rendering Small", value());
|
||||
"Small"
|
||||
};
|
||||
```
|
||||
|
||||
As a user clicks a button, you’d see something like this:
|
||||
|
||||
```
|
||||
1: rendering Small
|
||||
2: rendering Small
|
||||
3: rendering Small
|
||||
4: rendering Small
|
||||
5: rendering Small
|
||||
6: rendering Big
|
||||
7: rendering Big
|
||||
8: rendering Big
|
||||
... ad infinitum
|
||||
```
|
||||
|
||||
Every time `value` changes, it reruns the `if` statement. This makes sense, with
|
||||
how reactivity works. But it has a downside. For a simple text node, rerunning
|
||||
the `if` statement and rerendering isn’t a big deal. But imagine it were
|
||||
like this:
|
||||
|
||||
```rust
|
||||
let message = move || if value() > 5 {
|
||||
<Big/>
|
||||
} else {
|
||||
<Small/>
|
||||
};
|
||||
```
|
||||
|
||||
This rerenders `<Small/>` five times, then `<Big/>` infinitely. If they’re
|
||||
loading resources, creating signals, or even just creating DOM nodes, this is
|
||||
unnecessary work.
|
||||
|
||||
The [`<Show/>`](https://docs.rs/leptos/latest/leptos/fn.Show.html) component is
|
||||
the answer. You pass it a `when` condition function, a `fallback` to be shown if
|
||||
the `when` function returns `false`, and children to be rendered if `when` is `true`.
|
||||
|
||||
```rust
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<Show
|
||||
when=move || value() > 5
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
>
|
||||
<Big/>
|
||||
</Show>
|
||||
}
|
||||
```
|
||||
|
||||
`<Show/>` memoizes the `when` condition, so it only renders its `<Small/>` once,
|
||||
continuing to show the same component until `value` is greater than five;
|
||||
then it renders `<Big/>` once, continuing to show it indefinitely.
|
||||
|
||||
This is a helpful tool to avoid rerendering when using dynamic `if` expressions.
|
||||
As always, there's some overhead: for a very simple node (like updating a single
|
||||
text node, or updating a class or attribute), a `move || if ...` will be more
|
||||
efficient. But if it’s at all expensive to render either branch, reach for
|
||||
`<Show/>`.
|
||||
|
||||
## Note: Type Conversions
|
||||
|
||||
There‘s one final thing it’s important to say in this section.
|
||||
|
||||
The `view` macro doesn’t return the most-generic wrapping type
|
||||
[`View`](https://docs.rs/leptos/latest/leptos/enum.View.html).
|
||||
Instead, it returns things with types like `Fragment` or `HtmlElement<Input>`. This
|
||||
can be a little annoying if you’re returning different HTML elements from
|
||||
different branches of a conditional:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
```
|
||||
|
||||
This strong typing is actually very powerful, because
|
||||
[`HtmlElement`](https://docs.rs/leptos/0.1.3/leptos/struct.HtmlElement.html) is,
|
||||
among other things, a smart pointer: each `HtmlElement<T>` type implements
|
||||
`Deref` for the appropriate underlying `web_sys` type. In other words, in the browser
|
||||
your `view` returns real DOM elements, and you can access native DOM methods on
|
||||
them.
|
||||
|
||||
But it can be a little annoying in conditional logic like this, because you can’t
|
||||
return different types from different branches of a condition in Rust. There are two ways
|
||||
to get yourself out of this situation:
|
||||
|
||||
1. If you have multiple `HtmlElement` types, convert them to `HtmlElement<AnyElement>`
|
||||
with [`.into_any()`](https://docs.rs/leptos/latest/leptos/struct.HtmlElement.html#method.into_any)
|
||||
2. If you have a variety of view types that are not all `HtmlElement`, convert them to
|
||||
`View`s with [`.into_view(cx)`](https://docs.rs/leptos/latest/leptos/trait.IntoView.html#tymethod.into_view).
|
||||
|
||||
Here’s the same example, with the conversion added:
|
||||
|
||||
```rust,compile_error
|
||||
view! { cx,
|
||||
<main>
|
||||
{move || match is_odd() {
|
||||
true if value() == 1 => {
|
||||
// returns HtmlElement<Pre>
|
||||
view! { cx, <pre>"One"</pre> }.into_any()
|
||||
},
|
||||
false if value() == 2 => {
|
||||
// returns HtmlElement<P>
|
||||
view! { cx, <p>"Two"</p> }.into_any()
|
||||
}
|
||||
// returns HtmlElement<Textarea>
|
||||
_ => view! { cx, <textarea>{value()}</textarea> }.into_any()
|
||||
}}
|
||||
</main>
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/6-control-flow-in-view-zttwfx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
115
docs/book/src/view/07_errors.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Error Handling
|
||||
|
||||
[In the last chapter](./06_control_flow.md), we saw that you can render `Option<T>`:
|
||||
in the `None` case, it will render nothing, and in the `T` case, it will render `T`
|
||||
(that is, if `T` implements `IntoView`). You can actually do something very similar
|
||||
with a `Result<T, E>`. In the `Err(_)` case, it will render nothing. In the `Ok(T)`
|
||||
case, it will render the `T`.
|
||||
|
||||
Let’s start with a simple component to capture a number input.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
|
||||
// when input changes, try to parse a number from the input
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
<label>
|
||||
"Type a number (or not!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
<p>
|
||||
"You entered "
|
||||
<strong>{value}</strong>
|
||||
</p>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every time you change the input, `on_input` will attempt to parse its value into a 32-bit
|
||||
integer (`i32`), and store it in our `value` signal, which is a `Result<i32, _>`. If you
|
||||
type the number `42`, the UI will display
|
||||
|
||||
```
|
||||
You entered 42
|
||||
```
|
||||
|
||||
But if you type the string`foo`, it will display
|
||||
|
||||
```
|
||||
You entered
|
||||
```
|
||||
|
||||
This is not great. It saves us using `.unwrap_or_default()` or something, but it would be
|
||||
much nicer if we could catch the error and do something with it.
|
||||
|
||||
You can do that, with the [`<ErrorBoundary/>`](https://docs.rs/leptos/latest/leptos/fn.ErrorBoundary.html)
|
||||
component.
|
||||
|
||||
## `<ErrorBoundary/>`
|
||||
|
||||
An `<ErrorBoundary/>` is a little like the `<Show/>` component we saw in the last chapter.
|
||||
If everything’s okay—which is to say, if everything is `Ok(_)`—it renders its children.
|
||||
But if there’s an `Err(_)` rendered among those children, it will trigger the
|
||||
`<ErrorBoundary/>`’s `fallback`.
|
||||
|
||||
Let’s add an `<ErrorBoundary/>` to this example.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn NumericInput(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, Ok(0));
|
||||
|
||||
let on_input = move |ev| set_value(event_target_value(&ev).parse::<i32>());
|
||||
|
||||
view! { cx,
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|cx, errors| view! { cx,
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
// we can render a list of errors as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.unwrap()
|
||||
.get()
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>"You entered " <strong>{value}</strong></p>
|
||||
</ErrorBoundary>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, if you type `42`, `value` is `Ok(42)` and you’ll see
|
||||
|
||||
```
|
||||
You entered 42
|
||||
```
|
||||
|
||||
If you type `foo`, value is `Err(_)` and the `fallback` will render. We’ve chosen to render
|
||||
the list of errors as a `String`, so you’ll see something like
|
||||
|
||||
```
|
||||
Not a number! Errors:
|
||||
- cannot parse integer from empty string
|
||||
```
|
||||
|
||||
If you fix the error, the error message will disappear and the content you’re wrapping in
|
||||
an `<ErrorBoundary/>` will appear again.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/7-error-handling-and-error-boundaries-sroncx?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D" width="100%" height="1000px"></iframe>
|
||||
286
docs/book/src/view/08_parent_child.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# Parent-Child Communication
|
||||
|
||||
You can think of your application as a nested tree of components. Each component
|
||||
handles its own local state and manages a section of the user interface, so
|
||||
components tend to be relatively self-contained.
|
||||
|
||||
Sometimes, though, you’ll want to communicate between a parent component and its
|
||||
child. For example, imagine you’ve defined a `<FancyButton/>` component that adds
|
||||
some styling, logging, or something else to a `<button/>`. You want to use a
|
||||
`<FancyButton/>` in your `<App/>` component. But how can you communicate between
|
||||
the two?
|
||||
|
||||
It’s easy to communicate state from a parent component to a child component. We
|
||||
covered some of this in the material on [components and props](./03_components.md).
|
||||
Basically if you want the parent to communicate to the child, you can pass a
|
||||
[`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html), a
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html), or even a
|
||||
[`MaybeSignal`](https://docs.rs/leptos/latest/leptos/struct.MaybeSignal.html) as a prop.
|
||||
|
||||
But what about the other direction? How can a child send notifications about events
|
||||
or state changes back up to the parent?
|
||||
|
||||
There are four basic patterns of parent-child communication in Leptos.
|
||||
|
||||
## 1. Pass a [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html)
|
||||
|
||||
One approach is simply to pass a `WriteSignal` from the parent down to the child, and update
|
||||
it in the child. This lets you manipulate the state of the parent from the child.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonA setter=set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
"Toggle"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This pattern is simple, but you should be careful with it: passing around a `WriteSignal`
|
||||
can make it hard to reason about your code. In this example, it’s pretty clear when you
|
||||
read `<App/>` that you are handing off the ability to mutate `toggled`, but it’s not at
|
||||
all clear when or how it will change. In this small, local example it’s easy to understand,
|
||||
but if you find yourself passing around `WriteSignal`s like this throughout your code,
|
||||
you should really consider whether this is making it too easy to write spaghetti code.
|
||||
|
||||
## 2. Use a Callback
|
||||
|
||||
Another approach would be to pass a callback to the child: say, `on_click`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<ButtonB on_click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonB<F>(
|
||||
cx: Scope,
|
||||
on_click: F,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn(MouseEvent) + 'static,
|
||||
{
|
||||
view! { cx,
|
||||
<button on:click=on_click>
|
||||
"Toggle"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You’ll notice that whereas `<ButtonA/>` was given a `WriteSignal` and decided how to mutate it,
|
||||
`<ButtonB/>` simply fires an event: the mutation happens back in `<App/>`. This has the advantage
|
||||
of keeping local state local, preventing the problem of spaghetti mutation. But it also means
|
||||
the logic to mutate that signal needs to exist up in `<App/>`, not down in `<ButtonB/>`. These
|
||||
are real trade-offs, not a simple right-or-wrong choice.
|
||||
|
||||
> Note the way we declare the generic type `F` here for the callback. If you’re
|
||||
> confused, look back at the [generic props](./03_components.html#generic-props) section
|
||||
> of the chapter on components.
|
||||
|
||||
## 3. Use an Event Listener
|
||||
|
||||
You can actually write Option 2 in a slightly different way. If the callback maps directly onto
|
||||
a native DOM event, you can add an `on:` listener directly to the place you use the component
|
||||
in your `view` macro in `<App/>`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
// note the on:click instead of on_click
|
||||
// this is the same syntax as an HTML element event listener
|
||||
<ButtonC on:click=move |_| set_toggled.update(|value| *value = !*value)/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[component]
|
||||
pub fn ButtonC<F>(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<button>"Toggle"</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This lets you write way less code in `<ButtonC/>` than you did for `<ButtonB/>`,
|
||||
and still gives a correctly-typed event to the listener. This works by adding an
|
||||
`on:` event listener to each element that `<ButtonC/>` returns: in this case, just
|
||||
the one `<button>`.
|
||||
|
||||
Of course, this only works for actual DOM events that you’re passing directly through
|
||||
to the elements you’re rendering in the component. For more complex logic that
|
||||
doesn’t map directly onto an element (say you create `<ValidatedForm/>` and want an
|
||||
`on_valid_form_submit` callback) you should use Option 2.
|
||||
|
||||
## 4. Providing a Context
|
||||
|
||||
This version is actually a variant on Option 1. Say you have a deeply-nested component
|
||||
tree:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
<main>
|
||||
<Content/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD<F>(cx: Scope) -> impl IntoView {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
Now `<ButtonD/>` is no longer a direct child of `<App/>`, so you can’t simply
|
||||
pass your `WriteSignal` to its props. You could do what’s sometimes called
|
||||
“prop drilling,” adding a prop to each layer between the two:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout set_toggled/>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Layout(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header>
|
||||
<h1>"My Page"</h1>
|
||||
<main>
|
||||
<Content set_toggled/>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Content(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
view! { cx,
|
||||
<div class="content">
|
||||
<ButtonD set_toggled/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD<F>(cx: Scope, set_toggled: WriteSignal<bool>) -> impl IntoView {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
This is a mess. `<Layout/>` and `<Content/>` don’t need `set_toggled`; they just
|
||||
pass it through to `<ButtonD/>`. But I need to declare the prop in triplicate.
|
||||
This is not only annoying but hard to maintain: imagine we add a “half-toggled”
|
||||
option and the type of `set_toggled` needs to change to an `enum`. We have to change
|
||||
it in three places!
|
||||
|
||||
Isn’t there some way to skip levels?
|
||||
|
||||
There is!
|
||||
|
||||
### The Context API
|
||||
|
||||
You can provide data that skips levels by using [`provide_context`](https://docs.rs/leptos/latest/leptos/fn.provide_context.html)
|
||||
and [`use_context`](https://docs.rs/leptos/latest/leptos/fn.use_context.html). Contexts are identified
|
||||
by the type of the data you provide (in this example, `WriteSignal<bool>`), and they exist in a top-down
|
||||
tree that follows the contours of your UI tree. In this example, we can use context to skip the
|
||||
unnecessary prop drilling.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
let (toggled, set_toggled) = create_signal(cx, false);
|
||||
|
||||
// share `set_toggled` with all children of this component
|
||||
provide_context(cx, set_toggled);
|
||||
|
||||
view! { cx,
|
||||
<p>"Toggled? " {toggled}</p>
|
||||
<Layout/>
|
||||
}
|
||||
}
|
||||
|
||||
// <Layout/> and <Content/> omitted
|
||||
|
||||
#[component]
|
||||
pub fn ButtonD(cx: Scope) -> impl IntoView {
|
||||
// use_context searches up the context tree, hoping to
|
||||
// find a `WriteSignal<bool>`
|
||||
// in this case, I .expect() because I know I provided it
|
||||
let setter = use_context::<WriteSignal<bool>>(cx)
|
||||
.expect("to have found the setter provided");
|
||||
|
||||
view! { cx,
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
>
|
||||
"Toggle"
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The same caveats apply to this as to `<ButtonA/>`: passing a `WriteSignal`
|
||||
around should be done with caution, as it allows you to mutate state from
|
||||
arbitrary parts of your code. But when done carefully, this can be one of
|
||||
the most effective techniques for global state management in Leptos: simply
|
||||
provide the state at the highest level you’ll need it, and use it wherever
|
||||
you need it lower down.
|
||||
|
||||
Note that there are no performance downsides to this approach. Because you
|
||||
are passing a fine-grained reactive signal, _nothing happens_ in the intervening
|
||||
components (`<Layout/>` and `<Content/>`) when you update it. You are communicating
|
||||
directly between `<ButtonD/>` and `<App/>`. In fact—and this is the power of
|
||||
fine-grained reactivity—you are communicating directly between a button click
|
||||
in `<ButtonD/>` and a single text node in `<App/>`. It’s as if the components
|
||||
themselves don’t exist at all. And, well... at runtime, they don’t. It’s just
|
||||
signals and effects, all the way down.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/8-parent-child-communication-84we8m?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A3%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A3%7D%5D" width="100%" height="1000px"></iframe>
|
||||
5
docs/book/src/view/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Building User Interfaces
|
||||
|
||||
This first section will introduce you to the basic tools you need to build a reactive
|
||||
user interface using Leptos. By the end of this section, you should be able to
|
||||
build a simple, synchronous application that is rendered in the browser.
|
||||
BIN
docs/logos/Leptos_logo_RGB.png
Executable file
|
After Width: | Height: | Size: 72 KiB |
64
docs/logos/Leptos_logo_RGB.svg
Executable file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 437.4294 209.6185" style="enable-background:new 0 0 437.4294 209.6185;" xml:space="preserve">
|
||||
<path style="fill:none;" d="M130.0327,79.3931c-11.4854-0.23-22.52,9.3486-24.5034,21.0117l49.1157,0.0293
|
||||
c-2.1729-10.418-11.1821-21.0449-24.1987-21.0449C130.3081,79.3892,130.1714,79.3907,130.0327,79.3931z"/>
|
||||
<path style="fill:#181139;" d="M95.1109,128.1089H58.6797V65.6861c0-1.5234-0.8169-2.4331-2.1855-2.4331h-3.1187
|
||||
c-1.3159,0-2.2349,1.0005-2.2349,2.4331v67.4297c0,1.4521,0.8145,2.2852,2.2349,2.2852h41.7353c1.4844,0,2.4819-0.9375,2.4819-2.333
|
||||
v-2.7744C97.5928,128.9253,96.6651,128.1089,95.1109,128.1089z"/>
|
||||
<path style="fill:#181139;" d="M146.4561,77.1739c-4.8252-3.001-10.3037-4.5249-16.2837-4.5288c-0.0068,0-0.0137,0-0.0205,0
|
||||
c-5.7349,0-11.1377,1.4639-16.0566,4.3511c-4.916,2.8853-8.8721,6.8364-11.7593,11.7456
|
||||
c-2.8975,4.9248-4.3687,10.332-4.3721,16.0713c-0.0034,5.7188,1.4966,11.0654,4.4565,15.8887
|
||||
c2.9893,4.9209,6.8789,8.7334,11.8887,11.6514c4.8657,2.8633,10.2397,4.3174,15.9717,4.3203c0.0073,0,0.0146,0,0.022,0
|
||||
c8.123,0,14.7441-2.5869,21.4683-8.3906c0.5493-0.4805,0.8516-1.1201,0.8516-1.8008c0.001-0.6074-0.1743-1.1035-0.5205-1.4756
|
||||
l-1.3569-1.8428l-0.0732-0.0859c-0.2637-0.2637-0.6929-0.6152-1.3716-0.6152c-0.6421,0-1.2549,0.2217-1.7124,0.6143
|
||||
c-1.9346,1.585-3.5459,2.8008-4.7969,3.6182c-1.7979,1.208-5.8218,3.2314-12.5986,3.2314c-0.0073,0-0.0142,0-0.021,0
|
||||
c-0.1357,0.0029-0.269,0.0039-0.4043,0.0039c-12.2642,0-23.4736-10.3262-24.5088-22.4814l53.0127,0.0322c0.0015,0,0.0024,0,0.0034,0
|
||||
c2.2373,0,3.4697-1.1621,3.4712-3.2715c0.0034-5.2588-1.3574-10.3945-4.0464-15.2705
|
||||
C155.0015,84.0953,151.2188,80.1363,146.4561,77.1739z M154.6451,100.4341l-49.1157-0.0293
|
||||
c1.9834-11.6631,13.0181-21.2417,24.5034-21.0117c0.1387-0.0024,0.2754-0.0039,0.4136-0.0039
|
||||
C143.4629,79.3892,152.4722,90.0162,154.6451,100.4341z"/>
|
||||
<path style="fill:#181139;" d="M204.0386,136.6382c5.7319,0,11.1069-1.4502,15.9746-4.3115
|
||||
c4.938-2.9014,8.75-6.7129,11.6533-11.6533c2.8608-4.8672,4.311-10.2578,4.311-16.0244c0-5.7324-1.4502-11.1064-4.311-15.9746
|
||||
c-2.9019-4.9385-6.7134-8.75-11.6533-11.6533c-4.8687-2.8618-10.2437-4.3125-15.9746-4.3125
|
||||
c-9.938,0-19.2021,4.7583-24.3516,12.3174v-9.438c0-0.5946-0.1465-1.0788-0.411-1.4511c-0.3815-0.5369-1.0157-0.834-1.8727-0.834
|
||||
h-2.6738c-1.4521,0-2.2852,0.833-2.2852,2.2852v5.6964v46.4791v23.9676c0,1.2568,0.7808,2.0371,2.0371,2.0371h3.3667
|
||||
c0.9209,0,1.6421-0.6992,1.6421-1.5908v-17.098v-10.984C185.0884,131.8892,194.2749,136.6382,204.0386,136.6382z M186.6358,122.5591
|
||||
c-4.9346-4.9346-7.6831-11.4932-7.542-18.0254c-0.1367-6.3506,2.5439-12.751,7.3545-17.5605
|
||||
c4.8521-4.8521,11.3037-7.5547,17.7383-7.417c4.3691,0,8.4863,1.1465,12.2314,3.4043c3.7344,2.2979,6.7456,5.4053,8.9492,9.2354
|
||||
c2.1699,3.9072,3.2695,8.0967,3.2695,12.4697c0.1396,6.4619-2.5967,12.9844-7.5083,17.8955
|
||||
c-4.7617,4.7617-11.0469,7.3857-17.2544,7.2803C197.6856,129.9712,191.396,127.3208,186.6358,122.5591z"/>
|
||||
<path style="fill:#181139;" d="M241.8955,80.3975h7.5669v42.0259c0,6.8174,4.5674,12.1309,11.0825,12.9189
|
||||
c0.6836,0.1055,1.8379,0.1572,3.5303,0.1572c2.0078,0,3.0273-0.3535,3.0273-2.2842v-2.377c0-1.7891-1.334-2.0371-2.7568-2.0371
|
||||
c0,0-0.001,0-0.002,0l-1.7871-0.0488c-2.0117-0.0439-3.4883-0.7627-4.3896-2.1367c-0.9697-1.4805-1.4619-3.1738-1.4619-5.0352
|
||||
V80.3975h10.0928c1.3076,0,2.2852-1.3628,2.2852-2.5815v-1.9312c0-1.3999-0.8359-2.2354-2.2354-2.2354h-10.1426V60.6861
|
||||
c0-1.4619-0.7969-2.4829-1.9375-2.4829c-0.1865,0-0.4121,0-0.6392,0.0884l-2.6489,0.6865
|
||||
c-1.2109,0.3682-2.0171,0.9263-2.0171,2.4507v12.2207h-7.5669c-1.4185,0-2.335,0.897-2.335,2.2852v1.8813
|
||||
C239.5606,79.2393,240.6079,80.3975,241.8955,80.3975z"/>
|
||||
<path style="fill:#181139;" d="M379.1182,106.2691c-4.0488-2.9219-8.8545-5.0293-14.291-6.2646
|
||||
c-6.5049-1.3975-13.4473-5.2129-13.3203-10.3066c0-7.5225,6.6367-10.1914,12.3203-10.1914c5.3574,0,10.2207,3.002,13.001,8.0146
|
||||
c0.6729,1.2861,1.4785,1.9375,2.3955,1.9375c0.3311,0,0.7061-0.1113,0.9922-0.2832l2.2021-1.1523
|
||||
c0.5947-0.3408,0.9229-0.9414,0.9229-1.6924c0-0.5205-0.0908-0.9541-0.2617-1.292c-3.6367-8.2466-10.0967-12.4282-19.2021-12.4282
|
||||
c-11.7305,0-19.6123,6.9263-19.6123,17.2349c0,4.3125,1.8438,7.9746,5.4756,10.8809c3.4482,2.7979,7.9121,4.8623,13.2705,6.1377
|
||||
c4.5859,1.085,8.3193,2.5654,11.0977,4.4023c1.4159,0.9354,2.4412,2.0535,3.106,3.3672c0.6053,1.1962,0.9135,2.5535,0.9135,4.1005
|
||||
c0.0742,2.3857-0.79,4.5176-2.5684,6.3389c-3.1445,3.2178-8.4053,4.6689-12.0205,4.6689c-0.0361,0-0.0723,0-0.1074,0
|
||||
c-3.4268,0-6.4893-0.8438-9.1035-2.5068c-2.5918-1.6484-4.2363-3.8076-5.0293-6.6064c-0.3203-1.0996-0.751-2.1738-2.1553-2.1738
|
||||
c-0.0742,0-0.2109,0.0146-0.4062,0.0449c-0.1133,0.0166-0.2559,0.0381-0.5088,0.0742l-1.8818,0.4463l-0.1045,0.0332
|
||||
c-1.0244,0.4082-1.6113,1.1846-1.6113,2.1309c0,0.2285,0.0625,0.6592,0.2178,1.1094c1.9707,8.5801,10.2432,14.3447,20.5732,14.3447
|
||||
c0.125,0.002,0.249,0.002,0.374,0.002c6.5947,0,12.6748-2.3193,16.7275-6.3945c3.1895-3.208,4.8311-7.2363,4.748-11.6357
|
||||
c0-2.8187-0.6185-5.3109-1.8062-7.481C382.4437,109.2624,381.0062,107.631,379.1182,106.2691z"/>
|
||||
<path style="fill:#EF3939;" d="M348.9043,45.7325c0-6.3157-3.2826-11.8699-8.2238-15.0756
|
||||
c-2.811-1.8237-6.1537-2.8947-9.7469-2.8947c-9.9092,0-17.9707,8.0615-17.9707,17.9702c0,4.7659,1.8775,9.0925,4.9157,12.3123
|
||||
c-3.6619,4.3709-6.6334,9.3336-8.7663,14.7186c-1.5873-0.2422-3.2123-0.3683-4.8662-0.3683
|
||||
c-17.7158,0-32.1289,14.4131-32.1289,32.1289c0,14.6854,9.9077,27.0922,23.3869,30.9101
|
||||
c-6.7762,17.3461-23.6572,29.6719-43.3742,29.6719c-16.8195,0-31.583-8.9662-39.7656-22.369
|
||||
c-2.4778,0.5446-5.0429,0.8519-7.6721,0.9023c9.0226,16.99,26.8969,28.5917,47.4377,28.5917
|
||||
c23.2646,0,43.1121-14.8788,50.5461-35.6179c0.5204,0.0251,1.0435,0.0398,1.5701,0.0398c17.7158,0,32.1289-14.4131,32.1289-32.1289
|
||||
c0-13.557-8.4446-25.1712-20.3465-29.8811c1.9001-4.5678,4.5115-8.7646,7.6888-12.4641c0.9996,0.4404,2.0479,0.785,3.1324,1.0384
|
||||
c1.3144,0.3071,2.6773,0.486,4.0839,0.486C340.8428,63.7032,348.9043,55.6416,348.9043,45.7325z M304.2461,129.5279
|
||||
c-13.7871,0-25.0039-11.2168-25.0039-25.0039s11.2168-25.0039,25.0039-25.0039S329.25,90.7369,329.25,104.524
|
||||
S318.0332,129.5279,304.2461,129.5279z M330.9336,34.8872c0.645,0,1.2737,0.0671,1.8881,0.1755
|
||||
c5.0818,0.8974,8.9576,5.3347,8.9576,10.6697c0,5.9805-4.8652,10.8457-10.8457,10.8457s-10.8457-4.8652-10.8457-10.8457
|
||||
c0-1.3967,0.2746-2.7282,0.7576-3.9555C322.4306,37.7496,326.35,34.8872,330.9336,34.8872z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
61
docs/logos/Leptos_logo_Solid_Black.svg
Executable file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 437.4294 209.6185" style="enable-background:new 0 0 437.4294 209.6185;" xml:space="preserve">
|
||||
<path d="M95.1109,128.1089H58.6797V65.6861c0-1.5234-0.8169-2.4331-2.1855-2.4331h-3.1187c-1.3159,0-2.2349,1.0005-2.2349,2.4331
|
||||
v67.4297c0,1.4521,0.8145,2.2852,2.2349,2.2852h41.7353c1.4844,0,2.4819-0.9375,2.4819-2.333v-2.7744
|
||||
C97.5928,128.9253,96.6651,128.1089,95.1109,128.1089z"/>
|
||||
<path d="M146.4561,77.1739c-4.8252-3.001-10.3037-4.5249-16.2837-4.5288c-0.0068,0-0.0137,0-0.0205,0
|
||||
c-5.7349,0-11.1377,1.4639-16.0566,4.3511c-4.916,2.8853-8.8721,6.8364-11.7593,11.7456
|
||||
c-2.8975,4.9248-4.3687,10.332-4.3721,16.0713c-0.0034,5.7188,1.4966,11.0654,4.4565,15.8887
|
||||
c2.9893,4.9209,6.8789,8.7334,11.8887,11.6514c4.8657,2.8633,10.2397,4.3174,15.9717,4.3203c0.0073,0,0.0146,0,0.022,0
|
||||
c8.123,0,14.7441-2.5869,21.4683-8.3906c0.5493-0.4805,0.8516-1.1201,0.8516-1.8008c0.001-0.6074-0.1743-1.1035-0.5205-1.4756
|
||||
l-1.3569-1.8428l-0.0732-0.0859c-0.2637-0.2637-0.6929-0.6152-1.3716-0.6152c-0.6421,0-1.2549,0.2217-1.7124,0.6143
|
||||
c-1.9346,1.585-3.5459,2.8008-4.7969,3.6182c-1.7979,1.208-5.8218,3.2314-12.5986,3.2314c-0.0073,0-0.0142,0-0.021,0
|
||||
c-0.1357,0.0029-0.269,0.0039-0.4043,0.0039c-12.2642,0-23.4736-10.3262-24.5088-22.4814l53.0127,0.0322c0.0015,0,0.0024,0,0.0034,0
|
||||
c2.2373,0,3.4697-1.1621,3.4712-3.2715c0.0034-5.2588-1.3574-10.3945-4.0464-15.2705
|
||||
C155.0015,84.0953,151.2188,80.1363,146.4561,77.1739z M154.6451,100.4341l-49.1157-0.0293
|
||||
c1.9834-11.6631,13.0181-21.2417,24.5034-21.0117c0.1387-0.0024,0.2754-0.0039,0.4136-0.0039
|
||||
C143.4629,79.3892,152.4722,90.0162,154.6451,100.4341z"/>
|
||||
<path d="M204.0386,136.6382c5.7319,0,11.1069-1.4502,15.9746-4.3115c4.938-2.9014,8.75-6.7129,11.6533-11.6533
|
||||
c2.8608-4.8672,4.311-10.2578,4.311-16.0244c0-5.7324-1.4502-11.1064-4.311-15.9746c-2.9019-4.9385-6.7134-8.75-11.6533-11.6533
|
||||
c-4.8687-2.8618-10.2437-4.3125-15.9746-4.3125c-9.938,0-19.2021,4.7583-24.3516,12.3174v-9.438
|
||||
c0-0.5946-0.1465-1.0788-0.411-1.4511c-0.3815-0.5369-1.0157-0.834-1.8727-0.834h-2.6738c-1.4521,0-2.2852,0.833-2.2852,2.2852
|
||||
v5.6964v46.4791v23.9676c0,1.2568,0.7808,2.0371,2.0371,2.0371h3.3667c0.9209,0,1.6421-0.6992,1.6421-1.5908v-17.098v-10.984
|
||||
C185.0884,131.8892,194.2749,136.6382,204.0386,136.6382z M186.6358,122.5591c-4.9346-4.9346-7.6831-11.4932-7.542-18.0254
|
||||
c-0.1367-6.3506,2.5439-12.751,7.3545-17.5605c4.8521-4.8521,11.3037-7.5547,17.7383-7.417c4.3691,0,8.4863,1.1465,12.2314,3.4043
|
||||
c3.7344,2.2979,6.7456,5.4053,8.9492,9.2354c2.1699,3.9072,3.2695,8.0967,3.2695,12.4697
|
||||
c0.1396,6.4619-2.5967,12.9844-7.5083,17.8955c-4.7617,4.7617-11.0469,7.3857-17.2544,7.2803
|
||||
C197.6856,129.9712,191.396,127.3208,186.6358,122.5591z"/>
|
||||
<path d="M241.8955,80.3975h7.5669v42.0259c0,6.8174,4.5674,12.1309,11.0825,12.9189c0.6836,0.1055,1.8379,0.1572,3.5303,0.1572
|
||||
c2.0078,0,3.0273-0.3535,3.0273-2.2842v-2.377c0-1.7891-1.334-2.0371-2.7568-2.0371c0,0-0.001,0-0.002,0l-1.7871-0.0488
|
||||
c-2.0117-0.0439-3.4883-0.7627-4.3896-2.1367c-0.9697-1.4805-1.4619-3.1738-1.4619-5.0352V80.3975h10.0928
|
||||
c1.3076,0,2.2852-1.3628,2.2852-2.5815v-1.9312c0-1.3999-0.8359-2.2354-2.2354-2.2354h-10.1426V60.6861
|
||||
c0-1.4619-0.7969-2.4829-1.9375-2.4829c-0.1865,0-0.4121,0-0.6392,0.0884l-2.6489,0.6865
|
||||
c-1.2109,0.3682-2.0171,0.9263-2.0171,2.4507v12.2207h-7.5669c-1.4185,0-2.335,0.897-2.335,2.2852v1.8813
|
||||
C239.5606,79.2393,240.6079,80.3975,241.8955,80.3975z"/>
|
||||
<path d="M379.1182,106.2691c-4.0488-2.9219-8.8545-5.0293-14.291-6.2646c-6.5049-1.3975-13.4473-5.2129-13.3203-10.3066
|
||||
c0-7.5225,6.6367-10.1914,12.3203-10.1914c5.3574,0,10.2207,3.002,13.001,8.0146c0.6729,1.2861,1.4785,1.9375,2.3955,1.9375
|
||||
c0.3311,0,0.7061-0.1113,0.9922-0.2832l2.2021-1.1523c0.5947-0.3408,0.9229-0.9414,0.9229-1.6924
|
||||
c0-0.5205-0.0908-0.9541-0.2617-1.292c-3.6367-8.2466-10.0967-12.4282-19.2021-12.4282c-11.7305,0-19.6123,6.9263-19.6123,17.2349
|
||||
c0,4.3125,1.8438,7.9746,5.4756,10.8809c3.4482,2.7979,7.9121,4.8623,13.2705,6.1377c4.5859,1.085,8.3193,2.5654,11.0977,4.4023
|
||||
c1.4159,0.9354,2.4412,2.0535,3.106,3.3672c0.6053,1.1962,0.9135,2.5535,0.9135,4.1005c0.0742,2.3857-0.79,4.5176-2.5684,6.3389
|
||||
c-3.1445,3.2178-8.4053,4.6689-12.0205,4.6689c-0.0361,0-0.0723,0-0.1074,0c-3.4268,0-6.4893-0.8438-9.1035-2.5068
|
||||
c-2.5918-1.6484-4.2363-3.8076-5.0293-6.6064c-0.3203-1.0996-0.751-2.1738-2.1553-2.1738c-0.0742,0-0.2109,0.0146-0.4062,0.0449
|
||||
c-0.1133,0.0166-0.2559,0.0381-0.5088,0.0742l-1.8818,0.4463l-0.1045,0.0332c-1.0244,0.4082-1.6113,1.1846-1.6113,2.1309
|
||||
c0,0.2285,0.0625,0.6592,0.2178,1.1094c1.9707,8.5801,10.2432,14.3447,20.5732,14.3447c0.125,0.002,0.249,0.002,0.374,0.002
|
||||
c6.5947,0,12.6748-2.3193,16.7275-6.3945c3.1895-3.208,4.8311-7.2363,4.748-11.6357c0-2.8187-0.6185-5.3109-1.8062-7.481
|
||||
C382.4437,109.2624,381.0062,107.631,379.1182,106.2691z"/>
|
||||
<path d="M348.9043,45.7325c0-6.3157-3.2826-11.8699-8.2238-15.0756c-2.811-1.8237-6.1537-2.8947-9.7469-2.8947
|
||||
c-9.9092,0-17.9707,8.0615-17.9707,17.9702c0,4.7659,1.8775,9.0925,4.9157,12.3123c-3.6619,4.3709-6.6334,9.3336-8.7663,14.7186
|
||||
c-1.5873-0.2422-3.2123-0.3683-4.8662-0.3683c-17.7158,0-32.1289,14.4131-32.1289,32.1289c0,14.6854,9.9077,27.0922,23.3869,30.9101
|
||||
c-6.7762,17.3461-23.6572,29.6719-43.3742,29.6719c-16.8195,0-31.583-8.9662-39.7656-22.369
|
||||
c-2.4778,0.5446-5.0429,0.8519-7.6721,0.9023c9.0226,16.99,26.8969,28.5917,47.4377,28.5917
|
||||
c23.2646,0,43.1121-14.8788,50.5461-35.6179c0.5204,0.0251,1.0435,0.0398,1.5701,0.0398c17.7158,0,32.1289-14.4131,32.1289-32.1289
|
||||
c0-13.557-8.4446-25.1712-20.3465-29.8811c1.9001-4.5678,4.5115-8.7646,7.6888-12.4641c0.9996,0.4404,2.0479,0.785,3.1324,1.0384
|
||||
c1.3144,0.3071,2.6773,0.486,4.0839,0.486C340.8428,63.7032,348.9043,55.6416,348.9043,45.7325z M304.2461,129.5279
|
||||
c-13.7871,0-25.0039-11.2168-25.0039-25.0039s11.2168-25.0039,25.0039-25.0039S329.25,90.7369,329.25,104.524
|
||||
S318.0332,129.5279,304.2461,129.5279z M330.9336,34.8872c0.645,0,1.2737,0.0671,1.8881,0.1755
|
||||
c5.0818,0.8974,8.9576,5.3347,8.9576,10.6697c0,5.9805-4.8652,10.8457-10.8457,10.8457s-10.8457-4.8652-10.8457-10.8457
|
||||
c0-1.3967,0.2746-2.7282,0.7576-3.9555C322.4306,37.7496,326.35,34.8872,330.9336,34.8872z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
62
docs/logos/Leptos_logo_Solid_White.svg
Executable file
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 437.4294 209.6185" style="enable-background:new 0 0 437.4294 209.6185;" xml:space="preserve">
|
||||
<path style="fill:#FFFFFF;" d="M95.1109,128.1089H58.6797V65.6861c0-1.5234-0.8169-2.4331-2.1855-2.4331h-3.1187
|
||||
c-1.3159,0-2.2349,1.0005-2.2349,2.4331v67.4297c0,1.4521,0.8145,2.2852,2.2349,2.2852h41.7353c1.4844,0,2.4819-0.9375,2.4819-2.333
|
||||
v-2.7744C97.5928,128.9253,96.6651,128.1089,95.1109,128.1089z"/>
|
||||
<path style="fill:#FFFFFF;" d="M146.4561,77.1739c-4.8252-3.001-10.3037-4.5249-16.2837-4.5288c-0.0068,0-0.0137,0-0.0205,0
|
||||
c-5.7349,0-11.1377,1.4639-16.0566,4.3511c-4.916,2.8853-8.8721,6.8364-11.7593,11.7456
|
||||
c-2.8975,4.9248-4.3687,10.332-4.3721,16.0713c-0.0034,5.7188,1.4966,11.0654,4.4565,15.8887
|
||||
c2.9893,4.9209,6.8789,8.7334,11.8887,11.6514c4.8657,2.8633,10.2397,4.3174,15.9717,4.3203c0.0073,0,0.0146,0,0.022,0
|
||||
c8.123,0,14.7441-2.5869,21.4683-8.3906c0.5493-0.4805,0.8516-1.1201,0.8516-1.8008c0.001-0.6074-0.1743-1.1035-0.5205-1.4756
|
||||
l-1.3569-1.8428l-0.0732-0.0859c-0.2637-0.2637-0.6929-0.6152-1.3716-0.6152c-0.6421,0-1.2549,0.2217-1.7124,0.6143
|
||||
c-1.9346,1.585-3.5459,2.8008-4.7969,3.6182c-1.7979,1.208-5.8218,3.2314-12.5986,3.2314c-0.0073,0-0.0142,0-0.021,0
|
||||
c-0.1357,0.0029-0.269,0.0039-0.4043,0.0039c-12.2642,0-23.4736-10.3262-24.5088-22.4814l53.0127,0.0322c0.0015,0,0.0024,0,0.0034,0
|
||||
c2.2373,0,3.4697-1.1621,3.4712-3.2715c0.0034-5.2588-1.3574-10.3945-4.0464-15.2705
|
||||
C155.0015,84.0953,151.2188,80.1363,146.4561,77.1739z M154.6451,100.4341l-49.1157-0.0293
|
||||
c1.9834-11.6631,13.0181-21.2417,24.5034-21.0117c0.1387-0.0024,0.2754-0.0039,0.4136-0.0039
|
||||
C143.4629,79.3892,152.4722,90.0162,154.6451,100.4341z"/>
|
||||
<path style="fill:#FFFFFF;" d="M204.0386,136.6382c5.7319,0,11.1069-1.4502,15.9746-4.3115
|
||||
c4.938-2.9014,8.75-6.7129,11.6533-11.6533c2.8608-4.8672,4.311-10.2578,4.311-16.0244c0-5.7324-1.4502-11.1064-4.311-15.9746
|
||||
c-2.9019-4.9385-6.7134-8.75-11.6533-11.6533c-4.8687-2.8618-10.2437-4.3125-15.9746-4.3125
|
||||
c-9.938,0-19.2021,4.7583-24.3516,12.3174v-9.438c0-0.5946-0.1465-1.0788-0.411-1.4511c-0.3815-0.5369-1.0157-0.834-1.8727-0.834
|
||||
h-2.6738c-1.4521,0-2.2852,0.833-2.2852,2.2852v5.6964v46.4791v23.9676c0,1.2568,0.7808,2.0371,2.0371,2.0371h3.3667
|
||||
c0.9209,0,1.6421-0.6992,1.6421-1.5908v-17.098v-10.984C185.0884,131.8892,194.2749,136.6382,204.0386,136.6382z M186.6358,122.5591
|
||||
c-4.9346-4.9346-7.6831-11.4932-7.542-18.0254c-0.1367-6.3506,2.5439-12.751,7.3545-17.5605
|
||||
c4.8521-4.8521,11.3037-7.5547,17.7383-7.417c4.3691,0,8.4863,1.1465,12.2314,3.4043c3.7344,2.2979,6.7456,5.4053,8.9492,9.2354
|
||||
c2.1699,3.9072,3.2695,8.0967,3.2695,12.4697c0.1396,6.4619-2.5967,12.9844-7.5083,17.8955
|
||||
c-4.7617,4.7617-11.0469,7.3857-17.2544,7.2803C197.6856,129.9712,191.396,127.3208,186.6358,122.5591z"/>
|
||||
<path style="fill:#FFFFFF;" d="M241.8955,80.3975h7.5669v42.0259c0,6.8174,4.5674,12.1309,11.0825,12.9189
|
||||
c0.6836,0.1055,1.8379,0.1572,3.5303,0.1572c2.0078,0,3.0273-0.3535,3.0273-2.2842v-2.377c0-1.7891-1.334-2.0371-2.7568-2.0371
|
||||
c0,0-0.001,0-0.002,0l-1.7871-0.0488c-2.0117-0.0439-3.4883-0.7627-4.3896-2.1367c-0.9697-1.4805-1.4619-3.1738-1.4619-5.0352
|
||||
V80.3975h10.0928c1.3076,0,2.2852-1.3628,2.2852-2.5815v-1.9312c0-1.3999-0.8359-2.2354-2.2354-2.2354h-10.1426V60.6861
|
||||
c0-1.4619-0.7969-2.4829-1.9375-2.4829c-0.1865,0-0.4121,0-0.6392,0.0884l-2.6489,0.6865
|
||||
c-1.2109,0.3682-2.0171,0.9263-2.0171,2.4507v12.2207h-7.5669c-1.4185,0-2.335,0.897-2.335,2.2852v1.8813
|
||||
C239.5606,79.2393,240.6079,80.3975,241.8955,80.3975z"/>
|
||||
<path style="fill:#FFFFFF;" d="M379.1182,106.2691c-4.0488-2.9219-8.8545-5.0293-14.291-6.2646
|
||||
c-6.5049-1.3975-13.4473-5.2129-13.3203-10.3066c0-7.5225,6.6367-10.1914,12.3203-10.1914c5.3574,0,10.2207,3.002,13.001,8.0146
|
||||
c0.6729,1.2861,1.4785,1.9375,2.3955,1.9375c0.3311,0,0.7061-0.1113,0.9922-0.2832l2.2021-1.1523
|
||||
c0.5947-0.3408,0.9229-0.9414,0.9229-1.6924c0-0.5205-0.0908-0.9541-0.2617-1.292c-3.6367-8.2466-10.0967-12.4282-19.2021-12.4282
|
||||
c-11.7305,0-19.6123,6.9263-19.6123,17.2349c0,4.3125,1.8438,7.9746,5.4756,10.8809c3.4482,2.7979,7.9121,4.8623,13.2705,6.1377
|
||||
c4.5859,1.085,8.3193,2.5654,11.0977,4.4023c1.4159,0.9354,2.4412,2.0535,3.106,3.3672c0.6053,1.1962,0.9135,2.5535,0.9135,4.1005
|
||||
c0.0742,2.3857-0.79,4.5176-2.5684,6.3389c-3.1445,3.2178-8.4053,4.6689-12.0205,4.6689c-0.0361,0-0.0723,0-0.1074,0
|
||||
c-3.4268,0-6.4893-0.8438-9.1035-2.5068c-2.5918-1.6484-4.2363-3.8076-5.0293-6.6064c-0.3203-1.0996-0.751-2.1738-2.1553-2.1738
|
||||
c-0.0742,0-0.2109,0.0146-0.4062,0.0449c-0.1133,0.0166-0.2559,0.0381-0.5088,0.0742l-1.8818,0.4463l-0.1045,0.0332
|
||||
c-1.0244,0.4082-1.6113,1.1846-1.6113,2.1309c0,0.2285,0.0625,0.6592,0.2178,1.1094c1.9707,8.5801,10.2432,14.3447,20.5732,14.3447
|
||||
c0.125,0.002,0.249,0.002,0.374,0.002c6.5947,0,12.6748-2.3193,16.7275-6.3945c3.1895-3.208,4.8311-7.2363,4.748-11.6357
|
||||
c0-2.8187-0.6185-5.3109-1.8062-7.481C382.4437,109.2624,381.0062,107.631,379.1182,106.2691z"/>
|
||||
<path style="fill:#FFFFFF;" d="M348.9043,45.7325c0-6.3157-3.2826-11.8699-8.2238-15.0756
|
||||
c-2.811-1.8237-6.1537-2.8947-9.7469-2.8947c-9.9092,0-17.9707,8.0615-17.9707,17.9702c0,4.7659,1.8775,9.0925,4.9157,12.3123
|
||||
c-3.6619,4.3709-6.6334,9.3336-8.7663,14.7186c-1.5873-0.2422-3.2123-0.3683-4.8662-0.3683
|
||||
c-17.7158,0-32.1289,14.4131-32.1289,32.1289c0,14.6854,9.9077,27.0922,23.3869,30.9101
|
||||
c-6.7762,17.3461-23.6572,29.6719-43.3742,29.6719c-16.8195,0-31.583-8.9662-39.7656-22.369
|
||||
c-2.4778,0.5446-5.0429,0.8519-7.6721,0.9023c9.0226,16.99,26.8969,28.5917,47.4377,28.5917
|
||||
c23.2646,0,43.1121-14.8788,50.5461-35.6179c0.5204,0.0251,1.0435,0.0398,1.5701,0.0398c17.7158,0,32.1289-14.4131,32.1289-32.1289
|
||||
c0-13.557-8.4446-25.1712-20.3465-29.8811c1.9001-4.5678,4.5115-8.7646,7.6888-12.4641c0.9996,0.4404,2.0479,0.785,3.1324,1.0384
|
||||
c1.3144,0.3071,2.6773,0.486,4.0839,0.486C340.8428,63.7032,348.9043,55.6416,348.9043,45.7325z M304.2461,129.5279
|
||||
c-13.7871,0-25.0039-11.2168-25.0039-25.0039s11.2168-25.0039,25.0039-25.0039S329.25,90.7369,329.25,104.524
|
||||
S318.0332,129.5279,304.2461,129.5279z M330.9336,34.8872c0.645,0,1.2737,0.0671,1.8881,0.1755
|
||||
c5.0818,0.8974,8.9576,5.3347,8.9576,10.6697c0,5.9805-4.8652,10.8457-10.8457,10.8457s-10.8457-4.8652-10.8457-10.8457
|
||||
c0-1.3967,0.2746-2.7282,0.7576-3.9555C322.4306,37.7496,326.35,34.8872,330.9336,34.8872z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
BIN
docs/logos/Leptos_logo_abbreviation__circle_RGB.png
Executable file
|
After Width: | Height: | Size: 38 KiB |
37
docs/logos/Leptos_logo_abbreviation__circle_RGB.svg
Executable file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 115.9988 115.9988" style="enable-background:new 0 0 115.9988 115.9988;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path style="fill:#180D38;" d="M29.1281,108.2941c9.5736-4.5548,17.1531-12.6456,21.0335-22.5787
|
||||
c-12.0865-3.4232-20.9707-14.548-20.9707-27.7159c0-15.8849,12.9236-28.8085,28.8085-28.8085
|
||||
c1.4832,0,2.9404,0.113,4.3639,0.3303c1.9125-4.8287,4.5771-9.2786,7.8607-13.1979c-2.7243-2.8871-4.4077-6.7665-4.4077-11.0399
|
||||
c0-1.6191,0.2457-3.1808,0.6921-4.6562C63.7305,0.2186,60.8908,0,57.9995,0C25.9672,0,0,25.9672,0,57.9994
|
||||
C0,79.5165,11.7263,98.2828,29.1281,108.2941z"/>
|
||||
<path style="fill:#EF3939;" d="M81.9297,15.0082c3.6788,0,6.886-2.0536,8.5379-5.0742
|
||||
c-5.3185-3.5997-11.2684-6.3339-17.646-8.0151c-0.3903,1.0504-0.6168,2.1798-0.6168,3.3644
|
||||
C72.2049,10.6458,76.5673,15.0082,81.9297,15.0082z"/>
|
||||
<path style="fill:#180D38;" d="M95.5663,13.828c-2.8537,4.5375-7.8918,7.5688-13.6366,7.5688
|
||||
c-1.2614,0-2.4835-0.1604-3.6622-0.4359c-0.9722-0.2272-1.9121-0.5362-2.8083-0.931c-2.8492,3.3173-5.1907,7.0806-6.8945,11.1766
|
||||
c10.6715,4.2233,18.2432,14.6371,18.2432,26.7928c0,15.8849-12.9235,28.8085-28.8085,28.8085
|
||||
c-0.4718,0-0.9406-0.0131-1.4069-0.0357c-3.7532,10.4704-11.0354,19.2744-20.406,24.9696
|
||||
c6.7355,2.7367,14.0948,4.257,21.8129,4.257c32.0322,0,57.9994-25.9672,57.9994-57.9995
|
||||
C115.9988,40.3018,108.0628,24.4664,95.5663,13.828z"/>
|
||||
<circle style="fill:#EF3939;" cx="57.9994" cy="57.9994" r="22.4198"/>
|
||||
</g>
|
||||
<path style="fill:#FFFFFF;" d="M78.2676,20.961c1.1786,0.2755,2.4008,0.4359,3.6622,0.4359
|
||||
c5.7448,0,10.7829-3.0313,13.6366-7.5688c-1.6275-1.3855-3.3236-2.6925-5.0987-3.894c-1.6519,3.0206-4.8591,5.0742-8.5379,5.0742
|
||||
c-5.3624,0-9.7249-4.3624-9.7249-9.7249c0-1.1846,0.2264-2.3141,0.6168-3.3644c-2.062-0.5436-4.1682-0.9763-6.3133-1.2917
|
||||
c-0.4464,1.4753-0.6921,3.0371-0.6921,4.6562c0,4.2734,1.6834,8.1528,4.4077,11.0399c-3.2836,3.9193-5.9482,8.3692-7.8607,13.1979
|
||||
c-1.4235-0.2172-2.8807-0.3303-4.3639-0.3303c-15.8849,0-28.8085,12.9235-28.8085,28.8085
|
||||
c0,13.168,8.8842,24.2928,20.9707,27.7159c-3.8804,9.9332-11.4599,18.0239-21.0335,22.5787
|
||||
c2.2621,1.3013,4.6175,2.456,7.0584,3.4478c9.3706-5.6952,16.6528-14.4992,20.406-24.9696
|
||||
c0.4663,0.0226,0.9351,0.0357,1.4069,0.0357c15.8849,0,28.8085-12.9236,28.8085-28.8085c0-12.1557-7.5717-22.5695-18.2432-26.7928
|
||||
c1.7038-4.0959,4.0453-7.8593,6.8945-11.1766C76.3555,20.4248,77.2953,20.7338,78.2676,20.961z M80.4193,57.9994
|
||||
c0,12.3623-10.0576,22.4199-22.4198,22.4199c-12.3623,0-22.4199-10.0576-22.4199-22.4199
|
||||
c0-12.3622,10.0576-22.4199,22.4199-22.4199C70.3617,35.5795,80.4193,45.6371,80.4193,57.9994z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
BIN
docs/logos/Leptos_logo_abbreviation__square_RGB.png
Executable file
|
After Width: | Height: | Size: 27 KiB |
27
docs/logos/Leptos_logo_abbreviation__square_RGB.svg
Executable file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 115.5026 115.5026" style="enable-background:new 0 0 115.5026 115.5026;" xml:space="preserve">
|
||||
<path style="fill:#181139;" d="M115.5026,0h-13.957c0.0002,0.0315,0.0031,0.0623,0.0031,0.0938
|
||||
c0,9.718-7.9059,17.6239-17.6239,17.6239c-1.3796,0-2.7163-0.1754-4.0055-0.4767c-1.0634-0.2485-2.0913-0.5864-3.0715-1.0182
|
||||
c-3.1162,3.6283-5.6772,7.7443-7.5408,12.2242c11.6719,4.6192,19.9532,16.0091,19.9532,29.3043
|
||||
c0,17.374-14.1349,31.5089-31.5089,31.5089c-0.5161,0-1.0288-0.0143-1.5388-0.039c-3.8856,10.8397-11.2302,20.0454-20.6959,26.2814
|
||||
h79.986V0z"/>
|
||||
<circle style="fill:#EF3939;" cx="57.7513" cy="57.7513" r="24.5214"/>
|
||||
<path style="fill:#181139;" d="M49.1788,88.0652c-13.2195-3.744-22.9364-15.9116-22.9364-30.3139
|
||||
c0-17.374,14.1349-31.5089,31.5089-31.5089c1.6223,0,3.2161,0.1237,4.7729,0.3612c2.0918-5.2813,5.0061-10.1484,8.5975-14.4351
|
||||
c-2.9796-3.1577-4.8209-7.4008-4.8209-12.0747c0-0.0317,0.0046-0.0622,0.0048-0.0938H0v115.5026h18.8623
|
||||
C32.7495,111.6378,43.9877,101.3537,49.1788,88.0652z"/>
|
||||
<path style="fill:#EF3939;" d="M83.9248,10.7302c5.8651,0,10.6364-4.7714,10.6364-10.6364c0-0.0316-0.004-0.0623-0.0043-0.0938
|
||||
H73.293c-0.0003,0.0316-0.0046,0.0621-0.0046,0.0938C73.2884,5.9589,78.0598,10.7302,83.9248,10.7302z"/>
|
||||
<path style="fill:#FFFFFF;" d="M56.2125,89.2212c0.51,0.0247,1.0228,0.039,1.5388,0.039c17.374,0,31.5089-14.1349,31.5089-31.5089
|
||||
c0-13.2952-8.2814-24.6851-19.9532-29.3043c1.8635-4.4799,4.4246-8.5959,7.5408-12.2242c0.9802,0.4318,2.0082,0.7698,3.0715,1.0182
|
||||
c1.2892,0.3013,2.6259,0.4767,4.0055,0.4767c9.718,0,17.6239-7.9059,17.6239-17.6239c0-0.0315-0.0029-0.0623-0.0031-0.0938h-6.9887
|
||||
c0.0003,0.0316,0.0043,0.0622,0.0043,0.0938c0,5.8651-4.7714,10.6364-10.6364,10.6364S73.2884,5.9589,73.2884,0.0938
|
||||
c0-0.0317,0.0043-0.0622,0.0046-0.0938h-6.9874c-0.0002,0.0316-0.0048,0.0621-0.0048,0.0938c0,4.674,1.8413,8.9171,4.8209,12.0747
|
||||
c-3.5914,4.2867-6.5057,9.1537-8.5975,14.4351c-1.5569-0.2375-3.1507-0.3612-4.7729-0.3612
|
||||
c-17.374,0-31.5089,14.1349-31.5089,31.5089c0,14.4023,9.7169,26.5699,22.9364,30.3139
|
||||
c-5.1912,13.2885-16.4293,23.5726-30.3165,27.4374h16.6543C44.9824,109.2666,52.327,100.0609,56.2125,89.2212z M33.2299,57.7513
|
||||
c0-13.5211,11.0004-24.5214,24.5214-24.5214s24.5214,11.0004,24.5214,24.5214S71.2724,82.2727,57.7513,82.2727
|
||||
S33.2299,71.2723,33.2299,57.7513z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
13
docs/logos/Leptos_logo_pref_dark_RGB.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 437.4 209.6" style="enable-background:new 0 0 437.4 209.6;" xmlns="http://www.w3.org/2000/svg">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#EF3939;}
|
||||
</style>
|
||||
<path class="st0" d="M95.1,128.1H58.7V65.7c0-1.5-0.8-2.4-2.2-2.4h-3.1c-1.3,0-2.2,1-2.2,2.4v67.4c0,1.5,0.8,2.3,2.2,2.3h41.7 c1.5,0,2.5-0.9,2.5-2.3v-2.8C97.6,128.9,96.7,128.1,95.1,128.1z"/>
|
||||
<path class="st0" d="M146.5,77.2c-4.8-3-10.3-4.5-16.3-4.5c0,0,0,0,0,0c-5.7,0-11.1,1.5-16.1,4.4c-4.9,2.9-8.9,6.8-11.8,11.7 c-2.9,4.9-4.4,10.3-4.4,16.1c0,5.7,1.5,11.1,4.5,15.9c3,4.9,6.9,8.7,11.9,11.7c4.9,2.9,10.2,4.3,16,4.3c0,0,0,0,0,0 c8.1,0,14.7-2.6,21.5-8.4c0.5-0.5,0.9-1.1,0.9-1.8c0-0.6-0.2-1.1-0.5-1.5l-1.4-1.8l-0.1-0.1c-0.3-0.3-0.7-0.6-1.4-0.6 c-0.6,0-1.3,0.2-1.7,0.6c-1.9,1.6-3.5,2.8-4.8,3.6c-1.8,1.2-5.8,3.2-12.6,3.2c0,0,0,0,0,0c-0.1,0-0.3,0-0.4,0 c-12.3,0-23.5-10.3-24.5-22.5l53,0c0,0,0,0,0,0c2.2,0,3.5-1.2,3.5-3.3c0-5.3-1.4-10.4-4-15.3C155,84.1,151.2,80.1,146.5,77.2z M154.6,100.4l-49.1,0c2-11.7,13-21.2,24.5-21c0.1,0,0.3,0,0.4,0C143.5,79.4,152.5,90,154.6,100.4z"/>
|
||||
<path class="st0" d="M204,136.6c5.7,0,11.1-1.5,16-4.3c4.9-2.9,8.8-6.7,11.7-11.7c2.9-4.9,4.3-10.3,4.3-16c0-5.7-1.5-11.1-4.3-16 c-2.9-4.9-6.7-8.8-11.7-11.7c-4.9-2.9-10.2-4.3-16-4.3c-9.9,0-19.2,4.8-24.4,12.3v-9.4c0-0.6-0.1-1.1-0.4-1.5 c-0.4-0.5-1-0.8-1.9-0.8h-2.7c-1.5,0-2.3,0.8-2.3,2.3v5.7v46.5v24c0,1.3,0.8,2,2,2h3.4c0.9,0,1.6-0.7,1.6-1.6v-17.1v-11 C185.1,131.9,194.3,136.6,204,136.6z M186.6,122.6c-4.9-4.9-7.7-11.5-7.5-18c-0.1-6.4,2.5-12.8,7.4-17.6c4.9-4.9,11.3-7.6,17.7-7.4 c4.4,0,8.5,1.1,12.2,3.4c3.7,2.3,6.7,5.4,8.9,9.2c2.2,3.9,3.3,8.1,3.3,12.5c0.1,6.5-2.6,13-7.5,17.9c-4.8,4.8-11,7.4-17.3,7.3 C197.7,130,191.4,127.3,186.6,122.6z"/>
|
||||
<path class="st0" d="M241.9,80.4h7.6v42c0,6.8,4.6,12.1,11.1,12.9c0.7,0.1,1.8,0.2,3.5,0.2c2,0,3-0.4,3-2.3v-2.4c0-1.8-1.3-2-2.8-2 c0,0,0,0,0,0l-1.8,0c-2,0-3.5-0.8-4.4-2.1c-1-1.5-1.5-3.2-1.5-5V80.4h10.1c1.3,0,2.3-1.4,2.3-2.6v-1.9c0-1.4-0.8-2.2-2.2-2.2h-10.1 v-13c0-1.5-0.8-2.5-1.9-2.5c-0.2,0-0.4,0-0.6,0.1l-2.6,0.7c-1.2,0.4-2,0.9-2,2.5v12.2h-7.6c-1.4,0-2.3,0.9-2.3,2.3v1.9 C239.6,79.2,240.6,80.4,241.9,80.4z"/>
|
||||
<path class="st0" d="M379.1,106.3c-4-2.9-8.9-5-14.3-6.3c-6.5-1.4-13.4-5.2-13.3-10.3c0-7.5,6.6-10.2,12.3-10.2c5.4,0,10.2,3,13,8 c0.7,1.3,1.5,1.9,2.4,1.9c0.3,0,0.7-0.1,1-0.3l2.2-1.2c0.6-0.3,0.9-0.9,0.9-1.7c0-0.5-0.1-1-0.3-1.3c-3.6-8.2-10.1-12.4-19.2-12.4 c-11.7,0-19.6,6.9-19.6,17.2c0,4.3,1.8,8,5.5,10.9c3.4,2.8,7.9,4.9,13.3,6.1c4.6,1.1,8.3,2.6,11.1,4.4c1.4,0.9,2.4,2.1,3.1,3.4 c0.6,1.2,0.9,2.6,0.9,4.1c0.1,2.4-0.8,4.5-2.6,6.3c-3.1,3.2-8.4,4.7-12,4.7c0,0-0.1,0-0.1,0c-3.4,0-6.5-0.8-9.1-2.5 c-2.6-1.6-4.2-3.8-5-6.6c-0.3-1.1-0.8-2.2-2.2-2.2c-0.1,0-0.2,0-0.4,0c-0.1,0-0.3,0-0.5,0.1l-1.9,0.4l-0.1,0c-1,0.4-1.6,1.2-1.6,2.1 c0,0.2,0.1,0.7,0.2,1.1c2,8.6,10.2,14.3,20.6,14.3c0.1,0,0.2,0,0.4,0c6.6,0,12.7-2.3,16.7-6.4c3.2-3.2,4.8-7.2,4.7-11.6 c0-2.8-0.6-5.3-1.8-7.5C382.4,109.3,381,107.6,379.1,106.3z"/>
|
||||
<path class="st1" d="M348.9,45.7c0-6.3-3.3-11.9-8.2-15.1c-2.8-1.8-6.2-2.9-9.7-2.9c-9.9,0-18,8.1-18,18c0,4.8,1.9,9.1,4.9,12.3 c-3.7,4.4-6.6,9.3-8.8,14.7c-1.6-0.2-3.2-0.4-4.9-0.4c-17.7,0-32.1,14.4-32.1,32.1c0,14.7,9.9,27.1,23.4,30.9 c-6.8,17.3-23.7,29.7-43.4,29.7c-16.8,0-31.6-9-39.8-22.4c-2.5,0.5-5,0.9-7.7,0.9c9,17,26.9,28.6,47.4,28.6 c23.3,0,43.1-14.9,50.5-35.6c0.5,0,1,0,1.6,0c17.7,0,32.1-14.4,32.1-32.1c0-13.6-8.4-25.2-20.3-29.9c1.9-4.6,4.5-8.8,7.7-12.5 c1,0.4,2,0.8,3.1,1c1.3,0.3,2.7,0.5,4.1,0.5C340.8,63.7,348.9,55.6,348.9,45.7z M304.2,129.5c-13.8,0-25-11.2-25-25s11.2-25,25-25 s25,11.2,25,25S318,129.5,304.2,129.5z M330.9,34.9c0.6,0,1.3,0.1,1.9,0.2c5.1,0.9,9,5.3,9,10.7c0,6-4.9,10.8-10.8,10.8 s-10.8-4.9-10.8-10.8c0-1.4,0.3-2.7,0.8-4C322.4,37.7,326.4,34.9,330.9,34.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
@@ -1,44 +0,0 @@
|
||||
[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"]]
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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!
|
||||
9
examples/counter/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
examples/counter/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,14 +1,24 @@
|
||||
use leptos::*;
|
||||
|
||||
pub fn simple_counter(cx: Scope) -> web_sys::Element {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
/// A simple counter component.
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleCounter(
|
||||
cx: Scope,
|
||||
/// The starting value for the counter
|
||||
initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<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>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use counter::simple_counter;
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(simple_counter)
|
||||
mount_to_body(|cx| view! { cx,
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,18 +3,24 @@ 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();
|
||||
let dec = div
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
@@ -29,12 +35,16 @@ fn inc() {
|
||||
inc.click();
|
||||
inc.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("2".to_string()));
|
||||
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
|
||||
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("-2".to_string()));
|
||||
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
|
||||
1
examples/counter_isomorphic/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.leptos.kdl
|
||||
89
examples/counter_isomorphic/Cargo.toml
Normal file
@@ -0,0 +1,89 @@
|
||||
[package]
|
||||
name = "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 = ["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 = "4.0.0"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
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",
|
||||
]
|
||||
stable = ["leptos/stable", "leptos_router/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "stable"]
|
||||
skip_feature_sets = [["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "counter_isomorphic"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
# style-file = "src/styles/tailwind.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
9
examples/counter_isomorphic/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
43
examples/counter_isomorphic/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 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.
|
||||
|
||||
## Client Side Rendering
|
||||
For this example the server must store the counter state since it can be modified by many users.
|
||||
This means it is not possible to produce a working CSR-only version as a non-static server is required.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
1. Install cargo-leptos
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
BIN
examples/counter_isomorphic/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,5 +1,6 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos_meta::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
@@ -9,9 +10,9 @@ use broadcaster::BroadcastChannel;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
GetServerCount::register();
|
||||
AdjustServerCount::register();
|
||||
ClearServerCount::register();
|
||||
_ = GetServerCount::register();
|
||||
_ = AdjustServerCount::register();
|
||||
_ = ClearServerCount::register();
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
@@ -43,41 +44,41 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
Ok(0)
|
||||
}
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> Element {
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><A href="">"Simple"</A></li>
|
||||
<li><A href="form">"Form-Based"</A></li>
|
||||
<li><A href="multi">"Multi-User"</A></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" element=|cx| view! {
|
||||
cx,
|
||||
<Counter/>
|
||||
}/>
|
||||
<Route path="form" element=|cx| view! {
|
||||
cx,
|
||||
<FormCounter/>
|
||||
}/>
|
||||
<Route path="multi" element=|cx| view! {
|
||||
cx,
|
||||
<MultiuserCounter/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Server-Side Counters"</h1>
|
||||
<p>"Each of these counters stores its data in the same variable on the server."</p>
|
||||
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><A href="">"Simple"</A></li>
|
||||
<li><A href="form">"Form-Based"</A></li>
|
||||
<li><A href="multi">"Multi-User"</A></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<Counter/>
|
||||
}/>
|
||||
<Route path="form" view=|cx| view! {
|
||||
cx,
|
||||
<FormCounter/>
|
||||
}/>
|
||||
<Route path="multi" view=|cx| view! {
|
||||
cx,
|
||||
<MultiuserCounter/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,13 +87,13 @@ pub fn Counters(cx: Scope) -> Element {
|
||||
// it's invalidated by one of the user's own actions
|
||||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter(cx: Scope) -> Element {
|
||||
pub fn Counter(cx: Scope) -> impl IntoView {
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
move || (dec.version.get(), inc.version.get(), clear.version.get()),
|
||||
move || (dec.version().get(), inc.version().get(), clear.version().get()),
|
||||
|_| get_server_count(),
|
||||
);
|
||||
|
||||
@@ -115,9 +116,10 @@ pub fn Counter(cx: Scope) -> Element {
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -126,20 +128,15 @@ pub fn Counter(cx: Scope) -> Element {
|
||||
// It uses the same invalidation pattern as the plain counter,
|
||||
// but uses HTML forms to submit the actions
|
||||
#[component]
|
||||
pub fn FormCounter(cx: Scope) -> Element {
|
||||
pub fn FormCounter(cx: Scope) -> impl IntoView {
|
||||
let adjust = create_server_action::<AdjustServerCount>(cx);
|
||||
let clear = create_server_action::<ClearServerCount>(cx);
|
||||
|
||||
let counter = create_resource(
|
||||
cx,
|
||||
{
|
||||
let adjust = adjust.version;
|
||||
let clear = clear.version;
|
||||
move || (adjust.get(), clear.get())
|
||||
},
|
||||
move || (adjust.version().get(), clear.version().get()),
|
||||
|_| {
|
||||
log::debug!("FormCounter running fetcher");
|
||||
|
||||
get_server_count()
|
||||
},
|
||||
);
|
||||
@@ -153,8 +150,6 @@ pub fn FormCounter(cx: Scope) -> Element {
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
let adjust2 = adjust.clone();
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
@@ -174,7 +169,7 @@ pub fn FormCounter(cx: Scope) -> Element {
|
||||
<input type="submit" value="-1"/>
|
||||
</ActionForm>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
<ActionForm action=adjust2>
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="1"/>
|
||||
<input type="hidden" name="msg" value="form value up"/>
|
||||
<input type="submit" value="+1"/>
|
||||
@@ -189,7 +184,7 @@ pub fn FormCounter(cx: Scope) -> Element {
|
||||
// Whenever another user updates the value, it will update here
|
||||
// This is the primitive pattern for live chat, collaborative editing, etc.
|
||||
#[component]
|
||||
pub fn MultiuserCounter(cx: Scope) -> Element {
|
||||
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
|
||||
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(cx, |_| clear_server_count());
|
||||
@@ -198,7 +193,7 @@ pub fn MultiuserCounter(cx: Scope) -> Element {
|
||||
let multiplayer_value = {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut source = gloo::net::eventsource::futures::EventSource::new("/api/events")
|
||||
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect_throw("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
cx,
|
||||
@@ -217,8 +212,8 @@ pub fn MultiuserCounter(cx: Scope) -> Element {
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let multiplayer_value =
|
||||
create_signal_from_stream(cx, futures::stream::once(Box::pin(async { 0.to_string() })));
|
||||
let (multiplayer_value, _) =
|
||||
create_signal(cx, None::<i32>);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
@@ -228,7 +223,7 @@ pub fn MultiuserCounter(cx: Scope) -> Element {
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value().unwrap_or_default().to_string()}</span>
|
||||
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +14,7 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::hydrate(body().unwrap(), |cx| {
|
||||
mount_to_body(|cx| {
|
||||
view! { cx, <Counters/> }
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,7 @@ cfg_if! {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::counters::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/api/events")]
|
||||
async fn counter_events() -> impl Responder {
|
||||
@@ -29,30 +30,38 @@ cfg_if! {
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
||||
crate::counters::register_server_functions();
|
||||
|
||||
HttpServer::new(|| {
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let addr = conf.leptos_options.site_addr.clone();
|
||||
let routes = generate_route_list(|cx| view! { cx, <Counters/> });
|
||||
|
||||
HttpServer::new(move || {
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
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("leptos_counter_isomorphic", |cx| view! { cx, <Counters/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), |cx| view! { cx, <Counters/> })
|
||||
.service(Files::new("/", &site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(("127.0.0.1", 8081))?
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// client-only stuff for Trunk
|
||||
// client-only main 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/> });
|
||||
// isomorphic counters cannot work in a Client-Side-Rendered only
|
||||
// app as a server is required to maintain state
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/counter_without_macros/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "counter_without_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["stable"] }
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
9
examples/counter_without_macros/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
5
examples/counter_without_macros/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Leptos Counter Example
|
||||
|
||||
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
|
||||
|
||||
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.
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
examples/counter_without_macros/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
44
examples/counter_without_macros/src/lib.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use leptos::{ev, *};
|
||||
|
||||
pub struct Props {
|
||||
/// The starting value for the counter
|
||||
pub initial_value: i32,
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
pub step: i32,
|
||||
}
|
||||
|
||||
/// A simple counter view.
|
||||
pub fn view(cx: Scope, props: Props) -> impl IntoView {
|
||||
let Props {
|
||||
initial_value,
|
||||
step,
|
||||
} = props;
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
|
||||
div(cx)
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value = 0))
|
||||
.child((cx, "Clear")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value -= step))
|
||||
.child((cx, "-1")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
span(cx)
|
||||
.child((cx, "Value: "))
|
||||
.child((cx, move || value.get()))
|
||||
.child((cx, "!")),
|
||||
))
|
||||
.child((
|
||||
cx,
|
||||
button(cx)
|
||||
.on(ev::click, move |_| set_value.update(|value| *value += step))
|
||||
.child((cx, "+1")),
|
||||
))
|
||||
}
|
||||
16
examples/counter_without_macros/src/main.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
58
examples/counter_without_macros/tests/mod.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counter_without_macros as counter;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|cx| {
|
||||
counter::view(
|
||||
cx,
|
||||
counter::Props {
|
||||
initial_value: 0,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let document = leptos::document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let dec = clear
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let text = dec
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
let inc = text
|
||||
.next_sibling()
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
inc.click();
|
||||
inc.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
|
||||
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
dec.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
9
examples/counters/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -1,10 +1,9 @@
|
||||
# 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.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
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/)
|
||||
|
||||
@@ -11,7 +11,7 @@ struct CounterUpdater {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
@@ -39,7 +39,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<div>
|
||||
<>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
@@ -63,16 +63,17 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For each=counters key=|counter| counter.0>{
|
||||
|cx, (id, (value, set_value)): &(usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id=*id value=*value set_value=*set_value/>
|
||||
<For
|
||||
each=counters
|
||||
key=|counter| counter.0
|
||||
view=move |cx, (id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! { cx,
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
}</For>
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +83,7 @@ fn Counter(
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> web_sys::Element {
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
|
||||
|
||||
let input = move |ev| set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default());
|
||||
@@ -95,10 +96,10 @@ fn Counter(
|
||||
<li>
|
||||
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value().to_string()}
|
||||
prop:value={value}
|
||||
on:input=input
|
||||
/>
|
||||
<span>{move || value().to_string()}</span>
|
||||
<span>{value}</span>
|
||||
<button on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use leptos::{wasm_bindgen::JsValue, *};
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
use counters::{Counters, CountersProps};
|
||||
@@ -75,37 +75,39 @@ fn inc() {
|
||||
dec_button.click();
|
||||
}
|
||||
|
||||
// we can use RSX in test comparisons!
|
||||
// note that if RSX template creation is bugged, this probably won't catch it
|
||||
// (because the same bug will be reproduced in both sides of the assertion)
|
||||
// so I use HTML tests for most internal testing like this
|
||||
// but in user-land testing, RSX comparanda are cool
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Add Counter"</button>
|
||||
<button>"Add 1000 Counters"</button>
|
||||
<button>"Clear Counters"</button>
|
||||
<p>"Total: "<span>"3"</span>" from "<span>"2"</span>" counters."</p>
|
||||
<ul>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"1"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"2"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
.outer_html()
|
||||
);
|
||||
run_scope(create_runtime(), move |cx| {
|
||||
// we can use RSX in test comparisons!
|
||||
// note that if RSX template creation is bugged, this probably won't catch it
|
||||
// (because the same bug will be reproduced in both sides of the assertion)
|
||||
// so I use HTML tests for most internal testing like this
|
||||
// but in user-land testing, RSX comparanda are cool
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Add Counter"</button>
|
||||
<button>"Add 1000 Counters"</button>
|
||||
<button>"Clear Counters"</button>
|
||||
<p>"Total: "<span>"3"</span>" from "<span>"2"</span>" counters."</p>
|
||||
<ul>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"1"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
<li>
|
||||
<button>"-1"</button>
|
||||
<input type="text"/>
|
||||
<span>"2"</span>
|
||||
<button>"+1"</button>
|
||||
<button>"x"</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
.outer_html()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
4
examples/counters_stable/Makefile.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+stable", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -4,7 +4,6 @@ This example showcases a basic Leptos app with many counters. It is a good examp
|
||||
|
||||
## 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.
|
||||
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/)
|
||||
@@ -17,7 +17,7 @@ struct CounterUpdater {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
pub fn Counters(cx: Scope) -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
|
||||
provide_context(cx, CounterUpdater { set_counters });
|
||||
@@ -69,14 +69,16 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For each={move || counters.get()} key={|counter| counter.0}>{
|
||||
|cx, (id, (value, set_value)): &(usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
<For
|
||||
each={move || counters.get()}
|
||||
key={|counter| counter.0}
|
||||
view=move |cx, (id, (value, set_value))| {
|
||||
view! {
|
||||
cx,
|
||||
<Counter id=*id value=*value set_value=*set_value/>
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
}
|
||||
}</For>
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -88,14 +90,14 @@ fn Counter(
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> web_sys::Element {
|
||||
) -> impl IntoView {
|
||||
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
|
||||
|
||||
let input = move |ev| set_value.set(event_target_value(&ev).parse::<i32>().unwrap_or_default());
|
||||
|
||||
view! { cx,
|
||||
<li>
|
||||
<button on:click={move |_| set_value.update(move |value| *value -= 1)}>"-1"</button>
|
||||
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={move || value.get().to_string()}
|
||||
on:input=input
|
||||
94
examples/errors_axum/Cargo.toml
Normal file
@@ -0,0 +1,94 @@
|
||||
[package]
|
||||
name = "errors_axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
leptos_reactive = { path = "../../../leptos/leptos_reactive", 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" }
|
||||
thiserror = "1.0.38"
|
||||
tracing = "0.1.37"
|
||||
|
||||
[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", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"axum",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tokio",
|
||||
"leptos_axum",
|
||||
]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
||||
[package.metadata.leptos]
|
||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "errors_axum"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
|
||||
style-file = "./style.css"
|
||||
# [Optional] Files in the asset-dir will be copied to the site-root directory
|
||||
assets-dir = "public"
|
||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||
end2end-cmd = "npx playwright test"
|
||||
# The browserlist query used for optimizing the CSS.
|
||||
browserquery = "defaults"
|
||||
# Set by cargo-leptos watch when building with tha tool. Controls whether autoreload JS will be included in the head
|
||||
watch = false
|
||||
# The environment Leptos will run in, usually either "DEV" or "PROD"
|
||||
env = "DEV"
|
||||
# The features to use when compiling the bin target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --bin-features
|
||||
bin-features = ["ssr"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the bin target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
bin-default-features = false
|
||||
|
||||
# The features to use when compiling the lib target
|
||||
#
|
||||
# Optional. Can be over-ridden with the command line parameter --lib-features
|
||||
lib-features = ["hydrate"]
|
||||
|
||||
# If the --no-default-features flag should be used when compiling the lib target
|
||||
#
|
||||
# Optional. Defaults to false.
|
||||
lib-default-features = false
|
||||
9
examples/errors_axum/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
41
examples/errors_axum/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Leptos Errors Demonstration with Axum
|
||||
This example demonstrates how Leptos Errors can work with an Axum backend on a server.
|
||||
|
||||
## Client Side Rendering
|
||||
This example cannot be built as a trunk standalone CSR-only app as it requires the server to send status codes.
|
||||
|
||||
## Server Side Rendering with cargo-leptos
|
||||
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
|
||||
|
||||
1. Install cargo-leptos
|
||||
```bash
|
||||
cargo install --locked cargo-leptos
|
||||
```
|
||||
2. Build the site in watch mode, recompiling on file changes
|
||||
```bash
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
3. When ready to deploy, run
|
||||
```bash
|
||||
cargo leptos build --release
|
||||
```
|
||||
|
||||
## Server Side Rendering without cargo-leptos
|
||||
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
|
||||
|
||||
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
|
||||
1. Install wasm-pack
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
2. Build the Webassembly used to hydrate the HTML from the server
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
3. Run the server to serve the Webassembly, JS, and HTML
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
BIN
examples/errors_axum/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
62
examples/errors_axum/src/error_template.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::errors::AppError;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::Errors;
|
||||
use leptos::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
// A basic function to display errors served by the error boundaries.
|
||||
// Feel free to do more complicated things here than just displaying them.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
cx: Scope,
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(cx, e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
|
||||
// Get Errors from Signal
|
||||
let errors = errors.get().0;
|
||||
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<AppError> = errors
|
||||
.into_iter()
|
||||
.filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect();
|
||||
log!("Errors: {errors:#?}");
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
cfg_if! { if #[cfg(feature="ssr")] {
|
||||
let response = use_context::<ResponseOptions>(cx);
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
}
|
||||
}}
|
||||
|
||||
view! { cx,
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
view=move |cx, error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! { cx,
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}
|
||||
19
examples/errors_axum/src/errors.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use http::status::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
#[error("Internal Server Error")]
|
||||
InternalServerError,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
AppError::NotFound => StatusCode::NOT_FOUND,
|
||||
AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
45
examples/errors_axum/src/fallback.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
extract::Extension,
|
||||
response::IntoResponse,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use axum::response::Response as AxumResponse;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use std::sync::Arc;
|
||||
use leptos::{LeptosOptions, Errors, view};
|
||||
use crate::landing::{App, AppProps};
|
||||
|
||||
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
|
||||
let options = &*options;
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else{
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move |cx| view!{ cx, <App/> }
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, root: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}}
|
||||
86
examples/errors_axum/src/landing.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use crate::{
|
||||
error_template::{ErrorTemplate, ErrorTemplateProps},
|
||||
errors::AppError,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register_server_functions() {
|
||||
_ = CauseInternalServerError::register();
|
||||
}
|
||||
|
||||
#[server(CauseInternalServerError, "/api")]
|
||||
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
Err(ServerFnError::ServerError(
|
||||
"Generic Server Error".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
//let id = use_context::<String>(cx);
|
||||
provide_meta_context(cx);
|
||||
view! {
|
||||
cx,
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
|
||||
<Router fallback=|cx| {
|
||||
let mut outside_errors = Errors::default();
|
||||
outside_errors.insert_with_default_key(AppError::NotFound);
|
||||
view! { cx,
|
||||
<ErrorTemplate outside_errors/>
|
||||
}
|
||||
.into_view(cx)
|
||||
}>
|
||||
<header>
|
||||
<h1>"Error Examples:"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! {
|
||||
cx,
|
||||
<ExampleErrors/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ExampleErrors(cx: Scope) -> impl IntoView {
|
||||
let generate_internal_error = create_server_action::<CauseInternalServerError>(cx);
|
||||
|
||||
view! { cx,
|
||||
<p>
|
||||
"These links will load 404 pages since they do not exist. Verify with browser development tools: " <br/>
|
||||
<a href="/404">"This links to a page that does not exist"</a><br/>
|
||||
<a href="/404" target="_blank">"Same link, but in a new tab"</a>
|
||||
</p>
|
||||
<p>
|
||||
"After pressing this button check browser network tools. Can be used even when WASM is blocked:"
|
||||
<ActionForm action=generate_internal_error>
|
||||
<input name="error1" type="submit" value="Generate Internal Server Error"/>
|
||||
</ActionForm>
|
||||
</p>
|
||||
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
|
||||
<div>
|
||||
// note that the error boundries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribue to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|cx, errors| view!{cx, <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ReturnsError(_cx: Scope) -> impl IntoView {
|
||||
Err::<String, AppError>(AppError::InternalServerError)
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod todo;
|
||||
use leptos::*;
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
pub mod fallback;
|
||||
pub mod landing;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
use crate::landing::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
@@ -14,8 +17,8 @@ cfg_if! {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::hydrate(body().unwrap(), |cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
leptos::mount_to_body(|cx| {
|
||||
view! { cx, <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
72
examples/errors_axum/src/main.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::fallback::file_and_error_handler;
|
||||
use crate::landing::*;
|
||||
use axum::body::Body as AxumBody;
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use errors_axum::*;
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use std::sync::Arc;
|
||||
}}
|
||||
|
||||
//Define a handler to test extractor with state
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn custom_handler(
|
||||
Path(id): Path<String>,
|
||||
Extension(options): Extension<Arc<LeptosOptions>>,
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
(*options).clone(),
|
||||
move |cx| {
|
||||
provide_context(cx, id.clone());
|
||||
},
|
||||
|cx| view! { cx, <App/> },
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
crate::landing::register_server_functions();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
|
||||
.fallback(file_and_error_handler)
|
||||
.layer(Extension(Arc::new(leptos_options)));
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// this is if we were using client-only rending with Trunk
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// This example cannot be built as a trunk standalone CSR-only app.
|
||||
// The server is needed to demonstrate the error statuses.
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
9
examples/fetch/Makefile.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "build-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -4,7 +4,6 @@ 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.
|
||||
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/)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
</head>
|
||||
<style>
|
||||
img {
|
||||
|
||||
BIN
examples/fetch/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,6 +1,3 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -10,10 +7,6 @@ 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={}",
|
||||
@@ -34,50 +27,41 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_example(cx: Scope) -> web_sys::Element {
|
||||
pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
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?"
|
||||
<input type="number"
|
||||
value={move || cat_count.get().to_string()}
|
||||
prop:value={move || cat_count.get().to_string()}
|
||||
on:input=move |ev| {
|
||||
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
|
||||
set_cat_count(val);
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{move || pending().then(|| view! { cx, <p>"Loading more cats..."</p> })}
|
||||
<div>
|
||||
// <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,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}</div>
|
||||
},
|
||||
})
|
||||
}
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
{move || {
|
||||
cats.read().map(|data| match data {
|
||||
Err(_) => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
Ok(cats) => view! { cx,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}</div>
|
||||
}.into_view(cx),
|
||||
})
|
||||
}
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||