mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 15:44:42 -05:00
Compare commits
1081 Commits
fix-resour
...
v0.6.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e93a686f4 | ||
|
|
34cdff4cb3 | ||
|
|
530087d77d | ||
|
|
4bb43f6207 | ||
|
|
9e2fb62857 | ||
|
|
1da2fff706 | ||
|
|
9fd2987447 | ||
|
|
7996f835d0 | ||
|
|
d72b12524e | ||
|
|
8e79c5be5c | ||
|
|
de25658c36 | ||
|
|
e2e35a9659 | ||
|
|
bf1ba589c5 | ||
|
|
f70ebc1289 | ||
|
|
3cab09e015 | ||
|
|
b431315f7c | ||
|
|
5b40881e77 | ||
|
|
59d3cce3be | ||
|
|
6a83161368 | ||
|
|
4b00c16cb9 | ||
|
|
6d6019b956 | ||
|
|
3540291065 | ||
|
|
4809cf473e | ||
|
|
aa977001c1 | ||
|
|
c16189f095 | ||
|
|
d0a013c248 | ||
|
|
531ea74e33 | ||
|
|
545e87e540 | ||
|
|
2ca30a0b2d | ||
|
|
753bf1ed54 | ||
|
|
37c6387fea | ||
|
|
0a73487152 | ||
|
|
747aba0d7f | ||
|
|
04cf47d5da | ||
|
|
47abe00993 | ||
|
|
aa3700ffb9 | ||
|
|
0770b87cb7 | ||
|
|
330ebdb018 | ||
|
|
c3179d88cf | ||
|
|
ffcf3c2952 | ||
|
|
001ca5148e | ||
|
|
1e000afa78 | ||
|
|
0f7b8841b2 | ||
|
|
7dc0441f6c | ||
|
|
0a321a1bd7 | ||
|
|
88742952f0 | ||
|
|
8a4b972e0b | ||
|
|
95bd9cc544 | ||
|
|
23bc892a24 | ||
|
|
830fba794e | ||
|
|
98633c8700 | ||
|
|
b0f5c39711 | ||
|
|
b54aa7f3f5 | ||
|
|
e33ee7ec99 | ||
|
|
cd70b2f52b | ||
|
|
c75842ed0c | ||
|
|
4ad228bf47 | ||
|
|
bbe7115360 | ||
|
|
0658a550b0 | ||
|
|
4222c832b1 | ||
|
|
dfddbd6bf9 | ||
|
|
8a77691cb5 | ||
|
|
1dbe8b2d4b | ||
|
|
fe64f0d332 | ||
|
|
c00207aa46 | ||
|
|
65b7603192 | ||
|
|
d4bdc36062 | ||
|
|
1b55227d10 | ||
|
|
a903e19eb2 | ||
|
|
38bf73947f | ||
|
|
e4b89ba243 | ||
|
|
701e3077fb | ||
|
|
aec4d680aa | ||
|
|
06721c5fcd | ||
|
|
1ddb39e9bd | ||
|
|
15d4ca0638 | ||
|
|
85c3755f6d | ||
|
|
66ea072bc0 | ||
|
|
b0b3c21285 | ||
|
|
56088a9ead | ||
|
|
69d25d9c63 | ||
|
|
5029b8f315 | ||
|
|
0cba7bc22b | ||
|
|
fb97c50886 | ||
|
|
f1bc734dcf | ||
|
|
f71b4aae69 | ||
|
|
a834c03974 | ||
|
|
595013579c | ||
|
|
8b1bd1ae9e | ||
|
|
6ef1531059 | ||
|
|
9f1406250e | ||
|
|
1f6a892291 | ||
|
|
0ff1e279a2 | ||
|
|
c6096cc2a0 | ||
|
|
8a2ae7fc7c | ||
|
|
9de34b74cf | ||
|
|
1b5961edaa | ||
|
|
26d1aee9ad | ||
|
|
2bf09384df | ||
|
|
ac12e1a411 | ||
|
|
b367b68a43 | ||
|
|
1f9dad421f | ||
|
|
4648fb2cfc | ||
|
|
817ec045f7 | ||
|
|
ca3806e6bc | ||
|
|
936c2077c3 | ||
|
|
b3b18875c6 | ||
|
|
5cbab48713 | ||
|
|
5a8880dd2e | ||
|
|
ea6c957f3d | ||
|
|
694e5f1cb3 | ||
|
|
fce2c727ab | ||
|
|
7d1ce45a57 | ||
|
|
997b99081b | ||
|
|
d33e57d4b7 | ||
|
|
b450f0fd10 | ||
|
|
c84c6ee8cd | ||
|
|
567644df8f | ||
|
|
39f5481b8c | ||
|
|
c88bfbe0a0 | ||
|
|
40da1fe94e | ||
|
|
8df46fcdb9 | ||
|
|
b4a1d90327 | ||
|
|
d746f83387 | ||
|
|
2092c40bc7 | ||
|
|
70ec0c2d0a | ||
|
|
eb45d05f3b | ||
|
|
f19def9541 | ||
|
|
ddda785045 | ||
|
|
26d9d75cf2 | ||
|
|
46e7abf9ba | ||
|
|
1b1e02729e | ||
|
|
fdd576535a | ||
|
|
2a9e502893 | ||
|
|
a519859a66 | ||
|
|
25120c0e9f | ||
|
|
94cb4c0ec3 | ||
|
|
f9cd8539e4 | ||
|
|
14072457d0 | ||
|
|
e179db1d42 | ||
|
|
2fa60103b4 | ||
|
|
a3a15f244d | ||
|
|
0df5dfeaf8 | ||
|
|
3f22906053 | ||
|
|
33ad30515d | ||
|
|
c5bab09423 | ||
|
|
320179bc04 | ||
|
|
5065bed594 | ||
|
|
22b4537f27 | ||
|
|
8d23d5136a | ||
|
|
c7fac64054 | ||
|
|
047235e7c1 | ||
|
|
7a086ad159 | ||
|
|
bb923b3f9b | ||
|
|
6a8c26a820 | ||
|
|
21f8085851 | ||
|
|
9a5a102ce3 | ||
|
|
4d602c21f8 | ||
|
|
7d114c7414 | ||
|
|
1f017a2ade | ||
|
|
35e8e74dcf | ||
|
|
4366d786ac | ||
|
|
1777a4057a | ||
|
|
0571ebbc36 | ||
|
|
06c478b7cb | ||
|
|
90ba3529e9 | ||
|
|
13a2691806 | ||
|
|
1ad7ee8a03 | ||
|
|
88fee243a8 | ||
|
|
5e08253521 | ||
|
|
cc6f65cd83 | ||
|
|
9488114801 | ||
|
|
b0cdeab906 | ||
|
|
def4be80b2 | ||
|
|
15b04a8a85 | ||
|
|
0a9cdba22e | ||
|
|
1d1de4ac38 | ||
|
|
31b2b9e94c | ||
|
|
8f07818687 | ||
|
|
a5cbfa0aad | ||
|
|
6c8e704fb3 | ||
|
|
81fb5160e5 | ||
|
|
2af0d3d781 | ||
|
|
7f532cda70 | ||
|
|
c7941f7639 | ||
|
|
61148026d1 | ||
|
|
738eeefe73 | ||
|
|
be084a5d1d | ||
|
|
f5c007df7b | ||
|
|
a1bd84f3dc | ||
|
|
f6ce82c9d1 | ||
|
|
853c080707 | ||
|
|
f6b95e40f4 | ||
|
|
db1497b9c2 | ||
|
|
f53ac1a4ae | ||
|
|
5e6f4403ca | ||
|
|
4e3f1c834c | ||
|
|
566df034ff | ||
|
|
fd97e2e027 | ||
|
|
c8fbee18c8 | ||
|
|
e1a9856ca9 | ||
|
|
60efaefff4 | ||
|
|
db4158f5c3 | ||
|
|
af62d2e900 | ||
|
|
c3e3ce7878 | ||
|
|
dec17fc65b | ||
|
|
2dbc5899f3 | ||
|
|
dd368a845c | ||
|
|
9c258219dd | ||
|
|
6a1685936b | ||
|
|
7d45e6bb13 | ||
|
|
8fae76828e | ||
|
|
d5b9e84f36 | ||
|
|
197edebd51 | ||
|
|
2a5c855595 | ||
|
|
c9627bfeb4 | ||
|
|
c7422cd96e | ||
|
|
cadd217078 | ||
|
|
0c4cf5471d | ||
|
|
dd0c349554 | ||
|
|
dd5a0ae094 | ||
|
|
5cacb57283 | ||
|
|
b356d3cd28 | ||
|
|
ae1de88916 | ||
|
|
67dd188358 | ||
|
|
1d4772251a | ||
|
|
98f18e7c31 | ||
|
|
3f34d9bb23 | ||
|
|
44db400f6b | ||
|
|
3a5730800c | ||
|
|
041b86e6e5 | ||
|
|
d71feada7e | ||
|
|
1eaf886481 | ||
|
|
c7da3998ba | ||
|
|
10bbeea697 | ||
|
|
2d70229608 | ||
|
|
bbcef811f4 | ||
|
|
f5bf539148 | ||
|
|
855a3c65c0 | ||
|
|
16cf3c4eaf | ||
|
|
da533ad4e0 | ||
|
|
b4a9db51be | ||
|
|
15946c6136 | ||
|
|
9f4c480725 | ||
|
|
74f9c3cd82 | ||
|
|
34fb39c976 | ||
|
|
ea80a21a54 | ||
|
|
33590d487b | ||
|
|
880002de31 | ||
|
|
fb0a62f3f6 | ||
|
|
a4bd7c4274 | ||
|
|
8122941c3c | ||
|
|
21f4c27cfb | ||
|
|
b578ec82d7 | ||
|
|
50432e2651 | ||
|
|
fcc9242a63 | ||
|
|
6014a70d0d | ||
|
|
ed61ea9dd2 | ||
|
|
9bbd881757 | ||
|
|
8d68c34796 | ||
|
|
e8025705a4 | ||
|
|
19711e16b6 | ||
|
|
1272bd12f0 | ||
|
|
731b028b11 | ||
|
|
1a295bb3d6 | ||
|
|
9c44da2594 | ||
|
|
75c27e0b85 | ||
|
|
b95a79240e | ||
|
|
8e374efe8d | ||
|
|
b578660624 | ||
|
|
d6ee2a37f4 | ||
|
|
18a92bbfd8 | ||
|
|
4e8c3accf2 | ||
|
|
a8e25af523 | ||
|
|
d531848db5 | ||
|
|
670f415565 | ||
|
|
061213ca78 | ||
|
|
0ce4ee8a7a | ||
|
|
1cd6603da0 | ||
|
|
453911e6fc | ||
|
|
cb6267ad08 | ||
|
|
4518d3c89f | ||
|
|
e47a619556 | ||
|
|
414f5fc393 | ||
|
|
362e3bc603 | ||
|
|
4d549f70c9 | ||
|
|
85dd726d43 | ||
|
|
24febe11f3 | ||
|
|
64b1e9bed3 | ||
|
|
68c91a732d | ||
|
|
8573f22d96 | ||
|
|
61c7ff4256 | ||
|
|
860d887931 | ||
|
|
5e929a75fa | ||
|
|
d82cf0b76a | ||
|
|
cb7e07496a | ||
|
|
17881c5c6e | ||
|
|
2e816b26aa | ||
|
|
68d67c9e92 | ||
|
|
0dea6fdcea | ||
|
|
530dcff86a | ||
|
|
9d9a4932b3 | ||
|
|
bfb67d45e8 | ||
|
|
b1e8105442 | ||
|
|
7aced17976 | ||
|
|
191b40b2ac | ||
|
|
15ca5bec61 | ||
|
|
ba4d226004 | ||
|
|
3adfd334df | ||
|
|
d7ca5f2e96 | ||
|
|
67bdb3498f | ||
|
|
9e9386b223 | ||
|
|
4029de2d42 | ||
|
|
777095670e | ||
|
|
a11c6303e2 | ||
|
|
3394e316b7 | ||
|
|
4b0437394c | ||
|
|
d10a566e48 | ||
|
|
e0cca3e7a3 | ||
|
|
0c8ab7c725 | ||
|
|
a2bef05a4b | ||
|
|
6361985fb1 | ||
|
|
ad290f5ed2 | ||
|
|
5f53a1459e | ||
|
|
379623d548 | ||
|
|
db1113e5b3 | ||
|
|
d943a50df1 | ||
|
|
eb86899e08 | ||
|
|
e2842ede44 | ||
|
|
fdd4b3d919 | ||
|
|
7771052db8 | ||
|
|
d999ff857d | ||
|
|
30370a55e1 | ||
|
|
a7330d61b6 | ||
|
|
e2f6780de4 | ||
|
|
05b4f8e617 | ||
|
|
eb888029d1 | ||
|
|
3e08486385 | ||
|
|
12b0295906 | ||
|
|
2756327f12 | ||
|
|
b8ca8b7849 | ||
|
|
6abdca0597 | ||
|
|
bf14999eb2 | ||
|
|
c87328f5cf | ||
|
|
9a70898b09 | ||
|
|
4a83ffca6f | ||
|
|
319017f03f | ||
|
|
33e166a462 | ||
|
|
8994154b23 | ||
|
|
7b88df32d1 | ||
|
|
0d6ddfb71e | ||
|
|
4a4e16c206 | ||
|
|
11f6a5d341 | ||
|
|
ad208ec473 | ||
|
|
72ad1d7c68 | ||
|
|
d6a9d2efdf | ||
|
|
8eed999611 | ||
|
|
07f2cbfbba | ||
|
|
fc4dea6839 | ||
|
|
17b3300351 | ||
|
|
f3508cef36 | ||
|
|
5c41d20421 | ||
|
|
53e16751a7 | ||
|
|
18f7b56c03 | ||
|
|
c6f51e6a09 | ||
|
|
971fb734de | ||
|
|
200304402f | ||
|
|
4baa75ccf0 | ||
|
|
9af1c7e1a3 | ||
|
|
a302257129 | ||
|
|
4251f6c0f4 | ||
|
|
c080c2cbca | ||
|
|
0676348bd4 | ||
|
|
18ad7cde20 | ||
|
|
b61d0553a0 | ||
|
|
2a3b613230 | ||
|
|
0d4862b238 | ||
|
|
c7607f6fcc | ||
|
|
29216b226f | ||
|
|
32ba0ce4fb | ||
|
|
c781b4e1c7 | ||
|
|
be2d014f08 | ||
|
|
18cdf70864 | ||
|
|
1be25f0f47 | ||
|
|
cc93651bc9 | ||
|
|
a7a1559e01 | ||
|
|
15f08aaa30 | ||
|
|
d0295bae01 | ||
|
|
5220c37edd | ||
|
|
4f649e020c | ||
|
|
e0d15c1a09 | ||
|
|
6f9c40b0a8 | ||
|
|
a946a0181d | ||
|
|
6d44540ab3 | ||
|
|
6a547cb9db | ||
|
|
ac8dd7af67 | ||
|
|
0962f699e4 | ||
|
|
fcd1028fb7 | ||
|
|
dc429e33ff | ||
|
|
6b40ca36a5 | ||
|
|
d869bc6675 | ||
|
|
d8aeb82949 | ||
|
|
32e8213ebf | ||
|
|
fa2be59895 | ||
|
|
321c522fa5 | ||
|
|
7378b8581a | ||
|
|
2d634364a9 | ||
|
|
f7adf6f73d | ||
|
|
fb914e1a50 | ||
|
|
772bb1d60c | ||
|
|
bd4d2202ea | ||
|
|
870808e63f | ||
|
|
d7fff5a8ab | ||
|
|
609afce544 | ||
|
|
181bcadbe2 | ||
|
|
3f2a9facf8 | ||
|
|
c5c79234f1 | ||
|
|
de9fb5e382 | ||
|
|
c9d132f007 | ||
|
|
a1a9d41a7a | ||
|
|
73112c9faa | ||
|
|
50678dafe1 | ||
|
|
b1363a16ab | ||
|
|
ae986e71fa | ||
|
|
0531831fe8 | ||
|
|
18eeee8e1f | ||
|
|
d99269afac | ||
|
|
38d1727e9c | ||
|
|
e0265252d7 | ||
|
|
6cc92cee8d | ||
|
|
1d392483b4 | ||
|
|
3b864ac1a0 | ||
|
|
baa5ea83fa | ||
|
|
d651400fa2 | ||
|
|
b729a658df | ||
|
|
2c8f46466b | ||
|
|
f2117b1186 | ||
|
|
726cf47f17 | ||
|
|
1759a3e149 | ||
|
|
2374439cd8 | ||
|
|
f85bfd31db | ||
|
|
43b58bfba9 | ||
|
|
fafb6c01da | ||
|
|
1bd47f34e5 | ||
|
|
661a038780 | ||
|
|
e706a69139 | ||
|
|
2b59ae18bc | ||
|
|
7d3e2a41b9 | ||
|
|
7ef57345ca | ||
|
|
7e5169e66d | ||
|
|
73a85b4955 | ||
|
|
2c12256260 | ||
|
|
a821abfb11 | ||
|
|
20e5db22b8 | ||
|
|
54e8a536c4 | ||
|
|
afa67726c1 | ||
|
|
1db3e9c686 | ||
|
|
2fd6e0a2a8 | ||
|
|
af454c7643 | ||
|
|
1a589fcf32 | ||
|
|
af215d6ce8 | ||
|
|
e9fef73f53 | ||
|
|
7c9b118b2d | ||
|
|
5db2590bc6 | ||
|
|
dc1ba24470 | ||
|
|
e384d53996 | ||
|
|
946f9ff3e1 | ||
|
|
8d690ac146 | ||
|
|
8245d77738 | ||
|
|
59c7684568 | ||
|
|
a158e7f8bd | ||
|
|
c11c4b0e3e | ||
|
|
fe42ac11a8 | ||
|
|
00f8c9583d | ||
|
|
a317874f93 | ||
|
|
651356a9ec | ||
|
|
1c2327b2d6 | ||
|
|
8c3e0f23b0 | ||
|
|
1719c0d352 | ||
|
|
bb78f64cd5 | ||
|
|
2fe5be2483 | ||
|
|
929fe08525 | ||
|
|
66dfef8729 | ||
|
|
238d61ce1e | ||
|
|
2fa2bf1706 | ||
|
|
a07984be9e | ||
|
|
e8a7086546 | ||
|
|
23d48d4c0e | ||
|
|
3342faa039 | ||
|
|
6c24061c82 | ||
|
|
b9a1fb7743 | ||
|
|
3c3fc969ac | ||
|
|
c87212f2d7 | ||
|
|
b3a4c95dad | ||
|
|
de44b1f91f | ||
|
|
689022661d | ||
|
|
905d46a09d | ||
|
|
5585f20940 | ||
|
|
5c3ed3f018 | ||
|
|
03cabf6ea3 | ||
|
|
2798dc455f | ||
|
|
db20be5576 | ||
|
|
495862e9f9 | ||
|
|
2ca1c51fdc | ||
|
|
70e1ad41e2 | ||
|
|
53ec7ed272 | ||
|
|
d98a577740 | ||
|
|
fd834f48c2 | ||
|
|
7be65a37c6 | ||
|
|
3b5e2d86fb | ||
|
|
716b9fb50b | ||
|
|
006ca13797 | ||
|
|
6e008343c8 | ||
|
|
2ca24883ac | ||
|
|
4a43983f4e | ||
|
|
d9e83121c1 | ||
|
|
f5b4b97c9b | ||
|
|
bcfa430a40 | ||
|
|
7c51815cf5 | ||
|
|
fee2fb953b | ||
|
|
8ecb7f59c4 | ||
|
|
b85cb9fb3b | ||
|
|
a631c5ca1c | ||
|
|
bee9bd8f67 | ||
|
|
8d3874f8a9 | ||
|
|
bade16d227 | ||
|
|
e0a132bde3 | ||
|
|
4d7e1f4d26 | ||
|
|
700eee6604 | ||
|
|
694ed61e4c | ||
|
|
d7330097ba | ||
|
|
c65a3a6ca3 | ||
|
|
793c191619 | ||
|
|
6c3e2fe53e | ||
|
|
08c419e3ee | ||
|
|
736f4185b5 | ||
|
|
9cc0fc8c49 | ||
|
|
8f067dcde7 | ||
|
|
ad6eb58fe1 | ||
|
|
3f3ab1c3c8 | ||
|
|
9adae32847 | ||
|
|
b8098e7992 | ||
|
|
bef4d0dd3b | ||
|
|
a789100e22 | ||
|
|
abeca70625 | ||
|
|
cc293b1170 | ||
|
|
8ab62c17c6 | ||
|
|
cf14e857ca | ||
|
|
c322ef38fd | ||
|
|
c9cc493063 | ||
|
|
fb48f7f117 | ||
|
|
c344e54cf6 | ||
|
|
7306ecccbc | ||
|
|
b98174db7a | ||
|
|
e48f66694d | ||
|
|
533fccd1d3 | ||
|
|
ec4bd7600f | ||
|
|
65d4e98d38 | ||
|
|
195b843840 | ||
|
|
00ac66e450 | ||
|
|
351701036b | ||
|
|
2bead5dadd | ||
|
|
dbc707adcd | ||
|
|
5066242ef3 | ||
|
|
e9deff52a7 | ||
|
|
eb3d9b8714 | ||
|
|
18deb398ca | ||
|
|
d9abebb4be | ||
|
|
a480db8b77 | ||
|
|
1f26b68d45 | ||
|
|
937501c61b | ||
|
|
5523fb86fb | ||
|
|
7dcfcf8ca8 | ||
|
|
087c68569a | ||
|
|
6abfdd2345 | ||
|
|
cddd784e8d | ||
|
|
f6978217fb | ||
|
|
aa58cedc15 | ||
|
|
a0b0d72d19 | ||
|
|
fa8d0945e0 | ||
|
|
3ed49381e3 | ||
|
|
8ec3fb95f0 | ||
|
|
cc11430d16 | ||
|
|
0b650ee2dc | ||
|
|
4def35cb45 | ||
|
|
0e56f27e0d | ||
|
|
bd8983f462 | ||
|
|
7ef635d9cf | ||
|
|
19ea6fae6a | ||
|
|
651a111db9 | ||
|
|
3a98bdb3c2 | ||
|
|
f01b982cff | ||
|
|
69dd96f76f | ||
|
|
329ae08e60 | ||
|
|
1e13ad8fee | ||
|
|
e0c9a9523a | ||
|
|
0726a3034d | ||
|
|
a88d047eff | ||
|
|
4001561987 | ||
|
|
2f860b37bd | ||
|
|
b86009b9d0 | ||
|
|
54733e1b34 | ||
|
|
56f01888b7 | ||
|
|
8320f16716 | ||
|
|
0b16e5992d | ||
|
|
248beb4a55 | ||
|
|
c9f608d030 | ||
|
|
f837d3e6a2 | ||
|
|
8847d5fc42 | ||
|
|
7819a6fac0 | ||
|
|
c199185808 | ||
|
|
e0b5738606 | ||
|
|
f3e3880a57 | ||
|
|
d44b90c16d | ||
|
|
cc32a3e863 | ||
|
|
5740c9b76b | ||
|
|
80fa6ad3eb | ||
|
|
7bc1ad2b4f | ||
|
|
82a2fe7cbe | ||
|
|
40bf944957 | ||
|
|
7ef7546fa9 | ||
|
|
5e26e84d77 | ||
|
|
e67bc2083a | ||
|
|
a3cb3f7f77 | ||
|
|
daeb47e72e | ||
|
|
8c5ab99fa7 | ||
|
|
984a7388f1 | ||
|
|
274b105676 | ||
|
|
a689d1b4c0 | ||
|
|
1581e91317 | ||
|
|
20f4034c1c | ||
|
|
9fb1c4b67c | ||
|
|
2e559d6a06 | ||
|
|
71de6c395b | ||
|
|
b09f9e4814 | ||
|
|
ec4bfb0e8a | ||
|
|
39bf38d1e4 | ||
|
|
e6fd1379b8 | ||
|
|
1d9931a5a8 | ||
|
|
06164d34b5 | ||
|
|
f3de288e19 | ||
|
|
62bf315059 | ||
|
|
011c97e3a4 | ||
|
|
2ca3d2c7a4 | ||
|
|
cc52c94348 | ||
|
|
4b8cc96dfa | ||
|
|
338d2ab839 | ||
|
|
54fc6da24e | ||
|
|
825b3fb858 | ||
|
|
fd0212a142 | ||
|
|
3b397cb39c | ||
|
|
1e002c2c2f | ||
|
|
8f45daeca8 | ||
|
|
105ef989b7 | ||
|
|
9e7c31d1e4 | ||
|
|
771dfa6b68 | ||
|
|
fb52cfa73e | ||
|
|
b2c75d215b | ||
|
|
951607de74 | ||
|
|
122fd2bc74 | ||
|
|
f102125d3c | ||
|
|
14bda76b30 | ||
|
|
3af115a663 | ||
|
|
a344804734 | ||
|
|
c1c49ce53b | ||
|
|
d8eaa5c004 | ||
|
|
e8aa9b24f1 | ||
|
|
3036cd223e | ||
|
|
1ae5150b08 | ||
|
|
47148f2033 | ||
|
|
4d4d15436b | ||
|
|
7f4741b3a3 | ||
|
|
85644a7c1c | ||
|
|
f40ae6af30 | ||
|
|
708e1a5aab | ||
|
|
55613c9a31 | ||
|
|
e6590c7d31 | ||
|
|
5af2f4e98d | ||
|
|
8e68699435 | ||
|
|
77580401da | ||
|
|
7902e7edb7 | ||
|
|
4ad223277d | ||
|
|
6f5da11c72 | ||
|
|
3eed86fbf3 | ||
|
|
10d51a854a | ||
|
|
6c60bad757 | ||
|
|
79f666b5da | ||
|
|
3ea3a40395 | ||
|
|
193aa79956 | ||
|
|
3481a6ee53 | ||
|
|
d1ef5fce9f | ||
|
|
5d48911f01 | ||
|
|
8a90f97959 | ||
|
|
e9665b34e5 | ||
|
|
d4b1ceda90 | ||
|
|
a0fae88f7d | ||
|
|
03a8609680 | ||
|
|
3e40f9cc66 | ||
|
|
576bb078f7 | ||
|
|
3cdcc85c87 | ||
|
|
ec3a26dfbc | ||
|
|
c755dae6ee | ||
|
|
b67d51e019 | ||
|
|
7a34d6026f | ||
|
|
548eac8e60 | ||
|
|
05ac8e861f | ||
|
|
7a4d475cca | ||
|
|
eea8e60518 | ||
|
|
f6a272498d | ||
|
|
aef7c4ce8e | ||
|
|
b29eb8e032 | ||
|
|
da9183f4b5 | ||
|
|
ae3ddcb0e6 | ||
|
|
c6b8f0e8ed | ||
|
|
bab9f40a81 | ||
|
|
c2cfdf3678 | ||
|
|
8967eadc02 | ||
|
|
4cc65f837f | ||
|
|
22706e7371 | ||
|
|
9f9302662c | ||
|
|
6b90e1babd | ||
|
|
7e540a8f49 | ||
|
|
f06ffd72aa | ||
|
|
83d3d7579c | ||
|
|
39edb6eb45 | ||
|
|
d81c1a929e | ||
|
|
f69c28df18 | ||
|
|
66f54e7f1a | ||
|
|
81e416b085 | ||
|
|
a5f73b441c | ||
|
|
0f1ebccad5 | ||
|
|
2f01df6185 | ||
|
|
c4982319fe | ||
|
|
8fb4e88439 | ||
|
|
e821efca07 | ||
|
|
568f7b21ae | ||
|
|
d3c0f5320c | ||
|
|
5adc88bf50 | ||
|
|
67300adf41 | ||
|
|
4a3a67bf37 | ||
|
|
8150847218 | ||
|
|
8cb95b4646 | ||
|
|
df4ce904a0 | ||
|
|
1cc3a43268 | ||
|
|
d5a862a406 | ||
|
|
33c83c3e62 | ||
|
|
171adcd09e | ||
|
|
13f7cb9a9a | ||
|
|
ee7dbafc85 | ||
|
|
f5cfe4e8a2 | ||
|
|
c3e45d19d7 | ||
|
|
966100c2d6 | ||
|
|
bce1dea11b | ||
|
|
c55067ab7c | ||
|
|
9da4084561 | ||
|
|
1d7235d4ca | ||
|
|
2cb8171105 | ||
|
|
bbc7799b7c | ||
|
|
a9cbcce8b2 | ||
|
|
3531ca64bb | ||
|
|
e402b85dd6 | ||
|
|
8ae5cf0ccf | ||
|
|
5c34c3fc77 | ||
|
|
3a570dc0d9 | ||
|
|
3c6748b30d | ||
|
|
24945f67bf | ||
|
|
edddab1e51 | ||
|
|
acfc86d2a4 | ||
|
|
651868dec9 | ||
|
|
18bc03e660 | ||
|
|
5f0013e482 | ||
|
|
10c0a2de65 | ||
|
|
b24be2566d | ||
|
|
77439b5db5 | ||
|
|
23594a43ea | ||
|
|
601db7aa86 | ||
|
|
d15ba11104 | ||
|
|
d45d92433f | ||
|
|
97127a90c6 | ||
|
|
55bb63edea | ||
|
|
15a4e54435 | ||
|
|
3a522aef5d | ||
|
|
a98885a123 | ||
|
|
2b7923261b | ||
|
|
b043f829a6 | ||
|
|
f415f7b146 | ||
|
|
4e4e6864dd | ||
|
|
b0a23be07b | ||
|
|
f602cd7b5e | ||
|
|
6fac92cb62 | ||
|
|
b6d9060152 | ||
|
|
bb10b32200 | ||
|
|
e0be2fa4ba | ||
|
|
97d2829941 | ||
|
|
5779242bd7 | ||
|
|
abf90358fa | ||
|
|
76b73acb30 | ||
|
|
b24910271a | ||
|
|
3d75c71bfa | ||
|
|
8096d7c416 | ||
|
|
17adf7cc14 | ||
|
|
96f961ef54 | ||
|
|
4ade062cd8 | ||
|
|
53efcb989c | ||
|
|
e183bfe278 | ||
|
|
51a6147609 | ||
|
|
f6d856ee11 | ||
|
|
4e41fad107 | ||
|
|
2bafdf2752 | ||
|
|
84e8922aa6 | ||
|
|
53e09279a2 | ||
|
|
38a1c1102f | ||
|
|
c68df44717 | ||
|
|
55266f2efd | ||
|
|
f3e544b003 | ||
|
|
e9054b01e6 | ||
|
|
d0660cf6da | ||
|
|
8a27ca7c38 | ||
|
|
ff86b2ef4f | ||
|
|
1c236d74b6 | ||
|
|
af7e1d6a0f | ||
|
|
dfd03d4f27 | ||
|
|
5d70275c3a | ||
|
|
475566837e | ||
|
|
a08cebbef9 | ||
|
|
571e778bce | ||
|
|
2eb95395c8 | ||
|
|
7ff08615bb | ||
|
|
3628aaab55 | ||
|
|
cd195c3700 | ||
|
|
9dc5d93b99 | ||
|
|
f71e530810 | ||
|
|
6c471f7be4 | ||
|
|
f80f4ef110 | ||
|
|
4d3dd7a6e6 | ||
|
|
cc68d20758 | ||
|
|
20682e63ef | ||
|
|
40363df4a1 | ||
|
|
e3ea889d5f | ||
|
|
7f14da3026 | ||
|
|
06d28f7d67 | ||
|
|
27f2a672ba | ||
|
|
23f9d537e9 | ||
|
|
d86339bae3 | ||
|
|
846c338491 | ||
|
|
2d418dae93 | ||
|
|
91e0fcdc1b | ||
|
|
a9ed8461d1 | ||
|
|
5a71ca797a | ||
|
|
70eb07d7d6 | ||
|
|
71ee69af01 | ||
|
|
dd41c0586c | ||
|
|
aaf63dbf5c | ||
|
|
87f6802967 | ||
|
|
2cbf3581c5 | ||
|
|
5a67e208fd | ||
|
|
3391a4a035 | ||
|
|
076aa363a4 | ||
|
|
2cb68c0bd4 | ||
|
|
6eb24b5017 | ||
|
|
b2faa6b86c | ||
|
|
43990b5b67 | ||
|
|
9453164dd2 | ||
|
|
00fcd1c65e | ||
|
|
85ad7b0f38 | ||
|
|
f0a9940364 | ||
|
|
b472aaf6a0 | ||
|
|
059c1bf61c | ||
|
|
add13fd6a4 | ||
|
|
904c2e8a67 | ||
|
|
a5c3be586a | ||
|
|
9f5139d929 | ||
|
|
bae305340e | ||
|
|
40c1556f29 | ||
|
|
0db4f5821f | ||
|
|
12ebc95800 | ||
|
|
d7b919032e | ||
|
|
be8bf8b0d6 | ||
|
|
f84f1422f4 | ||
|
|
b01976e3bb | ||
|
|
50b48fb272 | ||
|
|
1617e31d69 | ||
|
|
51cd082d4c | ||
|
|
72414b7945 | ||
|
|
1afa14ccbd | ||
|
|
477c29cdf1 | ||
|
|
49a424314a | ||
|
|
598523cd9d | ||
|
|
1fdb6f1cdf | ||
|
|
9997487a9c | ||
|
|
b5e94e4054 | ||
|
|
a5f6e0bac4 | ||
|
|
2c9de79576 | ||
|
|
63dd00a050 | ||
|
|
99823a3d4f | ||
|
|
c0bdd464f6 | ||
|
|
7e7377f4f7 | ||
|
|
15448765dd | ||
|
|
f0f1c3144b | ||
|
|
630da4212d | ||
|
|
38bc24bb9e | ||
|
|
012285337b | ||
|
|
3ba4f62cef | ||
|
|
b4996769c1 | ||
|
|
9a6b1f53da | ||
|
|
ef45828ca7 | ||
|
|
ea153e4f26 | ||
|
|
59b8626277 | ||
|
|
d8e03773f0 | ||
|
|
5ab799bbf8 | ||
|
|
6c763a83cb | ||
|
|
9cf337309d | ||
|
|
1af35cdd3b | ||
|
|
fcb98474b8 | ||
|
|
54f7e9366a | ||
|
|
ddf9df2b5e | ||
|
|
7fe9f82d89 | ||
|
|
661adc4027 | ||
|
|
1011c464dc | ||
|
|
4b498a3b42 | ||
|
|
3c90b47e77 | ||
|
|
671b1e4a8f | ||
|
|
52021be806 | ||
|
|
75a7bd610a | ||
|
|
de553cf4fe | ||
|
|
0a65f43789 | ||
|
|
0f277c55ec | ||
|
|
04b01a6ced | ||
|
|
6c3381ce52 | ||
|
|
fa2e2248d3 | ||
|
|
362150a715 | ||
|
|
27b5991ee3 | ||
|
|
0a7dbb0ca4 | ||
|
|
234861a156 | ||
|
|
78d6d312f8 | ||
|
|
a1144a5b6b | ||
|
|
9723cc466e | ||
|
|
79c12c0129 | ||
|
|
a08d6bae10 | ||
|
|
39261a276c | ||
|
|
c471986024 | ||
|
|
d2e3a156e8 | ||
|
|
9badfa997b | ||
|
|
72f8bf4e20 | ||
|
|
c74b15b120 | ||
|
|
9a4f3ab08c | ||
|
|
a0935c169e | ||
|
|
0e2181fb90 | ||
|
|
732ec14302 | ||
|
|
ec95060b6e | ||
|
|
689afec26e | ||
|
|
bbf23ea40a | ||
|
|
34e0a8e47d | ||
|
|
81f330e888 | ||
|
|
e5d657dd55 | ||
|
|
f919127a7e | ||
|
|
2001bd808f | ||
|
|
f51857cedc | ||
|
|
f3b8d27c4f | ||
|
|
d3a577c365 | ||
|
|
b80f9e3871 | ||
|
|
328d42656d | ||
|
|
d3d2cbed7e | ||
|
|
d6f7aedec1 | ||
|
|
7a5a776cb9 | ||
|
|
06f782aa13 | ||
|
|
6b825fec37 | ||
|
|
b452d8af40 | ||
|
|
e96f1d2129 | ||
|
|
72d6af9c84 | ||
|
|
8198cd0b68 | ||
|
|
fe68b47ba2 | ||
|
|
384d39543c | ||
|
|
225e62d12f | ||
|
|
3905a2aa60 | ||
|
|
ff6ce2dac0 | ||
|
|
16675cbff2 | ||
|
|
9524c6e289 | ||
|
|
bc316c648c | ||
|
|
6753ba21c4 | ||
|
|
efbe32e081 | ||
|
|
55fd6d44f9 | ||
|
|
90972f2d94 | ||
|
|
7382c7e51c | ||
|
|
8a6d129575 | ||
|
|
e20c77710d | ||
|
|
93da88eac0 | ||
|
|
5072539917 | ||
|
|
78c59df1d1 | ||
|
|
75e40eafb2 | ||
|
|
274a1ac5f0 | ||
|
|
17040a4af4 | ||
|
|
b09a5f905e | ||
|
|
683511f311 | ||
|
|
151c58733b | ||
|
|
012ff56cd6 | ||
|
|
493c805993 | ||
|
|
764192af36 | ||
|
|
f969fd7eff | ||
|
|
2c7ee0d415 | ||
|
|
5430c78e18 | ||
|
|
6b052557d1 | ||
|
|
70f3edb0f5 | ||
|
|
4e1f963750 | ||
|
|
3c3d3b33f1 | ||
|
|
be7b9eea25 | ||
|
|
016ad6b7a6 | ||
|
|
5dab35447a | ||
|
|
63be819533 | ||
|
|
af8afb1204 | ||
|
|
2170be8e01 | ||
|
|
1187a506dd | ||
|
|
ff5ceddbe2 | ||
|
|
41a5e09caa | ||
|
|
60b96c9118 | ||
|
|
7ccb2d9f44 | ||
|
|
2c2090a194 | ||
|
|
de9b2998ac | ||
|
|
29b81a3d50 | ||
|
|
5bc0d89ce7 | ||
|
|
342b10c232 | ||
|
|
ba9d3c1602 | ||
|
|
d3b3ce6980 | ||
|
|
4b79a91287 | ||
|
|
de06c9b2ca | ||
|
|
84c7d00ea9 | ||
|
|
8f5ae0054d | ||
|
|
374f0c4e27 | ||
|
|
a6170f4da9 | ||
|
|
578dd5ef35 | ||
|
|
934a131deb | ||
|
|
5bc1c36e67 | ||
|
|
b1b9853f92 | ||
|
|
5d6a083d1d | ||
|
|
9478245986 | ||
|
|
4c1c12734a | ||
|
|
5d3a360456 | ||
|
|
b51da35a9a | ||
|
|
164dcd1b97 | ||
|
|
c0964c2b01 | ||
|
|
af5b226e53 | ||
|
|
4e7a0db950 | ||
|
|
cee6ed9a9f | ||
|
|
fa1013f7c3 | ||
|
|
3a1db3a191 | ||
|
|
8b57ba7aa8 | ||
|
|
ea638e37f6 | ||
|
|
4342d45a2f | ||
|
|
fe4d2382b8 | ||
|
|
2a13609eff | ||
|
|
c2ff1cabf1 | ||
|
|
54370e3153 | ||
|
|
e72ed26809 | ||
|
|
64e056ffa9 | ||
|
|
db9b7db53d | ||
|
|
a9e6590b5e | ||
|
|
b67121b755 | ||
|
|
7bce4de682 | ||
|
|
8bdb427133 | ||
|
|
4c23f3c478 | ||
|
|
9502de561b | ||
|
|
210c11a733 | ||
|
|
6917027204 | ||
|
|
e78ce7e6b9 | ||
|
|
a3327f8841 | ||
|
|
f727dd773b | ||
|
|
952646f066 | ||
|
|
1e037ecb60 | ||
|
|
c9f75d82d6 | ||
|
|
de3849c20c | ||
|
|
c391c2e938 | ||
|
|
1cde4b1f8a | ||
|
|
42360d109b | ||
|
|
7aa4d9e6db | ||
|
|
9ed3390b81 | ||
|
|
1ff56f7bfd | ||
|
|
16917997cd | ||
|
|
f42568d262 | ||
|
|
97bbdf561a | ||
|
|
f4043cbd9f | ||
|
|
e9ff26abb4 |
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Leptos Dependencies**
|
||||
|
||||
Please copy and paste the Leptos dependencies and features from your `Cargo.toml`.
|
||||
|
||||
For example:
|
||||
```toml
|
||||
leptos = { version = "0.3", features = ["serde"] }
|
||||
leptos_axum = { version = "0.3", optional = true }
|
||||
leptos_meta = { version = "0.3"}
|
||||
leptos_router = { version = "0.3"}
|
||||
```
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: Support or Question
|
||||
url: https://github.com/leptos-rs/leptos/discussions/new?category=q-a
|
||||
about: Do you need help figuring out how to do something, or want some help troubleshooting a bug? You can ask in our Discussions section.
|
||||
- name: Discord Discussions
|
||||
url: https://discord.gg/YdRAhS7eQB
|
||||
about: For more informal, real-time conversation and support, you can join our Discord server.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
45
.github/workflows/check-examples.yml
vendored
45
.github/workflows/check-examples.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make check-examples
|
||||
45
.github/workflows/check-stable.yml
vendored
45
.github/workflows/check-stable.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Check examples ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all examples
|
||||
run: cargo make check-stable
|
||||
45
.github/workflows/check.yml
vendored
45
.github/workflows/check.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run `cargo check` ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run cargo check on all libraries
|
||||
run: cargo make check
|
||||
32
.github/workflows/ci-changed-examples.yml
vendored
Normal file
32
.github/workflows/ci-changed-examples.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: CI Changed Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
uses: ./.github/workflows/get-example-changed.yml
|
||||
|
||||
get-matrix:
|
||||
needs: [get-example-changed]
|
||||
uses: ./.github/workflows/get-changed-examples-matrix.yml
|
||||
with:
|
||||
example_changed: ${{ fromJSON(needs.get-example-changed.outputs.example_changed) }}
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-example-changed, get-matrix]
|
||||
if: needs.get-example-changed.outputs.example_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-01-29
|
||||
27
.github/workflows/ci-examples.yml
vendored
Normal file
27
.github/workflows/ci-examples.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: CI Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
get-examples-matrix:
|
||||
uses: ./.github/workflows/get-examples-matrix.yml
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed, get-examples-matrix]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.get-examples-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-01-29
|
||||
26
.github/workflows/ci-stable-examples.yml
vendored
Normal file
26
.github/workflows/ci-stable-examples.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: CI Stable Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
directory: [examples/counters_stable, examples/counter_without_macros]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: stable
|
||||
43
.github/workflows/ci.yml
vendored
Normal file
43
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
get-leptos-changed:
|
||||
uses: ./.github/workflows/get-leptos-changed.yml
|
||||
|
||||
test:
|
||||
name: CI
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
directory:
|
||||
[
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/utils,
|
||||
leptos,
|
||||
leptos_config,
|
||||
leptos_dom,
|
||||
leptos_hot_reload,
|
||||
leptos_macro,
|
||||
leptos_reactive,
|
||||
leptos_server,
|
||||
meta,
|
||||
router,
|
||||
server_fn,
|
||||
server_fn/server_fn_macro_default,
|
||||
server_fn_macro,
|
||||
]
|
||||
uses: ./.github/workflows/run-cargo-make-task.yml
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-01-29
|
||||
34
.github/workflows/fmt.yml
vendored
34
.github/workflows/fmt.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run rustfmt
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Run Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
54
.github/workflows/get-changed-examples-matrix.yml
vendored
Normal file
54
.github/workflows/get-changed-examples-matrix.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Changed Examples Matrix Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
required: true
|
||||
type: boolean
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.get-example-changed.outputs.matrix }}
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
name: Get Changed Example Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
files: |
|
||||
examples/**
|
||||
!examples/cargo-make/**
|
||||
!examples/gtk/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
json: true
|
||||
quotepath: false
|
||||
|
||||
- name: List example project directories that changed
|
||||
run: echo '${{ steps.changed-dirs.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
if [ ${{ inputs.example_changed }} == 'true' ]; then
|
||||
# Create matrix with changed directories
|
||||
echo "matrix={\"directory\":${{ steps.changed-dirs.outputs.all_changed_files }}}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create matrix with one item to prevent an empty vector error
|
||||
echo "matrix={\"directory\":[\"NO_CHANGE\"]}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
39
.github/workflows/get-example-changed.yml
vendored
Normal file
39
.github/workflows/get-example-changed.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Examples Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
example_changed:
|
||||
description: "Example Changed"
|
||||
value: ${{ jobs.get-example-changed.outputs.example_changed }}
|
||||
|
||||
jobs:
|
||||
get-example-changed:
|
||||
name: Get Example Changed
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
example_changed: ${{ steps.set-example-changed.outputs.example_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
!examples/cargo-make/**
|
||||
!examples/gtk/**
|
||||
!examples/Makefile.toml
|
||||
!examples/*.md
|
||||
|
||||
- name: List example files that changed
|
||||
run: echo '${{ steps.changed-files.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set example_changed
|
||||
id: set-example-changed
|
||||
run: |
|
||||
echo "example_changed=${{ steps.changed-files.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
40
.github/workflows/get-examples-matrix.yml
vendored
Normal file
40
.github/workflows/get-examples-matrix.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Get Examples Matrix Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
matrix:
|
||||
description: "Matrix"
|
||||
value: ${{ jobs.create.outputs.matrix }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Create Examples Matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install JQ Tool
|
||||
uses: mbround18/install-jq@v1
|
||||
|
||||
- name: Set Matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
examples=$(ls examples |
|
||||
awk '{print "examples/" $0}' |
|
||||
grep -v .md |
|
||||
grep -v examples/Makefile.toml |
|
||||
grep -v examples/cargo-make |
|
||||
grep -v examples/gtk |
|
||||
jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "Example Directories: $examples"
|
||||
echo "matrix={\"directory\":$examples}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Print Location Info
|
||||
run: |
|
||||
echo "Workspace: ${{ github.workspace }}"
|
||||
pwd
|
||||
ls | sort -u
|
||||
44
.github/workflows/get-leptos-changed.yml
vendored
Normal file
44
.github/workflows/get-leptos-changed.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Get Leptos Changed Call
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
leptos_changed:
|
||||
description: "Leptos Changed"
|
||||
value: ${{ jobs.create.outputs.leptos_changed }}
|
||||
|
||||
jobs:
|
||||
create:
|
||||
name: Detect Source Change
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
integrations/**
|
||||
leptos/**
|
||||
leptos_config/**
|
||||
leptos_dom/**
|
||||
leptos_hot_reload/**
|
||||
leptos_macro/**
|
||||
leptos_reactive/**
|
||||
leptos_server/**
|
||||
meta/**
|
||||
router/**
|
||||
server_fn/**
|
||||
server_fn_macro/**
|
||||
|
||||
- name: List source files that changed
|
||||
run: echo '${{ steps.changed-source.outputs.all_changed_files }}'
|
||||
|
||||
- name: Set leptos_changed
|
||||
id: set-source-changed
|
||||
run: |
|
||||
echo "leptos_changed=${{ steps.changed-source.outputs.any_changed }}" >> "$GITHUB_OUTPUT"
|
||||
37
.github/workflows/publish-book.yml
vendored
Normal file
37
.github/workflows/publish-book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Deploy book
|
||||
on:
|
||||
push:
|
||||
paths: ["docs/book/**"]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pull-requests: write # To create a PR from that branch
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install mdbook
|
||||
run: |
|
||||
mkdir mdbook
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.27/mdbook-v0.4.27-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Deploy GitHub Pages
|
||||
run: |
|
||||
cd docs/book
|
||||
mdbook build
|
||||
git worktree add gh-pages
|
||||
git config user.name "Deploy book from CI"
|
||||
git config user.email ""
|
||||
cd gh-pages
|
||||
# Delete the ref to avoid keeping history.
|
||||
git update-ref -d refs/heads/gh-pages
|
||||
rm -rf *
|
||||
mv ../book/* .
|
||||
git add .
|
||||
git commit -m "Deploy book $GITHUB_SHA to gh-pages"
|
||||
git push --force --set-upstream origin gh-pages
|
||||
119
.github/workflows/run-cargo-make-task.yml
vendored
Normal file
119
.github/workflows/run-cargo-make-task.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
name: Run Task
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
directory:
|
||||
required: true
|
||||
type: string
|
||||
cargo_make_task:
|
||||
required: true
|
||||
type: string
|
||||
toolchain:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run ${{ inputs.cargo_make_task }} (${{ inputs.toolchain }})
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Setup environment
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install Trunk
|
||||
uses: jetli/trunk-action@v0.4.0
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Print Trunk Version
|
||||
run: trunk --version
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Maybe install chromedriver
|
||||
run: |
|
||||
project_makefile=${{inputs.directory}}/Makefile.toml
|
||||
webdriver_count=$(cat $project_makefile | grep "cargo-make/webdriver.toml" | wc -l)
|
||||
if [ $webdriver_count -eq 1 ]; then
|
||||
if ! command -v chromedriver &>/dev/null; then
|
||||
echo chromedriver required
|
||||
sudo apt-get update
|
||||
sudo apt-get install chromium-chromedriver
|
||||
else
|
||||
echo chromedriver is already installed
|
||||
fi
|
||||
else
|
||||
echo chromedriver is not required
|
||||
fi
|
||||
|
||||
- name: Maybe install playwright browser dependencies
|
||||
run: |
|
||||
for pw_path in $(find ${{inputs.directory}} -name playwright.config.ts)
|
||||
do
|
||||
pw_dir=$(dirname $pw_path)
|
||||
if [ ! -v $pw_dir ]; then
|
||||
echo "Playwright required in $pw_dir"
|
||||
cd $pw_dir
|
||||
pnpm dlx playwright install --with-deps
|
||||
else
|
||||
echo Playwright is not required
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
|
||||
# Run Cargo Make Task
|
||||
- name: ${{ inputs.cargo_make_task }}
|
||||
run: |
|
||||
cd ${{ inputs.directory }}
|
||||
cargo make --profile=github-actions ${{ inputs.cargo_make_task }}
|
||||
45
.github/workflows/test.yml
vendored
45
.github/workflows/test.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests ${{ matrix.os }} (using rustc ${{ matrix.rust }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- nightly
|
||||
os:
|
||||
- ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Add wasm32-unknown-unknown
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Setup cargo-make
|
||||
uses: davidB/rust-cargo-make@v1
|
||||
|
||||
- name: Cargo generate-lockfile
|
||||
run: cargo generate-lockfile
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Run tests with all features
|
||||
run: cargo make test
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,3 +7,8 @@ Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.idea
|
||||
.direnv
|
||||
.envrc
|
||||
|
||||
.vscode
|
||||
vendor
|
||||
231
ARCHITECTURE.md
Normal file
231
ARCHITECTURE.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Architecture
|
||||
|
||||
The goal of this document is to make it easier for contributors (and anyone
|
||||
who’s interested!) to understand the architecture of the framework.
|
||||
|
||||
The whole Leptos framework is built from a series of layers. Each of these layers
|
||||
depends on the one below it, but each can be used independently from the ones
|
||||
built on top of it. While running a command like `cargo leptos new --git
|
||||
leptos-rs/start` pulls in the whole framework, it’s important to remember that
|
||||
none of this is magic: each layer of that onion can be stripped away and
|
||||
reimplemented, configured, or adapted as needed, incrementally.
|
||||
|
||||
> Everything that follows will assume you have a good working understanding
|
||||
> of the framework. There will be explanations of how some parts of it work
|
||||
> or fit together, but these are not docs. They assume you know what I’m
|
||||
> talking about.
|
||||
|
||||
## The Reactive System: `leptos_reactive`
|
||||
|
||||
The reactive system allows you to define dynamic values (signals),
|
||||
the relationships between them (derived signals and memos), and the side effects
|
||||
that run in response to them (effects).
|
||||
|
||||
These concepts are completely independent of the DOM and can be used to drive
|
||||
any kind of reactive updates. The reactive system is based on the assumption
|
||||
that data is relatively cheap, and side effects are relatively expensive. Its
|
||||
goal is to minimize those side effects (like updating the DOM or making a network
|
||||
requests) as infrequently as possible.
|
||||
|
||||
The reactive system is implemented as a single data structure that exists at
|
||||
runtime. In exchange for giving ownership over a value to the reactive system
|
||||
(by creating a signal), you receive a `Copy + 'static` identifier for its
|
||||
location in the reactive system. This enables most of the ergonomics of storing
|
||||
and sharing state, the use of callback closures without lifetime issues, etc.
|
||||
This is implemented by storing signals in a slotmap arena. The signal, memo,
|
||||
and scope types that are exposed to users simply carry around an index into that
|
||||
slotmap.
|
||||
|
||||
> Items owned by the reactive system are dropped when the corresponding reactive
|
||||
> scope is dropped, i.e., when the component or section of the UI they’re
|
||||
> created in is removed. In a sense, Leptos implements a “garbage collector”
|
||||
> in which the lifetime of data is tied to the lifetime of the UI, not Rust’s
|
||||
> lexical scopes.
|
||||
|
||||
## The DOM Renderer: `leptos_dom`
|
||||
|
||||
The reactive system can be used to drive any kinds of side effects. One very
|
||||
common side effect is calling an imperative method, for example to update the
|
||||
DOM.
|
||||
|
||||
The entire DOM renderer is built on top of the reactive system. It provides
|
||||
a builder pattern that can be used to create DOM elements dynamically.
|
||||
|
||||
The renderer assumes, as a convention, that dynamic attributes, classes,
|
||||
styles, and children are defined by being passed a `Fn() -> T`, where their
|
||||
static equivalents just receive `T`. There’s nothing about this that is
|
||||
divinely ordained, but it’s a useful convention because it allows us to use
|
||||
zero-overhead derived signals as one of several ways to indicate dynamic
|
||||
content.
|
||||
|
||||
`leptos_dom` also contains code for server-side rendering of the same
|
||||
UI views to HTML, either for out-of-order streaming (`src/ssr.rs`) or
|
||||
in-order streaming/async rendering (`src/ssr_in_order.rs`).
|
||||
|
||||
## The Macros: `leptos_macro`
|
||||
|
||||
It’s entirely possible to write Leptos code with no macros at all. The
|
||||
`view` and `component` macros, the most common, can be replaced by
|
||||
the builder syntax and simple functions (see the `counter_without_macros`
|
||||
example). But the macros enable a JSX-like syntax for describing views.
|
||||
|
||||
This package also contains the `Params` derive macro used for typed
|
||||
queries and route params in the router.
|
||||
|
||||
### Macro-based Optimizations
|
||||
|
||||
Leptos 0.0.x was built much more heavily on macros. Taking its cues
|
||||
from SolidJS, the `view` macro emitted different code for CSR, SSR, and
|
||||
hydration, optimizing each. The CSR/hydrate versions worked by compiling
|
||||
the view to an HTML template string, cloning that `<template>`, and
|
||||
traversing the DOM to set up reactivity. The SSR version worked similarly
|
||||
by compiling the static parts of the view to strings at compile time,
|
||||
reducing the amount of work that needed to be done on each request.
|
||||
|
||||
Proc macros are hard, and this system was brittle. 0.1 introduced a
|
||||
more robust renderer, including the builder syntax, and rebuilt the `view`
|
||||
macro to use that builder syntax instead. It moved the optimized-but-buggy
|
||||
CSR version of the macro to a more-limited `template` macro.
|
||||
|
||||
The `view` macro now separately optimizes SSR to use the same static-string
|
||||
optimizations, which (by our benchmarks) makes Leptos about 3-4x faster
|
||||
than similar Rust frontend frameworks in its HTML rendering.
|
||||
|
||||
> The optimization is pretty straightforward. Consider the following view:
|
||||
>
|
||||
> ```rust
|
||||
> view! { cx,
|
||||
> <main class="text-center">
|
||||
> <div class="flex-col">
|
||||
> <button>"Click me."</button>
|
||||
> <p class="italic">"Text."</p>
|
||||
> </div>
|
||||
> </main>
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Internally, with the builder this is something like
|
||||
>
|
||||
> ```rust
|
||||
> Element {
|
||||
> tag: "main",
|
||||
> attrs: vec![("class", "text-center")],
|
||||
> children: vec![
|
||||
> Element {
|
||||
> tag: "div",
|
||||
> attrs: vec![("class", "flex-col")],
|
||||
> children: vec![
|
||||
> Element {
|
||||
> tag: "button",
|
||||
> attrs: vec![],
|
||||
> children: vec!["Click me"]
|
||||
> },
|
||||
> Element {
|
||||
> tag: "p",
|
||||
> attrs: vec![("class", "italic")],
|
||||
> children: vec!["Text"]
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> This is a _bunch_ of small allocations and separate strings,
|
||||
> and in early 0.1 versions we used a `SmallVec` for children and
|
||||
> attributes and actually caused some stack overflows.
|
||||
>
|
||||
> But if you look at the view itself you can see that none of this
|
||||
> will _ever_ change. So we can actually optimize it at compile
|
||||
> time to a single `&'static str`:
|
||||
>
|
||||
> ```rust
|
||||
> r#"<main class="text-center">
|
||||
> <div class="flex-col">
|
||||
> <button>"Click me."</button>
|
||||
> <p class="italic">"Text."</p>
|
||||
> </div>
|
||||
> </main>"#
|
||||
> ```
|
||||
|
||||
## Server Functions (`leptos_server`, `server_fn`, and `server_fn_macro`)
|
||||
|
||||
Server functions are a framework-agnostic shorthand for converting
|
||||
a function, whose body can only be run on the server, into an ad hoc
|
||||
REST API endpoint, and then generating code on the client to call that
|
||||
endpoint when you call the function.
|
||||
|
||||
These are inspired by Solid/Bling’s `server$` functions, and there’s
|
||||
similar work being done in a number of other JavaScript frameworks.
|
||||
|
||||
RPC is not a new idea, but these kinds of server functions may be.
|
||||
Specifically, by using web standards (defaulting to `POST`/`GET` requests
|
||||
with URL-encoded form data) they allow easy graceful degradation and the
|
||||
use of the `<form>` element.
|
||||
|
||||
This function is split across three packages so that `server_fn` and
|
||||
`server_fn_macro` can be used by other frameworks. `leptos_server`
|
||||
includes some Leptos-specific reactive functionality (like actions).
|
||||
|
||||
## `leptos`
|
||||
|
||||
This package is built on and reexports most of the layers already
|
||||
mentioned, and implements a number of control-flow components (`<Show/>`,
|
||||
`<ErrorBoundary/>`, `<For/>`, `<Suspense/>`, `<Transition/>`) that use
|
||||
public APIs of the other packages.
|
||||
|
||||
This is the main entrypoint for users, but is relatively light itself.
|
||||
|
||||
## `leptos_meta`
|
||||
|
||||
This package exists to allow you to work with tags normally found in
|
||||
the `<head>`, from within your components.
|
||||
|
||||
It is implemented as a distinct package, rather than part of
|
||||
`leptos_dom`, on the principle that “what can be implemented in userland,
|
||||
should be.” The framework can be used without it, so it’s not in core.
|
||||
|
||||
## `leptos_router`
|
||||
|
||||
The router originates as a direct port of `solid-router`, which is the
|
||||
origin of most of its terminology, architecture, and route-matching logic.
|
||||
|
||||
Subsequent developments (like animated routing, and managing route transitions
|
||||
given the lack of `useTransition` in Leptos) have caused it to diverge
|
||||
slightly from Solid’s exact code, but it is still very closely related.
|
||||
|
||||
The core principle here is “nested routing,” dividing a single page
|
||||
into independently-rendered parts. This is described in some detail in the docs.
|
||||
|
||||
Like `leptos_meta`, it is implemented as a distinct package, because it
|
||||
can be replaced with another router or with none. The framework can be used
|
||||
without it, so it’s not in core.
|
||||
|
||||
## Server Integrations
|
||||
|
||||
The server integrations are the most “frameworky” layer of the whole framework.
|
||||
These **do** assume the use of `leptos`, `leptos_router`, and `leptos_meta`.
|
||||
They specifically draw routing data from `leptos_router`, and inject the
|
||||
metadata from `leptos_meta` into the `<head>` appropriately.
|
||||
|
||||
But of course, if you one day create `leptos-helmet` and `leptos-better-router`,
|
||||
you can create new server integrations that plug them into the SSR rendering
|
||||
methods from `leptos_dom` instead. Everything involved is quite modular.
|
||||
|
||||
These packages essentially provide helpers that save the templates and user apps
|
||||
from including a huge amount of boilerplate to connect the various other packages
|
||||
correctly. Again, early versions of the framework examples are illustrative here
|
||||
for reference: they include large amounts of manual SSR route handling, etc.
|
||||
|
||||
## `cargo-leptos` helpers
|
||||
|
||||
`leptos_config` and `leptos_hot_reload` exist to support two different features
|
||||
of `cargo-leptos`, namely its configuration and its view-patching/hot-reloading
|
||||
features.
|
||||
|
||||
It’s important to say that the main feature `cargo-leptos` remains its ability
|
||||
to conveniently tie together different build tooling, compiling your app to
|
||||
WASM for the browser, building the server version, pulling in SASS and
|
||||
Tailwind, etc. It is an extremely good build tool, not a magic formula. Each
|
||||
of the examples includes instructions for how to run the examples without
|
||||
`cargo-leptos`.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
_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)
|
||||
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
|
||||
|
||||
94
CONTRIBUTING.md
Normal file
94
CONTRIBUTING.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Contributing to Leptos
|
||||
|
||||
Thanks for your interesting in contributing to Leptos! This is a truly
|
||||
community-driven framework, and while we have a central maintainer (@gbj)
|
||||
large parts of the renderer, reactive system, and server integrations have
|
||||
all been written by other contributors. Contributions are always welcome.
|
||||
|
||||
Participation in this community is governed by a [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
Some of the most active conversations around development take place on our
|
||||
[Discord server](https://discord.gg/YdRAhS7eQB).
|
||||
|
||||
This guide seeks to
|
||||
|
||||
- describe some of the framework’s values (in a technical, not an ethical, sense)
|
||||
- provide a high-level overview of how the pieces of the framework fit together
|
||||
- orient you to the organization of this repository
|
||||
|
||||
## Values
|
||||
|
||||
Leptos, as a framework, reflects certain technical values:
|
||||
|
||||
- **Expose primitives rather than imposing patterns.** Provide building blocks
|
||||
that users can combine together to build up more complex behavior, rather than
|
||||
requiring users follow certain templates, file formats, etc. e.g., components
|
||||
are defined as functions, rather than a bespoke single-file component format.
|
||||
The reactive system feeds into the rendering system, rather than being defined
|
||||
by it.
|
||||
- **Bottom-up over top-down.** If you envision a user’s application as a tree
|
||||
(like an HTML document), push meaning toward the leaves of the tree. e.g., If data
|
||||
needs to be loaded, load it in a granular primitive (resources) rather than a
|
||||
route- or page-level data structure.
|
||||
- **Performance by default.** When possible, users should only pay for what they
|
||||
use. e.g., we don’t make all component props reactive by default. This is
|
||||
because doing so would force the overhead of a reactive prop onto props that don’t
|
||||
need to be reactive.
|
||||
- **Full-stack performance.** Performance can’t be limited to a single metric,
|
||||
whether that’s a DOM rendering benchmark, WASM binary size, or server response
|
||||
time. Use methods like HTTP streaming and progressive enhancement to enable
|
||||
applications to load, become interactive, and respond as quickly as possible.
|
||||
- **Use safe Rust.** There’s no need for `unsafe` Rust in the framework, and
|
||||
avoiding it at all costs reduces the maintenance and testing burden significantly.
|
||||
- **Embrace Rust semantics.** Especially in things like UI templating, use Rust
|
||||
semantics or extend them in a predictable way with control-flow components
|
||||
rather than overloading the meaning of Rust terms like `if` or `for` in a
|
||||
framework-specific way.
|
||||
- **Enhance ergonomics without obfuscating what’s happening.** This is by far
|
||||
the hardest to achieve. It’s often the case that adding additional layers to
|
||||
improve DX (like a custom build tool and starter templates) comes across as
|
||||
“too magic” to some people who haven’t had to build the same things manually.
|
||||
When possible, make it easier to see how the pieces fit together, without
|
||||
sacrificing the improved DX.
|
||||
|
||||
## Processes
|
||||
|
||||
We do not have PR templates or formal processes for approving PRs. But there
|
||||
are a few guidelines that will make it a better experience for everyone:
|
||||
|
||||
- Run `cargo fmt` before submitting your code.
|
||||
- Keep PRs limited to addressing one feature or one issue, in general. In some
|
||||
cases (e.g., “reduce allocations in the reactive system”) this may touch a number
|
||||
of different areas, but is still conceptually one thing.
|
||||
- If it’s an unsolicited PR not linked to an open issue, please include a
|
||||
specific explanation for what it’s trying to achieve. For example: “When I
|
||||
was trying to deploy my app under _circumstances X_, I found that the way
|
||||
_function Y_ was implemented caused _issue Z_. This PR should fix that by
|
||||
_solution._”
|
||||
- Our CI tests every PR against all the existing examples, sometimes requiring
|
||||
compilation for both server and client side, etc. It’s thorough but slow. If
|
||||
you want to run CI locally to reduce frustration, you can do that by installing
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
check-examples`.
|
||||
|
||||
## Before Submitting a PR
|
||||
|
||||
We have a fairly extensive CI setup that runs both lints (like `rustfmt` and `clippy`)
|
||||
and tests on PRs. You can run most of these locally if you have `cargo-make` installed.
|
||||
|
||||
If you added an example, make sure to add it to the list in `examples/Makefile.toml`.
|
||||
|
||||
From the root directory of the repo, run
|
||||
- `cargo +nightly fmt`
|
||||
- `cargo +nightly make check`
|
||||
- `cargo +nightly make test`
|
||||
- `cargo +nightly make check-examples`
|
||||
- `cargo +nightly make --profile=github-actions ci`
|
||||
|
||||
If you modified an example:
|
||||
- `cd examples/your_example`
|
||||
- `cargo +nightly fmt -- --config-path ../..`
|
||||
- `cargo +nightly make --profile=github-actions verify-flow`
|
||||
|
||||
## Architecture
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
31
Cargo.toml
31
Cargo.toml
@@ -1,4 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
# core
|
||||
"leptos",
|
||||
@@ -15,7 +16,6 @@ members = [
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
"integrations/axum",
|
||||
"integrations/viz",
|
||||
"integrations/utils",
|
||||
|
||||
# libraries
|
||||
@@ -25,22 +25,23 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.4"
|
||||
version = "0.6.9"
|
||||
rust-version = "1.75"
|
||||
|
||||
[workspace.dependencies]
|
||||
leptos = { path = "./leptos", default-features = false, version = "0.2.4" }
|
||||
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.4" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.4" }
|
||||
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.4" }
|
||||
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.4" }
|
||||
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.4" }
|
||||
server_fn = { path = "./server_fn", default-features = false, version = "0.2.4" }
|
||||
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.4" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.4" }
|
||||
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.4" }
|
||||
leptos_router = { path = "./router", version = "0.2.4" }
|
||||
leptos_meta = { path = "./meta", default-feature = false, version = "0.2.4" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.4" }
|
||||
leptos = { path = "./leptos", version = "0.6.9" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.9" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.9" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.9" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.9" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.9" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.9" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.9" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.9" }
|
||||
leptos_router = { path = "./router", version = "0.6.9" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.9" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.9" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -3,74 +3,37 @@
|
||||
# cargo install --force cargo-make
|
||||
############
|
||||
|
||||
[config]
|
||||
# make tasks run at the workspace root
|
||||
default_to_workspace = false
|
||||
|
||||
[tasks.check]
|
||||
clear = true
|
||||
dependencies = [
|
||||
"check-all",
|
||||
"check-wasm",
|
||||
"check-all-release",
|
||||
"check-wasm-release",
|
||||
]
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm", path = "leptos" }]
|
||||
|
||||
[tasks.check-all-release]
|
||||
command = "cargo"
|
||||
args = ["+nightly", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
|
||||
[tasks.check-wasm-release]
|
||||
clear = true
|
||||
dependencies = [{ name = "check-wasm-release", path = "leptos" }]
|
||||
|
||||
[tasks.check-examples]
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter" },
|
||||
{ name = "check", path = "examples/counter_isomorphic" },
|
||||
{ name = "check", path = "examples/counters" },
|
||||
{ name = "check", path = "examples/error_boundary" },
|
||||
{ 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/login_with_token_csr_only" },
|
||||
{ name = "check", path = "examples/parent_child" },
|
||||
{ name = "check", path = "examples/router" },
|
||||
{ name = "check", path = "examples/session_auth_axum" },
|
||||
{ name = "check", path = "examples/ssr_modes" },
|
||||
{ name = "check", path = "examples/ssr_modes_axum" },
|
||||
{ name = "check", path = "examples/tailwind" },
|
||||
{ name = "check", path = "examples/tailwind_csr_trunk" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_axum" },
|
||||
{ name = "check", path = "examples/todo_app_sqlite_viz" },
|
||||
{ name = "check", path = "examples/todomvc" },
|
||||
]
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
|
||||
[tasks.check-stable]
|
||||
workspace = false
|
||||
clear = true
|
||||
dependencies = [
|
||||
{ name = "check", path = "examples/counter_without_macros" },
|
||||
{ name = "check", path = "examples/counters_stable" },
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
clear = true
|
||||
dependencies = ["test-all"]
|
||||
|
||||
[tasks.test-all]
|
||||
[tasks.ci-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["+nightly", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
args = ["make", "ci-clean"]
|
||||
|
||||
[tasks.check-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "check-clean"]
|
||||
|
||||
[tasks.build-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "build-clean"]
|
||||
|
||||
[tasks.clean-examples]
|
||||
workspace = false
|
||||
cwd = "examples"
|
||||
command = "cargo"
|
||||
args = ["make", "clean"]
|
||||
|
||||
121
README.md
121
README.md
@@ -6,6 +6,11 @@
|
||||
[](https://crates.io/crates/leptos)
|
||||
[](https://docs.rs/leptos)
|
||||
[](https://discord.gg/YdRAhS7eQB)
|
||||
[](https://matrix.to/#/#leptos:matrix.org)
|
||||
|
||||
[Website](https://leptos.dev) | [Book](https://leptos-rs.github.io/leptos/) | [Docs.rs](https://docs.rs/leptos/latest/leptos/) | [Playground](https://codesandbox.io/p/sandbox/leptos-rtfggt?file=%2Fsrc%2Fmain.rs%3A1%2C1) | [Discord](https://discord.gg/YdRAhS7eQB)
|
||||
|
||||
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
|
||||
|
||||
# Leptos
|
||||
|
||||
@@ -13,9 +18,9 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
// create a reactive signal with the initial value
|
||||
let (value, set_value) = create_signal(cx, initial_value);
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
|
||||
// create event handlers for our buttons
|
||||
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
@@ -25,21 +30,41 @@ pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
|
||||
|
||||
// create user interfaces with the declarative `view!` macro
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=clear>"Clear"</button>
|
||||
<button on:click=decrement>"-1"</button>
|
||||
<button on:click=clear>Clear</button>
|
||||
<button on:click=decrement>-1</button>
|
||||
// text nodes can be quoted or unquoted
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=increment>"+1"</button>
|
||||
<button on:click=increment>+1</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
|
||||
pub fn main() {
|
||||
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
|
||||
// we also support a builder syntax rather than the JSX-like `view` macro
|
||||
#[component]
|
||||
pub fn SimpleCounterWithBuilder(initial_value: i32) -> impl IntoView {
|
||||
use leptos::html::*;
|
||||
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let clear = move |_| set_value(0);
|
||||
let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
// the `view` macro above expands to this builder syntax
|
||||
div().child((
|
||||
button().on(ev::click, clear).child("Clear"),
|
||||
button().on(ev::click, decrement).child("-1"),
|
||||
span().child(("Value: ", value, "!")),
|
||||
button().on(ev::click, increment).child("+1")
|
||||
))
|
||||
}
|
||||
|
||||
// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
|
||||
pub fn main() {
|
||||
mount_to_body(|| view! {
|
||||
<SimpleCounter initial_value=3 />
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## About the Framework
|
||||
@@ -48,27 +73,27 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
|
||||
## What does that mean?
|
||||
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (_client-side rendering_), on the server (_server-side rendering_), or by rendering HTML on the server and then adding interactivity in the browser (_hydration_). This includes support for _HTTP streaming_ of both data (`Resource`s) and HTML (out-of-order streaming of `<Suspense/>` components.)
|
||||
- **Isomorphic**: 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.
|
||||
- **Full-stack**: Leptos can be used to build apps that run in the browser (client-side rendering), on the server (server-side rendering), or by rendering HTML on the server and then adding interactivity in the browser (server-side rendering with hydration). This includes support for HTTP streaming of both data ([`Resource`s](https://docs.rs/leptos/latest/leptos/struct.Resource.html)) and HTML (out-of-order or in-order streaming of [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) components.)
|
||||
- **Isomorphic**: Leptos provides primitives to write isomorphic [server functions](https://docs.rs/leptos_server/0.2.5/leptos_server/index.html), 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, without needing to create and maintain a separate REST or other API.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The [router](https://docs.rs/leptos_router/latest/leptos_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 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!_)
|
||||
- **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 overhead!)
|
||||
- **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:
|
||||
|
||||
- [Book](https://leptos-rs.github.io/leptos/) (work in progress)
|
||||
- [Examples](https://github.com/leptos-rs/leptos/tree/main/examples)
|
||||
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
|
||||
- [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.
|
||||
Most of the examples assume you’re using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
|
||||
|
||||
To set up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you haven’t already)
|
||||
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you haven’t already):
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
@@ -76,17 +101,19 @@ rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
If you’re on `stable`, note the following:
|
||||
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.2", 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/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
|
||||
for examples of the correct API.
|
||||
```toml
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
```
|
||||
|
||||
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`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).
|
||||
[`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 templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
@@ -95,13 +122,13 @@ cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
Open browser on [http://localhost:3000/](http://localhost:3000/)
|
||||
Open browser to [http://localhost:3000/](http://localhost:3000/).
|
||||
|
||||
## FAQs
|
||||
|
||||
### What’s up with the name?
|
||||
|
||||
*Leptos* (λεπτός) is an ancient Greek word meaning “thin, light, refine, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
_Leptos_ (λεπτός) is an ancient Greek word meaning “thin, light, refined, fine-grained.” To me, a classicist and not a dog owner, it evokes the lightweight reactive system that powers the framework. I've since learned the same word is at the root of the medical term “leptospirosis,” a blood infection that affects humans and animals... My bad. No dogs were harmed in the creation of this framework.
|
||||
|
||||
### Is it production ready?
|
||||
|
||||
@@ -109,7 +136,7 @@ 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.
|
||||
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 future releases, in terms of architecture.
|
||||
|
||||
2. **Are there bugs?**
|
||||
|
||||
@@ -119,11 +146,11 @@ Yes, I’m sure there are. You can see from the state of our issue tracker over
|
||||
|
||||
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.
|
||||
There are several people in the 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:
|
||||
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive any native GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
|
||||
|
||||
- Use signals, derived signals, and memos to create your reactive system
|
||||
- Create GUI widgets
|
||||
@@ -132,35 +159,27 @@ Sure! Obviously the `view` macro is for generating DOM nodes but you can use the
|
||||
|
||||
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?
|
||||
The new rendering approach being developed for 0.7 supports “universal rendering,” i.e., it can use any rendering library that supports a small set of 6-8 functions. (This is intended as a layer over typical retained-mode, OOP-style GUI toolkits like the DOM, GTK, etc.) That future rendering work will allow creating native UI in a way that is much more similar to the declarative approach used by the web framework.
|
||||
|
||||
On the surface level, these libraries may seem similar. Yew is, of course, the most mature Rust library for web UI development and has a huge ecosystem. Dioxus is similar in many ways, being heavily inspired by React. Here are some conceptual differences between Leptos and these frameworks:
|
||||
### How is this different from Yew?
|
||||
|
||||
Yew is the most-used library for Rust web UI development, but there are several differences between Yew and Leptos, in philosophy, approach, and performance.
|
||||
|
||||
- **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 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.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply much faster at both creating and updating the UI than Yew is.
|
||||
- **Server integration:** Yew was created in an era in which browser-rendered single-page apps (SPAs) were the dominant paradigm. While Leptos supports client-side rendering, it also focuses on integrating with the server side of your application via server functions and multiple modes of serving HTML, including out-of-order streaming.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
- ### How is this different from Dioxus?
|
||||
|
||||
Conceptually, these two frameworks are very similar: because both are built on fine-grained reactivity, most apps will end up looking very similar between the two, and Sycamore or Leptos apps will both look a lot like SolidJS apps, in the same way that Yew or Dioxus can look a lot like React.
|
||||
Like Leptos, Dioxus is a framework for building UIs using web technologies. However, there are significant differences in approach and features.
|
||||
|
||||
There are some practical differences that make a significant difference:
|
||||
- **VDOM vs. fine-grained:** While Dioxus has a performant virtual DOM (VDOM), it still uses coarse-grained/component-scoped reactivity: changing a stateful value reruns the component function and diffs the old UI against the new one. Leptos components use a different mental model, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Web vs. desktop priorities:** Dioxus uses Leptos server functions in its fullstack mode, but does not have the same `<Suspense>`-based support for things like streaming HTML rendering, or share the same focus on holistic web performance. Leptos tends to prioritize holistic web performance (streaming HTML rendering, smaller WASM binary sizes, etc.), whereas Dioxus has an unparalleled experience when building desktop apps, because your application logic runs as a native Rust binary.
|
||||
|
||||
- **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.
|
||||
- **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:
|
||||
- ### How is this different from Sycamore?
|
||||
|
||||
```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) { ... }
|
||||
```
|
||||
Sycamore and Leptos are both heavily influenced by SolidJS. At this point, Leptos has a larger community and ecosystem and is more actively developed. Other differences:
|
||||
|
||||
- **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.
|
||||
- **Templating DSLs:** Sycamore uses a custom templating language for its views, while Leptos uses a JSX-like template format.
|
||||
- **`'static` signals:** One of Leptos’s main innovations was the creation of `Copy + 'static` signals, which have excellent ergonomics. Sycamore is in the process of adopting the same pattern, but this is not yet released.
|
||||
- **Perseus vs. server functions:** The Perseus metaframework provides an opinionated way to build Sycamore apps that include server functionality. Leptos instead provides primitives like server functions in the core of the framework.
|
||||
|
||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a suspected security issue, please contact security@leptos.dev rather than opening
|
||||
a public issue.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The most-recently-released version of the library is supported with security updates.
|
||||
For example, if a security issue is discovered that affects 0.3.2 and all later releases,
|
||||
a 0.4.x patch will be released but a new 0.3.x patch release will not be made. You should
|
||||
plan to update to the latest version to receive any new features or bugfixes of any kind.
|
||||
@@ -2,12 +2,22 @@
|
||||
name = "benchmarks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
l021 = { package = "leptos", version = "0.2.1" }
|
||||
leptos = { path = "../leptos", default-features = false, features = ["ssr"] }
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
"nightly",
|
||||
"ssr",
|
||||
] }
|
||||
leptos = { path = "../leptos", features = ["ssr", "nightly"] }
|
||||
leptos_reactive = { path = "../leptos_reactive", features = ["ssr", "nightly"] }
|
||||
tachydom = { git = "https://github.com/gbj/tachys", features = [
|
||||
"nightly",
|
||||
"leptos",
|
||||
] }
|
||||
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { git = "https://github.com/yewstack/yew", features = ["ssr"] }
|
||||
yew = { version = "0.20", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
miniserde = "0.1"
|
||||
gloo = "0.8"
|
||||
@@ -17,15 +27,10 @@ lazy_static = "1"
|
||||
log = "0.4"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"]}
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Window",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlInputElement"
|
||||
]
|
||||
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
extern crate test;
|
||||
|
||||
mod reactive;
|
||||
//mod ssr;
|
||||
//mod todomvc;
|
||||
mod ssr;
|
||||
mod todomvc;
|
||||
|
||||
@@ -7,19 +7,16 @@ fn leptos_deep_creation(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
let signal = create_rw_signal(0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
let prev = memos.last().copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -31,21 +28,17 @@ fn leptos_deep_update(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
}
|
||||
let signal = create_rw_signal(0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(move |_| signal.get() + 1));
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
})
|
||||
.dispose()
|
||||
}
|
||||
signal.set(1);
|
||||
assert_eq!(memos[999].get(), 1001);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -57,17 +50,12 @@ fn leptos_narrowing_down(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
})
|
||||
.dispose()
|
||||
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo =
|
||||
create_memo(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -79,16 +67,13 @@ fn leptos_fanning_out(b: &mut Bencher) {
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let sig = create_rw_signal(cx, 0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(cx, move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
})
|
||||
.dispose()
|
||||
let sig = create_rw_signal(0);
|
||||
let memos = (0..1000)
|
||||
.map(|_| create_memo(move |_| sig.get()))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 0);
|
||||
sig.set(1);
|
||||
assert_eq!(memos.iter().map(|m| m.get()).sum::<i32>(), 1000);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
@@ -99,81 +84,44 @@ fn leptos_narrowing_update(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let sigs =
|
||||
(0..1000).map(|n| create_signal(cx, n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
writes[1].update(|n| *n += 1);
|
||||
writes[10].update(|n| *n += 1);
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn leptos_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
let acc = Rc::new(Cell::new(0));
|
||||
let disposers = (0..1000)
|
||||
.map(|_| {
|
||||
create_scope(runtime, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |cx| {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for disposer in disposers {
|
||||
disposer.dispose();
|
||||
}
|
||||
let sigs = (0..1000).map(|n| create_signal(n)).collect::<Vec<_>>();
|
||||
let reads = sigs.iter().map(|(r, _)| *r).collect::<Vec<_>>();
|
||||
let writes = sigs.iter().map(|(_, w)| *w).collect::<Vec<_>>();
|
||||
let memo =
|
||||
create_memo(move |_| reads.iter().map(|r| r.get()).sum::<i32>());
|
||||
assert_eq!(memo(), 499500);
|
||||
create_isomorphic_effect({
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
|
||||
writes[1].update(|n| *n += 1);
|
||||
writes[10].update(|n| *n += 1);
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_creation(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_deep_creation(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -187,17 +135,16 @@ fn l021_deep_creation(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_deep_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_deep_update(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
create_scope(runtime, |cx| {
|
||||
let signal = create_rw_signal(cx, 0);
|
||||
let mut memos = Vec::<Memo<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move |_| prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move |_| signal.get() + 1));
|
||||
@@ -213,8 +160,8 @@ fn l021_deep_update(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_narrowing_down(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_narrowing_down(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -236,8 +183,8 @@ fn l021_narrowing_down(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_fanning_out(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
fn l0410_fanning_out(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -256,8 +203,8 @@ fn l021_fanning_out(b: &mut Bencher) {
|
||||
runtime.dispose();
|
||||
}
|
||||
#[bench]
|
||||
fn l021_narrowing_update(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_narrowing_update(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -270,11 +217,11 @@ fn l021_narrowing_update(b: &mut Bencher) {
|
||||
let memo = create_memo(cx, move |_| {
|
||||
reads.iter().map(|r| r.get()).sum::<i32>()
|
||||
});
|
||||
assert_eq!(memo(), 499500);
|
||||
assert_eq!(memo.get(), 499500);
|
||||
create_isomorphic_effect(cx, {
|
||||
let acc = Rc::clone(&acc);
|
||||
move |_| {
|
||||
acc.set(memo());
|
||||
acc.set(memo.get());
|
||||
}
|
||||
});
|
||||
assert_eq!(acc.get(), 499500);
|
||||
@@ -284,7 +231,7 @@ fn l021_narrowing_update(b: &mut Bencher) {
|
||||
writes[100].update(|n| *n += 1);
|
||||
|
||||
assert_eq!(acc.get(), 499503);
|
||||
assert_eq!(memo(), 499503);
|
||||
assert_eq!(memo.get(), 499503);
|
||||
})
|
||||
.dispose()
|
||||
});
|
||||
@@ -293,8 +240,8 @@ fn l021_narrowing_update(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l021::*;
|
||||
fn l0410_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
use l0410::*;
|
||||
let runtime = create_runtime();
|
||||
|
||||
b.iter(|| {
|
||||
@@ -307,7 +254,7 @@ fn l021_scope_creation_and_disposal(b: &mut Bencher) {
|
||||
let (r, w) = create_signal(cx, 0);
|
||||
create_isomorphic_effect(cx, {
|
||||
move |_| {
|
||||
acc.set(r());
|
||||
acc.set(r.get());
|
||||
}
|
||||
});
|
||||
w.update(|n| *n += 1);
|
||||
@@ -373,9 +320,8 @@ fn sycamore_deep_creation(b: &mut Bencher) {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
@@ -394,9 +340,8 @@ fn sycamore_deep_update(b: &mut Bencher) {
|
||||
let d = create_scope(|cx| {
|
||||
let signal = create_signal(cx, 0);
|
||||
let mut memos = Vec::<&ReadSignal<usize>>::new();
|
||||
for i in 0..1000usize {
|
||||
let prev = memos.get(i.saturating_sub(1)).copied();
|
||||
if let Some(prev) = prev {
|
||||
for _ in 0..1000usize {
|
||||
if let Some(prev) = memos.last().copied() {
|
||||
memos.push(create_memo(cx, move || *prev.get() + 1));
|
||||
} else {
|
||||
memos.push(create_memo(cx, move || *signal.get() + 1));
|
||||
|
||||
@@ -2,15 +2,14 @@ use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::*;
|
||||
let r = create_runtime();
|
||||
b.iter(|| {
|
||||
use leptos::*;
|
||||
HydrationCtx::reset_id();
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
leptos::leptos_dom::HydrationCtx::reset_id();
|
||||
#[component]
|
||||
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, initial);
|
||||
fn Counter(initial: i32) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(initial);
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value().to_string()} "!"</span>
|
||||
@@ -20,7 +19,6 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
cx,
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
@@ -28,13 +26,53 @@ fn leptos_ssr_bench(b: &mut Bencher) {
|
||||
<Counter initial=2/>
|
||||
<Counter initial=3/>
|
||||
</main>
|
||||
}.into_view(cx).render_to_string(cx);
|
||||
}.into_view().render_to_string();
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<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>" );
|
||||
});
|
||||
"<main data-hk=\"0-0-0-1\"><h1 data-hk=\"0-0-0-2\">Welcome to our benchmark page.</h1><p data-hk=\"0-0-0-3\">Here's some introductory text.</p><div data-hk=\"0-0-0-5\"><button data-hk=\"0-0-0-6\">-1</button><span data-hk=\"0-0-0-7\">Value: <!>1<!--hk=0-0-0-8-->!</span><button data-hk=\"0-0-0-9\">+1</button></div><!--hk=0-0-0-4--><div data-hk=\"0-0-0-11\"><button data-hk=\"0-0-0-12\">-1</button><span data-hk=\"0-0-0-13\">Value: <!>2<!--hk=0-0-0-14-->!</span><button data-hk=\"0-0-0-15\">+1</button></div><!--hk=0-0-0-10--><div data-hk=\"0-0-0-17\"><button data-hk=\"0-0-0-18\">-1</button><span data-hk=\"0-0-0-19\">Value: <!>3<!--hk=0-0-0-20-->!</span><button data-hk=\"0-0-0-21\">+1</button></div><!--hk=0-0-0-16--></main>" );
|
||||
});
|
||||
r.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_ssr_bench(b: &mut Bencher) {
|
||||
use leptos::{create_runtime, create_signal, SignalGet, SignalUpdate};
|
||||
use tachy_maccy::view;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
use tachydom::html::element::ElementChild;
|
||||
use tachydom::html::attribute::global::ClassAttribute;
|
||||
use tachydom::html::attribute::global::GlobalAttributes;
|
||||
use tachydom::html::attribute::global::OnAttribute;
|
||||
use tachydom::renderer::dom::Dom;
|
||||
let rt = create_runtime();
|
||||
b.iter(|| {
|
||||
fn counter(initial: i32) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let (value, set_value) = create_signal(initial);
|
||||
view! {
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let rendered = view! {
|
||||
<main>
|
||||
<h1>"Welcome to our benchmark page."</h1>
|
||||
<p>"Here's some introductory text."</p>
|
||||
{counter(1)}
|
||||
{counter(2)}
|
||||
{counter(3)}
|
||||
</main>
|
||||
}.to_html();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>2<!>!</span><button>+1</button></div><div><button>-1</button><span>Value: <!>3<!>!</span><button>+1</button></div></main>"
|
||||
);
|
||||
});
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub use leptos::*;
|
||||
use miniserde::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -8,13 +9,13 @@ pub struct Todos(pub Vec<Todo>);
|
||||
const STORAGE_KEY: &str = "todos-leptos";
|
||||
|
||||
impl Todos {
|
||||
pub fn new(cx: Scope) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn new_with_1000(cx: Scope) -> Self {
|
||||
pub fn new_with_1000() -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
|
||||
.map(|id| Todo::new(id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
@@ -71,13 +72,17 @@ pub struct Todo {
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(cx: Scope, id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(cx, id, title, false)
|
||||
pub fn new(id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
|
||||
let (title, set_title) = create_signal(cx, title);
|
||||
let (completed, set_completed) = create_signal(cx, completed);
|
||||
pub fn new_with_completed(
|
||||
id: usize,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
let (title, set_title) = create_signal(title);
|
||||
let (completed, set_completed) = create_signal(completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
@@ -97,7 +102,7 @@ const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
#[component]
|
||||
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
pub fn TodoMVC(todos: Todos) -> impl IntoView {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
@@ -106,14 +111,10 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let (todos, set_todos) = create_signal(cx, todos);
|
||||
provide_context(cx, set_todos);
|
||||
let (todos, set_todos) = create_signal(todos);
|
||||
provide_context(set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(cx, Mode::All);
|
||||
window_event_listener("hashchange", move |_| {
|
||||
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
|
||||
set_mode(new_mode);
|
||||
});
|
||||
let (mode, set_mode) = create_signal(Mode::All);
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
@@ -123,7 +124,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, next_id, title.to_string());
|
||||
let new = Todo::new(next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
@@ -131,7 +132,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
@@ -151,7 +152,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
@@ -166,65 +167,87 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
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)
|
||||
view! {
|
||||
<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
|
||||
children=move |todo: Todo| {
|
||||
view! { <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()
|
||||
}
|
||||
|
||||
#[component]
|
||||
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);
|
||||
pub fn Todo(todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
|
||||
//let input = NodeRef::new();
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
@@ -236,42 +259,37 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<li
|
||||
class="todo"
|
||||
class:editing={editing}
|
||||
class:completed={move || (todo.completed)()}
|
||||
//_ref=input
|
||||
>
|
||||
view! {
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
<div class="view">
|
||||
<input
|
||||
class="toggle"
|
||||
type="checkbox"
|
||||
prop:checked={move || (todo.completed)()}
|
||||
|
||||
/>
|
||||
<label on:dblclick=move |_| set_editing(true)>
|
||||
{move || todo.title.get()}
|
||||
</label>
|
||||
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
|
||||
<input class="toggle" type="checkbox" prop:checked=move || (todo.completed)()/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
</div>
|
||||
{move || editing().then(|| view! { cx,
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden={move || !(editing)()}
|
||||
prop:value={move || todo.title.get()}
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup={move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! {
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
prop:value=move || todo.title.get()
|
||||
on:focusout=move |ev| save(&event_target_value(&ev))
|
||||
on:keyup=move |ev| {
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
save(&event_target_value(&ev));
|
||||
} else if key_code == ESCAPE_KEY {
|
||||
set_editing(false);
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -305,8 +323,8 @@ pub struct TodoSerialized {
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self, cx: Scope) -> Todo {
|
||||
Todo::new_with_completed(cx, self.id, self.title, self.completed)
|
||||
pub fn into_todo(self, ) -> Todo {
|
||||
Todo::new_with_completed(self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,32 +2,45 @@ use test::Bencher;
|
||||
|
||||
mod leptos;
|
||||
mod sycamore;
|
||||
mod tachys;
|
||||
mod tera;
|
||||
mod yew;
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
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);
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! { <TodoMVC todos=Todos::new()/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::tachys::*;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
|
||||
let rendered = TodoMVC(Todos::new()).to_html();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<main><section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input placeholder=\"What needs to be done?\" autofocus class=\"new-todo\"></header><section class=\"main hidden\"><input id=\"toggle-all\" type=\"checkbox\" class=\"toggle-all\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer hidden\"><span class=\"todo-count\"><strong>0</strong><!> items<!> left</span><ul class=\"filters\"><li><a href=\"#/\" class=\"selected selected\">All</a></li><li><a href=\"#/active\" class=\"\">Active</a></li><li><a href=\"#/completed\" class=\"\">Completed</a></li></ul><button class=\"clear-completed hidden hidden\">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>" );
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::sycamore::*;
|
||||
use ::sycamore::prelude::*;
|
||||
use ::sycamore::*;
|
||||
use ::sycamore::{prelude::*, *};
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
@@ -46,8 +59,7 @@ fn sycamore_todomvc_ssr(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
use self::yew::*;
|
||||
use ::yew::prelude::*;
|
||||
use ::yew::ServerRenderer;
|
||||
use ::yew::{prelude::*, ServerRenderer};
|
||||
|
||||
b.iter(|| {
|
||||
tokio_test::block_on(async {
|
||||
@@ -57,29 +69,40 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
/*
|
||||
|
||||
#[bench]
|
||||
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
b.iter(|| {
|
||||
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);
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! {
|
||||
<TodoMVC todos=Todos::new_with_1000()/>
|
||||
}
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tachys_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::tachys::*;
|
||||
use tachydom::view::{Render, RenderHtml};
|
||||
|
||||
let rendered = TodoMVC(Todos::new_with_1000()).to_html();
|
||||
assert!(rendered.len() > 20_000)
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::sycamore::*;
|
||||
use ::sycamore::prelude::*;
|
||||
use ::sycamore::*;
|
||||
use ::sycamore::{prelude::*, *};
|
||||
|
||||
b.iter(|| {
|
||||
_ = create_scope(|cx| {
|
||||
@@ -98,8 +121,7 @@ fn sycamore_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
use self::yew::*;
|
||||
use ::yew::prelude::*;
|
||||
use ::yew::ServerRenderer;
|
||||
use ::yew::{prelude::*, ServerRenderer};
|
||||
|
||||
b.iter(|| {
|
||||
tokio_test::block_on(async {
|
||||
@@ -109,4 +131,18 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
use ::leptos::*;
|
||||
let runtime = create_runtime();
|
||||
b.iter(|| {
|
||||
use crate::todomvc::leptos::*;
|
||||
|
||||
let html = ::leptos::ssr::render_to_string(|| {
|
||||
view! { <TodoMVC todos=Todos::new()/> }
|
||||
});
|
||||
assert!(html.len() > 1);
|
||||
});
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
333
benchmarks/src/todomvc/tachys.rs
Normal file
333
benchmarks/src/todomvc/tachys.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
pub use leptos_reactive::*;
|
||||
use miniserde::*;
|
||||
use tachy_maccy::view;
|
||||
use tachydom::{
|
||||
html::{
|
||||
attribute::global::{ClassAttribute, GlobalAttributes, OnAttribute},
|
||||
element::ElementChild,
|
||||
},
|
||||
renderer::dom::Dom,
|
||||
view::{keyed::keyed, Render, RenderHtml},
|
||||
};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Todos(pub Vec<Todo>);
|
||||
|
||||
const STORAGE_KEY: &str = "todos-leptos";
|
||||
|
||||
impl Todos {
|
||||
pub fn new() -> Self {
|
||||
Self(vec![])
|
||||
}
|
||||
|
||||
pub fn new_with_1000() -> Self {
|
||||
let todos = (0..1000)
|
||||
.map(|id| Todo::new(id, format!("Todo #{id}")))
|
||||
.collect();
|
||||
Self(todos)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, todo: Todo) {
|
||||
self.0.push(todo);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: usize) {
|
||||
self.0.retain(|todo| todo.id != id);
|
||||
}
|
||||
|
||||
pub fn remaining(&self) -> usize {
|
||||
self.0.iter().filter(|todo| !(todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn completed(&self) -> usize {
|
||||
self.0.iter().filter(|todo| (todo.completed)()).count()
|
||||
}
|
||||
|
||||
pub fn toggle_all(&self) {
|
||||
// if all are complete, mark them all active instead
|
||||
if self.remaining() == 0 {
|
||||
for todo in &self.0 {
|
||||
if todo.completed.get() {
|
||||
(todo.set_completed)(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// otherwise, mark them all complete
|
||||
else {
|
||||
for todo in &self.0 {
|
||||
(todo.set_completed)(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_completed(&mut self) {
|
||||
self.0.retain(|todo| !todo.completed.get());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Todo {
|
||||
pub id: usize,
|
||||
pub title: ReadSignal<String>,
|
||||
pub set_title: WriteSignal<String>,
|
||||
pub completed: ReadSignal<bool>,
|
||||
pub set_completed: WriteSignal<bool>,
|
||||
}
|
||||
|
||||
impl Todo {
|
||||
pub fn new(id: usize, title: String) -> Self {
|
||||
Self::new_with_completed(id, title, false)
|
||||
}
|
||||
|
||||
pub fn new_with_completed(
|
||||
id: usize,
|
||||
title: String,
|
||||
completed: bool,
|
||||
) -> Self {
|
||||
let (title, set_title) = create_signal(title);
|
||||
let (completed, set_completed) = create_signal(completed);
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
set_title,
|
||||
completed,
|
||||
set_completed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) {
|
||||
self.set_completed
|
||||
.update(|completed| *completed = !*completed);
|
||||
}
|
||||
}
|
||||
|
||||
const ESCAPE_KEY: u32 = 27;
|
||||
const ENTER_KEY: u32 = 13;
|
||||
|
||||
pub fn TodoMVC(todos: Todos) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let mut next_id = todos
|
||||
.0
|
||||
.iter()
|
||||
.map(|todo| todo.id)
|
||||
.max()
|
||||
.map(|last| last + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let (todos, set_todos) = create_signal(todos);
|
||||
provide_context(set_todos);
|
||||
|
||||
let (mode, set_mode) = create_signal(Mode::All);
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
todo!()
|
||||
/* let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
}
|
||||
} */
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
Mode::All => todos.0.to_vec(),
|
||||
Mode::Active => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| !todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Mode::Completed => todos
|
||||
.0
|
||||
.iter()
|
||||
.filter(|todo| todo.completed.get())
|
||||
.cloned()
|
||||
.collect(),
|
||||
})
|
||||
});
|
||||
|
||||
// effect to serialize to JSON
|
||||
// this does reactive reads, so it will automatically serialize on any relevant change
|
||||
create_effect(move |_| {
|
||||
()
|
||||
/* if let Ok(Some(storage)) = window().local_storage() {
|
||||
let objs = todos
|
||||
.get()
|
||||
.0
|
||||
.iter()
|
||||
.map(TodoSerialized::from)
|
||||
.collect::<Vec<_>>();
|
||||
let json = json::to_string(&objs);
|
||||
if storage.set_item(STORAGE_KEY, &json).is_err() {
|
||||
log::error!("error while trying to set item in localStorage");
|
||||
}
|
||||
} */
|
||||
});
|
||||
|
||||
view! {
|
||||
<main>
|
||||
<section class="todoapp">
|
||||
<header class="header">
|
||||
<h1>"todos"</h1>
|
||||
<input
|
||||
class="new-todo"
|
||||
placeholder="What needs to be done?"
|
||||
autofocus
|
||||
/>
|
||||
</header>
|
||||
<section class="main" class:hidden=move || todos.with(|t| t.is_empty())>
|
||||
<input
|
||||
id="toggle-all"
|
||||
class="toggle-all"
|
||||
r#type="checkbox"
|
||||
//prop:checked=move || todos.with(|t| t.remaining() > 0)
|
||||
on:input=move |_| set_todos.update(|t| t.toggle_all())
|
||||
/>
|
||||
<label r#for="toggle-all">"Mark all as complete"</label>
|
||||
<ul class="todo-list">
|
||||
{move || {
|
||||
keyed(filtered_todos.get(), |todo| todo.id, Todo)
|
||||
}}
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Todo(todo: Todo) -> impl Render<Dom> + RenderHtml<Dom> {
|
||||
let (editing, set_editing) = create_signal(false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>().unwrap();
|
||||
//let input = NodeRef::new();
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
set_todos.update(|t| t.remove(todo.id));
|
||||
} else {
|
||||
(todo.set_title)(value.to_string());
|
||||
}
|
||||
set_editing(false);
|
||||
};
|
||||
|
||||
view! {
|
||||
<li class="todo" class:editing=editing class:completed=move || (todo.completed)()>
|
||||
/* <div class="view">
|
||||
<input class="toggle" r#type="checkbox"/>
|
||||
<label on:dblclick=move |_| set_editing(true)>{move || todo.title.get()}</label>
|
||||
<button
|
||||
class="destroy"
|
||||
on:click=move |_| set_todos.update(|t| t.remove(todo.id))
|
||||
></button>
|
||||
</div>
|
||||
{move || {
|
||||
editing()
|
||||
.then(|| {
|
||||
view! {
|
||||
<input
|
||||
class="edit"
|
||||
class:hidden=move || !(editing)()
|
||||
/>
|
||||
}
|
||||
})
|
||||
}} */
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Active,
|
||||
Completed,
|
||||
All,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Mode::All
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route(hash: &str) -> Mode {
|
||||
match hash {
|
||||
"/active" => Mode::Active,
|
||||
"/completed" => Mode::Completed,
|
||||
_ => Mode::All,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TodoSerialized {
|
||||
pub id: usize,
|
||||
pub title: String,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl TodoSerialized {
|
||||
pub fn into_todo(self) -> Todo {
|
||||
Todo::new_with_completed(self.id, self.title, self.completed)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Todo> for TodoSerialized {
|
||||
fn from(todo: &Todo) -> Self {
|
||||
Self {
|
||||
id: todo.id,
|
||||
title: todo.title.get(),
|
||||
completed: (todo.completed)(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ static TEMPLATE: &str = r#"<main>
|
||||
</main>"#;
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc(b: &mut Bencher) {
|
||||
fn tera_todomvc_ssr(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
@@ -127,7 +127,7 @@ fn tera_todomvc(b: &mut Bencher) {
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn tera_todomvc_1000(b: &mut Bencher) {
|
||||
fn tera_todomvc_ssr_1000(b: &mut Bencher) {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::*;
|
||||
|
||||
|
||||
7
cargo-make/check.toml
Normal file
7
cargo-make/check.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tasks.check]
|
||||
alias = "check-all"
|
||||
|
||||
[tasks.check-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly-2024-01-29", "check-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
11
cargo-make/lint.toml
Normal file
11
cargo-make/lint.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[tasks.lint]
|
||||
dependencies = ["check-format-flow", "clippy-each-feature"]
|
||||
|
||||
[tasks.check-format]
|
||||
env = { LEPTOS_PROJECT_DIRECTORY = "../" }
|
||||
args = ["fmt", "--", "--check", "--config-path", "${LEPTOS_PROJECT_DIRECTORY}"]
|
||||
|
||||
[tasks.clippy-each-feature]
|
||||
dependencies = ["install-clippy"]
|
||||
command = "cargo"
|
||||
args = ["hack", "clippy", "--all", "--each-feature", "--no-dev-deps"]
|
||||
15
cargo-make/main.toml
Normal file
15
cargo-make/main.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
extend = [
|
||||
{ path = "./check.toml" },
|
||||
{ path = "./lint.toml" },
|
||||
{ path = "./test.toml" },
|
||||
]
|
||||
|
||||
[env]
|
||||
RUSTFLAGS = ""
|
||||
LEPTOS_OUTPUT_NAME = "ci" # allows examples to check/build without cargo-leptos
|
||||
|
||||
[env.github-actions]
|
||||
RUSTFLAGS = "-D warnings"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = ["lint", "test"]
|
||||
7
cargo-make/test.toml
Normal file
7
cargo-make/test.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[tasks.test]
|
||||
alias = "test-all"
|
||||
|
||||
[tasks.test-all]
|
||||
command = "cargo"
|
||||
args = ["+nightly-2024-01-29", "test-all-features"]
|
||||
install_crate = "cargo-all-features"
|
||||
@@ -9,10 +9,10 @@ This document is intended as a running list of common issues, with example code
|
||||
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, false);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let (b, set_b) = create_signal(false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
create_effect(move |_| {
|
||||
if a() > 5 {
|
||||
set_b(true);
|
||||
}
|
||||
@@ -24,10 +24,56 @@ This creates an inefficient chain of updates, and can easily lead to infinite lo
|
||||
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (a, set_a) = create_signal(0);
|
||||
let b = move || a () > 5;
|
||||
```
|
||||
|
||||
### Nested signal updates/reads triggering panic
|
||||
|
||||
Sometimes you have nested signals: for example, hash-map that can change over time, each of whose values can also change over time:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let resources = create_rw_signal(HashMap::new());
|
||||
|
||||
let update = move |id: usize| {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<pre>{move || format!("{:#?}", resources.get().into_iter().map(|(id, resource)| (id, resource.get())).collect::<Vec<_>>())}</pre>
|
||||
<button on:click=move |_| update(1)>"+"</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking the button twice will cause a panic, because of the nested signal _read_. Calling the `update` function on `resources` immediately takes out a mutable borrow on `resources`, then updates the `resource` signal—which re-runs the effect that reads from the signals, which tries to immutably access `resources` and panics. It's the nested update here which causes a problem, because the inner update triggers and effect that tries to read both signals while the outer is still updating.
|
||||
|
||||
You can fix this fairly easily by using the [`batch()`](https://docs.rs/leptos/latest/leptos/fn.batch.html) method:
|
||||
|
||||
```rust
|
||||
let update = move |id: usize| {
|
||||
batch(move || {
|
||||
resources.update(|resources| {
|
||||
resources
|
||||
.entry(id)
|
||||
.or_insert_with(|| create_rw_signal(0))
|
||||
.update(|amount| *amount += 1)
|
||||
})
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
This delays running any effects until after both updates are made, preventing the conflict entirely without requiring any other restructuring.
|
||||
|
||||
## Templates and the DOM
|
||||
|
||||
### `<input value=...>` doesn't update or stops updating
|
||||
@@ -37,11 +83,11 @@ Many DOM attributes can be updated either by setting an attribute on the DOM nod
|
||||
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
// ❌ reactivity doesn't work as expected: typing only updates the default
|
||||
// of each input, so if you start typing in the second input, it won't
|
||||
// update the first one
|
||||
@@ -51,11 +97,11 @@ view! {
|
||||
```
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, "Starting value".to_string());
|
||||
let (a, set_a) = create_signal("Starting value".to_string());
|
||||
let on_input = move |ev| set_a(event_target_value(&ev));
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
// ✅ works as intended by setting the value *property*
|
||||
<input prop:value=a on:input=on_input />
|
||||
<input prop:value=a on:input=on_input />
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
This project contains the core of a new introductory guide to Leptos.
|
||||
The Leptos book is now available at [https://book.leptos.dev](https://book.leptos.dev).
|
||||
|
||||
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`.
|
||||
The source code for the book has moved to [https://github.com/leptos-rs/book](https://github.com/leptos-rs/book). Please open issues or make PRs in that repository.
|
||||
|
||||
10
docs/book/book.toml
Normal file
10
docs/book/book.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[output.html]
|
||||
additional-css = ["./mdbook-admonish.css"]
|
||||
[output.html.playground]
|
||||
runnable = false
|
||||
|
||||
[preprocessor]
|
||||
|
||||
[preprocessor.admonish]
|
||||
command = "mdbook-admonish"
|
||||
assets_version = "3.0.1" # do not edit: managed by `mdbook-admonish install`
|
||||
345
docs/book/mdbook-admonish.css
Normal file
345
docs/book/mdbook-admonish.css
Normal file
@@ -0,0 +1,345 @@
|
||||
@charset "UTF-8";
|
||||
:root {
|
||||
--md-admonition-icon--admonish-note: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/></svg>");
|
||||
--md-admonition-icon--admonish-abstract: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'/></svg>");
|
||||
--md-admonition-icon--admonish-info: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z'/></svg>");
|
||||
--md-admonition-icon--admonish-tip: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M17.66 11.2c-.23-.3-.51-.56-.77-.82-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32-2.59 2.08-3.61 5.75-2.39 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5.14.6.41 1.2.71 1.73 1.08 1.73 2.95 2.97 4.96 3.22 2.14.27 4.43-.12 6.07-1.6 1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6-1.12.4-2.24-.16-2.9-.82 1.19-.28 1.9-1.16 2.11-2.05.17-.8-.15-1.46-.28-2.23-.12-.74-.1-1.37.17-2.06.19.38.39.76.63 1.06.77 1 1.98 1.44 2.24 2.8.04.14.06.28.06.43.03.82-.33 1.72-.93 2.27z'/></svg>");
|
||||
--md-admonition-icon--admonish-success: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m9 20.42-6.21-6.21 2.83-2.83L9 14.77l9.88-9.89 2.83 2.83L9 20.42z'/></svg>");
|
||||
--md-admonition-icon--admonish-question: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='m15.07 11.25-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 0 0-2-2 2 2 0 0 0-2 2H8a4 4 0 0 1 4-4 4 4 0 0 1 4 4 3.2 3.2 0 0 1-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10c0-5.53-4.5-10-10-10z'/></svg>");
|
||||
--md-admonition-icon--admonish-warning: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2 1 21z'/></svg>");
|
||||
--md-admonition-icon--admonish-failure: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20 6.91 17.09 4 12 9.09 6.91 4 4 6.91 9.09 12 4 17.09 6.91 20 12 14.91 17.09 20 20 17.09 14.91 12 20 6.91z'/></svg>");
|
||||
--md-admonition-icon--admonish-danger: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M11 15H6l7-14v8h5l-7 14v-8z'/></svg>");
|
||||
--md-admonition-icon--admonish-bug: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z'/></svg>");
|
||||
--md-admonition-icon--admonish-example: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 0 1 .75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z'/></svg>");
|
||||
--md-admonition-icon--admonish-quote: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z'/></svg>");
|
||||
--md-details-icon: url("data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8.59 16.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42Z'/></svg>");
|
||||
}
|
||||
|
||||
:is(.admonition) {
|
||||
display: flow-root;
|
||||
margin: 1.5625em 0;
|
||||
padding: 0 1.2rem;
|
||||
color: var(--fg);
|
||||
page-break-inside: avoid;
|
||||
background-color: var(--bg);
|
||||
border: 0 solid black;
|
||||
border-inline-start-width: 0.4rem;
|
||||
border-radius: 0.2rem;
|
||||
box-shadow: 0 0.2rem 1rem rgba(0, 0, 0, 0.05), 0 0 0.1rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@media print {
|
||||
:is(.admonition) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
:is(.admonition) > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:is(.admonition) :is(.admonition) {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
:is(.admonition) > .tabbed-set:only-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
html :is(.admonition) > :last-child {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
a.admonition-anchor-link {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: -1.2rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
a.admonition-anchor-link:link, a.admonition-anchor-link:visited {
|
||||
color: var(--fg);
|
||||
}
|
||||
a.admonition-anchor-link:link:hover, a.admonition-anchor-link:visited:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
a.admonition-anchor-link::before {
|
||||
content: "§";
|
||||
}
|
||||
|
||||
:is(.admonition-title, summary.admonition-title) {
|
||||
position: relative;
|
||||
min-height: 4rem;
|
||||
margin-block: 0;
|
||||
margin-inline: -1.6rem -1.2rem;
|
||||
padding-block: 0.8rem;
|
||||
padding-inline: 4.4rem 1.2rem;
|
||||
font-weight: 700;
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
display: flex;
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title) p {
|
||||
margin: 0;
|
||||
}
|
||||
html :is(.admonition-title, summary.admonition-title):last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title)::before {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-start: 1.6rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: #448aff;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
-webkit-mask-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"></svg>');
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
}
|
||||
:is(.admonition-title, summary.admonition-title):hover a.admonition-anchor-link {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
details.admonition > summary.admonition-title::after {
|
||||
position: absolute;
|
||||
top: 0.625em;
|
||||
inset-inline-end: 1.6rem;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
background-color: currentcolor;
|
||||
mask-image: var(--md-details-icon);
|
||||
-webkit-mask-image: var(--md-details-icon);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
content: "";
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
details[open].admonition > summary.admonition-title::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-note) {
|
||||
border-color: #448aff;
|
||||
}
|
||||
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(68, 138, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-note) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #448aff;
|
||||
mask-image: var(--md-admonition-icon--admonish-note);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-note);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-abstract, .admonish-summary, .admonish-tldr) {
|
||||
border-color: #00b0ff;
|
||||
}
|
||||
|
||||
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 176, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-abstract, .admonish-summary, .admonish-tldr) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b0ff;
|
||||
mask-image: var(--md-admonition-icon--admonish-abstract);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-abstract);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-info, .admonish-todo) {
|
||||
border-color: #00b8d4;
|
||||
}
|
||||
|
||||
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 184, 212, 0.1);
|
||||
}
|
||||
:is(.admonish-info, .admonish-todo) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00b8d4;
|
||||
mask-image: var(--md-admonition-icon--admonish-info);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-info);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-tip, .admonish-hint, .admonish-important) {
|
||||
border-color: #00bfa5;
|
||||
}
|
||||
|
||||
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 191, 165, 0.1);
|
||||
}
|
||||
:is(.admonish-tip, .admonish-hint, .admonish-important) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00bfa5;
|
||||
mask-image: var(--md-admonition-icon--admonish-tip);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-tip);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-success, .admonish-check, .admonish-done) {
|
||||
border-color: #00c853;
|
||||
}
|
||||
|
||||
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(0, 200, 83, 0.1);
|
||||
}
|
||||
:is(.admonish-success, .admonish-check, .admonish-done) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #00c853;
|
||||
mask-image: var(--md-admonition-icon--admonish-success);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-success);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-question, .admonish-help, .admonish-faq) {
|
||||
border-color: #64dd17;
|
||||
}
|
||||
|
||||
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(100, 221, 23, 0.1);
|
||||
}
|
||||
:is(.admonish-question, .admonish-help, .admonish-faq) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #64dd17;
|
||||
mask-image: var(--md-admonition-icon--admonish-question);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-question);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-warning, .admonish-caution, .admonish-attention) {
|
||||
border-color: #ff9100;
|
||||
}
|
||||
|
||||
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 145, 0, 0.1);
|
||||
}
|
||||
:is(.admonish-warning, .admonish-caution, .admonish-attention) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff9100;
|
||||
mask-image: var(--md-admonition-icon--admonish-warning);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-warning);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-failure, .admonish-fail, .admonish-missing) {
|
||||
border-color: #ff5252;
|
||||
}
|
||||
|
||||
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 82, 82, 0.1);
|
||||
}
|
||||
:is(.admonish-failure, .admonish-fail, .admonish-missing) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff5252;
|
||||
mask-image: var(--md-admonition-icon--admonish-failure);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-failure);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-danger, .admonish-error) {
|
||||
border-color: #ff1744;
|
||||
}
|
||||
|
||||
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(255, 23, 68, 0.1);
|
||||
}
|
||||
:is(.admonish-danger, .admonish-error) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #ff1744;
|
||||
mask-image: var(--md-admonition-icon--admonish-danger);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-danger);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-bug) {
|
||||
border-color: #f50057;
|
||||
}
|
||||
|
||||
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(245, 0, 87, 0.1);
|
||||
}
|
||||
:is(.admonish-bug) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #f50057;
|
||||
mask-image: var(--md-admonition-icon--admonish-bug);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-bug);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-example) {
|
||||
border-color: #7c4dff;
|
||||
}
|
||||
|
||||
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(124, 77, 255, 0.1);
|
||||
}
|
||||
:is(.admonish-example) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #7c4dff;
|
||||
mask-image: var(--md-admonition-icon--admonish-example);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-example);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:is(.admonition):is(.admonish-quote, .admonish-cite) {
|
||||
border-color: #9e9e9e;
|
||||
}
|
||||
|
||||
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title) {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
}
|
||||
:is(.admonish-quote, .admonish-cite) > :is(.admonition-title, summary.admonition-title)::before {
|
||||
background-color: #9e9e9e;
|
||||
mask-image: var(--md-admonition-icon--admonish-quote);
|
||||
-webkit-mask-image: var(--md-admonition-icon--admonish-quote);
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.navy :is(.admonition) {
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
.ayu :is(.admonition),
|
||||
.coal :is(.admonition) {
|
||||
background-color: var(--theme-hover);
|
||||
}
|
||||
|
||||
.rust :is(.admonition) {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.rust .admonition-anchor-link:link, .rust .admonition-anchor-link:visited {
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
@@ -1,20 +1,2 @@
|
||||
# Introduction
|
||||
|
||||
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 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), 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/).
|
||||
|
||||
**The guide is a work in progress.**
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/">
|
||||
<link rel="canonical" href="https://book.leptos.dev/">
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# 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.
|
||||
|
||||
If you don’t already have it installed, you can install Trunk by running
|
||||
|
||||
```bash
|
||||
cargo install trunk
|
||||
```
|
||||
|
||||
Create a basic Rust binary project
|
||||
|
||||
```bash
|
||||
cargo init leptos-tutorial
|
||||
```
|
||||
|
||||
> We recommend using `nightly` Rust, as it enables [a few nice features](https://github.com/leptos-rs/leptos#nightly-note). To use `nightly` Rust with WebAssembly, you can run
|
||||
>
|
||||
> ```bash
|
||||
> rustup toolchain install nightly
|
||||
> rustup default nightly
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
|
||||
`cd` into your new `leptos-tutorial` project and add `leptos` as a dependency
|
||||
|
||||
```bash
|
||||
cargo add leptos
|
||||
```
|
||||
|
||||
Create a simple `index.html` in the root of the `leptos-tutorial` directory
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
And add a simple “Hello, world!” to your `main.rs`
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
mount_to_body(|cx| view! { cx, <p>"Hello, world!"</p> })
|
||||
}
|
||||
```
|
||||
|
||||
Your directory structure should now look something like this
|
||||
|
||||
```
|
||||
leptos_tutorial
|
||||
├── src
|
||||
│ └── main.rs
|
||||
├── Cargo.toml
|
||||
├── index.html
|
||||
```
|
||||
|
||||
Now run `trunk serve --open` from the root of the `leptos-tutorial` directory.
|
||||
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,110 +0,0 @@
|
||||
# Responding to Changes with `create_effect`
|
||||
|
||||
Believe it or not, we’ve made it this far without having mentioned half of the reactive system: effects.
|
||||
|
||||
Leptos is built on a fine-grained reactive system, which means that individual reactive values (“signals,” sometimes known as observables) trigger rerunning the code that reacts to them (“effects,” sometimes known as observers). These two halves of the reactive system are inter-dependent. Without effects, signals can change within the reactive system but never be observed in a way that interacts with the outside world. Without signals, effects run once but never again, as there’s no observable value to subscribe to.
|
||||
|
||||
[`create_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_effect.html) takes a function as its argument. It immediately runs the function. If you access any reactive signal inside that function, it registers the fact that the effect depends on that signal with the reactive runtime. Whenever one of the signals that the effect depends on changes, the effect runs again.
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_b) = create_signal(cx, 0);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
// immediately prints "Value: 0" and subscribes to `a`
|
||||
log::debug!("Value: {}", a());
|
||||
});
|
||||
```
|
||||
|
||||
The effect function is called with an argument containing whatever value it returned the last time it ran. On the initial run, this is `None`.
|
||||
|
||||
By default, effects **do not run on the server**. This means you can call browser-specific APIs within the effect function without causing issues. If you need an effect to run on the server, use [`create_isomorphic_effect`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_isomorphic_effect.html).
|
||||
|
||||
## Autotracking and Dynamic Dependencies
|
||||
|
||||
If you’re familiar with a framework like React, you might notice one key difference. React and similar frameworks typically require you to pass a “dependency array,” an explicit set of variables that determine when the effect should rerun.
|
||||
|
||||
Because Leptos comes from the tradition of synchronous reactive programming, we don’t need this explicit dependency list. Instead, we automatically track dependencies depending on which signals are accessed within the effect.
|
||||
|
||||
This has two effects (no pun intended). Dependencies are
|
||||
|
||||
1. **Automatic**: You don’t need to maintain a dependency list, or worry about what should or shouldn’t be included. The framework simply tracks which signals might cause the effect to rerun, and handles it for you.
|
||||
2. **Dynamic**: The dependency list is cleared and updated every time the effect runs. If your effect contains a conditional (for example), only signals that are used in the current branch are tracked. This means that effects rerun the absolute minimum number of times.
|
||||
|
||||
> If this sounds like magic, and if you want a deep dive into how automatic dependency tracking works, [check out this video](https://www.youtube.com/watch?v=GWB3vTWeLd4). (Apologies for the low volume!)
|
||||
|
||||
## Effects as Zero-Cost-ish Abstraction
|
||||
|
||||
While they’re not a “zero-cost abstraction” in the most technical sense—they require some additional memory use, exist at runtime, etc.—at a higher level, from the perspective of whatever expensive API calls or other work you’re doing within them, effects are a zero-cost abstraction. They rerun the absolute minimum number of times necessary, given how you’ve described them.
|
||||
|
||||
Imagine that I’m creating some kind of chat software, and I want people to be able to display their full name, or just their first name, and to notify the server whenever their name changes:
|
||||
|
||||
```rust
|
||||
let (first, set_first) = create_signal(cx, String::new());
|
||||
let (last, set_last) = create_signal(cx, String::new());
|
||||
let (use_last, set_use_last) = create_signal(cx, true);
|
||||
|
||||
// this will add the name to the log
|
||||
// any time one of the source signals changes
|
||||
create_effect(cx, move |_| {
|
||||
log(
|
||||
cx,
|
||||
if use_last() {
|
||||
format!("{} {}", first(), last())
|
||||
} else {
|
||||
first()
|
||||
},
|
||||
)
|
||||
});
|
||||
```
|
||||
|
||||
If `use_last` is `true`, effect should rerun whenever `first`, `last`, or `use_last` changes. But if I toggle `use_last` to `false`, a change in `last` will never cause the full name to change. In fact, `last` will be removed from the dependency list until `use_last` toggles again. This saves us from sending multiple unnecessary requests to the API if I change `last` multiple times while `use_last` is still `false`.
|
||||
|
||||
## To `create_effect`, or not to `create_effect`?
|
||||
|
||||
Effects are intended to run _side-effects_ of the system, not to synchronize state _within_ the system. In other words: don’t write to signals within effects.
|
||||
|
||||
If you need to define a signal that depends on the value of other signals, use a derived signal or [`create_memo`](https://docs.rs/leptos_reactive/latest/leptos_reactive/fn.create_memo.html).
|
||||
|
||||
If you need to synchronize some reactive value with the non-reactive world outside—like a web API, the console, the filesystem, or the DOM—create an effect.
|
||||
|
||||
> If you’re curious for more information about when you should and shouldn’t use `create_effect`, [check out this video](https://www.youtube.com/watch?v=aQOFJQ2JkvQ) for a more in-depth consideration!
|
||||
|
||||
## Effects and Rendering
|
||||
|
||||
We’ve managed to get this far without mentioning effects because they’re built into the Leptos DOM renderer. We’ve seen that you can create a signal and pass it into the `view` macro, and it will update the relevant DOM node whenever the signal changes:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
view! { cx,
|
||||
<p>{count}</p>
|
||||
}
|
||||
```
|
||||
|
||||
This works because the framework essentially creates an effect wrapping this update. You can imagine Leptos translating this view into something like this:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// create a DOM element
|
||||
let p = create_element("p");
|
||||
|
||||
// create an effect to reactively update the text
|
||||
create_effect(cx, move |prev_value| {
|
||||
// first, access the signal’s value and convert it to a string
|
||||
let text = count().to_string();
|
||||
|
||||
// if this is different from the previous value, update the node
|
||||
if prev_value != Some(text) {
|
||||
p.set_text_content(&text);
|
||||
}
|
||||
|
||||
// return this value so we can memoize the next update
|
||||
text
|
||||
});
|
||||
```
|
||||
|
||||
Every time `count` is updated, this effect wil rerun. This is what allows reactive, fine-grained updates to the DOM.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/serene-thompson-40974n?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>
|
||||
@@ -1,171 +1,2 @@
|
||||
# Global State Management
|
||||
|
||||
So far, we've only been working with local state in components
|
||||
We've only seen how to communicate between parent and child components
|
||||
But there are also more general ways to manage global state
|
||||
|
||||
The three best approaches to global state are
|
||||
|
||||
1. Using the router to drive global state via the URL
|
||||
2. Passing signals through context
|
||||
3. Creating a global state struct and creating lenses into it with `create_slice`
|
||||
|
||||
## Option #1: URL as Global State
|
||||
|
||||
The next few sections of the tutorial will be about the router.
|
||||
So for now, we'll just look at options #2 and #3.
|
||||
|
||||
## Option #2: Passing Signals through Context
|
||||
|
||||
In virtual DOM libraries like React, using the Context API to manage global
|
||||
state is a bad idea: because the entire app exists in a tree, changing
|
||||
some value provided high up in the tree can cause the whole app to render.
|
||||
|
||||
In fine-grained reactive libraries like Leptos, this is simply not the case.
|
||||
You can create a signal in the root of your app and pass it down to other
|
||||
components using provide_context(). Changing it will only cause rerendering
|
||||
in the specific places it is actually used, not the whole app.
|
||||
|
||||
We start by creating a signal in the root of the app and providing it to
|
||||
all its children and descendants using `provide_context`.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
// here we create a signal in the root that can be consumed
|
||||
// anywhere in the app.
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
// we'll pass the setter to specific components,
|
||||
// but provide the count itself to the whole app via context
|
||||
provide_context(cx, count);
|
||||
|
||||
view! { cx,
|
||||
// SetterButton is allowed to modify the count
|
||||
<SetterButton set_count/>
|
||||
// These consumers can only read from it
|
||||
// But we could give them write access by passing `set_count` if we wanted
|
||||
<FancyMath/>
|
||||
<ListItems/>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`<SetterButton/>` is the kind of counter we’ve written several times now.
|
||||
(See the sandbox below if you don’t understand what I mean.)
|
||||
|
||||
`<FancyMath/>` and `<ListItems/>` both consume the signal we’re providing via
|
||||
`use_context` and do something with it.
|
||||
|
||||
```rust
|
||||
/// A component that does some "fancy" math with the global count
|
||||
#[component]
|
||||
fn FancyMath(cx: Scope) -> impl IntoView {
|
||||
// here we consume the global count signal with `use_context`
|
||||
let count = use_context::<ReadSignal<u32>>(cx)
|
||||
// we know we just provided this in the parent component
|
||||
.expect("there to be a `count` signal provided");
|
||||
let is_even = move || count() & 1 == 0;
|
||||
|
||||
view! { cx,
|
||||
<div class="consumer blue">
|
||||
"The number "
|
||||
<strong>{count}</strong>
|
||||
{move || if is_even() {
|
||||
" is"
|
||||
} else {
|
||||
" is not"
|
||||
}}
|
||||
" even."
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This kind of “provide a signal in a parent, consume it in a child” should be familiar
|
||||
from the chapter on [parent-child interactions](./view/08_parent_child.md). The same
|
||||
pattern you use to communicate between parents and children works for grandparents and
|
||||
grandchildren, or any ancestors and descendants: in other words, between “global” state
|
||||
in the root component of your app and any other components anywhere else in the app.
|
||||
|
||||
Because of the fine-grained nature of updates, this is usually all you need. However,
|
||||
in some cases with more complex state changes, you may want to use a slightly more
|
||||
structured approach to global state.
|
||||
|
||||
## Option #3: Create a Global State Struct
|
||||
|
||||
You can use this approach to build a single global data structure
|
||||
that holds the state for your whole app, and then access it by
|
||||
taking fine-grained slices using
|
||||
[`create_slice`](https://docs.rs/leptos/latest/leptos/fn.create_slice.html)
|
||||
or [`create_memo`](https://docs.rs/leptos/latest/leptos/fn.create_memo.html),
|
||||
so that changing one part of the state doesn't cause parts of your
|
||||
app that depend on other parts of the state to change.
|
||||
|
||||
You can begin by defining a simple state struct:
|
||||
|
||||
```rust
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct GlobalState {
|
||||
count: u32,
|
||||
name: String,
|
||||
}
|
||||
```
|
||||
|
||||
Provide it in the root of your app so it’s available everywhere.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
fn App(cx: Scope) -> impl IntoView {
|
||||
// we'll provide a single signal that holds the whole state
|
||||
// each component will be responsible for creating its own "lens" into it
|
||||
let state = create_rw_signal(cx, GlobalState::default());
|
||||
provide_context(cx, state);
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
Then child components can access “slices” of that state with fine-grained
|
||||
updates via `create_slice`. Each slice signal only updates when the particular
|
||||
piece of the larger struct it accesses updates. This means you can create a single
|
||||
root signal, and then take independent, fine-grained slices of it in different
|
||||
components, each of which can update without notifying the others of changes.
|
||||
|
||||
```rust
|
||||
/// A component that updates the count in the global state.
|
||||
#[component]
|
||||
fn GlobalStateCounter(cx: Scope) -> impl IntoView {
|
||||
let state = use_context::<RwSignal<GlobalState>>(cx).expect("state to have been provided");
|
||||
|
||||
// `create_slice` lets us create a "lens" into the data
|
||||
let (count, set_count) = create_slice(
|
||||
cx,
|
||||
// we take a slice *from* `state`
|
||||
state,
|
||||
// our getter returns a "slice" of the data
|
||||
|state| state.count,
|
||||
// our setter describes how to mutate that slice, given a new value
|
||||
|state, n| state.count = n,
|
||||
);
|
||||
|
||||
view! { cx,
|
||||
<div class="consumer blue">
|
||||
<button
|
||||
on:click=move |_| {
|
||||
set_count(count() + 1);
|
||||
}
|
||||
>
|
||||
"Increment Global Count"
|
||||
</button>
|
||||
<br/>
|
||||
<span>"Count is: " {count}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clicking this button only updates `state.count`, so if we create another slice
|
||||
somewhere else that only takes `state.name`, clicking the button won’t cause
|
||||
that other slice to update. This allows you to combine the benefits of a top-down
|
||||
data flow and of fine-grained reactive updates.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/1-basic-component-forked-8bte19?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px">
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/15_global_state.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/15_global_state.html">
|
||||
|
||||
@@ -1,48 +1,56 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](./01_introduction.md)
|
||||
- [Getting Started](./02_getting_started.md)
|
||||
- [Building User Interfaces](./view/README.md)
|
||||
- [Getting Started](./getting_started/README.md)
|
||||
- [Leptos DX](./getting_started/leptos_dx.md)
|
||||
- [The Leptos Community and leptos-* Crates](./getting_started/community_crates.md)
|
||||
- [Part 1: 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)
|
||||
- [Iterating over More Complex Data](./view/04b_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](./view/09_component_children.md)
|
||||
- [Interlude: Reactivity and Functions](./interlude_functions.md)
|
||||
- [No Macros: The View Builder Syntax](./view/builder.md)
|
||||
- [Reactivity](./reactivity/README.md)
|
||||
- [Working with Signals](./reactivity/working_with_signals.md)
|
||||
- [Responding to Changes with `create_effect`](./reactivity/14_create_effect.md)
|
||||
- [Interlude: Reactivity and Functions](./reactivity/interlude_functions.md)
|
||||
- [Testing](./testing.md)
|
||||
- [Async](./async/README.md)
|
||||
- [Loading Data with Resources](./async/10_resources.md)
|
||||
- [Suspense](./async/11_suspense.md)
|
||||
- [Transition](./async/12_transition.md)
|
||||
- [Actions](./async/13_actions.md)
|
||||
- [Responding to Changes with `create_effect`](./14_create_effect.md)
|
||||
- [Interlude: Projecting Children](./interlude_projecting_children.md)
|
||||
- [Global State Management](./15_global_state.md)
|
||||
- [Router](./router/README.md)
|
||||
- [Defining `<Routes/>`](./router/16_routes.md)
|
||||
- [Nested Routing](./router/17_nested_routing.md)
|
||||
- [Params and Queries](./router/18_params_and_queries.md)
|
||||
- [`<A/>`](./router/19_a.md)
|
||||
- [`<Form/>`]()
|
||||
- [Interlude: Styling — CSS, Tailwind, Style.rs, and more]()
|
||||
- [Metadata]()
|
||||
- [SSR]()
|
||||
- [Models of SSR]()
|
||||
- [`cargo-leptos`]()
|
||||
- [Hydration Footguns]()
|
||||
- [Request/Response]()
|
||||
- [Extractors]()
|
||||
- [Axum]()
|
||||
- [Actix]()
|
||||
- [Headers]()
|
||||
- [Cookies]()
|
||||
- [Server Functions]()
|
||||
- [Actions]()
|
||||
- [Forms]()
|
||||
- [`<ActionForm/>`s]()
|
||||
- [Turning off WebAssembly]()
|
||||
- [Advanced Reactivity]()
|
||||
- [Appendix: Optimizing WASM Binary Size]()
|
||||
- [`<Form/>`](./router/20_form.md)
|
||||
- [Interlude: Styling](./interlude_styling.md)
|
||||
- [Metadata](./metadata.md)
|
||||
- [Client-Side Rendering: Wrapping Up](./csr_wrapping_up.md)
|
||||
- [Part 2: Server Side Rendering](./ssr/README.md)
|
||||
- [`cargo-leptos`](./ssr/21_cargo_leptos.md)
|
||||
- [The Life of a Page Load](./ssr/22_life_cycle.md)
|
||||
- [Async Rendering and SSR “Modes”](./ssr/23_ssr_modes.md)
|
||||
- [Hydration Bugs](./ssr/24_hydration_bugs.md)
|
||||
- [Working with the Server](./server/README.md)
|
||||
- [Server Functions](./server/25_server_functions.md)
|
||||
- [Extractors](./server/26_extractors.md)
|
||||
- [Responses and Redirects](./server/27_response.md)
|
||||
- [Progressive Enhancement and Graceful Degradation](./progressive_enhancement/README.md)
|
||||
- [`<ActionForm/>`s](./progressive_enhancement/action_form.md)
|
||||
- [Deployment](./deployment/README.md)
|
||||
- [Optimizing WASM Binary Size](./deployment/binary_size.md)
|
||||
- [Guide: Islands](./islands.md)
|
||||
|
||||
- [Appendix: How Does the Reactive System Work?](./appendix_reactive_graph.md)
|
||||
|
||||
|
||||
2
docs/book/src/appendix_reactive_graph.md
Normal file
2
docs/book/src/appendix_reactive_graph.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/appendix_reactive_graph.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/appendix_reactive_graph.html">
|
||||
@@ -1,53 +1,2 @@
|
||||
# Loading Data with Resources
|
||||
|
||||
A [Resource](https://docs.rs/leptos/latest/leptos/struct.Resource.html) is a reactive data structure that reflects the current state of an asynchronous task, allowing you to integrate asynchronous `Future`s into the synchronous reactive system. Rather than waiting for its data to load with `.await`, you transform the `Future` into a signal that returns `Some(T)` if it has resolved, and `None` if it’s still pending.
|
||||
|
||||
You do this by using the [`create_resource`](https://docs.rs/leptos/latest/leptos/fn.create_resource.html) function. This takes two arguments (other than the ubiquitous `cx`):
|
||||
|
||||
1. a source signal, which will generate a new `Future` whenever it changes
|
||||
2. a fetcher function, which takes the data from that signal and returns a `Future`
|
||||
|
||||
Here’s an example
|
||||
|
||||
```rust
|
||||
// our source signal: some synchronous, local state
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// our resource
|
||||
let async_data = create_resource(cx,
|
||||
count,
|
||||
// every time `count` changes, this will run
|
||||
|value| async move {
|
||||
log!("loading data from API");
|
||||
load_data(value).await
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
To create a resource that simply runs once, you can pass a non-reactive, empty source signal:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
```
|
||||
|
||||
To access the value you can use `.read(cx)` or `.with(cx, |data| /* */)`. These work just like `.get()` and `.with()` on a signal—`read` clones the value and returns it, `with` applies a closure to it—but with two differences
|
||||
|
||||
1. For any `Resource<_, T>`, they always return `Option<T>`, not `T`: because it’s always possible that your resource is still loading.
|
||||
2. They take a `Scope` argument. You’ll see why in the next chapter, on `<Suspense/>`.
|
||||
|
||||
So, you can show the current state of a resource in your view:
|
||||
|
||||
```rust
|
||||
let once = create_resource(cx, || (), |_| async move { load_data().await });
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
Resources also provide a `refetch()` method that allows you to manually reload the data (for example, in response to a button click) and a `loading()` method that returns a `ReadSignal<bool>` indicating whether the resource is currently loading or not.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-4z0qt3?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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/10_resources.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/10_resources.html">
|
||||
|
||||
@@ -1,72 +1,2 @@
|
||||
# `<Suspense/>`
|
||||
|
||||
In the previous chapter, we showed how you can create a simple loading screen to show some fallback while a resource is loading.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match once.read(cx) {
|
||||
None => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
Some(data) => view! { cx, <ShowData data/> }.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
But what if we have two resources, and want to wait for both of them?
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
{move || match (a.read(cx), b.read(cx)) {
|
||||
_ => view! { cx, <p>"Loading..."</p> }.into_view(cx),
|
||||
(Some(a), Some(b)) => view! { cx,
|
||||
<ShowA a/>
|
||||
<ShowA b/>
|
||||
}.into_view(cx)
|
||||
}}
|
||||
}
|
||||
```
|
||||
|
||||
That’s not _so_ bad, but it’s kind of annoying. What if we could invert the flow of control?
|
||||
|
||||
The [`<Suspense/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html) component lets us do exactly that. You give it a `fallback` prop and children, one or more of which usually involves reading from a resource. Reading from a resource “under” a `<Suspense/>` (i.e., in one of its children) registers that resource with the `<Suspense/>`. If it’s still waiting for resources to load, it shows the `fallback`. When they’ve all loaded, it shows the children.
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
let (count2, set_count2) = create_signal(cx, 0);
|
||||
let a = create_resource(cx, count, |count| async move { load_a(count).await });
|
||||
let b = create_resource(cx, count2, |count| async move { load_b(count).await });
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Data"</h1>
|
||||
<Suspense
|
||||
fallback=move || view! { cx, <p>"Loading..."</p> }
|
||||
>
|
||||
<h2>"My Data"</h2>
|
||||
<h3>"A"</h3>
|
||||
{move || {
|
||||
a.read(cx)
|
||||
.map(|a| view! { cx, <ShowA a/> })
|
||||
}}
|
||||
<h3>"B"</h3>
|
||||
{move || {
|
||||
b.read(cx)
|
||||
.map(|b| view! { cx, <ShowB b/> })
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
```
|
||||
|
||||
Every time one of the resources is reloading, the `"Loading..."` fallback will show again.
|
||||
|
||||
This inversion of the flow of control makes it easier to add or remove individual resources, as you don’t need to handle the matching yourself. It also unlocks some massive performance improvements during server-side rendering, which we’ll talk about during a later chapter.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/11-suspense-907niv?file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/11_suspense.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/11_suspense.html">
|
||||
|
||||
@@ -1,9 +1,2 @@
|
||||
# `<Transition/>`
|
||||
|
||||
You’ll notice in the `<Suspense/>` example that if you keep reloading the data, it keeps flickering back to `"Loading..."`. Sometimes this is fine. For other times, there’s [`<Transition/>`](https://docs.rs/leptos/latest/leptos/fn.Suspense.html).
|
||||
|
||||
`<Transition/>` behaves exactly the same as `<Suspense/>`, but instead of falling back every time, it only shows the fallback the first time. On all subsequent loads, it continues showing the old data until the new data are ready. This can be really handy to prevent the flickering effect, and to allow users to continue interacting with your application.
|
||||
|
||||
This example shows how you can create a simple tabbed contact list with `<Transition/>`. When you select a new tab, it continues showing the current contact until the new data loads. This can be a much better user experience than constantly falling back to a loading message.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/12-transition-sn38sd?selection=%5B%7B%22endColumn%22%3A15%2C%22endLineNumber%22%3A2%2C%22startColumn%22%3A15%2C%22startLineNumber%22%3A2%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/12_transition.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/12_transition.html">
|
||||
|
||||
@@ -1,94 +1,2 @@
|
||||
# Mutating Data with Actions
|
||||
|
||||
We’ve talked about how to load `async` data with resources. Resources immediately load data and work closely with `<Suspense/>` and `<Transition/>` components to show whether data is loading in your app. But what if you just want to call some arbitrary `async` function and keep track of what it’s doing?
|
||||
|
||||
Well, you could always use [`spawn_local`](https://docs.rs/leptos/latest/leptos/fn.spawn_local.html). This allows you to just spawn an `async` task in a synchronous environment by handing the `Future` off to the browser (or, on the server, Tokio or whatever other runtime you’re using). But how do you know if it’s still pending? Well, you could just set a signal to show whether it’s loading, and another one to show the result...
|
||||
|
||||
All of this is true. Or you could use the final `async` primitive: [`create_action`](https://docs.rs/leptos/latest/leptos/fn.create_action.html).
|
||||
|
||||
Actions and resources seem similar, but they represent fundamentally different things. If you’re trying to load data by running an `async` function, either once or when some other value changes, you probably want to use `create_resource`. If you’re trying to occasionally run an `async` function in response to something like a user clicking a button, you probably want to use `create_action`.
|
||||
|
||||
Say we have some `async` function we want to run.
|
||||
|
||||
```rust
|
||||
async fn add_todo(new_title: &str) -> Uuid {
|
||||
/* do some stuff on the server to add a new todo */
|
||||
}
|
||||
```
|
||||
|
||||
`create_action` takes a reactive `Scope` and an `async` function that takes a reference to a single argument, which you could think of as its “input type.”
|
||||
|
||||
> The input is always a single type. If you want to pass in multiple arguments, you can do it with a struct or tuple.
|
||||
>
|
||||
> ```rust
|
||||
> // if there's a single argument, just use that
|
||||
> let action1 = create_action(cx, |input: &String| {
|
||||
> let input = input.clone();
|
||||
> async move { todo!() }
|
||||
> });
|
||||
>
|
||||
> // if there are no arguments, use the unit type `()`
|
||||
> let action2 = create_action(cx, |input: &()| async { todo!() });
|
||||
>
|
||||
> // if there are multiple arguments, use a tuple
|
||||
> let action3 = create_action(cx,
|
||||
> |input: &(usize, String)| async { todo!() }
|
||||
> );
|
||||
> ```
|
||||
>
|
||||
> Because the action function takes a reference but the `Future` needs to have a `'static` lifetime, you’ll usually need to clone the value to pass it into the `Future`. This is admittedly awkward but it unlocks some powerful features like optimistic UI. We’ll see a little more about that in future chapters.
|
||||
|
||||
So in this case, all we need to do to create an action is
|
||||
|
||||
```rust
|
||||
let add_todo = create_action(cx, |input: &String| {
|
||||
let input = input.to_owned();
|
||||
async move { add_todo(&input).await }
|
||||
});
|
||||
```
|
||||
|
||||
Rather than calling `add_todo` directly, we’ll call it with `.dispatch()`, as in
|
||||
|
||||
```rust
|
||||
add_todo.dispatch("Some value".to_string());
|
||||
```
|
||||
|
||||
You can do this from an event listener, a timeout, or anywhere; because `.dispatch()` isn’t an `async` function, it can be called from a synchronous context.
|
||||
|
||||
Actions provide access to a few signals that synchronize between the asynchronous action you’re calling and the synchronous reactive system:
|
||||
|
||||
```rust
|
||||
let submitted = add_todo.input(); // RwSignal<Option<String>>
|
||||
let pending = add_todo.pending(); // ReadSignal<bool>
|
||||
let todo_id = add_todo.value(); // RwSignal<Option<Uuid>>
|
||||
```
|
||||
|
||||
This makes it easy to track the current state of your request, show a loading indicator, or do “optimistic UI” based on the assumption that the submission will succeed.
|
||||
|
||||
```rust
|
||||
let input_ref = create_node_ref::<Input>(cx);
|
||||
|
||||
view! { cx,
|
||||
<form
|
||||
on:submit=move |ev| {
|
||||
ev.prevent_default(); // don't reload the page...
|
||||
let input = input_ref.get().expect("input to exist");
|
||||
add_todo.dispatch(input.value());
|
||||
}
|
||||
>
|
||||
<label>
|
||||
"What do you need to do?"
|
||||
<input type="text"
|
||||
node_ref=input_ref
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">"Add Todo"</button>
|
||||
</form>
|
||||
// use our loading state
|
||||
<p>{move || pending().then("Loading...")}</p>
|
||||
}
|
||||
```
|
||||
|
||||
Now, there’s a chance this all seems a little over-complicated, or maybe too restricted. I wanted to include actions here, alongside resources, as the missing piece of the puzzle. In a real Leptos app, you’ll actually most often use actions alongside server functions, [`create_server_action`](https://docs.rs/leptos/latest/leptos/fn.create_server_action.html), and the [`<ActionForm/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.ActionForm.html) component to create really powerful progressively-enhanced forms. So if this primitive seems useless to you... Don’t worry! Maybe it will make sense later. (Or check out our [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/blob/main/examples/todo_app_sqlite/src/todo.rs) example now.)
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/10-async-resources-forked-hgpfp0?selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A4%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D&file=%2Fsrc%2Fmain.rs" width="100%" height="1000px"></iframe>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/13_action.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/13_action.html">
|
||||
|
||||
@@ -1,9 +1,2 @@
|
||||
# Working with `async`
|
||||
|
||||
So far we’ve only been working with synchronous users interfaces: You provide some input,
|
||||
the app immediately process it and updates the interface. This is great, but is a tiny
|
||||
subset of what web applications do. In particular, most web apps have to deal with some kind
|
||||
of asynchronous data loading, usually loading something from an API.
|
||||
|
||||
Asynchronous data is notoriously hard to integrate with the synchronous parts of your code.
|
||||
In this chapter, we’ll see how Leptos helps smooth out that process for you.
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/async/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/async/index.html">
|
||||
|
||||
2
docs/book/src/csr_wrapping_up.md
Normal file
2
docs/book/src/csr_wrapping_up.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/csr_wrapping_up.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/csr_wrapping_up.html">
|
||||
2
docs/book/src/deployment/README.md
Normal file
2
docs/book/src/deployment/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/deployment/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/deployment/index.html">
|
||||
2
docs/book/src/deployment/binary_size.md
Normal file
2
docs/book/src/deployment/binary_size.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/deployment/binary_size.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/deployment/binary_size.html">
|
||||
2
docs/book/src/getting_started/README.md
Normal file
2
docs/book/src/getting_started/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/getting_started/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/getting_started/index.html">
|
||||
2
docs/book/src/getting_started/community_crates.md
Normal file
2
docs/book/src/getting_started/community_crates.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/getting_started/community_crates.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/getting_started/community_crates.html">
|
||||
2
docs/book/src/getting_started/leptos_dx.md
Normal file
2
docs/book/src/getting_started/leptos_dx.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/getting_started/leptos_dx.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/getting_started/leptos_dx.html">
|
||||
@@ -1,76 +0,0 @@
|
||||
# Interlude: Reactivity and Functions
|
||||
|
||||
One of our core contributors said to me recently: “I never used closures this often
|
||||
until I started using Leptos.” And it’s true. Closures are at the heart of any Leptos
|
||||
application. It sometimes looks a little silly:
|
||||
|
||||
```rust
|
||||
// a signal holds a value, and can be updated
|
||||
let (count, set_count) = create_signal(cx, 0);
|
||||
|
||||
// a derived signal is a function that accesses other signals
|
||||
let double_count = move || count() * 2;
|
||||
let count_is_odd = move || count() & 1 == 1;
|
||||
let text = move || if count_is_odd() {
|
||||
"odd"
|
||||
} else {
|
||||
"even"
|
||||
};
|
||||
|
||||
// an effect automatically tracks the signals it depends on
|
||||
// and reruns when they change
|
||||
create_effect(cx, move |_| {
|
||||
log!("text = {}", text());
|
||||
});
|
||||
|
||||
view! { cx,
|
||||
<p>{move || text().to_uppercase()}</p>
|
||||
}
|
||||
```
|
||||
|
||||
Closures, closures everywhere!
|
||||
|
||||
But why?
|
||||
|
||||
## Functions and UI Frameworks
|
||||
|
||||
Functions are at the heart of every UI framework. And this makes perfect sense. Creating a user interface is basically divided into two phases:
|
||||
|
||||
1. initial rendering
|
||||
2. updates
|
||||
|
||||
In a web framework, the framework does some kind of initial rendering. Then it hands control back over to the browser. When certain events fire (like a mouse click) or asynchronous tasks finish (like an HTTP request finishing), the browser wakes the framework back up to update something. The framework runs some kind of code to update your user interface, and goes back asleep until the browser wakes it up again.
|
||||
|
||||
The key phrase here is “runs some kind of code.” The natural way to “run some kind of code” at an arbitrary point in time—in Rust or in any other programming language—is to call a function. And in fact every UI framework is based on rerunning some kind of function over and over:
|
||||
|
||||
1. virtual DOM (VDOM) frameworks like React, Yew, or Dioxus rerun a component or render function over and over, to generate a virtual DOM tree that can be reconciled with the previous result to patch the DOM
|
||||
2. compiled frameworks like Angular and Svelte divide your component templates into “create” and “update” functions, rerunning the update function when they detect a change to the component’s state
|
||||
3. in fine-grained reactive frameworks like SolidJS, Sycamore, or Leptos, _you_ define the functions that rerun
|
||||
|
||||
That’s what all our components are doing.
|
||||
|
||||
Take our typical `<SimpleCounter/>` example in its simplest form:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn SimpleCounter(cx: Scope) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
let increment = move |_| set_value.update(|value| *value += 1);
|
||||
|
||||
view! { cx,
|
||||
<button on:click=increment>
|
||||
{value}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `SimpleCounter` function itself runs once. The `value` signal is created once. The framework hands off the `increment` function to the browser as an event listener. When you click the button, the browser calls `increment`, which updates `value` via `set_value`. And that updates the single text node represented in our view by `{value}`.
|
||||
|
||||
Closures are key to reactivity. They provide the framework with the ability to rerun the smallest possible unit of your application in responsive to a change.
|
||||
|
||||
So remember two things:
|
||||
|
||||
1. Your component function is a setup function, not a render function: it only runs once.
|
||||
2. For values in your view template to be reactive, they must be functions: either signals (which implement the `Fn` traits) or closures.
|
||||
2
docs/book/src/interlude_projecting_children.md
Normal file
2
docs/book/src/interlude_projecting_children.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/interlude_projecting_children.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/interlude_projecting_children.html">
|
||||
2
docs/book/src/interlude_styling.md
Normal file
2
docs/book/src/interlude_styling.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/interlude_styling.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/interlude_styling.html">
|
||||
2
docs/book/src/islands.md
Normal file
2
docs/book/src/islands.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/islands.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/islands.html">
|
||||
2
docs/book/src/metadata.md
Normal file
2
docs/book/src/metadata.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/metadata.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/metadata.html">
|
||||
2
docs/book/src/progressive_enhancement/README.md
Normal file
2
docs/book/src/progressive_enhancement/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/progressive_enhancement/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/progressive_enhancement/index.html">
|
||||
2
docs/book/src/progressive_enhancement/action_form.md
Normal file
2
docs/book/src/progressive_enhancement/action_form.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/progressive_enhancement/action_form.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/progressive_enhancement/action_form.html">
|
||||
2
docs/book/src/reactivity/14_create_effect.md
Normal file
2
docs/book/src/reactivity/14_create_effect.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/14_create_effect.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/reactivity/14_create_effect.html">
|
||||
2
docs/book/src/reactivity/README.md
Normal file
2
docs/book/src/reactivity/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/reactivity/index.html">
|
||||
2
docs/book/src/reactivity/interlude_functions.md
Normal file
2
docs/book/src/reactivity/interlude_functions.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/interlude_functions.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/reactivity/interlude_functions.html">
|
||||
2
docs/book/src/reactivity/working_with_signals.md
Normal file
2
docs/book/src/reactivity/working_with_signals.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/reactivity/working_with_signals.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/reactivity/working_with_signals.html">
|
||||
@@ -1,101 +1,2 @@
|
||||
# Defining Routes
|
||||
|
||||
## Getting Started
|
||||
|
||||
It’s easy to get started with the router.
|
||||
|
||||
First things first, make sure you’ve added the `leptos_router` package to your dependencies.
|
||||
|
||||
> It’s important that the router is a separate package from `leptos` itself. This means that everything in the router can be defined in user-land code. If you want to create your own router, or use no router, you’re completely free to do that!
|
||||
|
||||
And import the relevant types from the router, either with something like
|
||||
|
||||
```rust
|
||||
use leptos_router::{Route, RouteProps, Router, RouterProps, Routes, RoutesProps};
|
||||
```
|
||||
|
||||
or simply
|
||||
|
||||
```rust
|
||||
use leptos_router::*;
|
||||
```
|
||||
|
||||
## Providing the `<Router/>`
|
||||
|
||||
Routing behavior is provided by the [`<Router/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Router.html) component. This should usually be somewhere near the root of your application, the rest of the app.
|
||||
|
||||
> You shouldn’t try to use multiple `<Router/>`s in your app. Remember that the router drives global state: if you have multiple routers, which ones decides what to do when the URL changes?
|
||||
|
||||
Let’s start with a simple `<App/>` component using the router:
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
/* ... */
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Defining `<Routes/>`
|
||||
|
||||
The [`<Routes/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Routes.html) component is where you define all the routes to which a user can navigate in your application. Each possible route is defined by a [`<Route/>`](https://docs.rs/leptos_router/latest/leptos_router/fn.Route.html) component.
|
||||
|
||||
You should place the `<Routes/>` component at the location within your app where you want routes to be rendered. Everything outside `<Routes/>` will be present on every page, so you can leave things like a navigation bar or menu outside the `<Routes/>`.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
view! {
|
||||
<Router>
|
||||
<nav>
|
||||
/* ... */
|
||||
</nav>
|
||||
<main>
|
||||
// all our routes will appear inside <main>
|
||||
<Routes>
|
||||
/* ... */
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Individual routes are defined by providing children to `<Routes/>` with the `<Route/>` component. `<Route/>` takes a `path` and a `view`. When the current location matches `path`, the `view` will be created and displayed.
|
||||
|
||||
The `path` can include
|
||||
|
||||
- a static path (`/users`),
|
||||
- dynamic, named parameters beginning with a colon (`/:id`),
|
||||
- and/or a wildcard beginning with an asterisk (`/user/*any`)
|
||||
|
||||
The `view` is a function that takes a `Scope` and returns a view.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home/> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users/> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile/> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound/> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The router scores each route to see how good a match it is, so you can define your routes in any order.
|
||||
|
||||
Now if you navigate to `/` or to `/users` you’ll get the home page or the `<Users/>`. If you go to `/users/3` or `/blahblah` you’ll get a user profile or your 404 page (`<NotFound/>`). On every navigation, the router determines which `<Route/>` should be matched, and therefore what content should be displayed where the `<Routes/>` component is defined.
|
||||
|
||||
Simple enough?
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/16_routes.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/router/16_routes.html">
|
||||
|
||||
@@ -1,170 +1,2 @@
|
||||
# Nested Routing
|
||||
|
||||
We just defined the following set of routes:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
There’s a certain amount of duplication here: `/users` and `/users/:id`. This is fine for a small app, but you can probably already tell it won’t scale well. Wouldn’t it be nice if we could nest these routes?
|
||||
|
||||
Well... you can!
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/" view=|cx| view! { cx, <Home /> }/>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Route>
|
||||
<Route path="/*any" view=|cx| view! { cx, <NotFound /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
But wait. We’ve just subtly changed what our application does.
|
||||
|
||||
The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if there’s anything you don’t understand.
|
||||
|
||||
# Nested Routes as Layout
|
||||
|
||||
Nested routes are a form of layout, not a method of route definition.
|
||||
|
||||
Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple `<Route/>`s on the page at the same time, side by side.
|
||||
|
||||
Let’s look back at our practical example.
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }/>
|
||||
<Route path="/users/:id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users`, I get the `<Users/>` component.
|
||||
- If I go to `/users/3`, I get the `<UserProfile/>` component (with the parameter `id` set to `3`; more on that later)
|
||||
|
||||
Let’s say I use nested routes instead:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- If I go to `/users/3`, the path matches two `<Route/>`s: `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path is not matched.
|
||||
|
||||
I actually need to add a fallback route
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/users" view=|cx| view! { cx, <Users /> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <UserProfile /> }/>
|
||||
<Route path="" view=|cx| view! { cx, <NoUser /> }/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
Now:
|
||||
|
||||
- If I go to `/users/3`, the path matches `<Users/>` and `<UserProfile/>`.
|
||||
- If I go to `/users`, the path matches `<Users/>` and `<NoUser/>`.
|
||||
|
||||
When I use nested routes, in other words, each **path** can match multiple **routes**: each URL can render the views provided by multiple `<Route/>` components, at the same time, on the same page.
|
||||
|
||||
This may be counter-intuitive, but it’s very powerful, for reasons you’ll hopefully see in a few minutes.
|
||||
|
||||
## Why Nested Routing?
|
||||
|
||||
Why bother with this?
|
||||
|
||||
Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like `/contacts/greg`, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If there’s no contact selected, maybe you want to show a little instructional text.
|
||||
|
||||
You can easily define this with nested routes
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }/>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
You can go even deeper. Say you want to have tabs for each contact’s address, email/phone, and your conversations with them. You can add _another_ set of nested routes inside `:id`:
|
||||
|
||||
```rust
|
||||
<Routes>
|
||||
<Route path="/contacts" view=|cx| view! { cx, <ContactList/> }>
|
||||
<Route path=":id" view=|cx| view! { cx, <ContactInfo/> }>
|
||||
<Route path="" view=|cx| view! { cx, <EmailAndPhone/> }/>
|
||||
<Route path="address" view=|cx| view! { cx, <Address/> }/>
|
||||
<Route path="messages" view=|cx| view! { cx, <Messages/> }/>
|
||||
</Route>
|
||||
<Route path="" view=|cx| view! { cx,
|
||||
<p>"Select a contact to view more info."</p>
|
||||
}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
> The main page of the [Remix website](https://remix.run/), a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.
|
||||
|
||||
## `<Outlet/>`
|
||||
|
||||
Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick at at the end of the parent component” is not a great answer.
|
||||
|
||||
Instead, you tell a parent component where to render any nested components with an `<Outlet/>` component. The `<Outlet/>` simply renders one of two things:
|
||||
|
||||
- if there is no nested route that has been matched, it shows nothing
|
||||
- if there is a nested route that has been matched, it shows its `view`
|
||||
|
||||
That’s all! But it’s important to know and to remember, because it’s a common source of “Why isn’t this working?” frustration. If you don’t provide an `<Outlet/>`, the nested route won’t be displayed.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn ContactList(cx: Scope) -> impl IntoView {
|
||||
let contacts = todo!();
|
||||
|
||||
view! { cx,
|
||||
<div style="display: flex">
|
||||
// the contact list
|
||||
<For each=contacts
|
||||
key=|contact| contact.id
|
||||
view=|cx, contact| todo!()
|
||||
>
|
||||
// the nested child, if any
|
||||
// don’t forget this!
|
||||
<Outlet/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Routing and Performance
|
||||
|
||||
All of this is nice, conceptually, but again—what’s the big deal?
|
||||
|
||||
Performance.
|
||||
|
||||
In a fine-grained reactive library like Leptos, it’s always important to do the least amount of rendering work you can. Because we’re working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.
|
||||
|
||||
Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the `<ContactList/>` should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of `<ContactList/>`, navigating from Greg to Alice to Bob won’t clear the search.
|
||||
|
||||
In fact, in this case, we don’t even need to rerender the `<Contact/>` component when moving between contacts. The router will just reactively update the `:id` parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, we’ll update single text nodes to change the contact’s name, address, and so on, without doing _any_ additional rerendering.
|
||||
|
||||
> This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/17_nested_routing.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/router/17_nested_routing.html">
|
||||
|
||||
@@ -1,77 +1,2 @@
|
||||
# Params and Queries
|
||||
|
||||
Static paths are useful for distinguishing between different pages, but almost every application wants to pass data through the URL at some point.
|
||||
|
||||
There are two ways you can do this:
|
||||
|
||||
1. named route **params** like `id` in `/users/:id`
|
||||
2. named route **queries** like `q` in `/search?q=Foo`
|
||||
|
||||
Because of the way URLs are built, you can access the query from _any_ `<Route/>` view. You can access route params from the `<Route/>` that defines them or any of its nested children.
|
||||
|
||||
Accessing params and queries is pretty simple with a couple of hooks:
|
||||
|
||||
- [`use_query`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query.html) or [`use_query_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
- [`use_params`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_params.html) or [`use_params_map`](https://docs.rs/leptos_router/latest/leptos_router/fn.use_query_map.html)
|
||||
|
||||
Each of these comes with a typed option (`use_query` and `use_params`) and an untyped option (`use_query_map` and `use_params_map`).
|
||||
|
||||
The untyped versions hold a simple key-value map. To use the typed versions, derive the [`Params`](https://docs.rs/leptos_router/0.2.3/leptos_router/trait.Params.html) trait on a struct.
|
||||
|
||||
> `Params` is a very lightweight trait to convert a flat key-value map of strings into a struct by applying `FromStr` to each field. Because of the flat structure of route params and URL queries, it’s significantly less flexible than something like `serde`; it also adds much less weight to your binary.
|
||||
|
||||
```rust
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactParams {
|
||||
id: usize
|
||||
}
|
||||
|
||||
#[derive(Params)]
|
||||
struct ContactSearch {
|
||||
q: String
|
||||
}
|
||||
```
|
||||
|
||||
> Note: The `Params` derive macro is located at `leptos::Params`, and the `Params` trait is at `leptos_router::Params`. If you avoid using glob imports like `use leptos::*;`, make sure you’re importing the right one for the derive macro.
|
||||
|
||||
Now we can use them in a component. Imagine a URL that has both params and a query, like `/contacts/:id?q=Search`.
|
||||
|
||||
The typed versions return `Memo<Result<T>, _>`. It’s a Memo so it reacts to changes in the URL. It’s a `Result` because the params or query need to be parsed from the URL, and may or may not be valid.
|
||||
|
||||
```rust
|
||||
let params = use_params::<ContactParams>(cx);
|
||||
let query = use_query::<ContactSearch>(cx);
|
||||
|
||||
// id: || -> usize
|
||||
let id = move || {
|
||||
params.with(|params| {
|
||||
params
|
||||
.map(|params| params.id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
The untyped versions return `Memo<ParamsMap>`. Again, it’s memo to react to changes in the URL. [`ParamsMap`](https://docs.rs/leptos_router/0.2.3/leptos_router/struct.ParamsMap.html) behaves a lot like any other map type, with a `.get()` method that returns `Option<&String>`.
|
||||
|
||||
```rust
|
||||
let params = use_params_map(cx);
|
||||
let query = use_query_map(cx);
|
||||
|
||||
// id: || -> Option<String>
|
||||
let id = move || {
|
||||
params.with(|params| params.get("id").cloned())
|
||||
};
|
||||
```
|
||||
|
||||
This can get a little messy: deriving a signal that wraps an `Option<_>` or `Result<_>` can involve a couple steps. But it’s worth doing this for two reasons:
|
||||
|
||||
1. It’s correct, i.e., it forces you to consider the cases, “What if the user doesn’t pass a value for this query field? What if they pass an invalid value?”
|
||||
2. It’s performant. Specifically, when you navigate between different paths that match the same `<Route/>` with only params or the query changing, you can get fine-grained updates to different parts of your app without rerendering. For example, navigating between different contacts in our contact-list example does a targeted update to the name field (and eventually contact info) without needing to replacing or rerender the wrapping `<Contact/>`. This is what fine-grained reactivity is for.
|
||||
|
||||
> This is the same example from the previous section. The router is such an integrated system that it makes sense to provide a single example highlighting multiple features, even if we haven’t explain them all yet.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/18_params_and_queries.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/router/18_params_and_queries.html">
|
||||
|
||||
@@ -1,21 +1,2 @@
|
||||
# The `<A/>` Component
|
||||
|
||||
Client-side navigation works perfectly fine with ordinary HTML `<a>` elements. The router adds a listener that handles every click on a `<a>` element and tries to handle it on the client side, i.e., without doing another round trip to the server to request HTML. This is what enables the snappy “single-page app” navigations you’re probably familiar with from most modern web apps.
|
||||
|
||||
The router will bail out of handling an `<a>` click under a number of situations
|
||||
|
||||
- the click event has had `prevent_default()` called on it
|
||||
- the <kbd>Meta</kbd>, <kbd>Alt</kbd>, <kbd>Ctrl</kbd>, or <kbd>Shift</kbd> keys were held during click
|
||||
- the `<a>` has a `target` or `download` attribute, or `rel="external"`
|
||||
- the link has a different origin from the current location
|
||||
|
||||
In other words, the router will only try to do a client-side navigation when it’s pretty sure it can handle it, and it will upgrade every `<a>` element to get this special behavior.
|
||||
|
||||
The router also provides an [`<A>`](https://docs.rs/leptos_router/latest/leptos_router/fn.A.html) component, which does two additional things:
|
||||
|
||||
1. Correctly resolves relative nested routes. Relative routing with ordinary `<a>` tags can be tricky. For example, if you have a route like `/post/:id`, `<A href="1">` will generate the correct relative route, but `<a href="1">` likely will not (depending on where it appears in your view.) `<A/>` resolves routes relative to the path of the nested route within which it appears.
|
||||
2. Sets the `aria-current` attribute to `page` if this link is the active link (i.e., it’s a link to the page you’re on). This is helpful for accessibility and for styling. For example, if you want to set the link a different color if it’s a link to the page you’re currently on, you can match this attribute with a CSS selector.
|
||||
|
||||
> Once again, this is the same example. Check out the relative `<A/>` components, and take a look at the CSS in `index.html` to see the ARIA-based styling.
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/16-router-fy4tjv?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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/19_a.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/router/19_a.html">
|
||||
|
||||
2
docs/book/src/router/20_form.md
Normal file
2
docs/book/src/router/20_form.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/20_form.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/router/20_form.html">
|
||||
@@ -1,23 +1,2 @@
|
||||
# Routing
|
||||
|
||||
## The Basics
|
||||
|
||||
Routing drives most websites. A router is the answer to the question, “Given this URL, what should appear on the page?”
|
||||
|
||||
A URL consists of many parts. For example, the URL `https://leptos.dev/blog/search?q=Search#results` consists of
|
||||
|
||||
- a _scheme_: `https`
|
||||
- a _domain_: `leptos.dev`
|
||||
- a **path**: `/blog/search`
|
||||
- a **query** (or **search**): `?q=Search`
|
||||
- a _hash_: `#results`
|
||||
|
||||
The Leptos Router works with the path and query (`/blog/search?q=Search`). Given this piece of the URL, what should the app render on the page?
|
||||
|
||||
## The Philosophy
|
||||
|
||||
In most cases, the path should drive what is displayed on the page. From the user’s perspective, for most applications, most major changes in the state of the app should be reflected in the URL. If you copy and paste the URL and open it in another tab, you should find yourself more or less in the same place.
|
||||
|
||||
In this sense, the router is really at the heart of the global state management for your application. More than anything else, it drives what is displayed on the page.
|
||||
|
||||
The router handles most of this work for you by mapping the current location to particular components.
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/router/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/router/index.html">
|
||||
|
||||
2
docs/book/src/server/25_server_functions.md
Normal file
2
docs/book/src/server/25_server_functions.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/25_server_functions.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/server/25_server_functions.html">
|
||||
2
docs/book/src/server/26_extractors.md
Normal file
2
docs/book/src/server/26_extractors.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/26_extractors.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/server/26_extractors.html">
|
||||
2
docs/book/src/server/27_response.md
Normal file
2
docs/book/src/server/27_response.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/27_response.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/server/27_response.html">
|
||||
2
docs/book/src/server/README.md
Normal file
2
docs/book/src/server/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/server/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/server/index.html">
|
||||
2
docs/book/src/ssr/21_cargo_leptos.md
Normal file
2
docs/book/src/ssr/21_cargo_leptos.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/21_cargo_leptos.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/ssr/21_cargo_leptos.html">
|
||||
2
docs/book/src/ssr/22_life_cycle.md
Normal file
2
docs/book/src/ssr/22_life_cycle.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/22_life_cycle.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/ssr/22_life_cycle.html">
|
||||
2
docs/book/src/ssr/23_ssr_modes.md
Normal file
2
docs/book/src/ssr/23_ssr_modes.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/23_ssr_modes.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/ssr/23_ssr_modes.html">
|
||||
2
docs/book/src/ssr/24_hydration_bugs.md
Normal file
2
docs/book/src/ssr/24_hydration_bugs.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/24_hydration_bugs.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/ssr/24_hydration_bugs.html">
|
||||
2
docs/book/src/ssr/README.md
Normal file
2
docs/book/src/ssr/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/ssr/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/ssr/index.html">
|
||||
@@ -1,180 +1,2 @@
|
||||
# Testing Your Components
|
||||
|
||||
Testing user interfaces can be relatively tricky, but really important. This article
|
||||
will discuss a couple principles and approaches for testing a Leptos app.
|
||||
|
||||
## 1. Test business logic with ordinary Rust tests
|
||||
|
||||
In many cases, it makes sense to pull the logic out of your components and test
|
||||
it separately. For some simple components, there’s no particular logic to test, but
|
||||
for many it’s worth using a testable wrapping type and implementing the logic in
|
||||
ordinary Rust `impl` blocks.
|
||||
|
||||
For example, instead of embedding logic in a component directly like this:
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, vec![Todo { /* ... */ }]);
|
||||
// ⚠️ this is hard to test because it's embedded in the component
|
||||
let num_remaining = move || todos.with(|todos| {
|
||||
todos.iter().filter(|todo| !todo.completed).sum()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
You could pull that logic out into a separate data structure and test it:
|
||||
|
||||
```rust
|
||||
pub struct Todos(Vec<Todo>);
|
||||
|
||||
impl Todos {
|
||||
pub fn num_remaining(&self) -> usize {
|
||||
todos.iter().filter(|todo| !todo.completed).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_remaining {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> impl IntoView {
|
||||
let (todos, set_todos) = create_signal(cx, Todos(vec![Todo { /* ... */ }]));
|
||||
// ✅ this has a test associated with it
|
||||
let num_remaining = move || todos.with(Todos::num_remaining);
|
||||
}
|
||||
```
|
||||
|
||||
In general, the less of your logic is wrapped into your components themselves, the
|
||||
more idiomatic your code will feel and the easier it will be to test.
|
||||
|
||||
## 2. Test components with `wasm-bindgen-test`
|
||||
|
||||
[`wasm-bindgen-test`](https://crates.io/crates/wasm-bindgen-test) is a great utility
|
||||
for integrating or end-to-end testing WebAssembly apps in a headless browser.
|
||||
|
||||
To use this testing utility, you need to add `wasm-bindgen-test` to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
```
|
||||
|
||||
You should create tests in a separate `tests` directory. You can then run your tests in the browser of your choice:
|
||||
|
||||
```bash
|
||||
wasm-pack test --firefox
|
||||
```
|
||||
|
||||
> To see the full setup, check out the tests for the [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) example.
|
||||
|
||||
### Writing Your Tests
|
||||
|
||||
Most tests will involve some combination of vanilla DOM manipulation and comparison to a `view`. For example, here’s a test [for the
|
||||
`counter` example](https://github.com/leptos-rs/leptos/blob/main/examples/counter/tests/mod.rs).
|
||||
|
||||
First, we set up the testing environment.
|
||||
|
||||
```rust
|
||||
use wasm_bindgen_test::*;
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
// tell the test runner to run tests in the browser
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
```
|
||||
|
||||
I’m going to create a simpler wrapper for each test case, and mount it there.
|
||||
This makes it easy to encapsulate the test results.
|
||||
|
||||
```rust
|
||||
// like marking a regular test with #[test]
|
||||
#[wasm_bindgen_test]
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
// start by rendering our counter and mounting it to the DOM
|
||||
// note that we start at the initial value of 10
|
||||
mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
```
|
||||
|
||||
We’ll use some manual DOM operations to grab the `<div>` that wraps
|
||||
the whole component, as well as the `clear` button.
|
||||
|
||||
```rust
|
||||
// now we extract the buttons by iterating over the DOM
|
||||
// this would be easier if they had IDs
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = test_wrapper
|
||||
.query_selector("button")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::HtmlElement>();
|
||||
```
|
||||
|
||||
Now we can use ordinary DOM APIs to simulate user interaction.
|
||||
|
||||
```rust
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
```
|
||||
|
||||
You can test individual DOM element attributes or text node values. Sometimes
|
||||
I like to test the whole view at once. We can do this by testing the element’s
|
||||
`outerHTML` against our expectations.
|
||||
|
||||
```rust
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system to render the test case
|
||||
run_scope(create_runtime(), |cx| {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, set_value) = create_signal(cx, 0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! { cx,
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
// the view returned an HtmlElement<Div>, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html()
|
||||
.outer_html()
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
That test involved us manually replicating the `view` that’s inside the component.
|
||||
There's actually an easier way to do this... We can just test against a `<SimpleCounter/>`
|
||||
with the initial value `0`. This is where our wrapping element comes in: I’ll just test
|
||||
the wrapper’s `innerHTML` against another comparison case.
|
||||
|
||||
```rust
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|cx| view! { cx, <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
This is only a very limited introduction to testing. But I hope it’s useful as you begin to build applications.
|
||||
|
||||
> For more, see [the testing section of the `wasm-bindgen` guide](https://rustwasm.github.io/wasm-bindgen/wasm-bindgen-test/index.html#testing-on-wasm32-unknown-unknown-with-wasm-bindgen-test).
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/testing.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/testing.html">
|
||||
|
||||
@@ -1,143 +1,2 @@
|
||||
# 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 reruns 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`, reruns,
|
||||
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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/01_basic_component.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/01_basic_component.html">
|
||||
|
||||
@@ -1,104 +1,2 @@
|
||||
# `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() % 2 == 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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/02_dynamic_attributes.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/02_dynamic_attributes.html">
|
||||
|
||||
@@ -1,317 +1,2 @@
|
||||
# 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=count
|
||||
/>
|
||||
<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 + 'static,
|
||||
{
|
||||
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 + 'static,`, 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 props,
|
||||
which allows you to easily pass props with different values.
|
||||
|
||||
In this case, it’s helpful to know about the
|
||||
[`Signal`](https://docs.rs/leptos/latest/leptos/struct.Signal.html) type. `Signal`
|
||||
is an 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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/03_components.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/03_components.html">
|
||||
|
||||
@@ -1,88 +1,2 @@
|
||||
# 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 words, 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="1000px"></iframe>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/04_iteration.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/04_iteration.html">
|
||||
|
||||
2
docs/book/src/view/04b_iteration.md
Normal file
2
docs/book/src/view/04b_iteration.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/04b_iteration.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/04b_iteration.html">
|
||||
@@ -1,107 +1,2 @@
|
||||
# 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<Input> = create_node_ref(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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/05_forms.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/05_forms.html">
|
||||
|
||||
@@ -1,285 +1,2 @@
|
||||
# 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 rerun
|
||||
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.
|
||||
|
||||
### `<Show/>`
|
||||
|
||||
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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/06_control_flow.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/06_control_flow.html">
|
||||
|
||||
@@ -1,113 +1,2 @@
|
||||
# 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.get()
|
||||
.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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/07_errors.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/07_errors.html">
|
||||
|
||||
@@ -1,288 +1,2 @@
|
||||
# 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>
|
||||
</header>
|
||||
<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>
|
||||
</header>
|
||||
<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>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/08_parent_child.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/08_parent_child.html">
|
||||
|
||||
@@ -1,126 +1,2 @@
|
||||
# Component Children
|
||||
|
||||
It’s pretty common to want to pass children into a component, just as you can pass
|
||||
children into an HTML element. For example, imagine I have a `<FancyForm/>` component
|
||||
that enhances an HTML `<form>`. I need some way to pass all its inputs.
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Form>
|
||||
<fieldset>
|
||||
<label>
|
||||
"Some Input"
|
||||
<input type="text" name="something"/>
|
||||
</label>
|
||||
</fieldset>
|
||||
<button>"Submit"</button>
|
||||
</Form>
|
||||
}
|
||||
```
|
||||
|
||||
How can you do this in Leptos? There are basically two ways to pass components to
|
||||
other components:
|
||||
|
||||
1. **render props**: properties that are functions that return a view
|
||||
2. the **`children`** prop: a special component property that includes anything
|
||||
you pass as a child to the component.
|
||||
|
||||
In fact, you’ve already seen these both in action in the [`<Show/>`](/view/06_control_flow.html#show) component:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<Show
|
||||
// `when` is a normal prop
|
||||
when=move || value() > 5
|
||||
// `fallback` is a "render prop": a function that returns a view
|
||||
fallback=|cx| view! { cx, <Small/> }
|
||||
>
|
||||
// `<Big/>` (and anything else here)
|
||||
// will be given to the `children` prop
|
||||
<Big/>
|
||||
</Show>
|
||||
}
|
||||
```
|
||||
|
||||
Let’s define a component that takes some children and a render prop.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn TakesChildren<F, IV>(
|
||||
cx: Scope,
|
||||
/// Takes a function (type F) that returns anything that can be
|
||||
/// converted into a View (type IV)
|
||||
render_prop: F,
|
||||
/// `children` takes the `Children` type
|
||||
children: Children,
|
||||
) -> impl IntoView
|
||||
where
|
||||
F: Fn() -> IV,
|
||||
IV: IntoView,
|
||||
{
|
||||
view! { cx,
|
||||
<h2>"Render Prop"</h2>
|
||||
{render_prop()}
|
||||
|
||||
<h2>"Children"</h2>
|
||||
{children(cx)}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`render_prop` and `children` are both functions, so we can call them to generate
|
||||
the appropriate views. `children`, in particular, is an alias for
|
||||
`Box<dyn FnOnce(Scope) -> Fragment>`. (Aren't you glad we named it `Children` instead?)
|
||||
|
||||
> If you need a `Fn` or `FnMut` here because you need to call `children` more than once,
|
||||
> we also provide `ChildrenFn` and `ChildrenMut` aliases.
|
||||
|
||||
We can use the component like this:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<TakesChildren render_prop=|| view! { cx, <p>"Hi, there!"</p> }>
|
||||
// these get passed to `children`
|
||||
"Some text"
|
||||
<span>"A span"</span>
|
||||
</TakesChildren>
|
||||
}
|
||||
```
|
||||
|
||||
## Manipulating Children
|
||||
|
||||
The [`Fragment`](https://docs.rs/leptos/latest/leptos/struct.Fragment.html) type is
|
||||
basically a way of wrapping a `Vec<View>`. You can insert it anywhere into your view.
|
||||
|
||||
But you can also access those inner views directly to manipulate them. For example, here’s
|
||||
a component that takes its children and turns them into an unordered list.
|
||||
|
||||
```rust
|
||||
#[component]
|
||||
pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
|
||||
// Fragment has `nodes` field that contains a Vec<View>
|
||||
let children = children(cx)
|
||||
.nodes
|
||||
.into_iter()
|
||||
.map(|child| view! { cx, <li>{child}</li> })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
view! { cx,
|
||||
<ul>{children}</ul>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Calling it like this will create a list:
|
||||
|
||||
```rust
|
||||
view! { cx,
|
||||
<WrappedChildren>
|
||||
"A"
|
||||
"B"
|
||||
"C"
|
||||
</WrappedChildren>
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://codesandbox.io/p/sandbox/9-component-children-2wrdfd?file=%2Fsrc%2Fmain.rs&selection=%5B%7B%22endColumn%22%3A12%2C%22endLineNumber%22%3A19%2C%22startColumn%22%3A12%2C%22startLineNumber%22%3A19%7D%5D" width="100%" height="1000px"></iframe>
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/09_component_children.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/09_component_children.html">
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
# 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.
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/index.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/index.html">
|
||||
|
||||
2
docs/book/src/view/builder.md
Normal file
2
docs/book/src/view/builder.md
Normal file
@@ -0,0 +1,2 @@
|
||||
<meta http-equiv="refresh" content="0; URL=https://book.leptos.dev/view/builder.html">
|
||||
<link rel="canonical" href="https://book.leptos.dev/view/builder.html">
|
||||
BIN
docs/video/async.mov
Normal file
BIN
docs/video/async.mov
Normal file
Binary file not shown.
BIN
docs/video/in-order.mov
Normal file
BIN
docs/video/in-order.mov
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user