mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 11:21:55 -05:00
Compare commits
726 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67ccd37336 | ||
|
|
3b1b2e2dcc | ||
|
|
7831e4ad05 | ||
|
|
e7bb859cd9 | ||
|
|
9fc26e609c | ||
|
|
4f1ee65e6c | ||
|
|
ceff827a77 | ||
|
|
a7db918775 | ||
|
|
be20ecd366 | ||
|
|
5790d8ad12 | ||
|
|
7dc58e248c | ||
|
|
7b03e63b23 | ||
|
|
55fd7c6421 | ||
|
|
ba1ea4c2bb | ||
|
|
6a4fc96835 | ||
|
|
58476bb98e | ||
|
|
88d4f14541 | ||
|
|
6bba233ba7 | ||
|
|
1d99764740 | ||
|
|
53cc479c14 | ||
|
|
d3707d9b88 | ||
|
|
1df4076fd2 | ||
|
|
28337bb6c9 | ||
|
|
c5dea52e69 | ||
|
|
d5096ff2e6 | ||
|
|
84734f1110 | ||
|
|
7094308287 | ||
|
|
3b88c8ccd2 | ||
|
|
b932bd5e04 | ||
|
|
a0638b786c | ||
|
|
1033133d3a | ||
|
|
ba40560ad7 | ||
|
|
78dc8b4410 | ||
|
|
c6bf525270 | ||
|
|
fcdfd617f5 | ||
|
|
d9c8d4ff66 | ||
|
|
621f112f4c | ||
|
|
cfe2341dec | ||
|
|
5ed2cc9596 | ||
|
|
47b07b0143 | ||
|
|
0793d56648 | ||
|
|
2dde9738b2 | ||
|
|
1b276e1e02 | ||
|
|
9771619b0d | ||
|
|
382a36406c | ||
|
|
a898d3f2f7 | ||
|
|
951f4a73ee | ||
|
|
2a26a648ba | ||
|
|
66d7cb5d12 | ||
|
|
aa49ad760b | ||
|
|
3469e9335c | ||
|
|
62408d9202 | ||
|
|
db1c15f4e4 | ||
|
|
3f751551a8 | ||
|
|
2470036f57 | ||
|
|
e01dfbf497 | ||
|
|
49c1661f92 | ||
|
|
b9dfd9a5ae | ||
|
|
17a150b3bf | ||
|
|
c860f524ad | ||
|
|
20af4928b2 | ||
|
|
7b62ad44d2 | ||
|
|
5657abc07d | ||
|
|
9fc351ceac | ||
|
|
893d47f1c5 | ||
|
|
3c2a2304e4 | ||
|
|
3de4b7b982 | ||
|
|
36957cb569 | ||
|
|
27dbadb7d2 | ||
|
|
84590b98ed | ||
|
|
0222182286 | ||
|
|
7ed4d08dab | ||
|
|
4fa2e58551 | ||
|
|
5fe58369f5 | ||
|
|
10860ebb1e | ||
|
|
887eb99cf6 | ||
|
|
9f99571b28 | ||
|
|
b3e2040ec9 | ||
|
|
693861434c | ||
|
|
d1248d3faf | ||
|
|
f1fae63064 | ||
|
|
f7053ac960 | ||
|
|
8670365594 | ||
|
|
e0cc6fd7b9 | ||
|
|
84457b45ff | ||
|
|
8c959d3e24 | ||
|
|
288da232ac | ||
|
|
ef52e8620b | ||
|
|
a8c5ce2722 | ||
|
|
a40ba9a39e | ||
|
|
83a110fb85 | ||
|
|
5deedf721e | ||
|
|
0ae67cf122 | ||
|
|
97e0222061 | ||
|
|
a7aedee4fd | ||
|
|
f180941b7b | ||
|
|
83cb3cb764 | ||
|
|
f174688974 | ||
|
|
1f4c410f78 | ||
|
|
3dbedfc871 | ||
|
|
026152d20e | ||
|
|
c70009243a | ||
|
|
dcdad73476 | ||
|
|
42a8e49e1a | ||
|
|
9ba894764e | ||
|
|
1d97494e19 | ||
|
|
982bfd8011 | ||
|
|
a0ad927097 | ||
|
|
4845459511 | ||
|
|
54e4205541 | ||
|
|
25f0186098 | ||
|
|
00fb8f29d3 | ||
|
|
82ea4eb7ce | ||
|
|
34382c0c23 | ||
|
|
605e4b1eec | ||
|
|
71ca02a432 | ||
|
|
ac751dd0d3 | ||
|
|
114df8797c | ||
|
|
35b457aa82 | ||
|
|
c43b71d3fa | ||
|
|
5ad17d6b6c | ||
|
|
805be42f7a | ||
|
|
bb63a1521e | ||
|
|
05cd1bc6f0 | ||
|
|
84ac64284c | ||
|
|
8beb988ecc | ||
|
|
05999069b7 | ||
|
|
dbb4c79d9c | ||
|
|
d1d6126259 | ||
|
|
0e979a0767 | ||
|
|
3a4ad07a91 | ||
|
|
f2f35cd785 | ||
|
|
c953432659 | ||
|
|
27f2e60d16 | ||
|
|
9330cf23b1 | ||
|
|
04f5207457 | ||
|
|
c88409a333 | ||
|
|
a2d3d9431c | ||
|
|
213365e4e9 | ||
|
|
ab3e94dafa | ||
|
|
3e05b5bcb4 | ||
|
|
632ce0f401 | ||
|
|
93c893f4b3 | ||
|
|
4fa72a94fb | ||
|
|
24775fb59b | ||
|
|
6c749f5e24 | ||
|
|
c22f20ac28 | ||
|
|
11011c2bda | ||
|
|
a51faea9a9 | ||
|
|
dcc43e574b | ||
|
|
6cc0604497 | ||
|
|
8c375534bb | ||
|
|
f2d6375d93 | ||
|
|
5fb80aaa40 | ||
|
|
29b0dca1d8 | ||
|
|
a6a65ba562 | ||
|
|
260c624461 | ||
|
|
0f50aced26 | ||
|
|
1aa2752842 | ||
|
|
b92bfa4ea7 | ||
|
|
30cf1167f2 | ||
|
|
1c05389707 | ||
|
|
1534dd5261 | ||
|
|
8b9685e01d | ||
|
|
d9b590b8e0 | ||
|
|
25bfc27544 | ||
|
|
89bbdc58af | ||
|
|
4fb00e29d6 | ||
|
|
54401c6f69 | ||
|
|
6a705e2a21 | ||
|
|
43421c56d5 | ||
|
|
4c4d3dcfa3 | ||
|
|
55053da00c | ||
|
|
be83b5f27e | ||
|
|
a4ce79769a | ||
|
|
e3f64188c2 | ||
|
|
f7abe727d9 | ||
|
|
eb29d84169 | ||
|
|
200047a8bc | ||
|
|
634ac1c4a3 | ||
|
|
efe832e39a | ||
|
|
1f2b13a976 | ||
|
|
d4ec5e187b | ||
|
|
4fe7fe725f | ||
|
|
a1ca8549a1 | ||
|
|
e7a8067f9b | ||
|
|
6be090079f | ||
|
|
8635887ca7 | ||
|
|
e3482b433b | ||
|
|
e6c2f8c614 | ||
|
|
28fcfe4a46 | ||
|
|
75336bc265 | ||
|
|
f4f129caaf | ||
|
|
873aec5787 | ||
|
|
a2385e4c42 | ||
|
|
d24f97b59f | ||
|
|
51f368c5c5 | ||
|
|
4107203da2 | ||
|
|
efb699a319 | ||
|
|
93e6456e19 | ||
|
|
b24ae7a5e3 | ||
|
|
bf8d2e079c | ||
|
|
7752ab78e3 | ||
|
|
64bc2580ff | ||
|
|
ddb596feb5 | ||
|
|
dac4589194 | ||
|
|
8f0a8e05b4 | ||
|
|
05d01141c5 | ||
|
|
66d6038f2d | ||
|
|
3b09312e1a | ||
|
|
62cb361031 | ||
|
|
04c67cb8b6 | ||
|
|
efd060c955 | ||
|
|
6290c42159 | ||
|
|
0a89f151be | ||
|
|
c72c2f4803 | ||
|
|
c771ab7e71 | ||
|
|
2c4f11b238 | ||
|
|
12a9e06c5e | ||
|
|
3515469835 | ||
|
|
e5c159f7a5 | ||
|
|
6590749956 | ||
|
|
db8f5e4899 | ||
|
|
6ca3639c3e | ||
|
|
37db7b5d0a | ||
|
|
8ac1564b90 | ||
|
|
e2721d53bd | ||
|
|
e1f3be6416 | ||
|
|
9536480739 | ||
|
|
5d3a1752c4 | ||
|
|
4b539b524b | ||
|
|
67fe4cc540 | ||
|
|
fa731d5018 | ||
|
|
ccf6703274 | ||
|
|
504c958001 | ||
|
|
f7b16b726b | ||
|
|
e9c7b50dfd | ||
|
|
208ab97867 | ||
|
|
4a0f173bb5 | ||
|
|
0cf3113812 | ||
|
|
87f9fa23d5 | ||
|
|
746bf8e453 | ||
|
|
8b9bcffbb9 | ||
|
|
dc80e387e3 | ||
|
|
2006eca1a0 | ||
|
|
1dae77d6b4 | ||
|
|
2f58191a56 | ||
|
|
a68653b385 | ||
|
|
d7ca969848 | ||
|
|
fd48a61eef | ||
|
|
52a3f84de5 | ||
|
|
f8283f4674 | ||
|
|
989f2989fa | ||
|
|
33a3708f91 | ||
|
|
4eea1f046d | ||
|
|
8f46288973 | ||
|
|
059c8abd2f | ||
|
|
6885777c75 | ||
|
|
ddc7abf081 | ||
|
|
180511e9bb | ||
|
|
381ff8a7b0 | ||
|
|
3ed1ad7b7f | ||
|
|
2ccf5e99a9 | ||
|
|
055701ebf6 | ||
|
|
88af893703 | ||
|
|
ce4fe632a2 | ||
|
|
c76208aad0 | ||
|
|
514c51ca30 | ||
|
|
7e3781b5dd | ||
|
|
97dc3cc2e5 | ||
|
|
61e51cbe1c | ||
|
|
efa6d603f9 | ||
|
|
da045f7358 | ||
|
|
8502745036 | ||
|
|
0a5e6fd85a | ||
|
|
64fc6cd514 | ||
|
|
a2d8fde8cf | ||
|
|
44eae4c2ed | ||
|
|
38d51b01d7 | ||
|
|
61876dff10 | ||
|
|
c676cf921d | ||
|
|
fc59cdaf61 | ||
|
|
081f4ec550 | ||
|
|
598c59b9c2 | ||
|
|
9de6c5bb4a | ||
|
|
f65eaec9ba | ||
|
|
95756aa2f7 | ||
|
|
fd121fd8c1 | ||
|
|
c1877354f0 | ||
|
|
be92dc56aa | ||
|
|
165a593b32 | ||
|
|
18b33c7606 | ||
|
|
d2ee093132 | ||
|
|
83e0438527 | ||
|
|
095dc78893 | ||
|
|
3ebea79e05 | ||
|
|
fe7c7c3a99 | ||
|
|
8b142c72f0 | ||
|
|
70655b57b1 | ||
|
|
c6192badfb | ||
|
|
5b7f5e3db3 | ||
|
|
ae14644806 | ||
|
|
7ca810d8bd | ||
|
|
04e09d2005 | ||
|
|
2916873985 | ||
|
|
2a558aa3f0 | ||
|
|
36d16d9253 | ||
|
|
722fd0f6c2 | ||
|
|
a42e371e79 | ||
|
|
11119144d2 | ||
|
|
a0b158f016 | ||
|
|
8dc7338b85 | ||
|
|
737949cff6 | ||
|
|
d7e17a2ec9 | ||
|
|
7c5b7fcbb1 | ||
|
|
1182aff410 | ||
|
|
bdcd4cb1cc | ||
|
|
c74a791d9f | ||
|
|
772a837050 | ||
|
|
92552deb0d | ||
|
|
417d345b83 | ||
|
|
3fb2d49d89 | ||
|
|
27feaf4309 | ||
|
|
35f489a52e | ||
|
|
ba8bd2bc82 | ||
|
|
76506c03e1 | ||
|
|
4323e30133 | ||
|
|
81c0947ce5 | ||
|
|
309a3d504a | ||
|
|
2a236e043a | ||
|
|
63f8da2fb5 | ||
|
|
c9e32b66bf | ||
|
|
a32c71539d | ||
|
|
f7ee0c4764 | ||
|
|
7034375cdd | ||
|
|
3c9c5aaf83 | ||
|
|
ce832cef21 | ||
|
|
10230d6d65 | ||
|
|
e4d25608df | ||
|
|
bd1601e892 | ||
|
|
602ac60a85 | ||
|
|
9e4c0b86f2 | ||
|
|
4a80c8b65b | ||
|
|
f191bb8324 | ||
|
|
1ff1d48e6e | ||
|
|
df6a4628c3 | ||
|
|
e28e5ceb1e | ||
|
|
e69f62b939 | ||
|
|
2c48b07186 | ||
|
|
0d867ba016 | ||
|
|
3f83ad7dda | ||
|
|
50403846c9 | ||
|
|
4ead16e5d3 | ||
|
|
32f77cc42b | ||
|
|
d8834a0423 | ||
|
|
0d665c9c05 | ||
|
|
a03d74494d | ||
|
|
131c83e28e | ||
|
|
6d93185478 | ||
|
|
202abd1d35 | ||
|
|
a50c6b0140 | ||
|
|
705ea3a3bb | ||
|
|
cb788758df | ||
|
|
1afdc4fe1e | ||
|
|
3382047857 | ||
|
|
a29ffc8dcb | ||
|
|
a18dd6dfd7 | ||
|
|
626bcdc9ae | ||
|
|
d6dce76725 | ||
|
|
36272a0b1b | ||
|
|
96c956efdf | ||
|
|
29cf1f4814 | ||
|
|
39c3a63787 | ||
|
|
068865b7de | ||
|
|
fa8bb15a67 | ||
|
|
faa481f2b6 | ||
|
|
b41d988865 | ||
|
|
025c28b489 | ||
|
|
0c7c7c9b38 | ||
|
|
b109c3e9a3 | ||
|
|
0a559935e7 | ||
|
|
bccc05fec8 | ||
|
|
e0f98dc0fd | ||
|
|
5d9bd8f913 | ||
|
|
0a41ae9a5e | ||
|
|
fbc6be922d | ||
|
|
b5551863fe | ||
|
|
14b3877293 | ||
|
|
98ea18009d | ||
|
|
d133cff092 | ||
|
|
48028b476a | ||
|
|
404ad50bd3 | ||
|
|
b89fbe027b | ||
|
|
0ba53afa08 | ||
|
|
c384b53a0f | ||
|
|
2f53e09bb6 | ||
|
|
319eefb169 | ||
|
|
949f43d145 | ||
|
|
a47759007f | ||
|
|
095faf15b1 | ||
|
|
f9eb562050 | ||
|
|
7f57b88e8d | ||
|
|
8a8862be9e | ||
|
|
619dc59e1d | ||
|
|
5f49504137 | ||
|
|
ca68fa5a3d | ||
|
|
e6a472b467 | ||
|
|
f8da9e30e0 | ||
|
|
984ede8887 | ||
|
|
c3656416a2 | ||
|
|
7ecfbd9109 | ||
|
|
531c39759a | ||
|
|
f5d06577f4 | ||
|
|
39902d1e66 | ||
|
|
7def5f65ed | ||
|
|
6b60d48203 | ||
|
|
9ef51166d3 | ||
|
|
8da6bbc3be | ||
|
|
3c39674622 | ||
|
|
914b07491e | ||
|
|
1d2d11b83d | ||
|
|
07e878adf7 | ||
|
|
f32d43ce94 | ||
|
|
65e3c57ed1 | ||
|
|
2e40bace88 | ||
|
|
b9945e0ce1 | ||
|
|
d7f70214b9 | ||
|
|
adf57f5771 | ||
|
|
bae79e2b2c | ||
|
|
e2b1210461 | ||
|
|
7c24b7482d | ||
|
|
25c66a4624 | ||
|
|
71ddacef8e | ||
|
|
338b01bee3 | ||
|
|
a36f22e439 | ||
|
|
56977411f2 | ||
|
|
0fc47e3a35 | ||
|
|
caf797dba0 | ||
|
|
2f54d937a1 | ||
|
|
40c1f38a07 | ||
|
|
402d6297f4 | ||
|
|
93734a5222 | ||
|
|
770d02d8e6 | ||
|
|
e275862a20 | ||
|
|
17f1d25d03 | ||
|
|
0a99a378aa | ||
|
|
14b7073863 | ||
|
|
4e4deef144 | ||
|
|
c360f0ed0d | ||
|
|
88ab9693db | ||
|
|
6dfea0b0a2 | ||
|
|
9fd881603f | ||
|
|
9666c9c0c5 | ||
|
|
9e8b304b8a | ||
|
|
064ccce5b1 | ||
|
|
2e31177f62 | ||
|
|
4215cef04b | ||
|
|
de3dd3c296 | ||
|
|
846ff2fefb | ||
|
|
6003212f6e | ||
|
|
054cff7883 | ||
|
|
ce5738d7c4 | ||
|
|
47331b5c8d | ||
|
|
a6cee3b1e9 | ||
|
|
43c0e384c4 | ||
|
|
db654cbfda | ||
|
|
e13b1561d8 | ||
|
|
02f76dec35 | ||
|
|
4bd99a41e5 | ||
|
|
85d29a5af5 | ||
|
|
4d54574f9e | ||
|
|
f6c7ac473a | ||
|
|
747d847183 | ||
|
|
8dd63a402b | ||
|
|
694eccbadc | ||
|
|
24f2e71563 | ||
|
|
1766bfedb9 | ||
|
|
242d35cc37 | ||
|
|
85b9f87620 | ||
|
|
db33bc2e61 | ||
|
|
a1329ea044 | ||
|
|
050bf8f821 | ||
|
|
1a68743fcc | ||
|
|
2925db8676 | ||
|
|
13d5f12d7f | ||
|
|
3d9c295613 | ||
|
|
b2c0068e2c | ||
|
|
94a3f7c092 | ||
|
|
330dcfeb7c | ||
|
|
f7bbec5f06 | ||
|
|
8815529955 | ||
|
|
12db58a7e0 | ||
|
|
83c9edde26 | ||
|
|
2037a6966a | ||
|
|
4f041f5a5e | ||
|
|
6467e067ef | ||
|
|
3814879d80 | ||
|
|
5e16ae6a26 | ||
|
|
6d474713f6 | ||
|
|
0d47399424 | ||
|
|
ae254836d7 | ||
|
|
2dd5efc5d0 | ||
|
|
15eeda9c7a | ||
|
|
1a739015e1 | ||
|
|
8385287123 | ||
|
|
c4aa3ba1ba | ||
|
|
ce5f2c81ed | ||
|
|
941689fc5b | ||
|
|
961bf89a8b | ||
|
|
d360cc280f | ||
|
|
bb7bb8f4c2 | ||
|
|
b29b8fb5ff | ||
|
|
4ffa3c46b6 | ||
|
|
32294d6cab | ||
|
|
46d286755e | ||
|
|
b936e0352f | ||
|
|
b5bd70ab94 | ||
|
|
0dd1932b7f | ||
|
|
f5d203f0c9 | ||
|
|
1bc0b414e3 | ||
|
|
d6e19c0a60 | ||
|
|
fc60d6b2d7 | ||
|
|
292e7c1f27 | ||
|
|
1da84db1aa | ||
|
|
535e3e3880 | ||
|
|
109244b28b | ||
|
|
fd048295a4 | ||
|
|
26cf4848db | ||
|
|
757a5c73c3 | ||
|
|
da496def16 | ||
|
|
3a755bd8c3 | ||
|
|
e514f7144d | ||
|
|
b881167b8f | ||
|
|
1e9d345831 | ||
|
|
7f7bba6ea3 | ||
|
|
015a4b63ec | ||
|
|
dcec7af4f3 | ||
|
|
5bc97654dc | ||
|
|
2788d93e96 | ||
|
|
604043b4d8 | ||
|
|
ab28c80593 | ||
|
|
49da073fed | ||
|
|
3629302f88 | ||
|
|
274e31018b | ||
|
|
802fcc5c2a | ||
|
|
da084a2ece | ||
|
|
d9f6836933 | ||
|
|
d8d2fdac5d | ||
|
|
9818e7cb68 | ||
|
|
986fbe5328 | ||
|
|
711175a760 | ||
|
|
00a536a5dc | ||
|
|
a7b1152910 | ||
|
|
cfba7a2797 | ||
|
|
cebe744a84 | ||
|
|
e93a34a2c9 | ||
|
|
9ec30d71d2 | ||
|
|
3c13280bf6 | ||
|
|
45fd9423f8 | ||
|
|
7a92208c4f | ||
|
|
89b972e3c5 | ||
|
|
8dac92b251 | ||
|
|
b24eaedfe9 | ||
|
|
4336051f78 | ||
|
|
97ce5adb8e | ||
|
|
20fb5454b0 | ||
|
|
aac607f338 | ||
|
|
738986415d | ||
|
|
3406446ebd | ||
|
|
21dd7e9c76 | ||
|
|
9bab4da172 | ||
|
|
420dccda60 | ||
|
|
53b22a9b74 | ||
|
|
e68730d15f | ||
|
|
11d134c4ba | ||
|
|
2239f04f6b | ||
|
|
78e5a7ebc3 | ||
|
|
0148d92f48 | ||
|
|
ab67bea7ec | ||
|
|
0beef3b2e0 | ||
|
|
81fc7e6ada | ||
|
|
75d6763f4e | ||
|
|
da4d2cf538 | ||
|
|
2470637b0b | ||
|
|
2934c295b5 | ||
|
|
789eef914d | ||
|
|
782cb93743 | ||
|
|
8642c563d8 | ||
|
|
755fbd3866 | ||
|
|
d83471e02b | ||
|
|
2dd2bb5958 | ||
|
|
12f2cec5c7 | ||
|
|
a41bf2784f | ||
|
|
ebdd31cd9f | ||
|
|
acec3bb313 | ||
|
|
464f157186 | ||
|
|
b53e4d8ff8 | ||
|
|
cd438e0bcf | ||
|
|
13da1e743d | ||
|
|
0c9167fd30 | ||
|
|
52da0e43ac | ||
|
|
dad91f5960 | ||
|
|
72e97047a5 | ||
|
|
883fd57fe1 | ||
|
|
42b99dd912 | ||
|
|
851e1f73fd | ||
|
|
e11eea1af1 | ||
|
|
f508cc4510 | ||
|
|
e4f3cf9cca | ||
|
|
60d883a26c | ||
|
|
add3be0ff5 | ||
|
|
a01640cafd | ||
|
|
e837e9fded | ||
|
|
e0e67360aa | ||
|
|
439deea066 | ||
|
|
e5f5710f46 | ||
|
|
8626db27d7 | ||
|
|
ec3f0933fe | ||
|
|
b50de3a005 | ||
|
|
aa878534ad | ||
|
|
603f9f96c4 | ||
|
|
f78e675506 | ||
|
|
cc2714c03d | ||
|
|
c06110128b | ||
|
|
d7c62622ae | ||
|
|
1edec6c36a | ||
|
|
c5049ca1bb | ||
|
|
f69dbb48ca | ||
|
|
5feaf1aea6 | ||
|
|
ec3ab6a355 | ||
|
|
100ed7d926 | ||
|
|
88b93f40f9 | ||
|
|
b8b77138ea | ||
|
|
20c29cab89 | ||
|
|
54fd74839a | ||
|
|
ea3790d91c | ||
|
|
f5935c6333 | ||
|
|
c8e5e1b16b | ||
|
|
a12c707f3f | ||
|
|
6d9906111d | ||
|
|
5ea314c998 | ||
|
|
2bc04444e1 | ||
|
|
b41fde3ff9 | ||
|
|
c29081b12a | ||
|
|
2fefc8b4bf | ||
|
|
72b43d1e2b | ||
|
|
39607adc94 | ||
|
|
30c1cd921b | ||
|
|
abfe3cabd2 | ||
|
|
16bd2942db | ||
|
|
13cccced06 | ||
|
|
db4c1cb4b3 | ||
|
|
9cdd8cac15 | ||
|
|
84ebdc1b92 | ||
|
|
9f02cc8cc1 | ||
|
|
c3b9932172 | ||
|
|
dbd9951a85 | ||
|
|
6eb8b44fff | ||
|
|
4fa31be5dc | ||
|
|
b46dffb729 | ||
|
|
ca54762806 | ||
|
|
f122f9109f | ||
|
|
f894d6e4f6 | ||
|
|
4cc925c950 | ||
|
|
21e53042e8 | ||
|
|
4d3fb37b35 | ||
|
|
d3a21c922d | ||
|
|
317f90e1e3 | ||
|
|
26869a78a0 | ||
|
|
f46f864f05 | ||
|
|
b21f1853c6 | ||
|
|
1454c5d272 | ||
|
|
c1f4616a31 | ||
|
|
a3c3478831 | ||
|
|
1ca8a9189c | ||
|
|
9e276a8879 | ||
|
|
53703f208a | ||
|
|
9a60b21a0a | ||
|
|
524ed395fa | ||
|
|
5bc8c4e0d3 | ||
|
|
7f7003f7f1 | ||
|
|
ddf2ac0cf7 | ||
|
|
992e2bce78 | ||
|
|
6c2469ec3a | ||
|
|
a7162d7907 | ||
|
|
f584154156 | ||
|
|
13464b10c9 | ||
|
|
696bf14d13 | ||
|
|
be92bab3e5 | ||
|
|
4bb2bc4797 | ||
|
|
a8adf8eea2 | ||
|
|
1a7da39fb7 | ||
|
|
201adb7406 | ||
|
|
4df42cbc60 | ||
|
|
44a0a0a93a | ||
|
|
66e1e6d7a1 | ||
|
|
8252c4a977 | ||
|
|
6a2eafcbc6 | ||
|
|
b49a13f8c1 | ||
|
|
0d5c67408f | ||
|
|
1eddd5a5f1 | ||
|
|
ca1e62c0b9 | ||
|
|
043cd7dc61 | ||
|
|
68486cfb72 | ||
|
|
eea971b9fe | ||
|
|
d726b56b71 | ||
|
|
0fddfb4823 | ||
|
|
17732a6e6a | ||
|
|
c8441f0f00 | ||
|
|
ff4cde0764 | ||
|
|
1d38439bd8 | ||
|
|
9ca1cba504 | ||
|
|
63dacdcc95 | ||
|
|
61f5294f67 | ||
|
|
0149632a4c | ||
|
|
96384ed116 | ||
|
|
f56023bb25 | ||
|
|
6bb5d58369 | ||
|
|
d50012f8d4 | ||
|
|
c9d4ea9307 | ||
|
|
77c74bccbb | ||
|
|
528d1eae65 | ||
|
|
5809c8f699 | ||
|
|
b9c620d4cd | ||
|
|
8c9dfd9c9d | ||
|
|
8848eb8b87 |
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "cargo"
|
||||
directories:
|
||||
- "/"
|
||||
- "/examples/*"
|
||||
- "/benchmarks"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
6
.github/workflows/ci-semver.yml
vendored
6
.github/workflows/ci-semver.yml
vendored
@@ -14,8 +14,8 @@ jobs:
|
||||
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true'
|
||||
name: Run semver check (nightly-2024-04-14)
|
||||
if: github.event.pull_request.labels[0].name == 'semver' # needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2024-08-01)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -25,4 +25,4 @@ jobs:
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
rust-toolchain: nightly-2024-04-14
|
||||
rust-toolchain: nightly-2024-08-01
|
||||
|
||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -20,6 +20,11 @@ jobs:
|
||||
matrix:
|
||||
directory:
|
||||
[
|
||||
any_error,
|
||||
any_spawner,
|
||||
const_str_slice_concat,
|
||||
either_of,
|
||||
hydration_context,
|
||||
integrations/actix,
|
||||
integrations/axum,
|
||||
integrations/utils,
|
||||
@@ -28,10 +33,14 @@ jobs:
|
||||
leptos_dom,
|
||||
leptos_hot_reload,
|
||||
leptos_macro,
|
||||
leptos_reactive,
|
||||
leptos_server,
|
||||
meta,
|
||||
next_tuple,
|
||||
oco,
|
||||
or_poisoned,
|
||||
reactive_graph,
|
||||
router,
|
||||
router_macro,
|
||||
server_fn,
|
||||
server_fn/server_fn_macro_default,
|
||||
server_fn_macro,
|
||||
@@ -40,4 +49,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-04-14
|
||||
toolchain: nightly-2024-08-01
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Get example project directories that changed
|
||||
id: changed-dirs
|
||||
uses: tj-actions/changed-files@v41
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
dir_names: true
|
||||
dir_names_max_depth: "2"
|
||||
|
||||
2
.github/workflows/get-example-changed.yml
vendored
2
.github/workflows/get-example-changed.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
- name: Get example files that changed
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v43
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
examples/**
|
||||
|
||||
18
.github/workflows/get-leptos-changed.yml
vendored
18
.github/workflows/get-leptos-changed.yml
vendored
@@ -21,20 +21,32 @@ jobs:
|
||||
|
||||
- name: Get source files that changed
|
||||
id: changed-source
|
||||
uses: tj-actions/changed-files@v43
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: |
|
||||
integrations/**
|
||||
any_error/**
|
||||
any_spawner/**
|
||||
const_str_slice_concat/**
|
||||
either_of/**
|
||||
hydration_context/**
|
||||
integrations/actix/**
|
||||
integrations/axum/**
|
||||
integrations/utils/**
|
||||
leptos/**
|
||||
leptos_config/**
|
||||
leptos_dom/**
|
||||
leptos_hot_reload/**
|
||||
leptos_macro/**
|
||||
leptos_reactive/**
|
||||
leptos_server/**
|
||||
meta/**
|
||||
next_tuple/**
|
||||
oco/**
|
||||
or_poisoned/**
|
||||
reactive_graph/**
|
||||
router/**
|
||||
router_macro/**
|
||||
server_fn/**
|
||||
server_fn/server_fn_macro_default/**
|
||||
server_fn_macro/**
|
||||
|
||||
- name: List source files that changed
|
||||
|
||||
2
.github/workflows/run-cargo-make-task.yml
vendored
2
.github/workflows/run-cargo-make-task.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
|
||||
60
Cargo.toml
60
Cargo.toml
@@ -3,18 +3,28 @@ resolver = "2"
|
||||
members = [
|
||||
# utilities
|
||||
"oco",
|
||||
"any_spawner",
|
||||
"const_str_slice_concat",
|
||||
"either_of",
|
||||
"next_tuple",
|
||||
"oco",
|
||||
"or_poisoned",
|
||||
|
||||
# core
|
||||
"hydration_context",
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_config",
|
||||
"leptos_hot_reload",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
"reactive_graph",
|
||||
"reactive_stores",
|
||||
"reactive_stores_macro",
|
||||
"server_fn",
|
||||
"server_fn_macro",
|
||||
"server_fn/server_fn_macro_default",
|
||||
"tachys",
|
||||
|
||||
# integrations
|
||||
"integrations/actix",
|
||||
@@ -24,28 +34,42 @@ members = [
|
||||
# libraries
|
||||
"meta",
|
||||
"router",
|
||||
"router_macro",
|
||||
"any_error",
|
||||
]
|
||||
exclude = ["benchmarks", "examples"]
|
||||
exclude = ["benchmarks", "examples", "projects"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.12"
|
||||
rust-version = "1.75"
|
||||
version = "0.7.0-beta4"
|
||||
edition = "2021"
|
||||
rust-version = "1.76"
|
||||
|
||||
[workspace.dependencies]
|
||||
oco_ref = { path = "./oco", version = "0.1.0" }
|
||||
leptos = { path = "./leptos", version = "0.6.12" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.6.12" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.6.12" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.6.12" }
|
||||
leptos_reactive = { path = "./leptos_reactive", version = "0.6.12" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.6.12" }
|
||||
server_fn = { path = "./server_fn", version = "0.6.12" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.12" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.6.12" }
|
||||
leptos_router = { path = "./router", version = "0.6.12" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.12" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.6.12" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-beta4" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1.0" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1.0" }
|
||||
either_of = { path = "./either_of/", version = "0.1.0" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-beta4" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-beta4" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-beta4" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta4" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta4" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta4" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta4" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-beta4" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta4" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-beta4" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-beta4" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-beta4" }
|
||||
oco_ref = { path = "./oco", version = "0.2.0" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta4" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta4" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta4" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-beta4" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta4" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta4" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-beta4" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
You can find a list of useful libraries and example projects at [`awesome-leptos`](https://github.com/leptos-rs/awesome-leptos).
|
||||
|
||||
# The `main` branch is currently undergoing major changes in preparation for the [0.7](https://github.com/leptos-rs/leptos/milestone/4) release. For a stable version, please use the [v0.6.13 tag](https://github.com/leptos-rs/leptos/tree/v0.6.13)
|
||||
|
||||
# Leptos
|
||||
|
||||
```rust
|
||||
|
||||
40
TODO.md
Normal file
40
TODO.md
Normal file
@@ -0,0 +1,40 @@
|
||||
- core examples
|
||||
- [x] counter
|
||||
- [x] counters
|
||||
- [x] fetch
|
||||
- [x] todomvc
|
||||
- [x] error_boundary
|
||||
- [x] parent\_child
|
||||
- [x] on: on components
|
||||
- [ ] router
|
||||
- [ ] slots
|
||||
- [ ] hackernews
|
||||
- [ ] counter\_isomorphic
|
||||
- [ ] todo\_app\_sqlite
|
||||
- other ssr examples
|
||||
- [ ] error boundary SSR
|
||||
- reactivity
|
||||
- Signal wrappers
|
||||
- SignalDispose implementations on all Copy types
|
||||
- untracked access warnings
|
||||
- ErrorBoundary
|
||||
- [ ] RenderHtml implementation
|
||||
- [ ] Separate component?
|
||||
- Suspense/Transition components?
|
||||
- callbacks
|
||||
- unsync StoredValue
|
||||
- SSR
|
||||
- escaping HTML correctly (attributes + text nodes)
|
||||
- router
|
||||
- nested routes
|
||||
- trailing slashes
|
||||
- \_meta package (and use in hackernews)
|
||||
- integrations
|
||||
- update tests
|
||||
- hackernews example
|
||||
- TODOs
|
||||
- Suspense/Transition/Await components
|
||||
- nicer routing components
|
||||
- async routing (waiting for data to load before navigation)
|
||||
- `<A>` component
|
||||
- figure out rebuilding issues: list (needs new signal IDs) vs. regular rebuild
|
||||
13
any_error/Cargo.toml
Normal file
13
any_error/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
version = "0.2.0-beta4"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities for wrapping, throwing, and catching errors."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.14"
|
||||
2
any_error/README.md
Normal file
2
any_error/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
A utility library for wrapping arbitrary errors, and for “throwing” errors in a way
|
||||
that can be caught by user-defined error hooks.
|
||||
165
any_error/src/lib.rs
Normal file
165
any_error/src/lib.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! A utility library for wrapping arbitrary errors, and for “throwing” errors in a way
|
||||
//! that can be caught by user-defined error hooks.
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
mem, ops,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/* Wrapper Types */
|
||||
|
||||
/// This is a result type into which any error can be converted.
|
||||
///
|
||||
/// Results are stored as [`Error`].
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
/// A generic wrapper for any error.
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct Error(Arc<dyn error::Error + Send + Sync>);
|
||||
|
||||
impl Error {
|
||||
/// Converts the wrapper into the inner reference-counted error.
|
||||
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
|
||||
Arc::clone(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Error {
|
||||
type Target = Arc<dyn error::Error + Send + Sync>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Error
|
||||
where
|
||||
T: error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Error(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements behavior that allows for global or scoped error handling.
|
||||
///
|
||||
/// This allows for both "throwing" errors to register them, and "clearing" errors when they are no
|
||||
/// longer valid. This is useful for something like a user interface, in which an error can be
|
||||
/// "thrown" on some invalid user input, and later "cleared" if the user corrects the input.
|
||||
/// Keeping a unique identifier for each error allows the UI to be updated accordingly.
|
||||
pub trait ErrorHook: Send + Sync {
|
||||
/// Handles the given error, returning a unique identifier.
|
||||
fn throw(&self, error: Error) -> ErrorId;
|
||||
|
||||
/// Clears the error associated with the given identifier.
|
||||
fn clear(&self, id: &ErrorId);
|
||||
}
|
||||
|
||||
/// A unique identifier for an error. This is returned when you call [`throw`], which calls a
|
||||
/// global error handler.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
|
||||
pub struct ErrorId(usize);
|
||||
|
||||
impl Display for ErrorId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for ErrorId {
|
||||
fn from(value: usize) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
/// Resets the error hook to its previous state when dropped.
|
||||
pub struct ResetErrorHookOnDrop(Option<Arc<dyn ErrorHook>>);
|
||||
|
||||
impl Drop for ResetErrorHookOnDrop {
|
||||
fn drop(&mut self) {
|
||||
ERROR_HOOK.with_borrow_mut(|this| *this = self.0.take())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current error hook.
|
||||
pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
|
||||
ERROR_HOOK.with_borrow(Clone::clone)
|
||||
}
|
||||
|
||||
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
|
||||
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
|
||||
ResetErrorHookOnDrop(
|
||||
ERROR_HOOK.with_borrow_mut(|this| mem::replace(this, Some(hook))),
|
||||
)
|
||||
}
|
||||
|
||||
/// Invokes the error hook set by [`set_error_hook`] with the given error.
|
||||
pub fn throw(error: impl Into<Error>) -> ErrorId {
|
||||
ERROR_HOOK
|
||||
.with_borrow(|hook| hook.as_ref().map(|hook| hook.throw(error.into())))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Clears the given error from the current error hook.
|
||||
pub fn clear(id: &ErrorId) {
|
||||
ERROR_HOOK
|
||||
.with_borrow(|hook| hook.as_ref().map(|hook| hook.clear(id)))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
/// A [`Future`] that reads the error hook that is set when it is created, and sets this as the
|
||||
/// current error hook whenever it is polled.
|
||||
pub struct ErrorHookFuture<Fut> {
|
||||
hook: Option<Arc<dyn ErrorHook>>,
|
||||
#[pin]
|
||||
inner: Fut
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> ErrorHookFuture<Fut> {
|
||||
/// Reads the current hook and wraps the given [`Future`], returning a new `Future` that will
|
||||
/// set the error hook whenever it is polled.
|
||||
pub fn new(inner: Fut) -> Self {
|
||||
Self {
|
||||
hook: ERROR_HOOK.with_borrow(Clone::clone),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut> Future for ErrorHookFuture<Fut>
|
||||
where
|
||||
Fut: Future,
|
||||
{
|
||||
type Output = Fut::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let _hook = this
|
||||
.hook
|
||||
.as_ref()
|
||||
.map(|hook| set_error_hook(Arc::clone(hook)));
|
||||
this.inner.poll(cx)
|
||||
}
|
||||
}
|
||||
33
any_spawner/Cargo.toml
Normal file
33
any_spawner/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "any_spawner"
|
||||
version = "0.1.1"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Spawn asynchronous tasks in an executor-independent way."
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
glib = { version = "0.20.0", optional = true }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.39", optional = true, default-features = false, features = [
|
||||
"rt",
|
||||
] }
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
wasm-bindgen-futures = { version = "0.4.42", optional = true }
|
||||
|
||||
[features]
|
||||
tracing = ["dep:tracing"]
|
||||
tokio = ["dep:tokio"]
|
||||
glib = ["dep:glib"]
|
||||
wasm-bindgen = ["dep:wasm-bindgen-futures"]
|
||||
futures-executor = ["futures/thread-pool", "futures/executor"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["tracing"]
|
||||
26
any_spawner/README.md
Normal file
26
any_spawner/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
This crate makes it easier to write asynchronous code that is executor-agnostic, by providing a
|
||||
utility that can be used to spawn tasks in a variety of executors.
|
||||
|
||||
It only supports single executor per program, but that executor can be set at runtime, anywhere
|
||||
in your crate (or an application that depends on it).
|
||||
|
||||
This can be extended to support any executor or runtime that supports spawning [`Future`]s.
|
||||
|
||||
This is a least common denominator implementation in many ways. Limitations include:
|
||||
|
||||
- setting an executor is a one-time, global action
|
||||
- no "join handle" or other result is returned from the spawn
|
||||
- the `Future` must output `()`
|
||||
|
||||
```rust
|
||||
use any_spawner::Executor;
|
||||
|
||||
Executor::init_futures_executor()
|
||||
.expect("executor should only be initialized once");
|
||||
|
||||
// spawn a thread-safe Future
|
||||
Executor::spawn(async { /* ... */ });
|
||||
|
||||
// spawn a Future that is !Send
|
||||
Executor::spawn_local(async { /* ... */ });
|
||||
```
|
||||
245
any_spawner/src/lib.rs
Normal file
245
any_spawner/src/lib.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
//! This crate makes it easier to write asynchronous code that is executor-agnostic, by providing a
|
||||
//! utility that can be used to spawn tasks in a variety of executors.
|
||||
//!
|
||||
//! It only supports single executor per program, but that executor can be set at runtime, anywhere
|
||||
//! in your crate (or an application that depends on it).
|
||||
//!
|
||||
//! This can be extended to support any executor or runtime that supports spawning [`Future`]s.
|
||||
//!
|
||||
//! This is a least common denominator implementation in many ways. Limitations include:
|
||||
//! - setting an executor is a one-time, global action
|
||||
//! - no "join handle" or other result is returned from the spawn
|
||||
//! - the `Future` must output `()`
|
||||
//!
|
||||
//! ```rust
|
||||
//! use any_spawner::Executor;
|
||||
//!
|
||||
//! // make sure an Executor has been initialized with one of the init_ functions
|
||||
//!
|
||||
//! # if false {
|
||||
//! // spawn a thread-safe Future
|
||||
//! Executor::spawn(async { /* ... */ });
|
||||
//!
|
||||
//! // spawn a Future that is !Send
|
||||
//! Executor::spawn_local(async { /* ... */ });
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use std::{future::Future, pin::Pin, sync::OnceLock};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
||||
pub(crate) type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
static SPAWN: OnceLock<fn(PinnedFuture<()>)> = OnceLock::new();
|
||||
static SPAWN_LOCAL: OnceLock<fn(PinnedLocalFuture<()>)> = OnceLock::new();
|
||||
|
||||
/// Errors that can occur when using the executor.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ExecutorError {
|
||||
/// The executor has already been set.
|
||||
#[error("Executor has already been set.")]
|
||||
AlreadySet,
|
||||
}
|
||||
|
||||
/// A global async executor that can spawn tasks.
|
||||
pub struct Executor;
|
||||
|
||||
impl Executor {
|
||||
/// Spawns a thread-safe [`Future`].
|
||||
/// ```rust
|
||||
/// use any_spawner::Executor;
|
||||
/// # if false {
|
||||
/// // spawn a thread-safe Future
|
||||
/// Executor::spawn(async { /* ... */ });
|
||||
/// # }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
|
||||
if let Some(spawner) = SPAWN.get() {
|
||||
spawner(Box::pin(fut))
|
||||
} else {
|
||||
#[cfg(all(debug_assertions, feature = "tracing"))]
|
||||
tracing::error!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn() before \
|
||||
the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
panic!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn() before \
|
||||
the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a [`Future`] that cannot be sent across threads.
|
||||
/// ```rust
|
||||
/// use any_spawner::Executor;
|
||||
///
|
||||
/// # if false {
|
||||
/// // spawn a thread-safe Future
|
||||
/// Executor::spawn_local(async { /* ... */ });
|
||||
/// # }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
pub fn spawn_local(fut: impl Future<Output = ()> + 'static) {
|
||||
if let Some(spawner) = SPAWN_LOCAL.get() {
|
||||
spawner(Box::pin(fut))
|
||||
} else {
|
||||
#[cfg(all(debug_assertions, feature = "tracing"))]
|
||||
tracing::error!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn_local() \
|
||||
before the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
#[cfg(all(debug_assertions, not(feature = "tracing")))]
|
||||
panic!(
|
||||
"At {}, tried to spawn a Future with Executor::spawn_local() \
|
||||
before the Executor had been set.",
|
||||
std::panic::Location::caller()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until the next "tick" of the current async executor.
|
||||
pub async fn tick() {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
Executor::spawn(async move {
|
||||
_ = tx.send(());
|
||||
});
|
||||
_ = rx.await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Executor {
|
||||
/// Globally sets the [`tokio`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `tokio` feature to be activated on this crate.
|
||||
#[cfg(feature = "tokio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
|
||||
pub fn init_tokio() -> Result<(), ExecutorError> {
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
tokio::task::spawn_local(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets the [`wasm-bindgen-futures`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `wasm-bindgen` feature to be activated on this crate.
|
||||
#[cfg(feature = "wasm-bindgen")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-bindgen")))]
|
||||
pub fn init_wasm_bindgen() -> Result<(), ExecutorError> {
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
wasm_bindgen_futures::spawn_local(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
wasm_bindgen_futures::spawn_local(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets the [`glib`] runtime as the executor used to spawn tasks.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `glib` feature to be activated on this crate.
|
||||
#[cfg(feature = "glib")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "glib")))]
|
||||
pub fn init_glib() -> Result<(), ExecutorError> {
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
let main_context = glib::MainContext::default();
|
||||
main_context.spawn(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
let main_context = glib::MainContext::default();
|
||||
main_context.spawn_local(fut);
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Globally sets the [`futures`] executor as the executor used to spawn tasks,
|
||||
/// lazily creating a thread pool to spawn tasks into.
|
||||
///
|
||||
/// Returns `Err(_)` if an executor has already been set.
|
||||
///
|
||||
/// Requires the `futures-executor` feature to be activated on this crate.
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "futures-executor")))]
|
||||
pub fn init_futures_executor() -> Result<(), ExecutorError> {
|
||||
use futures::{
|
||||
executor::{LocalPool, ThreadPool},
|
||||
task::{LocalSpawnExt, SpawnExt},
|
||||
};
|
||||
|
||||
static THREAD_POOL: OnceLock<ThreadPool> = OnceLock::new();
|
||||
thread_local! {
|
||||
static LOCAL_POOL: LocalPool = LocalPool::new();
|
||||
}
|
||||
|
||||
fn get_thread_pool() -> &'static ThreadPool {
|
||||
THREAD_POOL.get_or_init(|| {
|
||||
ThreadPool::new()
|
||||
.expect("could not create futures executor ThreadPool")
|
||||
})
|
||||
}
|
||||
|
||||
SPAWN
|
||||
.set(|fut| {
|
||||
get_thread_pool()
|
||||
.spawn(fut)
|
||||
.expect("failed to spawn future");
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
SPAWN_LOCAL
|
||||
.set(|fut| {
|
||||
LOCAL_POOL.with(|pool| {
|
||||
let spawner = pool.spawner();
|
||||
spawner.spawn_local(fut).expect("failed to spawn future");
|
||||
});
|
||||
})
|
||||
.map_err(|_| ExecutorError::AlreadySet)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "futures-executor")]
|
||||
#[test]
|
||||
fn can_spawn_local_future() {
|
||||
use crate::Executor;
|
||||
use std::rc::Rc;
|
||||
Executor::init_futures_executor().expect("couldn't set executor");
|
||||
let rc = Rc::new(());
|
||||
Executor::spawn_local(async {
|
||||
_ = rc;
|
||||
});
|
||||
Executor::spawn(async {});
|
||||
}
|
||||
}
|
||||
@@ -6,31 +6,31 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
l0410 = { package = "leptos", version = "0.4.10", features = [
|
||||
"nightly",
|
||||
"ssr",
|
||||
"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",
|
||||
"nightly",
|
||||
"leptos",
|
||||
] }
|
||||
tachy_maccy = { git = "https://github.com/gbj/tachys", features = ["nightly"] }
|
||||
sycamore = { version = "0.8", features = ["ssr"] }
|
||||
yew = { version = "0.20", features = ["ssr"] }
|
||||
tokio-test = "0.4"
|
||||
miniserde = "0.1"
|
||||
gloo = "0.8"
|
||||
uuid = { version = "1", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wasm-bindgen = "0.2"
|
||||
lazy_static = "1"
|
||||
log = "0.4"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
sycamore = { version = "0.8.0", features = ["ssr"] }
|
||||
yew = { version = "0.20.0", features = ["ssr"] }
|
||||
tokio-test = "0.4.0"
|
||||
miniserde = "0.1.0"
|
||||
gloo = "0.8.0"
|
||||
uuid = { version = "1.0", features = ["serde", "v4", "wasm-bindgen"] }
|
||||
wasm-bindgen = "0.2.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4.0"
|
||||
strum = "0.24.0"
|
||||
strum_macros = "0.24.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
tera = "1.0"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
version = "0.3.0"
|
||||
features = ["Window", "Document", "HtmlElement", "HtmlInputElement"]
|
||||
|
||||
12
const_str_slice_concat/Cargo.toml
Normal file
12
const_str_slice_concat/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "const_str_slice_concat"
|
||||
version = "0.1.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities for const concatenation of string slices."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
1
const_str_slice_concat/Makefile.toml
Normal file
1
const_str_slice_concat/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
139
const_str_slice_concat/src/lib.rs
Normal file
139
const_str_slice_concat/src/lib.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! Utilities for const concatenation of string slices.
|
||||
|
||||
pub(crate) const MAX_TEMPLATE_SIZE: usize = 4096;
|
||||
|
||||
/// Converts a zero-terminated buffer of bytes into a UTF-8 string.
|
||||
pub const fn str_from_buffer(buf: &[u8; MAX_TEMPLATE_SIZE]) -> &str {
|
||||
match core::ffi::CStr::from_bytes_until_nul(buf) {
|
||||
Ok(cstr) => match cstr.to_str() {
|
||||
Ok(str) => str,
|
||||
Err(_) => panic!("TEMPLATE FAILURE"),
|
||||
},
|
||||
Err(_) => panic!("TEMPLATE FAILURE"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Concatenates any number of static strings into a single array.
|
||||
// credit to Rainer Stropek, "Constant fun," Rust Linz, June 2022
|
||||
pub const fn const_concat(
|
||||
strs: &'static [&'static str],
|
||||
) -> [u8; MAX_TEMPLATE_SIZE] {
|
||||
let mut buffer = [0; MAX_TEMPLATE_SIZE];
|
||||
let mut position = 0;
|
||||
let mut remaining = strs;
|
||||
|
||||
while let [current, tail @ ..] = remaining {
|
||||
let x = current.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
remaining = tail;
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Converts a zero-terminated buffer of bytes into a UTF-8 string with the given prefix.
|
||||
pub const fn const_concat_with_prefix(
|
||||
strs: &'static [&'static str],
|
||||
prefix: &'static str,
|
||||
suffix: &'static str,
|
||||
) -> [u8; MAX_TEMPLATE_SIZE] {
|
||||
let mut buffer = [0; MAX_TEMPLATE_SIZE];
|
||||
let mut position = 0;
|
||||
let mut remaining = strs;
|
||||
|
||||
while let [current, tail @ ..] = remaining {
|
||||
let x = current.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
remaining = tail;
|
||||
}
|
||||
|
||||
if buffer[0] == 0 {
|
||||
buffer
|
||||
} else {
|
||||
let mut new_buf = [0; MAX_TEMPLATE_SIZE];
|
||||
let prefix = prefix.as_bytes();
|
||||
let suffix = suffix.as_bytes();
|
||||
let mut position = 0;
|
||||
let mut i = 0;
|
||||
while i < prefix.len() {
|
||||
new_buf[position] = prefix[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
i = 0;
|
||||
while i < buffer.len() {
|
||||
if buffer[i] == 0 {
|
||||
break;
|
||||
}
|
||||
new_buf[position] = buffer[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
i = 0;
|
||||
while i < suffix.len() {
|
||||
new_buf[position] = suffix[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
new_buf
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts any number of strings into a UTF-8 string, separated by the given string.
|
||||
pub const fn const_concat_with_separator(
|
||||
strs: &[&str],
|
||||
separator: &'static str,
|
||||
) -> [u8; MAX_TEMPLATE_SIZE] {
|
||||
let mut buffer = [0; MAX_TEMPLATE_SIZE];
|
||||
let mut position = 0;
|
||||
let mut remaining = strs;
|
||||
|
||||
while let [current, tail @ ..] = remaining {
|
||||
let x = current.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
// have it iterate over bytes manually, because, again,
|
||||
// no mutable refernces in const fns
|
||||
while i < x.len() {
|
||||
buffer[position] = x[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
if !x.is_empty() {
|
||||
let mut position = 0;
|
||||
let separator = separator.as_bytes();
|
||||
while i < separator.len() {
|
||||
buffer[position] = separator[i];
|
||||
position += 1;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
remaining = tail;
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
13
either_of/Cargo.toml
Normal file
13
either_of/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "either_of"
|
||||
version = "0.1.0"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
repository = "https://github.com/leptos-rs/leptos"
|
||||
description = "Utilities for working with enumerated types that contain one of 2..n other types."
|
||||
rust-version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
pin-project-lite = "0.2.14"
|
||||
1
either_of/Makefile.toml
Normal file
1
either_of/Makefile.toml
Normal file
@@ -0,0 +1 @@
|
||||
extend = { path = "../cargo-make/main.toml" }
|
||||
1
either_of/README.md
Normal file
1
either_of/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Utilities for working with enumerated types that contain one of `2..n` other types.
|
||||
135
either_of/src/lib.rs
Normal file
135
either_of/src/lib.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Utilities for working with enumerated types that contain one of `2..n` other types.
|
||||
|
||||
use core::{
|
||||
fmt::Display,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Either<A, B> {
|
||||
Left(A),
|
||||
Right(B),
|
||||
}
|
||||
|
||||
impl<Item, A, B> Iterator for Either<A, B>
|
||||
where
|
||||
A: Iterator<Item = Item>,
|
||||
B: Iterator<Item = Item>,
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
Either::Left(i) => i.next(),
|
||||
Either::Right(i) => i.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[project = EitherFutureProj]
|
||||
pub enum EitherFuture<A, B> {
|
||||
Left { #[pin] inner: A },
|
||||
Right { #[pin] inner: B },
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, B> Future for EitherFuture<A, B>
|
||||
where
|
||||
A: Future,
|
||||
B: Future,
|
||||
{
|
||||
type Output = Either<A::Output, B::Output>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
match this {
|
||||
EitherFutureProj::Left { inner } => match inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(inner) => Poll::Ready(Either::Left(inner)),
|
||||
},
|
||||
EitherFutureProj::Right { inner } => match inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(inner) => Poll::Ready(Either::Right(inner)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tuples {
|
||||
($name:ident + $fut_name:ident + $fut_proj:ident => $($ty:ident),*) => {
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub enum $name<$($ty,)*> {
|
||||
$($ty ($ty),)*
|
||||
}
|
||||
|
||||
impl<$($ty,)*> Display for $name<$($ty,)*>
|
||||
where
|
||||
$($ty: Display,)*
|
||||
{
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
$($name::$ty(this) => this.fmt(f),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, $($ty,)*> Iterator for $name<$($ty,)*>
|
||||
where
|
||||
$($ty: Iterator<Item = Item>,)*
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
$($name::$ty(i) => i.next(),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[project = $fut_proj]
|
||||
pub enum $fut_name<$($ty,)*> {
|
||||
$($ty { #[pin] inner: $ty },)*
|
||||
}
|
||||
}
|
||||
|
||||
impl<$($ty,)*> Future for $fut_name<$($ty,)*>
|
||||
where
|
||||
$($ty: Future,)*
|
||||
{
|
||||
type Output = $name<$($ty::Output,)*>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
match this {
|
||||
$($fut_proj::$ty { inner } => match inner.poll(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(inner) => Poll::Ready($name::$ty(inner)),
|
||||
},)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tuples!(EitherOf3 + EitherOf3Future + EitherOf3FutureProj => A, B, C);
|
||||
tuples!(EitherOf4 + EitherOf4Future + EitherOf4FutureProj => A, B, C, D);
|
||||
tuples!(EitherOf5 + EitherOf5Future + EitherOf5FutureProj => A, B, C, D, E);
|
||||
tuples!(EitherOf6 + EitherOf6Future + EitherOf6FutureProj => A, B, C, D, E, F);
|
||||
tuples!(EitherOf7 + EitherOf7Future + EitherOf7FutureProj => A, B, C, D, E, F, G);
|
||||
tuples!(EitherOf8 + EitherOf8Future + EitherOf8FutureProj => A, B, C, D, E, F, G, H);
|
||||
tuples!(EitherOf9 + EitherOf9Future + EitherOf9FutureProj => A, B, C, D, E, F, G, H, I);
|
||||
tuples!(EitherOf10 + EitherOf10Future + EitherOf10FutureProj => A, B, C, D, E, F, G, H, I, J);
|
||||
tuples!(EitherOf11 + EitherOf11Future + EitherOf11FutureProj => A, B, C, D, E, F, G, H, I, J, K);
|
||||
tuples!(EitherOf12 + EitherOf12Future + EitherOf12FutureProj => A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
tuples!(EitherOf13 + EitherOf13Future + EitherOf13FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tuples!(EitherOf14 + EitherOf14Future + EitherOf14FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(EitherOf15 + EitherOf15Future + EitherOf15FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(EitherOf16 + EitherOf16Future + EitherOf16FutureProj => A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
@@ -7,21 +7,19 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
cfg-if = "1"
|
||||
http = { version = "0.2", optional = true }
|
||||
actix-files = { version = "0.6.6", optional = true }
|
||||
actix-web = { version = "4.8", optional = true, features = ["macros"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1.0"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
|
||||
@@ -1,68 +1,9 @@
|
||||
<picture>
|
||||
<source srcset="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_Solid_White.svg" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://raw.githubusercontent.com/leptos-rs/leptos/main/docs/logos/Leptos_logo_RGB.svg" alt="Leptos Logo">
|
||||
</picture>
|
||||
# Action Form Error Handling Example
|
||||
|
||||
# Leptos Starter Template
|
||||
## Getting Started
|
||||
|
||||
This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## Creating your template repo
|
||||
## Quick Start
|
||||
|
||||
If you don't have `cargo-leptos` installed you can install it with
|
||||
|
||||
`cargo install cargo-leptos`
|
||||
|
||||
Then run
|
||||
|
||||
`cargo leptos new --git leptos-rs/start`
|
||||
|
||||
to generate a new project template (you will be prompted to enter a project name).
|
||||
|
||||
`cd {projectname}`
|
||||
|
||||
to go to your newly created project.
|
||||
|
||||
Of course, you should explore around the project structure, but the best place to start with your application code is in `src/app.rs`.
|
||||
|
||||
## Running your project
|
||||
|
||||
`cargo leptos watch`
|
||||
By default, you can access your local project at `http://localhost:3000`
|
||||
|
||||
## Installing Additional Tools
|
||||
|
||||
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
|
||||
|
||||
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
|
||||
2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
|
||||
3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
|
||||
4. `npm install -g sass` - install `dart-sass` (should be optional in future)
|
||||
|
||||
## Executing a Server on a Remote Machine Without the Toolchain
|
||||
After running a `cargo leptos build --release` the minimum files needed are:
|
||||
|
||||
1. The server binary located in `target/server/release`
|
||||
2. The `site` directory and all files within located in `target/site`
|
||||
|
||||
Copy these files to your remote server. The directory structure should be:
|
||||
```text
|
||||
leptos_start
|
||||
site/
|
||||
```
|
||||
Set the following environment variables (updating for your project as needed):
|
||||
```sh
|
||||
export LEPTOS_OUTPUT_NAME="leptos_start"
|
||||
export LEPTOS_SITE_ROOT="site"
|
||||
export LEPTOS_SITE_PKG_DIR="pkg"
|
||||
export LEPTOS_SITE_ADDR="127.0.0.1:3000"
|
||||
export LEPTOS_RELOAD_PORT="3001"
|
||||
```
|
||||
Finally, run the server binary.
|
||||
|
||||
## Notes about CSR and Trunk:
|
||||
Although it is not recommended, you can also run your project without server integration using the feature `csr` and `trunk serve`:
|
||||
|
||||
`trunk serve --open --features csr`
|
||||
|
||||
This may be useful for integrating external tools which require a static site, e.g. `tauri`.
|
||||
Execute `cargo leptos watch` to run this example.
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos::{logging, prelude::*};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
StaticSegment,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
<Stylesheet id="leptos" href="/pkg/leptos_start.css"/>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
<main id="app">
|
||||
<Routes>
|
||||
<Route path="" view=HomePage/>
|
||||
<Route path="/*any" view=NotFound/>
|
||||
</Routes>
|
||||
<FlatRoutes fallback=NotFound>
|
||||
<Route path=StaticSegment("") view=HomePage/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
@@ -43,7 +34,7 @@ async fn do_something(
|
||||
/// Renders the home page of your application.
|
||||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
let do_something_action = Action::<DoSomething, _>::server();
|
||||
let do_something_action = ServerAction::<DoSomething>::new();
|
||||
let value = Signal::derive(move || {
|
||||
do_something_action
|
||||
.value()
|
||||
@@ -57,17 +48,12 @@ fn HomePage() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<h1>"Test the action form!"</h1>
|
||||
<ErrorBoundary fallback=move |error| format!("{:#?}", error
|
||||
.get()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1.into_inner()
|
||||
.to_string())
|
||||
>
|
||||
{value}
|
||||
<ActionForm action=do_something_action class="form">
|
||||
<label>Should error: <input type="checkbox" name="should_error"/></label>
|
||||
<ErrorBoundary fallback=move |error| {
|
||||
move || format!("{:#?}", error.get())
|
||||
}>
|
||||
<pre>{value}</pre>
|
||||
<ActionForm action=do_something_action attr:class="form">
|
||||
<label>"Should error: "<input type="checkbox" name="should_error"/></label>
|
||||
<button type="submit">Submit</button>
|
||||
</ActionForm>
|
||||
</ErrorBoundary>
|
||||
@@ -91,7 +77,5 @@ fn NotFound() -> impl IntoView {
|
||||
resp.set_status(actix_web::http::StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
view! {
|
||||
<h1>"Not Found"</h1>
|
||||
}
|
||||
view! { <h1>"Not Found"</h1> }
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
pub mod app;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
}
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
|
||||
@@ -4,25 +4,47 @@ async fn main() -> std::io::Result<()> {
|
||||
use action_form_error_handling::app::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use leptos_meta::MetaTags;
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
println!("listening on http://{}", &addr);
|
||||
|
||||
HttpServer::new(move || {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
// serve JS/WASM/CSS from `pkg`
|
||||
.service(Files::new("/pkg", format!("{site_root}/pkg")))
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.app_data(web::Data::new(leptos_options.to_owned()))
|
||||
.leptos_routes(routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || {
|
||||
use leptos::prelude::*;
|
||||
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<AutoReload options=leptos_options.clone()/>
|
||||
<HydrationScripts options=leptos_options.clone()/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
@@ -30,24 +52,10 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "ssr", feature = "csr")))]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn main() {
|
||||
// no client-side main function
|
||||
// unless we want this to work with e.g., Trunk for pure client-side testing
|
||||
// see lib.rs for hydration function instead
|
||||
// see optional feature `csr` instead
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "ssr"), feature = "csr"))]
|
||||
pub fn main() {
|
||||
// a client-side main function is required for using `trunk serve`
|
||||
// prefer using `cargo leptos serve` instead
|
||||
// to run: `trunk serve --open --features csr`
|
||||
use action_form_error_handling::app::*;
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "animated-show"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
@@ -1,14 +0,0 @@
|
||||
# Animated Show Example
|
||||
|
||||
This is a very simple example of the `<AnimatedShow>` component.
|
||||
|
||||
The `<AnimatedShow>` component is an extension for the `<Show>` component and it will not take in a fallback, but it will unmount the component from the DOM after a given duration. This makes it possible to have really easy unmount animations with just
|
||||
CSS.
|
||||
|
||||
## Getting Started
|
||||
|
||||
See the [Examples README](../README.md) for setup and run instructions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run `trunk serve --open` to run this example.
|
||||
@@ -1,42 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z"/>
|
||||
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
|
||||
<style>
|
||||
.hover-me {
|
||||
width: 100px;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
.here-i-am {
|
||||
width: 100px;
|
||||
margin: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
background: black;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
.fade-in-1000 {
|
||||
animation: 1000ms fade-in forwards;
|
||||
}
|
||||
.fade-out-1000 {
|
||||
animation: 1000ms fade-out forwards;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,34 +0,0 @@
|
||||
use core::time::Duration;
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let show = create_rw_signal(false);
|
||||
|
||||
// the CSS classes in this example are just written directly inside the `index.html`
|
||||
view! {
|
||||
<div
|
||||
class="hover-me"
|
||||
on:mouseenter=move |_| show.set(true)
|
||||
on:mouseleave=move |_| show.set(false)
|
||||
>
|
||||
"Hover Me"
|
||||
</div>
|
||||
|
||||
<AnimatedShow
|
||||
when=show
|
||||
// optional CSS class which will be applied if `when == true`
|
||||
show_class="fade-in-1000"
|
||||
// optional CSS class which will be applied if `when == false` and before the
|
||||
// `hide_delay` starts -> makes CSS unmount animations really easy
|
||||
hide_class="fade-out-1000"
|
||||
// the given unmount delay which should match your unmount animation duration
|
||||
hide_delay=Duration::from_millis(1000)
|
||||
>
|
||||
// provide any `Children` inside here
|
||||
<div class="here-i-am">
|
||||
"Here I Am!"
|
||||
</div>
|
||||
</AnimatedShow>
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
use animated_show::App;
|
||||
use leptos::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(App);
|
||||
}
|
||||
@@ -4,16 +4,18 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 'z'
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_log = "1.0"
|
||||
log = "0.4.22"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
gloo-timers = { version = "0.3.0", features = ["futures"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
web-sys = "0.3"
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
web-sys = "0.3.70"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
@@ -10,12 +10,12 @@ pub fn SimpleCounter(
|
||||
/// The change that should be applied each time the button is clicked.
|
||||
step: i32,
|
||||
) -> impl IntoView {
|
||||
let (value, set_value) = create_signal(initial_value);
|
||||
let (value, set_value) = signal(initial_value);
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value.set(0)>"Clear"</button>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
|
||||
<button on:click=move |_| *set_value.write() -= step>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
use counter::SimpleCounter;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! {
|
||||
<SimpleCounter
|
||||
initial_value=0
|
||||
step=1
|
||||
/>
|
||||
}
|
||||
view! { <SimpleCounter initial_value=0 step=1/> }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use counter::*;
|
||||
use leptos::*;
|
||||
use leptos::mount::mount_to;
|
||||
use leptos::prelude::*;
|
||||
use leptos::spawn::tick;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn clear() {
|
||||
let document = leptos::document();
|
||||
async fn clear() {
|
||||
let document = document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
let _ = 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(
|
||||
let _dispose = mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|| view! { <SimpleCounter initial_value=10 step=1/> },
|
||||
);
|
||||
@@ -30,59 +32,63 @@ fn clear() {
|
||||
// now let's click the `clear` button
|
||||
clear.click();
|
||||
|
||||
// the reactive system is built on top of the async system, so changes are not reflected
|
||||
// synchronously in the DOM
|
||||
// in order to detect the changes here, we'll just yield for a brief time after each change,
|
||||
// allowing the effects that update the view to run
|
||||
tick().await;
|
||||
|
||||
// now let's test the <div> against the expected value
|
||||
// we can do this by testing its `outerHTML`
|
||||
let runtime = create_runtime();
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
// here we spawn a mini reactive system, just to render the
|
||||
// test case
|
||||
{
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, _set_value) = create_signal(0);
|
||||
assert_eq!(div.outer_html(), {
|
||||
// it's as if we're creating it with a value of 0, right?
|
||||
let (value, _set_value) = signal(0);
|
||||
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! {
|
||||
<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()
|
||||
// we can remove the event listeners because they're not rendered to HTML
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
<button>"-1"</button>
|
||||
<span>"Value: " {value} "!"</span>
|
||||
<button>"+1"</button>
|
||||
</div>
|
||||
}
|
||||
);
|
||||
// Leptos supports multiple backend renderers for HTML elements
|
||||
// .into_view() here is just a convenient way of specifying "use the regular DOM renderer"
|
||||
.into_view()
|
||||
// views are lazy -- they describe a DOM tree but don't create it yet
|
||||
// calling .build() will actually build the DOM elements
|
||||
.build()
|
||||
// .build() returned an ElementState, which is a smart pointer for
|
||||
// a DOM element. So we can still just call .outer_html(), which access the outerHTML on
|
||||
// the actual DOM element
|
||||
.outer_html()
|
||||
});
|
||||
|
||||
// There's actually an easier way to do this...
|
||||
// We can just test against a <SimpleCounter/> with the initial value 0
|
||||
assert_eq!(test_wrapper.inner_html(), {
|
||||
let comparison_wrapper = document.create_element("section").unwrap();
|
||||
leptos::mount_to(
|
||||
let _dispose = mount_to(
|
||||
comparison_wrapper.clone().unchecked_into(),
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/>},
|
||||
);
|
||||
comparison_wrapper.inner_html()
|
||||
});
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
let document = leptos::document();
|
||||
async fn inc() {
|
||||
let document = document();
|
||||
let test_wrapper = document.create_element("section").unwrap();
|
||||
let _ = document.body().unwrap().append_child(&test_wrapper);
|
||||
|
||||
mount_to(
|
||||
let _dispose = mount_to(
|
||||
test_wrapper.clone().unchecked_into(),
|
||||
|| view! { <SimpleCounter initial_value=0 step=1/> },
|
||||
);
|
||||
|
||||
// You can do testing with vanilla DOM operations
|
||||
let _document = leptos::document();
|
||||
let div = test_wrapper.query_selector("div").unwrap().unwrap();
|
||||
let clear = div
|
||||
.first_child()
|
||||
@@ -108,6 +114,8 @@ fn inc() {
|
||||
inc.click();
|
||||
inc.click();
|
||||
|
||||
tick().await;
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
|
||||
|
||||
dec.click();
|
||||
@@ -115,19 +123,21 @@ fn inc() {
|
||||
dec.click();
|
||||
dec.click();
|
||||
|
||||
tick().await;
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
|
||||
|
||||
clear.click();
|
||||
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
tick().await;
|
||||
|
||||
let runtime = create_runtime();
|
||||
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
|
||||
|
||||
// Or you can test against a sample view!
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
{
|
||||
let (value, _) = create_signal(0);
|
||||
let (value, _) = signal(0);
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
@@ -137,16 +147,20 @@ fn inc() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.into_view()
|
||||
.build()
|
||||
.outer_html()
|
||||
);
|
||||
|
||||
inc.click();
|
||||
|
||||
tick().await;
|
||||
|
||||
assert_eq!(
|
||||
div.outer_html(),
|
||||
{
|
||||
// because we've clicked, it's as if the signal is starting at 1
|
||||
let (value, _) = create_signal(1);
|
||||
let (value, _) = signal(1);
|
||||
view! {
|
||||
<div>
|
||||
<button>"Clear"</button>
|
||||
@@ -156,8 +170,8 @@ fn inc() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
.into_view()
|
||||
.build()
|
||||
.outer_html()
|
||||
);
|
||||
|
||||
runtime.dispose();
|
||||
}
|
||||
|
||||
@@ -11,35 +11,34 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
broadcaster = "1"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
lazy_static = "1"
|
||||
actix-files = { version = "0.6.6", optional = true }
|
||||
actix-web = { version = "4.8", optional = true, features = ["macros"] }
|
||||
broadcaster = "1.0"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.30"
|
||||
lazy_static = "1.5"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
once_cell = "1.18"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.3"
|
||||
tracing = { version = "0.1", optional = true }
|
||||
log = "0.4.22"
|
||||
once_cell = "1.19"
|
||||
gloo-net = { version = "0.6.0" }
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
simple_logger = "5.0"
|
||||
tracing = { version = "0.1.40", optional = true }
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:tracing",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:tracing",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos::{prelude::*, reactive_graph::actions::Action};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, A},
|
||||
StaticSegment,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use tracing::instrument;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod ssr_imports {
|
||||
pub use broadcaster::BroadcastChannel;
|
||||
pub use once_cell::sync::OnceCell;
|
||||
pub use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
pub static COUNT: AtomicI32 = AtomicI32::new(0);
|
||||
@@ -15,14 +16,6 @@ pub mod ssr_imports {
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
|
||||
}
|
||||
|
||||
static LOG_INIT: OnceCell<()> = OnceCell::new();
|
||||
|
||||
pub fn init_logging() {
|
||||
LOG_INIT.get_or_init(|| {
|
||||
simple_logger::SimpleLogger::new().env().init().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
@@ -59,10 +52,6 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
|
||||
}
|
||||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
#[cfg(feature = "ssr")]
|
||||
ssr_imports::init_logging();
|
||||
|
||||
provide_meta_context();
|
||||
view! {
|
||||
<Router>
|
||||
<header>
|
||||
@@ -85,28 +74,12 @@ pub fn Counters() -> impl IntoView {
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path=""
|
||||
view=|| {
|
||||
view! { <Counter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="form"
|
||||
view=|| {
|
||||
view! { <FormCounter/> }
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="multi"
|
||||
view=|| {
|
||||
view! { <MultiuserCounter/> }
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=StaticSegment("") view=Counter/>
|
||||
<Route path=StaticSegment("form") view=FormCounter/>
|
||||
<Route path=StaticSegment("multi") view=MultiuserCounter/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
@@ -118,10 +91,10 @@ pub fn Counters() -> impl IntoView {
|
||||
// This is the typical pattern for a CRUD app
|
||||
#[component]
|
||||
pub fn Counter() -> impl IntoView {
|
||||
let dec = create_action(|_: &()| adjust_server_count(-1, "decing".into()));
|
||||
let inc = create_action(|_: &()| adjust_server_count(1, "incing".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
let counter = create_resource(
|
||||
let dec = Action::new(|_: &()| adjust_server_count(-1, "decing".into()));
|
||||
let inc = Action::new(|_: &()| adjust_server_count(1, "incing".into()));
|
||||
let clear = Action::new(|_: &()| clear_server_count());
|
||||
let counter = Resource::new(
|
||||
move || {
|
||||
(
|
||||
dec.version().get(),
|
||||
@@ -138,27 +111,14 @@ pub fn Counter() -> impl IntoView {
|
||||
<p>
|
||||
"This counter sets the value on the server and automatically reloads the new value."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<span>
|
||||
"Value: "
|
||||
<Suspense>
|
||||
{move || counter.and_then(|count| *count)} "!"
|
||||
</Suspense>
|
||||
</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
</div>
|
||||
<Suspense>
|
||||
{move || {
|
||||
counter.get().and_then(|res| match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e),
|
||||
}).map(|msg| {
|
||||
view! { <p>"Error: " {msg.to_string()}</p> }
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
<ErrorBoundary fallback=|errors| move || format!("Error: {:#?}", errors.get())>
|
||||
<div>
|
||||
<button on:click=move |_| { clear.dispatch(()); }>"Clear"</button>
|
||||
<button on:click=move |_| { dec.dispatch(()); }>"-1"</button>
|
||||
<span>"Value: " <Suspense>{counter} "!"</Suspense></span>
|
||||
<button on:click=move |_| { inc.dispatch(()); }>"+1"</button>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -170,10 +130,10 @@ pub fn Counter() -> impl IntoView {
|
||||
pub fn FormCounter() -> impl IntoView {
|
||||
// these struct names are auto-generated by #[server]
|
||||
// they are just the PascalCased versions of the function names
|
||||
let adjust = create_server_action::<AdjustServerCount>();
|
||||
let clear = create_server_action::<ClearServerCount>();
|
||||
let adjust = ServerAction::<AdjustServerCount>::new();
|
||||
let clear = ServerAction::<ClearServerCount>::new();
|
||||
|
||||
let counter = create_resource(
|
||||
let counter = Resource::new(
|
||||
move || (adjust.version().get(), clear.version().get()),
|
||||
|_| {
|
||||
log::debug!("FormCounter running fetcher");
|
||||
@@ -204,7 +164,7 @@ pub fn FormCounter() -> impl IntoView {
|
||||
<input type="hidden" name="msg" value="form value down"/>
|
||||
<input type="submit" value="-1"/>
|
||||
</ActionForm>
|
||||
<span>"Value: " <Suspense>{move || value().to_string()} "!"</Suspense></span>
|
||||
<span>"Value: " <Suspense>{value} "!"</Suspense></span>
|
||||
<ActionForm action=adjust>
|
||||
<input type="hidden" name="delta" value="1"/>
|
||||
<input type="hidden" name="msg" value="form value up"/>
|
||||
@@ -222,19 +182,21 @@ pub fn FormCounter() -> impl IntoView {
|
||||
#[component]
|
||||
pub fn MultiuserCounter() -> impl IntoView {
|
||||
let dec =
|
||||
create_action(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
|
||||
Action::new(|_: &()| adjust_server_count(-1, "dec dec goose".into()));
|
||||
let inc =
|
||||
create_action(|_: &()| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = create_action(|_: &()| clear_server_count());
|
||||
Action::new(|_: &()| adjust_server_count(1, "inc inc moose".into()));
|
||||
let clear = Action::new(|_: &()| clear_server_count());
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let multiplayer_value = {
|
||||
use futures::StreamExt;
|
||||
use send_wrapper::SendWrapper;
|
||||
|
||||
let mut source =
|
||||
let mut source = SendWrapper::new(
|
||||
gloo_net::eventsource::futures::EventSource::new("/api/events")
|
||||
.expect("couldn't connect to SSE stream");
|
||||
let s = create_signal_from_stream(
|
||||
.expect("couldn't connect to SSE stream"),
|
||||
);
|
||||
let s = ReadSignal::from_stream_unsync(
|
||||
source
|
||||
.subscribe("message")
|
||||
.unwrap()
|
||||
@@ -248,12 +210,12 @@ pub fn MultiuserCounter() -> impl IntoView {
|
||||
}),
|
||||
);
|
||||
|
||||
on_cleanup(move || source.close());
|
||||
on_cleanup(move || source.take().close());
|
||||
s
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let (multiplayer_value, _) = create_signal(None::<i32>);
|
||||
let (multiplayer_value, _) = signal(None::<i32>);
|
||||
|
||||
view! {
|
||||
<div>
|
||||
@@ -262,12 +224,12 @@ pub fn MultiuserCounter() -> impl IntoView {
|
||||
"This one uses server-sent events (SSE) to live-update when other users make changes."
|
||||
</p>
|
||||
<div>
|
||||
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
|
||||
<button on:click=move |_| dec.dispatch(())>"-1"</button>
|
||||
<button on:click=move |_| { clear.dispatch(()); }>"Clear"</button>
|
||||
<button on:click=move |_| { dec.dispatch(()); }>"-1"</button>
|
||||
<span>
|
||||
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
|
||||
</span>
|
||||
<button on:click=move |_| inc.dispatch(())>"+1"</button>
|
||||
<button on:click=move |_| { inc.dispatch(()); }>"+1"</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ pub mod counters;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::counters::*;
|
||||
use leptos::*;
|
||||
use crate::counters::Counters;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
mount_to_body(Counters);
|
||||
leptos::mount::hydrate_body(Counters);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ mod counters;
|
||||
use crate::counters::*;
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/api/events")]
|
||||
@@ -27,26 +26,44 @@ async fn counter_events() -> impl Responder {
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use leptos::prelude::*;
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let routes = generate_route_list(Counters);
|
||||
println!("listening on http://{}", &addr);
|
||||
|
||||
HttpServer::new(move || {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(Counters);
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(counter_events)
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
Counters,
|
||||
)
|
||||
.leptos_routes(routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<AutoReload options=leptos_options.clone()/>
|
||||
<HydrationScripts options=leptos_options.clone()/>
|
||||
</head>
|
||||
<body>
|
||||
<Counters/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
|
||||
@@ -9,12 +9,10 @@ lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
leptos_router = { path = "../../router", features = [] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
web-sys = "0.3"
|
||||
wasm-bindgen = "0.2.93"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
web-sys = "0.3.70"
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::query_signal;
|
||||
|
||||
/// A simple counter component.
|
||||
///
|
||||
/// You can use doc comments like this to document your component.
|
||||
#[component]
|
||||
pub fn SimpleQueryCounter() -> impl IntoView {
|
||||
let (count, set_count) = create_query_signal::<i32>("count");
|
||||
let (count, set_count) = query_signal::<i32>("count");
|
||||
let clear = move |_| set_count.set(None);
|
||||
let decrement = move |_| set_count.set(Some(count.get().unwrap_or(0) - 1));
|
||||
let increment = move |_| set_count.set(Some(count.get().unwrap_or(0) + 1));
|
||||
|
||||
let (msg, set_msg) = create_query_signal::<String>("message");
|
||||
let (msg, set_msg) = query_signal::<String>("message");
|
||||
let update_msg = move |ev| {
|
||||
let new_msg = event_target_value(&ev);
|
||||
if new_msg.is_empty() {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use counter_url_query::SimpleQueryCounter;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::Router;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
leptos::mount::mount_to_body(|| {
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="" view=SimpleQueryCounter />
|
||||
</Routes>
|
||||
<SimpleQueryCounter/>
|
||||
</Router>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,16 +10,14 @@ lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-test = "0.3.34"
|
||||
pretty_assertions = "1.3.0"
|
||||
rstest = "0.17.0"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
pretty_assertions = "1.4"
|
||||
rstest = "0.22.0"
|
||||
|
||||
[dev-dependencies.web-sys]
|
||||
features = ["HtmlElement", "XPathResult"]
|
||||
version = "0.3.61"
|
||||
version = "0.3.70"
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use leptos::{html::*, *};
|
||||
use leptos::{
|
||||
ev,
|
||||
html::{button, div, span},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
/// A simple counter view.
|
||||
// A component is really just a function call: it runs once to create the DOM and reactive system
|
||||
pub fn counter(initial_value: i32, step: u32) -> impl IntoView {
|
||||
let count = RwSignal::new(Count::new(initial_value, step));
|
||||
Effect::new(move |_| {
|
||||
leptos::logging::log!("count = {:?}", count.get());
|
||||
});
|
||||
|
||||
// the function name is the same as the HTML tag name
|
||||
div()
|
||||
@@ -44,6 +51,7 @@ impl Count {
|
||||
}
|
||||
|
||||
pub fn value(&self) -> i32 {
|
||||
leptos::logging::log!("value = {}", self.value);
|
||||
self.value
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
|
||||
/// Show the counter
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| counter(0, 1))
|
||||
leptos::mount::mount_to_body(|| counter(0, 1))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use counter_without_macros::counter;
|
||||
use leptos::*;
|
||||
use leptos::{prelude::*, spawn::tick};
|
||||
use pretty_assertions::assert_eq;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
@@ -8,27 +8,32 @@ use web_sys::HtmlElement;
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_increment_counter() {
|
||||
async fn should_increment_counter() {
|
||||
open_counter();
|
||||
|
||||
click_increment();
|
||||
click_increment();
|
||||
|
||||
// reactive changes run asynchronously, so yield briefly before observing the DOM
|
||||
tick().await;
|
||||
|
||||
assert_eq!(see_text(), Some("Value: 2!".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_decrement_counter() {
|
||||
async fn should_decrement_counter() {
|
||||
open_counter();
|
||||
|
||||
click_decrement();
|
||||
click_decrement();
|
||||
|
||||
tick().await;
|
||||
|
||||
assert_eq!(see_text(), Some("Value: -2!".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn should_clear_counter() {
|
||||
async fn should_clear_counter() {
|
||||
open_counter();
|
||||
|
||||
click_increment();
|
||||
@@ -36,18 +41,18 @@ fn should_clear_counter() {
|
||||
|
||||
click_clear();
|
||||
|
||||
tick().await;
|
||||
|
||||
assert_eq!(see_text(), Some("Value: 0!".to_string()));
|
||||
}
|
||||
|
||||
fn open_counter() {
|
||||
remove_existing_counter();
|
||||
mount_to_body(move || counter(0, 1));
|
||||
leptos::mount::mount_to_body(move || counter(0, 1));
|
||||
}
|
||||
|
||||
fn remove_existing_counter() {
|
||||
if let Some(counter) =
|
||||
leptos::document().query_selector("body div").unwrap()
|
||||
{
|
||||
if let Some(counter) = document().query_selector("body div").unwrap() {
|
||||
counter.remove();
|
||||
}
|
||||
}
|
||||
@@ -74,7 +79,7 @@ fn see_text() -> Option<String> {
|
||||
|
||||
fn find_by_text(text: &str) -> HtmlElement {
|
||||
let xpath = format!("//*[text()='{}']", text);
|
||||
let document = leptos::document();
|
||||
let document = document();
|
||||
document
|
||||
.evaluate(&xpath, &document)
|
||||
.unwrap()
|
||||
|
||||
6
examples/counters/.gitignore
vendored
Normal file
6
examples/counters/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Support playwright testing
|
||||
node_modules/
|
||||
test-results/
|
||||
end2end/playwright-report/
|
||||
playwright/.cache/
|
||||
pnpm-lock.yaml
|
||||
@@ -4,12 +4,10 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = "0.3"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
wasm-bindgen = "0.2.93"
|
||||
web-sys = "0.3.70"
|
||||
|
||||
@@ -2,4 +2,5 @@ extend = [
|
||||
{ path = "../cargo-make/main.toml" },
|
||||
{ path = "../cargo-make/wasm-test.toml" },
|
||||
{ path = "../cargo-make/trunk_server.toml" },
|
||||
{ path = "../cargo-make/playwright-trunk-test.toml" },
|
||||
]
|
||||
|
||||
4
examples/counters/e2e/.gitignore
vendored
Normal file
4
examples/counters/e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
83
examples/counters/e2e/package-lock.json
generated
Normal file
83
examples/counters/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "grip",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "grip",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.35.1"
|
||||
}
|
||||
},
|
||||
"node_modules/.pnpm/@playwright+test@1.33.0": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/.pnpm/@types+node@20.2.1/node_modules/@types/node": {
|
||||
"version": "20.2.1",
|
||||
"extraneous": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/.pnpm/playwright-core@1.33.0/node_modules/playwright-core": {
|
||||
"version": "1.33.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz",
|
||||
"integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
examples/counters/e2e/package.json
Normal file
10
examples/counters/e2e/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"private": "true",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pnpm": "^9.7.1"
|
||||
}
|
||||
}
|
||||
77
examples/counters/e2e/playwright.config.ts
Normal file
77
examples/counters/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !process.env.DEV,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.DEV ? 0 : 10,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.DEV ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:8080",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: "cd ../ && trunk serve",
|
||||
// url: "http://127.0.0.1:8080",
|
||||
// reuseExistingServer: false, //!process.env.CI,
|
||||
// },
|
||||
});
|
||||
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/add_1k_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add 1000 Counters", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
|
||||
await Promise.all([
|
||||
await ui.goto(),
|
||||
await ui.addOneThousandCountersButton.waitFor(),
|
||||
]);
|
||||
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
await ui.addOneThousandCounters();
|
||||
|
||||
await expect(ui.counters).toHaveText("3000");
|
||||
});
|
||||
});
|
||||
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
15
examples/counters/e2e/tests/add_counter.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Add Counter", () => {
|
||||
test("should increase the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await expect(ui.counters).toHaveText("3");
|
||||
});
|
||||
});
|
||||
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
18
examples/counters/e2e/tests/clear_counters.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Clear Counters", () => {
|
||||
test("should reset the counts", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.clearCounters();
|
||||
|
||||
await expect(ui.total).toHaveText("0");
|
||||
await expect(ui.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
16
examples/counters/e2e/tests/decrement_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Decrement Count", () => {
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
await ui.decrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("-3");
|
||||
});
|
||||
});
|
||||
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
30
examples/counters/e2e/tests/enter_count.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Enter Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("5");
|
||||
|
||||
await expect(ui.total).toHaveText("5");
|
||||
await expect(ui.counters).toHaveText("1");
|
||||
});
|
||||
|
||||
test("should decrease the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.enterCount("100");
|
||||
await ui.enterCount("100", 1);
|
||||
await ui.enterCount("100", 2);
|
||||
await ui.enterCount("50", 1);
|
||||
|
||||
await expect(ui.total).toHaveText("250");
|
||||
});
|
||||
});
|
||||
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
98
examples/counters/e2e/tests/fixtures/counters_page.ts
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
|
||||
export class CountersPage {
|
||||
readonly page: Page;
|
||||
readonly addCounterButton: Locator;
|
||||
readonly addOneThousandCountersButton: Locator;
|
||||
readonly clearCountersButton: Locator;
|
||||
|
||||
readonly incrementCountButton: Locator;
|
||||
readonly counterInput: Locator;
|
||||
readonly decrementCountButton: Locator;
|
||||
readonly removeCountButton: Locator;
|
||||
|
||||
readonly total: Locator;
|
||||
readonly counters: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
this.addCounterButton = page.locator("button", { hasText: "Add Counter" });
|
||||
|
||||
this.addOneThousandCountersButton = page.locator("button", {
|
||||
hasText: "Add 1000 Counters",
|
||||
});
|
||||
|
||||
this.clearCountersButton = page.locator("button", {
|
||||
hasText: "Clear Counters",
|
||||
});
|
||||
|
||||
this.decrementCountButton = page.locator("button", {
|
||||
hasText: "-1",
|
||||
});
|
||||
|
||||
this.incrementCountButton = page.locator("button", {
|
||||
hasText: "+1",
|
||||
});
|
||||
|
||||
this.removeCountButton = page.locator("button", {
|
||||
hasText: "x",
|
||||
});
|
||||
|
||||
this.total = page.getByTestId("total");
|
||||
|
||||
this.counters = page.getByTestId("counters");
|
||||
|
||||
this.counterInput = page.getByRole("textbox");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
}
|
||||
|
||||
async addCounter() {
|
||||
await Promise.all([
|
||||
this.addCounterButton.waitFor(),
|
||||
this.addCounterButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async addOneThousandCounters() {
|
||||
this.addOneThousandCountersButton.click();
|
||||
}
|
||||
|
||||
async decrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.decrementCountButton.nth(index).waitFor(),
|
||||
this.decrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async incrementCount(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.incrementCountButton.nth(index).waitFor(),
|
||||
this.incrementCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async clearCounters() {
|
||||
await Promise.all([
|
||||
this.clearCountersButton.waitFor(),
|
||||
this.clearCountersButton.click(),
|
||||
]);
|
||||
}
|
||||
|
||||
async enterCount(count: string, index: number = 0) {
|
||||
await Promise.all([
|
||||
this.counterInput.nth(index).waitFor(),
|
||||
this.counterInput.nth(index).fill(count),
|
||||
]);
|
||||
}
|
||||
|
||||
async removeCounter(index: number = 0) {
|
||||
await Promise.all([
|
||||
this.removeCountButton.nth(index).waitFor(),
|
||||
this.removeCountButton.nth(index).click(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
16
examples/counters/e2e/tests/increment_count.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Increment Count", () => {
|
||||
test("should increase the total count", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
await ui.incrementCount();
|
||||
|
||||
await expect(ui.total).toHaveText("3");
|
||||
});
|
||||
});
|
||||
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
17
examples/counters/e2e/tests/remove_counter.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("Remove Counter", () => {
|
||||
test("should decrement the number of counters", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
await ui.addCounter();
|
||||
|
||||
await ui.removeCounter(1);
|
||||
|
||||
await expect(ui.counters).toHaveText("2");
|
||||
});
|
||||
});
|
||||
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
19
examples/counters/e2e/tests/view_counters.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { CountersPage } from "./fixtures/counters_page";
|
||||
|
||||
test.describe("View Counters", () => {
|
||||
test("should see the title", async ({ page }) => {
|
||||
const ui = new CountersPage(page);
|
||||
await ui.goto();
|
||||
|
||||
await expect(page).toHaveTitle("Counters");
|
||||
});
|
||||
|
||||
test("should see the initial counts", async ({ page }) => {
|
||||
const counters = new CountersPage(page);
|
||||
await counters.goto();
|
||||
|
||||
await expect(counters.total).toHaveText("0");
|
||||
await expect(counters.counters).toHaveText("0");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs/>
|
||||
<title>Counters</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
const MANY_COUNTERS: usize = 1000;
|
||||
|
||||
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
|
||||
type CounterHolder = Vec<(usize, ArcRwSignal<i32>)>;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct CounterUpdater {
|
||||
@@ -11,13 +11,13 @@ struct CounterUpdater {
|
||||
|
||||
#[component]
|
||||
pub fn Counters() -> impl IntoView {
|
||||
let (next_counter_id, set_next_counter_id) = create_signal(0);
|
||||
let (counters, set_counters) = create_signal::<CounterHolder>(vec![]);
|
||||
let (next_counter_id, set_next_counter_id) = signal(0);
|
||||
let (counters, set_counters) = signal::<CounterHolder>(vec![]);
|
||||
provide_context(CounterUpdater { set_counters });
|
||||
|
||||
let add_counter = move |_| {
|
||||
let id = next_counter_id.get();
|
||||
let sig = create_signal(0);
|
||||
let sig = ArcRwSignal::new(0);
|
||||
set_counters.update(move |counters| counters.push((id, sig)));
|
||||
set_next_counter_id.update(|id| *id += 1);
|
||||
};
|
||||
@@ -25,7 +25,7 @@ pub fn Counters() -> impl IntoView {
|
||||
let add_many_counters = move |_| {
|
||||
let next_id = next_counter_id.get();
|
||||
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
|
||||
let signal = create_signal(0);
|
||||
let signal = ArcRwSignal::new(0);
|
||||
(id, signal)
|
||||
});
|
||||
|
||||
@@ -39,78 +39,56 @@ pub fn Counters() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<button on:click=add_counter>
|
||||
"Add Counter"
|
||||
</button>
|
||||
<button on:click=add_many_counters>
|
||||
{format!("Add {MANY_COUNTERS} Counters")}
|
||||
</button>
|
||||
<button on:click=clear_counters>
|
||||
"Clear Counters"
|
||||
</button>
|
||||
<button on:click=add_counter>"Add Counter"</button>
|
||||
<button on:click=add_many_counters>{format!("Add {MANY_COUNTERS} Counters")}</button>
|
||||
<button on:click=clear_counters>"Clear Counters"</button>
|
||||
<p>
|
||||
"Total: "
|
||||
<span>{move ||
|
||||
counters.get()
|
||||
.iter()
|
||||
.map(|(_, (count, _))| count.get())
|
||||
.sum::<i32>()
|
||||
.to_string()
|
||||
}</span>
|
||||
" from "
|
||||
<span>{move || counters.get().len().to_string()}</span>
|
||||
<span data-testid="total">
|
||||
{move || {
|
||||
counters.get().iter().map(|(_, count)| count.get()).sum::<i32>().to_string()
|
||||
}}
|
||||
|
||||
</span> " from "
|
||||
<span data-testid="counters">{move || counters.get().len().to_string()}</span>
|
||||
" counters."
|
||||
</p>
|
||||
<ul>
|
||||
<For
|
||||
each=move || counters.get()
|
||||
key=|counter| counter.0
|
||||
children=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
|
||||
view! {
|
||||
<Counter id value set_value/>
|
||||
}
|
||||
children=move |(id, value)| {
|
||||
view! { <Counter id value/> }
|
||||
}
|
||||
/>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Counter(
|
||||
id: usize,
|
||||
value: ReadSignal<i32>,
|
||||
set_value: WriteSignal<i32>,
|
||||
) -> impl IntoView {
|
||||
fn Counter(id: usize, value: ArcRwSignal<i32>) -> impl IntoView {
|
||||
let value = RwSignal::from(value);
|
||||
let CounterUpdater { set_counters } = use_context().unwrap();
|
||||
|
||||
let input = move |ev| {
|
||||
set_value
|
||||
.set(event_target_value(&ev).parse::<i32>().unwrap_or_default())
|
||||
};
|
||||
|
||||
// this will run when the scope is disposed, i.e., when this row is deleted
|
||||
// because the signal was created in the parent scope, it won't be disposed
|
||||
// of until the parent scope is. but we no longer need it, so we'll dispose of
|
||||
// it when this row is deleted, instead. if we don't dispose of it here,
|
||||
// this memory will "leak," i.e., the signal will continue to exist until the
|
||||
// parent component is removed. in the case of this component, where it's the
|
||||
// root, that's the lifetime of the program.
|
||||
on_cleanup(move || {
|
||||
log::debug!("deleted a row");
|
||||
value.dispose();
|
||||
});
|
||||
|
||||
view! {
|
||||
<li>
|
||||
<button on:click=move |_| set_value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input type="text"
|
||||
prop:value={value}
|
||||
on:input=input
|
||||
<button on:click=move |_| value.update(move |value| *value -= 1)>"-1"</button>
|
||||
<input
|
||||
type="text"
|
||||
prop:value=value
|
||||
on:input:target=move |ev| {
|
||||
value.set(ev.target().value().parse::<i32>().unwrap_or_default())
|
||||
}
|
||||
/>
|
||||
|
||||
<span>{value}</span>
|
||||
<button on:click=move |_| set_value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| set_counters.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))>"x"</button>
|
||||
<button on:click=move |_| value.update(move |value| *value += 1)>"+1"</button>
|
||||
<button on:click=move |_| {
|
||||
set_counters
|
||||
.update(move |counters| counters.retain(|(counter_id, _)| counter_id != &id))
|
||||
}>"x"</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
|
||||
fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| view! { <Counters/> })
|
||||
leptos::mount::mount_to_body(Counters)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use counters::Counters;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos::spawn::tick;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inc() {
|
||||
mount_to_body(|| view! { <Counters/> });
|
||||
async fn inc() {
|
||||
mount_to_body(Counters);
|
||||
|
||||
let document = leptos::document();
|
||||
let document = document();
|
||||
let div = document.query_selector("div").unwrap().unwrap();
|
||||
let add_counter = div
|
||||
.first_child()
|
||||
@@ -18,31 +19,33 @@ fn inc() {
|
||||
.dyn_into::<HtmlElement>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">0</span> counters.</p><ul><!----></ul>"
|
||||
);
|
||||
|
||||
// add 3 counters
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
add_counter.click();
|
||||
|
||||
tick().await;
|
||||
|
||||
// check HTML
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->0<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->0<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">0</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
type=\"text\"><span>0</span><button>+1</button><button>x</button></\
|
||||
li><!----></ul>"
|
||||
);
|
||||
|
||||
let counters = div
|
||||
@@ -71,25 +74,20 @@ fn inc() {
|
||||
}
|
||||
}
|
||||
|
||||
tick().await;
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->6<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->1<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">6</span> from <span data-testid=\"counters\">3</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>1</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
type=\"text\"><span>3</span><button>+1</button><button>x</button></\
|
||||
li><!----></ul>"
|
||||
);
|
||||
|
||||
// remove the first counter
|
||||
@@ -101,20 +99,17 @@ fn inc() {
|
||||
.unchecked_into::<HtmlElement>()
|
||||
.click();
|
||||
|
||||
tick().await;
|
||||
|
||||
assert_eq!(
|
||||
div.inner_html(),
|
||||
"<button>Add Counter</button><button>Add 1000 \
|
||||
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
|
||||
<DynChild> -->5<!-- </DynChild> --></span> from <span><!-- \
|
||||
<DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- \
|
||||
<Each> --><!-- <EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->2<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
|
||||
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
|
||||
-->3<!-- </DynChild> \
|
||||
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
|
||||
--><!-- </EachItem> --><!-- </Each> --></ul>"
|
||||
Counters</button><button>Clear Counters</button><p>Total: \
|
||||
<span data-testid=\"total\">5</span> from <span data-testid=\"counters\">2</span> \
|
||||
counters.</p><ul><li><button>-1</button><input \
|
||||
type=\"text\"><span>2</span><button>+1</button><button>x</button></\
|
||||
li><li><button>-1</button><input \
|
||||
type=\"text\"><span>3</span><button>+1</button><button>x</button></\
|
||||
li><!----></ul>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
log = "0.4.22"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
web-sys = { version = "0.3", features = ["Clipboard", "Navigator"] }
|
||||
web-sys = { version = "0.3.70", features = ["Clipboard", "Navigator"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = "0.3"
|
||||
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
wasm-bindgen = "0.2.93"
|
||||
web-sys = { version = "0.3.70", features = ["NodeList"] }
|
||||
@@ -1,36 +1,34 @@
|
||||
use leptos::{ev::click, html::AnyElement, *};
|
||||
use leptos::{ev::click, prelude::*};
|
||||
use web_sys::Element;
|
||||
|
||||
// no extra parameter
|
||||
pub fn highlight(el: HtmlElement<AnyElement>) {
|
||||
pub fn highlight(el: Element) {
|
||||
let mut highlighted = false;
|
||||
|
||||
let _ = el.clone().on(click, move |_| {
|
||||
let handle = el.clone().on(click, move |_| {
|
||||
highlighted = !highlighted;
|
||||
|
||||
if highlighted {
|
||||
let _ = el.clone().style("background-color", "yellow");
|
||||
el.style(("background-color", "yellow"));
|
||||
} else {
|
||||
let _ = el.clone().style("background-color", "transparent");
|
||||
el.style(("background-color", "transparent"));
|
||||
}
|
||||
});
|
||||
on_cleanup(move || drop(handle));
|
||||
}
|
||||
|
||||
// one extra parameter
|
||||
pub fn copy_to_clipboard(el: HtmlElement<AnyElement>, content: &str) {
|
||||
let content = content.to_string();
|
||||
|
||||
let _ = el.clone().on(click, move |evt| {
|
||||
pub fn copy_to_clipboard(el: Element, content: &str) {
|
||||
let content = content.to_owned();
|
||||
let handle = el.clone().on(click, move |evt| {
|
||||
evt.prevent_default();
|
||||
evt.stop_propagation();
|
||||
|
||||
let _ = window()
|
||||
.navigator()
|
||||
.clipboard()
|
||||
.expect("navigator.clipboard to be available")
|
||||
.write_text(&content);
|
||||
let _ = window().navigator().clipboard().write_text(&content);
|
||||
|
||||
let _ = el.clone().inner_html(format!("Copied \"{}\"", &content));
|
||||
el.set_inner_html(&format!("Copied \"{}\"", &content));
|
||||
});
|
||||
on_cleanup(move || drop(handle));
|
||||
}
|
||||
|
||||
// custom parameter
|
||||
@@ -51,15 +49,18 @@ impl From<()> for Amount {
|
||||
}
|
||||
}
|
||||
|
||||
// .into() will automatically be called on the parameter
|
||||
pub fn add_dot(el: HtmlElement<AnyElement>, amount: Amount) {
|
||||
_ = el.clone().on(click, move |_| {
|
||||
pub fn add_dot(el: Element, amount: Amount) {
|
||||
use leptos::wasm_bindgen::JsCast;
|
||||
let el = el.unchecked_into::<web_sys::HtmlElement>();
|
||||
|
||||
let handle = el.clone().on(click, move |_| {
|
||||
el.set_inner_text(&format!(
|
||||
"{}{}",
|
||||
el.inner_text(),
|
||||
".".repeat(amount.0)
|
||||
))
|
||||
})
|
||||
});
|
||||
on_cleanup(move || drop(handle));
|
||||
}
|
||||
|
||||
#[component]
|
||||
@@ -76,12 +77,17 @@ pub fn App() -> impl IntoView {
|
||||
let data = "Hello World!";
|
||||
|
||||
view! {
|
||||
<a href="#" use:copy_to_clipboard=data>"Copy \"" {data} "\" to clipboard"</a>
|
||||
<a href="#" use:copy_to_clipboard=data>
|
||||
"Copy \""
|
||||
{data}
|
||||
"\" to clipboard"
|
||||
</a>
|
||||
// automatically applies the directive to every root element in `SomeComponent`
|
||||
<SomeComponent use:highlight />
|
||||
<SomeComponent use:highlight/>
|
||||
// no value will default to `().into()`
|
||||
<button use:add_dot>"Add a dot"</button>
|
||||
// `5.into()` automatically called
|
||||
<button use:add_dot=5>"Add 5 dots"</button>
|
||||
// can manually call `.into()` to convert to the correct type
|
||||
// (automatically calling `.into()` prevents using generics in directive functions)
|
||||
<button use:add_dot=5.into()>"Add 5 dots"</button>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use directives::App;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
use gloo_timers::future::sleep;
|
||||
use std::time::Duration;
|
||||
use directives::App;
|
||||
use leptos::{prelude::*, spawn::tick};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
use directives::App;
|
||||
use leptos::*;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_directives() {
|
||||
mount_to_body(|| view! { <App/> });
|
||||
sleep(Duration::ZERO).await;
|
||||
leptos::mount::mount_to_body(App);
|
||||
tick().await;
|
||||
|
||||
let document = leptos::document();
|
||||
let document = document();
|
||||
let paragraphs = document.query_selector_all("p").unwrap();
|
||||
|
||||
assert_eq!(paragraphs.length(), 3);
|
||||
|
||||
@@ -9,6 +9,6 @@ lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_log = "1.0"
|
||||
log = "0.4.22"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -8,7 +8,7 @@ test.describe("Clear Number", () => {
|
||||
|
||||
await ui.clearInput();
|
||||
|
||||
await expect(ui.errorMessage).toHaveText("Not a number! Errors: ");
|
||||
await expect(ui.errorMessage).toHaveText("Not an integer! Errors: ");
|
||||
});
|
||||
test("should see the error list", async ({ page }) => {
|
||||
const ui = new HomePage(page);
|
||||
|
||||
@@ -14,7 +14,7 @@ export class HomePage {
|
||||
|
||||
this.pageTitle = page.locator("h1");
|
||||
this.numberInput = page.getByLabel(
|
||||
"Type a number (or something that's not a number!)"
|
||||
"Type an integer (or something that's not an integer!)"
|
||||
);
|
||||
this.successMessage = page.locator("label p");
|
||||
this.errorMessage = page.locator("div p");
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
let (value, set_value) = create_signal(Ok(0));
|
||||
|
||||
// when input changes, try to parse a number from the input
|
||||
let on_input =
|
||||
move |ev| set_value.set(event_target_value(&ev).parse::<i32>());
|
||||
let (value, set_value) = signal("".parse::<i32>());
|
||||
|
||||
view! {
|
||||
<h1>"Error Handling"</h1>
|
||||
<label>
|
||||
"Type a number (or something that's not a number!)"
|
||||
<input type="number" on:input=on_input/>
|
||||
// If an `Err(_) had been rendered inside the <ErrorBoundary/>,
|
||||
"Type an integer (or something that's not an integer!)"
|
||||
<input
|
||||
type="number"
|
||||
value=move || value.get().unwrap_or_default()
|
||||
// when input changes, try to parse a number from the input
|
||||
on:input:target=move |ev| set_value.set(ev.target().value().parse::<i32>())
|
||||
/>
|
||||
// If an `Err(_) has been rendered inside the <ErrorBoundary/>,
|
||||
// the fallback will be displayed. Otherwise, the children of the
|
||||
// <ErrorBoundary/> will be displayed.
|
||||
<ErrorBoundary
|
||||
// the fallback receives a signal containing current errors
|
||||
fallback=|errors| view! {
|
||||
// the fallback receives a signal containing current errors
|
||||
<ErrorBoundary fallback=|errors| {
|
||||
let errors = errors.clone();
|
||||
view! {
|
||||
<div class="error">
|
||||
<p>"Not a number! Errors: "</p>
|
||||
<p>"Not an integer! Errors: "</p>
|
||||
// we can render a list of errors
|
||||
// as strings, if we'd like
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li>})
|
||||
.collect_view()
|
||||
}
|
||||
{move || {
|
||||
errors
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li> })
|
||||
.collect::<Vec<_>>()
|
||||
}}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
}>
|
||||
|
||||
<p>
|
||||
"You entered "
|
||||
// because `value` is `Result<i32, _>`,
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use error_boundary::*;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(|| {
|
||||
view! {
|
||||
<App/>
|
||||
}
|
||||
})
|
||||
mount_to_body(App)
|
||||
}
|
||||
|
||||
@@ -7,25 +7,22 @@ edition = "2021"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.0"
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
http = { version = "1.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
http = { version = "1.1" }
|
||||
thiserror = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen = "0.2.93"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::errors::AppError;
|
||||
use leptos::{logging::log, *};
|
||||
use leptos::{logging::log, prelude::*};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos_axum::ResponseOptions;
|
||||
|
||||
@@ -7,25 +7,18 @@ use leptos_axum::ResponseOptions;
|
||||
// Feel free to do more complicated things here than just displaying them.
|
||||
#[component]
|
||||
pub fn ErrorTemplate(
|
||||
#[prop(optional)] outside_errors: Option<Errors>,
|
||||
#[prop(optional)] errors: Option<RwSignal<Errors>>,
|
||||
#[prop(into)] errors: MaybeSignal<Errors>,
|
||||
) -> impl IntoView {
|
||||
let errors = match outside_errors {
|
||||
Some(e) => create_rw_signal(e),
|
||||
None => match errors {
|
||||
Some(e) => e,
|
||||
None => panic!("No Errors found and we expected errors!"),
|
||||
},
|
||||
};
|
||||
|
||||
// Get Errors from Signal
|
||||
// Downcast lets us take a type that implements `std::error::Error`
|
||||
let errors: Vec<AppError> = errors
|
||||
.get_untracked()
|
||||
.into_iter()
|
||||
.filter_map(|(_, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect();
|
||||
log!("Errors: {errors:#?}");
|
||||
let errors = Memo::new(move |_| {
|
||||
errors
|
||||
.get_untracked()
|
||||
.into_iter()
|
||||
.filter_map(|(_, v)| v.downcast_ref::<AppError>().cloned())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
log!("Errors: {:#?}", &*errors.read_untracked());
|
||||
|
||||
// Only the response code for the first error is actually sent from the server
|
||||
// this may be customized by the specific application
|
||||
@@ -33,26 +26,30 @@ pub fn ErrorTemplate(
|
||||
{
|
||||
let response = use_context::<ResponseOptions>();
|
||||
if let Some(response) = response {
|
||||
response.set_status(errors[0].status_code());
|
||||
response.set_status(errors.read_untracked()[0].status_code());
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _)| *index
|
||||
// renders each item to a view
|
||||
children=move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
view! {
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
}
|
||||
/>
|
||||
<h1>{move || {
|
||||
if errors.read().len() > 1 {
|
||||
"Errors"
|
||||
} else {
|
||||
"Error"
|
||||
}}}
|
||||
</h1>
|
||||
{move || {
|
||||
errors.get()
|
||||
.into_iter()
|
||||
.map(|error| {
|
||||
let error_string = error.to_string();
|
||||
let error_code= error.status_code();
|
||||
view! {
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use http::status::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
use crate::landing::App;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
response::{IntoResponse, Response as AxumResponse},
|
||||
};
|
||||
use leptos::{view, LeptosOptions};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_and_error_handler(
|
||||
uri: Uri,
|
||||
State(options): State<LeptosOptions>,
|
||||
req: Request<Body>,
|
||||
) -> AxumResponse {
|
||||
let root = options.site_root.clone();
|
||||
let res = get_static_file(uri.clone(), &root).await.unwrap();
|
||||
|
||||
if res.status() == StatusCode::OK {
|
||||
res.into_response()
|
||||
} else {
|
||||
let handler = leptos_axum::render_app_to_stream(
|
||||
options.to_owned(),
|
||||
move || view! { <App/> },
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(
|
||||
uri: Uri,
|
||||
root: &str,
|
||||
) -> Result<Response<Body>, (StatusCode, String)> {
|
||||
let req = Request::builder()
|
||||
.uri(uri.clone())
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// This path is relative to the cargo root
|
||||
match ServeDir::new(root).oneshot(req).await {
|
||||
Ok(res) => Ok(res.into_response()),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {err}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::{error_template::ErrorTemplate, errors::AppError};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
StaticSegment,
|
||||
};
|
||||
|
||||
#[server(CauseInternalServerError, "/api")]
|
||||
pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
|
||||
@@ -13,28 +16,44 @@ pub async fn cause_internal_server_error() -> Result<(), ServerFnError> {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=options.clone() />
|
||||
<HydrationScripts options/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
//let id = use_context::<String>();
|
||||
provide_meta_context();
|
||||
view! {
|
||||
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Stylesheet id="leptos" href="/pkg/errors_axum.css"/>
|
||||
<Router fallback=|| {
|
||||
let mut outside_errors = Errors::default();
|
||||
outside_errors.insert_with_default_key(AppError::NotFound);
|
||||
view! {
|
||||
<ErrorTemplate outside_errors/>
|
||||
}
|
||||
.into_view()
|
||||
}>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"Error Examples:"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=ExampleErrors/>
|
||||
<Routes fallback=|| {
|
||||
let mut errors = Errors::default();
|
||||
errors.insert_with_default_key(AppError::NotFound);
|
||||
view! {
|
||||
<ErrorTemplate errors/>
|
||||
}
|
||||
.into_view()
|
||||
}>
|
||||
<Route path=StaticSegment("") view=ExampleErrors/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
@@ -44,7 +63,7 @@ pub fn App() -> impl IntoView {
|
||||
#[component]
|
||||
pub fn ExampleErrors() -> impl IntoView {
|
||||
let generate_internal_error =
|
||||
create_server_action::<CauseInternalServerError>();
|
||||
ServerAction::<CauseInternalServerError>::new();
|
||||
|
||||
view! {
|
||||
<p>
|
||||
@@ -54,18 +73,18 @@ pub fn ExampleErrors() -> impl IntoView {
|
||||
</p>
|
||||
<p>
|
||||
"After pressing this button check browser network tools. Can be used even when WASM is blocked:"
|
||||
<ActionForm action=generate_internal_error>
|
||||
<input name="error1" type="submit" value="Generate Internal Server Error"/>
|
||||
</ActionForm>
|
||||
</p>
|
||||
<ActionForm action=generate_internal_error>
|
||||
<input name="error1" type="submit" value="Generate Internal Server Error"/>
|
||||
</ActionForm>
|
||||
<p>"The following <div> will always contain an error and cause this page to produce status 500. Check browser dev tools. "</p>
|
||||
<div>
|
||||
// note that the error boundaries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribute to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|errors| view!{ <ErrorTemplate errors=errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
// note that the error boundaries could be placed above in the Router or lower down
|
||||
// in a particular route. The generated errors on the entire page contribute to the
|
||||
// final status code sent by the server when producing ssr pages.
|
||||
<ErrorBoundary fallback=|errors| view!{ <ErrorTemplate errors/>}>
|
||||
<ReturnsError/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
pub mod error_template;
|
||||
pub mod errors;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fallback;
|
||||
pub mod landing;
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::landing::*;
|
||||
use leptos::*;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
use crate::landing::App;
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(|| {
|
||||
view! { <App/> }
|
||||
});
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr_imports {
|
||||
use axum::extract::State;
|
||||
pub use axum::{
|
||||
body::Body as AxumBody,
|
||||
extract::{Path, State},
|
||||
extract::Path,
|
||||
http::Request,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
pub use errors_axum::{fallback::*, landing::App};
|
||||
pub use leptos::{logging::log, *};
|
||||
use errors_axum::landing::shell;
|
||||
pub use errors_axum::landing::App;
|
||||
use leptos::{config::LeptosOptions, context::provide_context};
|
||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
|
||||
// This custom handler lets us provide Axum State via context
|
||||
@@ -19,11 +21,10 @@ mod ssr_imports {
|
||||
req: Request<AxumBody>,
|
||||
) -> Response {
|
||||
let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
options.clone(),
|
||||
move || {
|
||||
provide_context(id.clone());
|
||||
},
|
||||
App,
|
||||
move || shell(options.clone()),
|
||||
);
|
||||
handler(req).await.into_response()
|
||||
}
|
||||
@@ -32,18 +33,12 @@ mod ssr_imports {
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use errors_axum::landing::shell;
|
||||
use leptos::config::get_configuration;
|
||||
use ssr_imports::*;
|
||||
|
||||
simple_logger::init_with_level(log::Level::Debug)
|
||||
.expect("couldn't initialize logging");
|
||||
|
||||
// Explicit server function registration is no longer required
|
||||
// on the main branch. On 0.3.0 and earlier, uncomment the lines
|
||||
// below to register the server functions.
|
||||
// _ = CauseInternalServerError::register();
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
@@ -51,13 +46,16 @@ async fn main() {
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/special/:id", get(custom_handler))
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
// run our app with hyper
|
||||
// `axum::Server` is a re-export of `hyper::Server`
|
||||
log!("listening on http://{}", &addr);
|
||||
println!("listening on http://{}", &addr);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
|
||||
@@ -8,13 +8,17 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
reqwasm = "0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
thiserror = "1"
|
||||
leptos = { path = "../../leptos", features = ["csr", "tracing"] }
|
||||
reqwasm = "0.5.0"
|
||||
gloo-timers = { version = "0.3.0", features = ["futures"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4.22"
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing-subscriber-wasm = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
wasm-bindgen-test = "0.3.42"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use leptos::{error::Result, *};
|
||||
use leptos::prelude::*;
|
||||
use leptos::tachys::html::style::style;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -17,6 +18,7 @@ type CatCount = usize;
|
||||
|
||||
async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
if count > 0 {
|
||||
gloo_timers::future::TimeoutFuture::new(1000).await;
|
||||
// make the request
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={count}",
|
||||
@@ -33,26 +35,26 @@ async fn fetch_cats(count: CatCount) -> Result<Vec<String>> {
|
||||
.collect::<Vec<_>>();
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(CatError::NonZeroCats.into())
|
||||
Err(CatError::NonZeroCats)?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_example() -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<CatCount>(0);
|
||||
let (cat_count, set_cat_count) = signal::<CatCount>(1);
|
||||
|
||||
// we use local_resource here because
|
||||
// 1) our error type isn't serializable/deserializable
|
||||
// 2) we're not doing server-side rendering in this example anyway
|
||||
// (during SSR, create_resource will begin loading on the server and resolve on the client)
|
||||
let cats = create_local_resource(move || cat_count.get(), fetch_cats);
|
||||
// we use new_unsync here because the reqwasm request type isn't Send
|
||||
// if we were doing SSR, then
|
||||
// 1) we'd want to use a Resource, so the data would be serialized to the client
|
||||
// 2) we'd need to make sure there was a thread-local spawner set up
|
||||
let cats = AsyncDerived::new_unsync(move || fetch_cats(cat_count.get()));
|
||||
|
||||
let fallback = move |errors: RwSignal<Errors>| {
|
||||
let fallback = move |errors: ArcRwSignal<Errors>| {
|
||||
let error_list = move || {
|
||||
errors.with(|errors| {
|
||||
errors
|
||||
.iter()
|
||||
.map(|(_, e)| view! { <li>{e.to_string()}</li> })
|
||||
.collect_view()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
};
|
||||
|
||||
@@ -64,17 +66,7 @@ pub fn fetch_example() -> impl IntoView {
|
||||
}
|
||||
};
|
||||
|
||||
// the renderer can handle Option<_> and Result<_> states
|
||||
// by displaying nothing for None if the resource is still loading
|
||||
// and by using the ErrorBoundary fallback to catch Err(_)
|
||||
// so we'll just use `.and_then()` to map over the happy path
|
||||
let cats_view = move || {
|
||||
cats.and_then(|data| {
|
||||
data.iter()
|
||||
.map(|s| view! { <p><img src={s}/></p> })
|
||||
.collect_view()
|
||||
})
|
||||
};
|
||||
let spreadable = style(("background-color", "AliceBlue"));
|
||||
|
||||
view! {
|
||||
<div>
|
||||
@@ -83,19 +75,32 @@ pub fn fetch_example() -> impl IntoView {
|
||||
<input
|
||||
type="number"
|
||||
prop:value=move || cat_count.get().to_string()
|
||||
on:input=move |ev| {
|
||||
let val = event_target_value(&ev).parse::<CatCount>().unwrap_or(0);
|
||||
on:input:target=move |ev| {
|
||||
let val = ev.target().value().parse::<CatCount>().unwrap_or(0);
|
||||
set_cat_count.set(val);
|
||||
}
|
||||
/>
|
||||
|
||||
</label>
|
||||
<Transition fallback=move || {
|
||||
view! { <div>"Loading (Suspense Fallback)..."</div> }
|
||||
}>
|
||||
<Transition fallback=|| view! { <div>"Loading..."</div> } {..spreadable}>
|
||||
<ErrorBoundary fallback>
|
||||
<div>
|
||||
{cats_view}
|
||||
</div>
|
||||
<ul>
|
||||
{move || Suspend::new(async move {
|
||||
cats.await
|
||||
.map(|cats| {
|
||||
cats.iter()
|
||||
.map(|s| {
|
||||
view! {
|
||||
<li>
|
||||
<img src=s.clone()/>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})}
|
||||
|
||||
</ul>
|
||||
</ErrorBoundary>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
use fetch::fetch_example;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
pub fn main() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
use tracing_subscriber::fmt;
|
||||
use tracing_subscriber_wasm::MakeConsoleWriter;
|
||||
|
||||
fmt()
|
||||
.with_writer(
|
||||
// To avoide trace events in the browser from showing their
|
||||
// JS backtrace, which is very annoying, in my opinion
|
||||
MakeConsoleWriter::default()
|
||||
.map_trace_level_to(tracing::Level::DEBUG),
|
||||
)
|
||||
// For some reason, if we don't do this in the browser, we get
|
||||
// a runtime error.
|
||||
.without_time()
|
||||
.init();
|
||||
console_error_panic_hook::set_once();
|
||||
mount_to_body(fetch_example)
|
||||
}
|
||||
|
||||
@@ -4,5 +4,15 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
gtk = { version = "0.5.0", package = "gtk4" }
|
||||
leptos = { path = "../../leptos" }
|
||||
throw_error = { path = "../../any_error/" }
|
||||
|
||||
# these are used to build the integration
|
||||
gtk = { version = "0.9.0", package = "gtk4" }
|
||||
next_tuple = { path = "../../next_tuple/" }
|
||||
paste = "1.0"
|
||||
|
||||
# we want to support using glib for the reactive runtime event loop
|
||||
any_spawner = { path = "../../any_spawner/", features = ["glib"] }
|
||||
# yes, we want effects to run: this is a "frontend," not a backend
|
||||
reactive_graph = { path = "../../reactive_graph", features = ["effects"] }
|
||||
|
||||
8
examples/gtk/index.html
Normal file
8
examples/gtk/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="color-scheme" content="dark">
|
||||
<link rel="css" href="style.css" data-trunk>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
627
examples/gtk/src/leptos_gtk.rs
Normal file
627
examples/gtk/src/leptos_gtk.rs
Normal file
@@ -0,0 +1,627 @@
|
||||
use self::properties::Connect;
|
||||
use gtk::{
|
||||
ffi::GtkWidget,
|
||||
glib::{
|
||||
object::{IsA, IsClass, ObjectExt},
|
||||
Object, Value,
|
||||
},
|
||||
prelude::{Cast, WidgetExt},
|
||||
Label, Orientation, Widget,
|
||||
};
|
||||
use leptos::{
|
||||
reactive_graph::effect::RenderEffect,
|
||||
tachys::{
|
||||
renderer::{CastFrom, Renderer},
|
||||
view::{Mountable, Render},
|
||||
},
|
||||
};
|
||||
use next_tuple::NextTuple;
|
||||
use std::{borrow::Cow, marker::PhantomData};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LeptosGtk;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Element(pub Widget);
|
||||
|
||||
impl Element {
|
||||
pub fn remove(&self) {
|
||||
self.0.unparent();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Text(pub Element);
|
||||
|
||||
impl<T> From<T> for Element
|
||||
where
|
||||
T: Into<Widget>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Element(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<LeptosGtk> for Element {
|
||||
fn unmount(&mut self) {
|
||||
self.remove()
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
if let Some(parent) = self.0.parent() {
|
||||
child.mount(&Element(parent), Some(self));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Mountable<LeptosGtk> for Text {
|
||||
fn unmount(&mut self) {
|
||||
self.0.remove()
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.0
|
||||
.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|m| &m.0));
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
self.0.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastFrom<Element> for Element {
|
||||
fn cast_from(source: Element) -> Option<Self> {
|
||||
Some(source)
|
||||
}
|
||||
}
|
||||
|
||||
impl CastFrom<Element> for Text {
|
||||
fn cast_from(source: Element) -> Option<Self> {
|
||||
source
|
||||
.0
|
||||
.downcast::<Label>()
|
||||
.ok()
|
||||
.map(|n| Text(Element::from(n)))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Element> for Element {
|
||||
fn as_ref(&self) -> &Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Element> for Text {
|
||||
fn as_ref(&self) -> &Element {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for LeptosGtk {
|
||||
type Node = Element;
|
||||
type Element = Element;
|
||||
type Text = Text;
|
||||
type Placeholder = Element;
|
||||
|
||||
fn intern(text: &str) -> &str {
|
||||
text
|
||||
}
|
||||
|
||||
fn create_text_node(text: &str) -> Self::Text {
|
||||
Text(Element::from(Label::new(Some(text))))
|
||||
}
|
||||
|
||||
fn create_placeholder() -> Self::Placeholder {
|
||||
let label = Label::new(None);
|
||||
label.set_visible(false);
|
||||
Element::from(label)
|
||||
}
|
||||
|
||||
fn set_text(node: &Self::Text, text: &str) {
|
||||
let node_as_text = node.0 .0.downcast_ref::<Label>().unwrap();
|
||||
node_as_text.set_label(text);
|
||||
}
|
||||
|
||||
fn set_attribute(node: &Self::Element, name: &str, value: &str) {
|
||||
node.0.set_property(name, value);
|
||||
}
|
||||
|
||||
fn remove_attribute(node: &Self::Element, name: &str) {
|
||||
node.0.set_property(name, None::<&str>);
|
||||
}
|
||||
|
||||
fn insert_node(
|
||||
parent: &Self::Element,
|
||||
new_child: &Self::Node,
|
||||
marker: Option<&Self::Node>,
|
||||
) {
|
||||
new_child
|
||||
.0
|
||||
.insert_before(&parent.0, marker.as_ref().map(|n| &n.0));
|
||||
}
|
||||
|
||||
fn remove_node(
|
||||
parent: &Self::Element,
|
||||
child: &Self::Node,
|
||||
) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn remove(node: &Self::Node) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_parent(node: &Self::Node) -> Option<Self::Node> {
|
||||
node.0.parent().map(Element::from)
|
||||
}
|
||||
|
||||
fn first_child(node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn next_sibling(node: &Self::Node) -> Option<Self::Node> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn log_node(node: &Self::Node) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn clear_children(parent: &Self::Element) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root<Chil>(children: Chil) -> (Widget, impl Mountable<LeptosGtk>)
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
let state = r#box()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
.build();
|
||||
(state.as_widget().clone(), state)
|
||||
}
|
||||
|
||||
pub trait WidgetClass {
|
||||
type Widget: Into<Widget> + IsA<Object> + IsClass;
|
||||
}
|
||||
|
||||
pub struct LGtkWidget<Widg, Props, Chil> {
|
||||
widget: PhantomData<Widg>,
|
||||
properties: Props,
|
||||
children: Chil,
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Chil: NextTuple,
|
||||
{
|
||||
pub fn child<T>(
|
||||
self,
|
||||
child: T,
|
||||
) -> LGtkWidget<Widg, Props, Chil::Output<T>> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children: children.next_tuple(child),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn connect<F>(
|
||||
self,
|
||||
signal_name: &'static str,
|
||||
callback: F,
|
||||
) -> LGtkWidget<Widg, Props::Output<Connect<F>>, Chil>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Connect {
|
||||
signal_name,
|
||||
callback,
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
Props: Property,
|
||||
Widg: WidgetClass,
|
||||
{
|
||||
ty: PhantomData<Widg>,
|
||||
widget: Element,
|
||||
properties: Props::State,
|
||||
children: Chil::State,
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Chil: Render<LeptosGtk>,
|
||||
Props: Property,
|
||||
Widg: WidgetClass,
|
||||
{
|
||||
pub fn as_widget(&self) -> &Widget {
|
||||
&self.widget.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> Render<LeptosGtk> for LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: Property,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
type State = LGtkWidgetState<Widg, Props, Chil>;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let widget = Object::new::<Widg::Widget>();
|
||||
let widget = Element::from(widget);
|
||||
let properties = self.properties.build(&widget);
|
||||
let mut children = self.children.build();
|
||||
children.mount(&widget, None);
|
||||
LGtkWidgetState {
|
||||
ty: PhantomData,
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
self.properties
|
||||
.rebuild(&state.widget, &mut state.properties);
|
||||
self.children.rebuild(&mut state.children);
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> Mountable<LeptosGtk>
|
||||
for LGtkWidgetState<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: Property,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
fn unmount(&mut self) {
|
||||
self.children.unmount();
|
||||
self.widget.remove();
|
||||
}
|
||||
|
||||
fn mount(
|
||||
&mut self,
|
||||
parent: &<LeptosGtk as Renderer>::Element,
|
||||
marker: Option<&<LeptosGtk as Renderer>::Node>,
|
||||
) {
|
||||
self.children.mount(&self.widget, None);
|
||||
LeptosGtk::insert_node(parent, &self.widget, marker);
|
||||
}
|
||||
|
||||
fn insert_before_this(&self, child: &mut dyn Mountable<LeptosGtk>) -> bool {
|
||||
self.widget.insert_before_this(child)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Property {
|
||||
type State;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State;
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State);
|
||||
}
|
||||
|
||||
impl<T, F> Property for F
|
||||
where
|
||||
T: Property,
|
||||
T::State: 'static,
|
||||
F: Fn() -> T + 'static,
|
||||
{
|
||||
type State = RenderEffect<T::State>;
|
||||
|
||||
fn build(self, widget: &Element) -> Self::State {
|
||||
let widget = widget.clone();
|
||||
RenderEffect::new(move |prev| {
|
||||
let value = self();
|
||||
if let Some(mut prev) = prev {
|
||||
value.rebuild(&widget, &mut prev);
|
||||
prev
|
||||
} else {
|
||||
value.build(&widget)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn rebuild(self, widget: &Element, state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
pub fn button() -> LGtkWidget<gtk::Button, (), ()> {
|
||||
LGtkWidget {
|
||||
widget: PhantomData,
|
||||
properties: (),
|
||||
children: (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#box() -> LGtkWidget<gtk::Box, (), ()> {
|
||||
LGtkWidget {
|
||||
widget: PhantomData,
|
||||
properties: (),
|
||||
children: (),
|
||||
}
|
||||
}
|
||||
|
||||
mod widgets {
|
||||
use super::WidgetClass;
|
||||
|
||||
impl WidgetClass for gtk::Button {
|
||||
type Widget = Self;
|
||||
}
|
||||
|
||||
impl WidgetClass for gtk::Box {
|
||||
type Widget = Self;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod properties {
|
||||
use super::{
|
||||
Element, LGtkWidget, LGtkWidgetState, LeptosGtk, Property, WidgetClass,
|
||||
};
|
||||
use gtk::glib::{object::ObjectExt, Value};
|
||||
use leptos::tachys::{renderer::Renderer, view::Render};
|
||||
use next_tuple::NextTuple;
|
||||
|
||||
pub struct Connect<F>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
pub signal_name: &'static str,
|
||||
pub callback: F,
|
||||
}
|
||||
|
||||
impl<F> Property for Connect<F>
|
||||
where
|
||||
F: Fn(&[Value]) -> Option<Value> + Send + Sync + 'static,
|
||||
{
|
||||
type State = ();
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.connect(self.signal_name, false, self.callback);
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
/* examples for macro */
|
||||
pub struct Orientation {
|
||||
value: gtk::Orientation,
|
||||
}
|
||||
|
||||
pub struct OrientationState {
|
||||
value: gtk::Orientation,
|
||||
}
|
||||
|
||||
impl Property for Orientation {
|
||||
type State = OrientationState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.set_property("orientation", self.value);
|
||||
OrientationState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
if self.value != state.value {
|
||||
element.0.set_property("orientation", self.value);
|
||||
state.value = self.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn orientation(
|
||||
self,
|
||||
value: impl Into<gtk::Orientation>,
|
||||
) -> LGtkWidget<Widg, Props::Output<Orientation>, Chil> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Orientation {
|
||||
value: value.into(),
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Spacing {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
pub struct SpacingState {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
impl Property for Spacing {
|
||||
type State = SpacingState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
element.0.set_property("spacing", self.value);
|
||||
SpacingState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
if self.value != state.value {
|
||||
element.0.set_property("spacing", self.value);
|
||||
state.value = self.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Widg, Props, Chil> LGtkWidget<Widg, Props, Chil>
|
||||
where
|
||||
Widg: WidgetClass,
|
||||
Props: NextTuple,
|
||||
Chil: Render<LeptosGtk>,
|
||||
{
|
||||
pub fn spacing(
|
||||
self,
|
||||
value: impl Into<i32>,
|
||||
) -> LGtkWidget<Widg, Props::Output<Spacing>, Chil> {
|
||||
let LGtkWidget {
|
||||
widget,
|
||||
properties,
|
||||
children,
|
||||
} = self;
|
||||
LGtkWidget {
|
||||
widget,
|
||||
properties: properties.next_tuple(Spacing {
|
||||
value: value.into(),
|
||||
}),
|
||||
children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* end examples for properties macro */
|
||||
|
||||
pub struct Label {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self {
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LabelState {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Property for Label {
|
||||
type State = LabelState;
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
LeptosGtk::set_attribute(element, "label", &self.value);
|
||||
LabelState { value: self.value }
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Property for () {
|
||||
type State = ();
|
||||
|
||||
fn build(self, _element: &Element) -> Self::State {}
|
||||
|
||||
fn rebuild(self, _element: &Element, _state: &mut Self::State) {}
|
||||
}
|
||||
|
||||
macro_rules! tuples {
|
||||
($($ty:ident),* $(,)?) => {
|
||||
impl<$($ty,)*> Property for ($($ty,)*)
|
||||
where $($ty: Property,)*
|
||||
{
|
||||
type State = ($($ty::State,)*);
|
||||
|
||||
fn build(self, element: &Element) -> Self::State {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($ty,)*) = self;
|
||||
($($ty.build(element),)*)
|
||||
}
|
||||
|
||||
fn rebuild(self, element: &Element, state: &mut Self::State) {
|
||||
paste::paste! {
|
||||
#[allow(non_snake_case)]
|
||||
let ($($ty,)*) = self;
|
||||
#[allow(non_snake_case)]
|
||||
let ($([<state_ $ty:lower>],)*) = state;
|
||||
$($ty.rebuild(element, [<state_ $ty:lower>]));*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tuples!(A);
|
||||
tuples!(A, B);
|
||||
tuples!(A, B, C);
|
||||
tuples!(A, B, C, D);
|
||||
tuples!(A, B, C, D, E);
|
||||
tuples!(A, B, C, D, E, F);
|
||||
tuples!(A, B, C, D, E, F, G);
|
||||
tuples!(A, B, C, D, E, F, G, H);
|
||||
tuples!(A, B, C, D, E, F, G, H, I);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
|
||||
tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W
|
||||
);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X
|
||||
);
|
||||
tuples!(
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X,
|
||||
Y
|
||||
);
|
||||
}
|
||||
@@ -1,59 +1,107 @@
|
||||
use gtk::{prelude::*, Application, ApplicationWindow, Button};
|
||||
use leptos::*;
|
||||
use any_spawner::Executor;
|
||||
use gtk::{prelude::*, Application, ApplicationWindow, Orientation};
|
||||
use leptos::prelude::*;
|
||||
use leptos_gtk::LeptosGtk;
|
||||
use std::{mem, thread, time::Duration};
|
||||
mod leptos_gtk;
|
||||
|
||||
const APP_ID: &str = "dev.leptos.Counter";
|
||||
|
||||
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
|
||||
fn main() {
|
||||
let _ = create_runtime();
|
||||
// Create a new application
|
||||
// use the glib event loop to power the reactive system
|
||||
_ = Executor::init_glib();
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
// Connect to "activate" signal of `app`
|
||||
app.connect_activate(build_ui);
|
||||
app.connect_startup(|_| load_css());
|
||||
|
||||
app.connect_activate(|app| {
|
||||
// Connect to "activate" signal of `app`
|
||||
let owner = Owner::new();
|
||||
let view = owner.with(ui);
|
||||
let (root, state) = leptos_gtk::root(view);
|
||||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("TachyGTK")
|
||||
.child(&root)
|
||||
.build();
|
||||
// Present window
|
||||
window.present();
|
||||
mem::forget((owner, state));
|
||||
});
|
||||
|
||||
// Run the application
|
||||
app.run();
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
let button = counter_button();
|
||||
fn ui() -> impl Render<LeptosGtk> {
|
||||
let value = RwSignal::new(0);
|
||||
let rows = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
|
||||
// Create a window and set the title
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("Leptos-GTK")
|
||||
.child(&button)
|
||||
.build();
|
||||
|
||||
// Present window
|
||||
window.present();
|
||||
}
|
||||
|
||||
fn counter_button() -> Button {
|
||||
let (value, set_value) = create_signal(0);
|
||||
|
||||
// Create a button with label and margins
|
||||
let button = Button::builder()
|
||||
.label("Count: ")
|
||||
.margin_top(12)
|
||||
.margin_bottom(12)
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.build();
|
||||
|
||||
// Connect to "clicked" signal of `button`
|
||||
button.connect_clicked(move |_| {
|
||||
// Set the label to "Hello World!" after the button has been clicked on
|
||||
set_value.update(|value| *value += 1);
|
||||
Effect::new(move |_| {
|
||||
println!("value = {}", value.get());
|
||||
});
|
||||
|
||||
create_effect({
|
||||
let button = button.clone();
|
||||
move |_| {
|
||||
button.set_label(&format!("Count: {}", value.get()));
|
||||
}
|
||||
// just an example of multithreaded reactivity
|
||||
thread::spawn(move || loop {
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
value.update(|n| *n += 1);
|
||||
});
|
||||
|
||||
button
|
||||
vstack((
|
||||
hstack((
|
||||
button("-1", move || {
|
||||
println!("clicked -1");
|
||||
value.update(|n| *n -= 1);
|
||||
}),
|
||||
move || value.get().to_string(),
|
||||
button("+1", move || value.update(|n| *n += 1)),
|
||||
)),
|
||||
button("Swap", move || {
|
||||
rows.update(|items| {
|
||||
items.swap(1, 3);
|
||||
})
|
||||
}),
|
||||
hstack(rows),
|
||||
))
|
||||
}
|
||||
|
||||
fn button(
|
||||
label: impl Render<LeptosGtk>,
|
||||
callback: impl Fn() + Send + Sync + 'static,
|
||||
) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::button()
|
||||
.child(label)
|
||||
.connect("clicked", move |_| {
|
||||
callback();
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
fn vstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::r#box()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
}
|
||||
|
||||
fn hstack(children: impl Render<LeptosGtk>) -> impl Render<LeptosGtk> {
|
||||
leptos_gtk::r#box()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(12)
|
||||
.child(children)
|
||||
}
|
||||
|
||||
fn load_css() {
|
||||
use gtk::{gdk::Display, CssProvider};
|
||||
|
||||
let provider = CssProvider::new();
|
||||
provider.load_from_path("style.css");
|
||||
|
||||
// Add the provider to the default screen
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&Display::default().expect("Could not connect to a display."),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,37 +8,32 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
actix-files = { version = "0.6.6", optional = true }
|
||||
actix-web = { version = "4.8", optional = true, features = ["macros"] }
|
||||
console_log = "1.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tracing = "0.1"
|
||||
# openssl = { version = "0.10", features = ["v110"] }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
gloo-net = { version = "0.6.0", features = ["http"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
web-sys = { version = "0.3.70", features = ["AbortController", "AbortSignal"] }
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["dep:actix-files", "dep:actix-web", "dep:leptos_actix", "leptos/ssr"]
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::Serializable;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
@@ -10,46 +10,51 @@ pub fn user(path: &str) -> String {
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
pub fn fetch_api<T>(
|
||||
path: &str,
|
||||
) -> impl std::future::Future<Output = Option<T>> + Send + '_
|
||||
where
|
||||
T: Serializable,
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
let abort_controller = web_sys::AbortController::new().ok();
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
use leptos::prelude::on_cleanup;
|
||||
use send_wrapper::SendWrapper;
|
||||
|
||||
// abort in-flight requests if, e.g., we've navigated away from this page
|
||||
leptos::on_cleanup(move || {
|
||||
if let Some(abort_controller) = abort_controller {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
SendWrapper::new(async move {
|
||||
let abort_controller =
|
||||
SendWrapper::new(web_sys::AbortController::new().ok());
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
|
||||
let json = gloo_net::http::Request::get(path)
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
// abort in-flight requests if, e.g., we've navigated away from this page
|
||||
on_cleanup(move || {
|
||||
if let Some(abort_controller) = abort_controller.take() {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
|
||||
T::de(&json).ok()
|
||||
gloo_net::http::Request::get(path)
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.json()
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
let json = reqwest::get(path)
|
||||
reqwest::get(path)
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.text()
|
||||
.json()
|
||||
.await
|
||||
.ok()?;
|
||||
T::de(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
mod api;
|
||||
mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router, RoutingProgress},
|
||||
ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
use std::time::Duration;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
let (is_routing, set_is_routing) = create_signal(false);
|
||||
let (is_routing, set_is_routing) = signal(false);
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
// adding `set_is_routing` causes the router to wait for async data to load on new pages
|
||||
<Router set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<div class="routing-progress">
|
||||
<RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/>
|
||||
<RoutingProgress is_routing max_time=Duration::from_millis(250)/>
|
||||
</div>
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
// TODO allow optional params without duplication
|
||||
<Route path=StaticSegment("") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
@@ -35,7 +40,6 @@ pub fn App() -> impl IntoView {
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App);
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ mod ssr_imports {
|
||||
pub use actix_files::Files;
|
||||
pub use actix_web::*;
|
||||
pub use hackernews::App;
|
||||
pub use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
|
||||
#[get("/style.css")]
|
||||
pub async fn css() -> impl Responder {
|
||||
@@ -19,24 +18,44 @@ mod ssr_imports {
|
||||
#[cfg(feature = "ssr")]
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use leptos::get_configuration;
|
||||
use leptos::prelude::*;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use leptos_meta::MetaTags;
|
||||
use ssr_imports::*;
|
||||
|
||||
// Setting this to None means we'll be using cargo-leptos and its env vars.
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
HttpServer::new(move || {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
|
||||
App::new()
|
||||
.service(css)
|
||||
.service(favicon)
|
||||
.leptos_routes(leptos_options.to_owned(), routes.to_owned(), App)
|
||||
.leptos_routes(routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || {
|
||||
use leptos::prelude::*;
|
||||
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<AutoReload options=leptos_options.clone() />
|
||||
<HydrationScripts options=leptos_options.clone()/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}})
|
||||
.service(Files::new("/", site_root))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
@@ -49,8 +68,6 @@ async fn main() -> std::io::Result<()> {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn main() {
|
||||
use hackernews::App;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App)
|
||||
leptos::mount::mount_to_body(App)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use leptos::{component, view, IntoView};
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
|
||||
#[component]
|
||||
pub fn Nav() -> impl IntoView {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user