mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 07:52:34 -05:00
Compare commits
629 Commits
v0.7.0-bet
...
custom-vie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cc6ccaacb | ||
|
|
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 |
4
.github/workflows/ci-semver.yml
vendored
4
.github/workflows/ci-semver.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2024-04-14)
|
||||
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
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -49,4 +49,4 @@ jobs:
|
||||
with:
|
||||
directory: ${{ matrix.directory }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2024-04-14
|
||||
toolchain: nightly-2024-08-01
|
||||
|
||||
41
Cargo.toml
41
Cargo.toml
@@ -40,34 +40,35 @@ members = [
|
||||
exclude = ["benchmarks", "examples"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.0-alpha"
|
||||
version = "0.7.0-beta"
|
||||
rust-version = "1.75"
|
||||
|
||||
[workspace.dependencies]
|
||||
throw_error = { path = "./any_error/", version = "0.1" }
|
||||
throw_error = { path = "./any_error/", version = "0.2.0-beta" }
|
||||
any_spawner = { path = "./any_spawner/", version = "0.1" }
|
||||
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
|
||||
either_of = { path = "./either_of/", version = "0.1" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-alpha" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-alpha" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-alpha" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-alpha" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-alpha" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-alpha" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-alpha" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-alpha" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-alpha" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-alpha" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-alpha" }
|
||||
hydration_context = { path = "./hydration_context", version = "0.2.0-beta" }
|
||||
leptos = { path = "./leptos", version = "0.7.0-beta" }
|
||||
leptos_config = { path = "./leptos_config", version = "0.7.0-beta" }
|
||||
leptos_dom = { path = "./leptos_dom", version = "0.7.0-beta" }
|
||||
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.7.0-beta" }
|
||||
leptos_integration_utils = { path = "./integrations/utils", version = "0.7.0-beta" }
|
||||
leptos_macro = { path = "./leptos_macro", version = "0.7.0-beta" }
|
||||
leptos_router = { path = "./router", version = "0.7.0-beta" }
|
||||
leptos_router_macro = { path = "./router_macro", version = "0.7.0-beta" }
|
||||
leptos_server = { path = "./leptos_server", version = "0.7.0-beta" }
|
||||
leptos_meta = { path = "./meta", version = "0.7.0-beta" }
|
||||
next_tuple = { path = "./next_tuple", version = "0.1.0-beta" }
|
||||
oco_ref = { path = "./oco", version = "0.2" }
|
||||
or_poisoned = { path = "./or_poisoned", version = "0.1" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-alpha" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-alpha" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-alpha" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-alpha" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-alpha" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-alpha" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-alpha" }
|
||||
reactive_graph = { path = "./reactive_graph", version = "0.1.0-beta" }
|
||||
reactive_stores = { path = "./reactive_stores", version = "0.1.0-beta" }
|
||||
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.1.0-beta" }
|
||||
server_fn = { path = "./server_fn", version = "0.7.0-beta" }
|
||||
server_fn_macro = { path = "./server_fn_macro", version = "0.7.0-beta" }
|
||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.7.0-beta" }
|
||||
tachys = { path = "./tachys", version = "0.1.0-beta" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "throw_error"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0-beta"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
error,
|
||||
fmt::{self, Display},
|
||||
future::Future,
|
||||
ops,
|
||||
mem, ops,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
@@ -92,9 +92,25 @@ 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>) {
|
||||
ERROR_HOOK.with_borrow_mut(|this| *this = Some(hook))
|
||||
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.
|
||||
@@ -140,9 +156,10 @@ where
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
if let Some(hook) = &this.hook {
|
||||
set_error_hook(Arc::clone(hook))
|
||||
}
|
||||
let _hook = this
|
||||
.hook
|
||||
.as_ref()
|
||||
.map(|hook| set_error_hook(Arc::clone(hook)));
|
||||
this.inner.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
log = "0.4"
|
||||
once_cell = "1.18"
|
||||
gloo-net = { git = "https://github.com/rustwasm/gloo" }
|
||||
gloo-net = { version = "0.6" }
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
simple_logger = "4.3"
|
||||
@@ -33,12 +33,12 @@ send_wrapper = "0.6.0"
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:tracing",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_router/ssr",
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:tracing",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
@@ -113,10 +113,10 @@ pub fn Counter() -> impl IntoView {
|
||||
</p>
|
||||
<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>
|
||||
<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>
|
||||
<button on:click=move |_| { inc.dispatch(()); }>"+1"</button>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
@@ -224,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>
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ 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"] }
|
||||
gloo-net = { version = "0.6", features = ["http"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
send_wrapper = "0.6.0"
|
||||
@@ -32,12 +32,7 @@ send_wrapper = "0.6.0"
|
||||
[features]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
]
|
||||
ssr = ["dep:actix-files", "dep:actix-web", "dep:leptos_actix", "leptos/ssr"]
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
|
||||
@@ -18,8 +18,8 @@ leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
gloo-net = { version = "0.6", features = ["http"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
axum = { version = "0.7", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
|
||||
@@ -12,16 +12,14 @@ lto = true
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos", features = [
|
||||
"experimental-islands",
|
||||
] }
|
||||
leptos = { path = "../../leptos", features = ["experimental-islands"] }
|
||||
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router"}
|
||||
leptos_router = { path = "../../router" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.4", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
gloo-net = { version = "0.6", features = ["http"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
axum = { version = "0.7", optional = true, features = ["http2"] }
|
||||
tower = { version = "0.4", optional = true }
|
||||
tower-http = { version = "0.5", features = [
|
||||
@@ -34,7 +32,11 @@ http = { version = "1.0", optional = true }
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
wasm-bindgen = "0.2"
|
||||
lazy_static = "1.4.0"
|
||||
rust-embed = { version = "8", features = ["axum", "mime_guess", "tokio"], optional = true }
|
||||
rust-embed = { version = "8", features = [
|
||||
"axum",
|
||||
"mime_guess",
|
||||
"tokio",
|
||||
], optional = true }
|
||||
mime_guess = { version = "2.0.4", optional = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -21,7 +21,7 @@ leptos_router = { path = "../../router" }
|
||||
leptos_server = { path = "../../leptos_server", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
gloo-net = { version = "0.5", features = ["http"] }
|
||||
gloo-net = { version = "0.6", features = ["http"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
axum = { version = "0.7", default-features = false, optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
|
||||
@@ -21,11 +21,7 @@ actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", features = ["macros"], optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:wasm-bindgen",
|
||||
"dep:console_error_panic_hook",
|
||||
]
|
||||
hydrate = ["leptos/hydrate", "dep:wasm-bindgen", "dep:console_error_panic_hook"]
|
||||
ssr = [
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
|
||||
@@ -7,5 +7,5 @@ edition = "2021"
|
||||
leptos = { path = "../../leptos", features = ["csr"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_router = { path = "../../router" }
|
||||
gloo-net = { version = "0.5", features = ["http"] }
|
||||
gloo-net = { version = "0.6", features = ["http"] }
|
||||
console_error_panic_hook = { version = "0.1" }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "hydration_context"
|
||||
edition = "2021"
|
||||
version = "0.2.0-alpha"
|
||||
version = "0.2.0-beta"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
readme = "../README.md"
|
||||
|
||||
@@ -44,6 +44,12 @@ pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
|
||||
/// from the server to the client.
|
||||
pub struct SerializedDataId(usize);
|
||||
|
||||
impl From<SerializedDataId> for ErrorId {
|
||||
fn from(value: SerializedDataId) -> Self {
|
||||
value.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information that will be shared between the server and the client.
|
||||
pub trait SharedContext: Debug {
|
||||
/// Whether the application is running in the browser.
|
||||
|
||||
@@ -16,6 +16,7 @@ use actix_web::{
|
||||
};
|
||||
use futures::{stream::once, Stream, StreamExt};
|
||||
use http::StatusCode;
|
||||
use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
context::{provide_context, use_context},
|
||||
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||
@@ -301,8 +302,9 @@ pub fn handle_server_fns_with_context(
|
||||
let additional_context = additional_context.clone();
|
||||
|
||||
let path = req.path();
|
||||
let method = req.method();
|
||||
if let Some(mut service) =
|
||||
server_fn::actix::get_server_fn_service(path)
|
||||
server_fn::actix::get_server_fn_service(path, method)
|
||||
{
|
||||
let owner = Owner::new();
|
||||
owner
|
||||
@@ -384,7 +386,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
@@ -406,11 +408,7 @@ pub fn handle_server_fns_with_context(
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_app_to_stream(
|
||||
/// leptos_options.to_owned(),
|
||||
/// || view! { <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// leptos_actix::render_app_to_stream(MyApp, Method::Get),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
@@ -452,7 +450,7 @@ where
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
@@ -475,8 +473,7 @@ where
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_app_to_stream_in_order(
|
||||
/// leptos_options.to_owned(),
|
||||
/// || view! { <MyApp/> },
|
||||
/// MyApp,
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// )
|
||||
@@ -518,7 +515,7 @@ where
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use actix_web::{App, HttpServer};
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_router::Method;
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
@@ -540,11 +537,7 @@ where
|
||||
/// // the actual routing will be handled by `leptos_router`
|
||||
/// .route(
|
||||
/// "/{tail:.*}",
|
||||
/// leptos_actix::render_app_async(
|
||||
/// leptos_options.to_owned(),
|
||||
/// || view! { <MyApp/> },
|
||||
/// Method::Get,
|
||||
/// ),
|
||||
/// leptos_actix::render_app_async(MyApp, Method::Get),
|
||||
/// )
|
||||
/// })
|
||||
/// .bind(&addr)?
|
||||
@@ -944,7 +937,7 @@ where
|
||||
{
|
||||
let _ = any_spawner::Executor::init_tokio();
|
||||
|
||||
let owner = Owner::new_root(None);
|
||||
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
|
||||
let (mock_meta, _) = ServerMetaContext::new();
|
||||
let routes = owner
|
||||
.with(|| {
|
||||
@@ -1380,18 +1373,21 @@ impl LeptosRoutes for &mut ServiceConfig {
|
||||
///
|
||||
/// Any error that occurs during extraction is converted to a [`ServerFnError`].
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // MyQuery is some type that implements `Deserialize + Serialize`
|
||||
/// ```rust
|
||||
/// use leptos::prelude::*;
|
||||
///
|
||||
/// #[server]
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use actix_web::web::Query;
|
||||
/// pub async fn extract_connection_info() -> Result<String, ServerFnError> {
|
||||
/// use actix_web::dev::ConnectionInfo;
|
||||
/// use leptos_actix::*;
|
||||
///
|
||||
/// let Query(data) = extract().await?;
|
||||
/// // this can be any type you can use an Actix extractor with, as long as
|
||||
/// // it works on the head, not the body of the request
|
||||
/// let info: ConnectionInfo = extract().await?;
|
||||
///
|
||||
/// // do something with the data
|
||||
///
|
||||
/// Ok(data)
|
||||
/// Ok(format!("{info:?}"))
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract<T>() -> Result<T, ServerFnError>
|
||||
@@ -1399,7 +1395,7 @@ where
|
||||
T: actix_web::FromRequest,
|
||||
<T as FromRequest>::Error: Display,
|
||||
{
|
||||
let req = use_context::<HttpRequest>().ok_or_else(|| {
|
||||
let req = use_context::<Request>().ok_or_else(|| {
|
||||
ServerFnError::new("HttpRequest should have been provided via context")
|
||||
})?;
|
||||
|
||||
|
||||
@@ -1,148 +1,153 @@
|
||||
use leptos::*;
|
||||
use leptos_actix::generate_route_list;
|
||||
use leptos_router::{Route, Router, Routes, TrailingSlash};
|
||||
// TODO these tests relate to trailing-slash logic, which is still TBD for 0.7
|
||||
|
||||
#[component]
|
||||
fn DefaultApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_app() {
|
||||
let routes = generate_route_list(DefaultApp);
|
||||
|
||||
// We still have access to the original (albeit normalized) Leptos paths:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&["/bar", "/baz/*any", "/baz/:id", "/baz/:name", "/foo"],
|
||||
);
|
||||
|
||||
// ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&["/bar", "/baz/{id}", "/baz/{name}", "/baz/{tail:.*}", "/foo"],
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn ExactApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Exact;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_app() {
|
||||
let routes = generate_route_list(ExactApp);
|
||||
|
||||
// In Exact mode, the Leptos paths no longer have their trailing slashes stripped:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&["/bar/", "/baz/*any", "/baz/:id", "/baz/:name/", "/foo"],
|
||||
);
|
||||
|
||||
// Actix paths also have trailing slashes as a result:
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&[
|
||||
"/bar/",
|
||||
"/baz/{id}",
|
||||
"/baz/{name}/",
|
||||
"/baz/{tail:.*}",
|
||||
"/foo",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn RedirectApp() -> impl IntoView {
|
||||
let view = || view! { "" };
|
||||
let trailing_slash = TrailingSlash::Redirect;
|
||||
view! {
|
||||
<Router trailing_slash>
|
||||
<Routes>
|
||||
<Route path="/foo" view/>
|
||||
<Route path="/bar/" view/>
|
||||
<Route path="/baz/:id" view/>
|
||||
<Route path="/baz/:name/" view/>
|
||||
<Route path="/baz/*any" view/>
|
||||
</Routes>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redirect_app() {
|
||||
let routes = generate_route_list(RedirectApp);
|
||||
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.leptos_path(),
|
||||
&[
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/*any",
|
||||
"/baz/:id",
|
||||
"/baz/:id/",
|
||||
"/baz/:name",
|
||||
"/baz/:name/",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
],
|
||||
);
|
||||
|
||||
// ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
assert_same(
|
||||
&routes,
|
||||
|r| r.path(),
|
||||
&[
|
||||
"/bar",
|
||||
"/bar/",
|
||||
"/baz/{id}",
|
||||
"/baz/{id}/",
|
||||
"/baz/{name}",
|
||||
"/baz/{name}/",
|
||||
"/baz/{tail:.*}",
|
||||
"/foo",
|
||||
"/foo/",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_same<'t, T, F, U>(
|
||||
input: &'t Vec<T>,
|
||||
mapper: F,
|
||||
expected_sorted_values: &[U],
|
||||
) where
|
||||
F: Fn(&'t T) -> U + 't,
|
||||
U: Ord + std::fmt::Debug,
|
||||
{
|
||||
let mut values: Vec<U> = input.iter().map(mapper).collect();
|
||||
values.sort();
|
||||
assert_eq!(values, expected_sorted_values);
|
||||
}
|
||||
// use leptos::*;
|
||||
// use leptos_actix::generate_route_list;
|
||||
// use leptos_router::{
|
||||
// components::{Route, Router, Routes},
|
||||
// path,
|
||||
// };
|
||||
//
|
||||
// #[component]
|
||||
// fn DefaultApp() -> impl IntoView {
|
||||
// let view = || view! { "" };
|
||||
// view! {
|
||||
// <Router>
|
||||
// <Routes>
|
||||
// <Route path=path!("/foo") view/>
|
||||
// <Route path=path!("/bar/") view/>
|
||||
// <Route path=path!("/baz/:id") view/>
|
||||
// <Route path=path!("/baz/:name/") view/>
|
||||
// <Route path=path!("/baz/*any") view/>
|
||||
// </Routes>
|
||||
// </Router>
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_default_app() {
|
||||
// let routes = generate_route_list(DefaultApp);
|
||||
//
|
||||
// // We still have access to the original (albeit normalized) Leptos paths:
|
||||
// assert_same(
|
||||
// &routes,
|
||||
// |r| r.leptos_path(),
|
||||
// &["/bar", "/baz/*any", "/baz/:id", "/baz/:name", "/foo"],
|
||||
// );
|
||||
//
|
||||
// // ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
// assert_same(
|
||||
// &routes,
|
||||
// |r| r.path(),
|
||||
// &["/bar", "/baz/{id}", "/baz/{name}", "/baz/{tail:.*}", "/foo"],
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// #[component]
|
||||
// fn ExactApp() -> impl IntoView {
|
||||
// let view = || view! { "" };
|
||||
// //let trailing_slash = TrailingSlash::Exact;
|
||||
// view! {
|
||||
// <Router>
|
||||
// <Routes>
|
||||
// <Route path=path!("/foo") view/>
|
||||
// <Route path=path!("/bar/") view/>
|
||||
// <Route path=path!("/baz/:id") view/>
|
||||
// <Route path=path!("/baz/:name/") view/>
|
||||
// <Route path=path!("/baz/*any") view/>
|
||||
// </Routes>
|
||||
// </Router>
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_exact_app() {
|
||||
// let routes = generate_route_list(ExactApp);
|
||||
//
|
||||
// // In Exact mode, the Leptos paths no longer have their trailing slashes stripped:
|
||||
// assert_same(
|
||||
// &routes,
|
||||
// |r| r.leptos_path(),
|
||||
// &["/bar/", "/baz/*any", "/baz/:id", "/baz/:name/", "/foo"],
|
||||
// );
|
||||
//
|
||||
// // Actix paths also have trailing slashes as a result:
|
||||
// assert_same(
|
||||
// &routes,
|
||||
// |r| r.path(),
|
||||
// &[
|
||||
// "/bar/",
|
||||
// "/baz/{id}",
|
||||
// "/baz/{name}/",
|
||||
// "/baz/{tail:.*}",
|
||||
// "/foo",
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// #[component]
|
||||
// fn RedirectApp() -> impl IntoView {
|
||||
// let view = || view! { "" };
|
||||
// //let trailing_slash = TrailingSlash::Redirect;
|
||||
// view! {
|
||||
// <Router>
|
||||
// <Routes>
|
||||
// <Route path=path!("/foo") view/>
|
||||
// <Route path=path!("/bar/") view/>
|
||||
// <Route path=path!("/baz/:id") view/>
|
||||
// <Route path=path!("/baz/:name/") view/>
|
||||
// <Route path=path!("/baz/*any") view/>
|
||||
// </Routes>
|
||||
// </Router>
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_redirect_app() {
|
||||
// let routes = generate_route_list(RedirectApp);
|
||||
//
|
||||
// assert_same(
|
||||
// &routes,
|
||||
// |r| r.leptos_path(),
|
||||
// &[
|
||||
// "/bar",
|
||||
// "/bar/",
|
||||
// "/baz/*any",
|
||||
// "/baz/:id",
|
||||
// "/baz/:id/",
|
||||
// "/baz/:name",
|
||||
// "/baz/:name/",
|
||||
// "/foo",
|
||||
// "/foo/",
|
||||
// ],
|
||||
// );
|
||||
//
|
||||
// // ... But leptos-actix has also reformatted "paths" to work for Actix.
|
||||
// assert_same(
|
||||
// &routes,
|
||||
// |r| r.path(),
|
||||
// &[
|
||||
// "/bar",
|
||||
// "/bar/",
|
||||
// "/baz/{id}",
|
||||
// "/baz/{id}/",
|
||||
// "/baz/{name}",
|
||||
// "/baz/{name}/",
|
||||
// "/baz/{tail:.*}",
|
||||
// "/foo",
|
||||
// "/foo/",
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// fn assert_same<'t, T, F, U>(
|
||||
// input: &'t Vec<T>,
|
||||
// mapper: F,
|
||||
// expected_sorted_values: &[U],
|
||||
// ) where
|
||||
// F: Fn(&'t T) -> U + 't,
|
||||
// U: Ord + std::fmt::Debug,
|
||||
// {
|
||||
// let mut values: Vec<U> = input.iter().map(mapper).collect();
|
||||
// values.sort();
|
||||
// assert_eq!(values, expected_sorted_values);
|
||||
// }
|
||||
|
||||
@@ -32,7 +32,7 @@ tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["net"] }
|
||||
tokio = { version = "1", features = ["net", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
wasm = []
|
||||
|
||||
@@ -34,16 +34,22 @@
|
||||
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
extract::{FromRef, FromRequestParts, MatchedPath, State},
|
||||
extract::{FromRequestParts, MatchedPath},
|
||||
http::{
|
||||
header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER},
|
||||
request::Parts,
|
||||
HeaderMap, Method, Request, Response, StatusCode, Uri,
|
||||
HeaderMap, Method, Request, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, patch, post, put},
|
||||
};
|
||||
#[cfg(feature = "default")]
|
||||
use axum::{
|
||||
extract::{FromRef, State},
|
||||
http::Uri,
|
||||
};
|
||||
use futures::{stream::once, Future, Stream, StreamExt};
|
||||
use hydration_context::SsrSharedContext;
|
||||
use leptos::{
|
||||
config::LeptosOptions,
|
||||
context::{provide_context, use_context},
|
||||
@@ -62,6 +68,7 @@ use leptos_router::{
|
||||
use parking_lot::RwLock;
|
||||
use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
|
||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
|
||||
#[cfg(feature = "default")]
|
||||
use tower::ServiceExt;
|
||||
#[cfg(feature = "default")]
|
||||
use tower_http::services::ServeDir;
|
||||
@@ -95,7 +102,9 @@ impl ResponseParts {
|
||||
///
|
||||
/// If you provide your own handler, you will need to provide `ResponseOptions` via context
|
||||
/// yourself if you want to access it via context.
|
||||
/// ```rust,ignore
|
||||
/// ```
|
||||
/// use leptos::prelude::*;
|
||||
///
|
||||
/// #[server]
|
||||
/// pub async fn get_opts() -> Result<(), ServerFnError> {
|
||||
/// let opts = expect_context::<leptos_axum::ResponseOptions>();
|
||||
@@ -228,7 +237,7 @@ pub fn generate_request_and_parts(
|
||||
///
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, routing::post, Router};
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use std::net::SocketAddr;
|
||||
///
|
||||
/// # if false { // don't actually try to run a server in a doctest...
|
||||
@@ -312,10 +321,13 @@ async fn handle_server_fns_inner(
|
||||
) -> impl IntoResponse {
|
||||
use server_fn::middleware::Service;
|
||||
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let (req, parts) = generate_request_and_parts(req);
|
||||
|
||||
if let Some(mut service) = server_fn::axum::get_server_fn_service(&path) {
|
||||
if let Some(mut service) =
|
||||
server_fn::axum::get_server_fn_service(&path, method)
|
||||
{
|
||||
let owner = Owner::new();
|
||||
owner
|
||||
.with(|| {
|
||||
@@ -388,8 +400,7 @@ pub type PinnedHtmlStream =
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::*;
|
||||
/// use leptos_config::get_configuration;
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -407,8 +418,7 @@ pub type PinnedHtmlStream =
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new().fallback(leptos_axum::render_app_to_stream(
|
||||
/// leptos_options,
|
||||
/// || view! { <MyApp/> },
|
||||
/// || { /* your application here */ },
|
||||
/// ));
|
||||
///
|
||||
/// // run our app with hyper
|
||||
@@ -476,8 +486,7 @@ where
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::*;
|
||||
/// use leptos_config::get_configuration;
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -494,11 +503,9 @@ where
|
||||
/// let addr = leptos_options.site_addr.clone();
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app =
|
||||
/// Router::new().fallback(leptos_axum::render_app_to_stream_in_order(
|
||||
/// leptos_options,
|
||||
/// || view! { <MyApp/> },
|
||||
/// ));
|
||||
/// let app = Router::new().fallback(
|
||||
/// leptos_axum::render_app_to_stream_in_order(|| view! { <MyApp/> }),
|
||||
/// );
|
||||
///
|
||||
/// // run our app with hyper
|
||||
/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
@@ -536,14 +543,25 @@ where
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```ignore
|
||||
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
|
||||
/// let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
|
||||
/// || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || view! { <TodoApp/> }
|
||||
/// );
|
||||
/// ```
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::{config::LeptosOptions, context::provide_context, prelude::*};
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
/// Path(id): Path<String>,
|
||||
/// req: Request<Body>,
|
||||
/// ) -> Response {
|
||||
/// let handler = leptos_axum::render_app_to_stream_with_context(
|
||||
/// move || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || { /* your app here */ },
|
||||
/// );
|
||||
/// handler(req).await.into_response()
|
||||
/// }
|
||||
/// ```
|
||||
@@ -694,14 +712,25 @@ where
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```ignore
|
||||
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
|
||||
/// let handler = leptos_axum::render_app_to_stream_in_order_with_context((*options).clone(),
|
||||
/// move || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || view! { <TodoApp/> }
|
||||
/// );
|
||||
/// ```
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
/// Path(id): Path<String>,
|
||||
/// req: Request<Body>,
|
||||
/// ) -> Response {
|
||||
/// let handler = leptos_axum::render_app_to_stream_in_order_with_context(
|
||||
/// move || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || { /* your application here */ },
|
||||
/// );
|
||||
/// handler(req).await.into_response()
|
||||
/// }
|
||||
/// ```
|
||||
@@ -834,8 +863,7 @@ fn provide_contexts(
|
||||
/// This can then be set up at an appropriate route in your application:
|
||||
/// ```
|
||||
/// use axum::{handler::Handler, Router};
|
||||
/// use leptos::*;
|
||||
/// use leptos_config::get_configuration;
|
||||
/// use leptos::{config::get_configuration, prelude::*};
|
||||
/// use std::{env, net::SocketAddr};
|
||||
///
|
||||
/// #[component]
|
||||
@@ -852,10 +880,8 @@ fn provide_contexts(
|
||||
/// let addr = leptos_options.site_addr.clone();
|
||||
///
|
||||
/// // build our application with a route
|
||||
/// let app = Router::new().fallback(leptos_axum::render_app_async(
|
||||
/// leptos_options,
|
||||
/// || view! { <MyApp/> },
|
||||
/// ));
|
||||
/// let app = Router::new()
|
||||
/// .fallback(leptos_axum::render_app_async(|| view! { <MyApp/> }));
|
||||
///
|
||||
/// // run our app with hyper
|
||||
/// // `axum::Server` is a re-export of `hyper::Server`
|
||||
@@ -896,14 +922,25 @@ where
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```ignore
|
||||
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
|
||||
/// let handler = leptos_axum::render_app_async_with_context((*options).clone(),
|
||||
/// move || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || view! { <TodoApp/> }
|
||||
/// );
|
||||
/// ```
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
/// Path(id): Path<String>,
|
||||
/// req: Request<Body>,
|
||||
/// ) -> Response {
|
||||
/// let handler = leptos_axum::render_app_async_with_context(
|
||||
/// move || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || { /* your application here */ },
|
||||
/// );
|
||||
/// handler(req).await.into_response()
|
||||
/// }
|
||||
/// ```
|
||||
@@ -950,14 +987,25 @@ where
|
||||
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
|
||||
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
|
||||
/// the data to leptos in a closure. An example is below
|
||||
/// ```ignore
|
||||
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
|
||||
/// let handler = leptos_axum::render_app_async_with_context((*options).clone(),
|
||||
/// move || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || view! { <TodoApp/> }
|
||||
/// );
|
||||
/// ```
|
||||
/// use axum::{
|
||||
/// body::Body,
|
||||
/// extract::Path,
|
||||
/// response::{IntoResponse, Response},
|
||||
/// };
|
||||
/// use http::Request;
|
||||
/// use leptos::context::provide_context;
|
||||
///
|
||||
/// async fn custom_handler(
|
||||
/// Path(id): Path<String>,
|
||||
/// req: Request<Body>,
|
||||
/// ) -> Response {
|
||||
/// let handler = leptos_axum::render_app_async_with_context(
|
||||
/// move || {
|
||||
/// provide_context(id.clone());
|
||||
/// },
|
||||
/// || { /* your application here */ },
|
||||
/// );
|
||||
/// handler(req).await.into_response()
|
||||
/// }
|
||||
/// ```
|
||||
@@ -1166,7 +1214,7 @@ where
|
||||
{
|
||||
init_executor();
|
||||
|
||||
let owner = Owner::new_root(None);
|
||||
let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new())));
|
||||
let routes = owner
|
||||
.with(|| {
|
||||
// stub out a path for now
|
||||
@@ -1676,15 +1724,19 @@ where
|
||||
///
|
||||
/// Any error that occurs during extraction is converted to a [`ServerFnError`].
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // MyQuery is some type that implements `Deserialize + Serialize`
|
||||
/// #[server]
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::*;
|
||||
/// let Query(query) = extract().await?;
|
||||
/// ```rust
|
||||
/// use leptos::prelude::*;
|
||||
///
|
||||
/// Ok(query)
|
||||
/// #[server]
|
||||
/// pub async fn request_method() -> Result<String, ServerFnError> {
|
||||
/// use http::Method;
|
||||
/// use leptos_axum::extract;
|
||||
///
|
||||
/// // you can extract anything that a regular Axum extractor can extract
|
||||
/// // from the head (not from the body of the request)
|
||||
/// let method: Method = extract().await?;
|
||||
///
|
||||
/// Ok(format!("{method:?}"))
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract<T>() -> Result<T, ServerFnError>
|
||||
@@ -1702,18 +1754,6 @@ where
|
||||
/// therefore be used in an extractor. The compiler can often infer this type.
|
||||
///
|
||||
/// Any error that occurs during extraction is converted to a [`ServerFnError`].
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // MyQuery is some type that implements `Deserialize + Serialize`
|
||||
/// #[server]
|
||||
/// pub async fn query_extract() -> Result<MyQuery, ServerFnError> {
|
||||
/// use axum::{extract::Query, http::Method};
|
||||
/// use leptos_axum::*;
|
||||
/// let Query(query) = extract().await?;
|
||||
///
|
||||
/// Ok(query)
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn extract_with_state<T, S>(state: &S) -> Result<T, ServerFnError>
|
||||
where
|
||||
T: Sized + FromRequestParts<S>,
|
||||
|
||||
@@ -68,21 +68,19 @@ ssr = [
|
||||
"hydration",
|
||||
"tachys/ssr",
|
||||
]
|
||||
nightly = ["leptos_dom/nightly", "leptos_macro/nightly", "tachys/nightly"]
|
||||
nightly = ["leptos_macro/nightly", "reactive_graph/nightly", "tachys/nightly"]
|
||||
rkyv = ["server_fn/rkyv"]
|
||||
tracing = [
|
||||
"reactive_graph/tracing",
|
||||
"tachys/tracing",
|
||||
] #, "leptos_macro/tracing", "leptos_dom/tracing"]
|
||||
nonce = ["base64", "leptos_dom/nonce", "rand"]
|
||||
nonce = ["base64", "rand"]
|
||||
spin = ["leptos-spin-macro"]
|
||||
experimental-islands = [
|
||||
"leptos_dom/experimental-islands",
|
||||
"leptos_macro/experimental-islands",
|
||||
"dep:serde_json",
|
||||
]
|
||||
trace-component-props = [
|
||||
"leptos_dom/trace-component-props",
|
||||
"leptos_macro/trace-component-props",
|
||||
]
|
||||
delegation = ["tachys/delegation"]
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
//! Callbacks can be created manually from any function or closure, but the easiest way
|
||||
//! to create them is to use `#[prop(into)]]` when defining a component.
|
||||
//! ```
|
||||
//! # use leptos::*;
|
||||
//! use leptos::prelude::*;
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn MyComponent(
|
||||
//! #[prop(into)] render_number: Callback<i32, String>,
|
||||
@@ -118,8 +119,7 @@ macro_rules! impl_from_fn {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO
|
||||
//impl_from_fn!(UnsyncCallback);
|
||||
impl_from_fn!(UnsyncCallback);
|
||||
|
||||
#[cfg(feature = "nightly")]
|
||||
impl<In, Out> FnOnce<(In,)> for UnsyncCallback<In, Out> {
|
||||
@@ -144,13 +144,12 @@ impl<In, Out> Fn<(In,)> for UnsyncCallback<In, Out> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO update these docs to swap the two
|
||||
/// Callbacks define a standard way to store functions and closures.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::{Callable, Callback};
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos::callback::{Callable, Callback};
|
||||
/// #[component]
|
||||
/// fn MyComponent(
|
||||
/// #[prop(into)] render_number: Callback<i32, String>,
|
||||
@@ -246,75 +245,30 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
callback::{Callback, UnsyncCallback},
|
||||
create_runtime,
|
||||
};
|
||||
use crate::callback::{Callback, UnsyncCallback};
|
||||
|
||||
struct NoClone {}
|
||||
|
||||
#[test]
|
||||
fn clone_callback() {
|
||||
let rt = create_runtime();
|
||||
let callback =
|
||||
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback.clone();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_sync_callback() {
|
||||
let rt = create_runtime();
|
||||
let callback = Callback::new(move |_no_clone: NoClone| NoClone {});
|
||||
fn clone_unsync_callback() {
|
||||
let callback =
|
||||
UnsyncCallback::new(move |_no_clone: NoClone| NoClone {});
|
||||
let _cloned = callback.clone();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_from() {
|
||||
let rt = create_runtime();
|
||||
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_from_html() {
|
||||
let rt = create_runtime();
|
||||
use leptos::{
|
||||
html::{AnyElement, HtmlElement},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
let _callback: UnsyncCallback<String, HtmlElement<AnyElement>> =
|
||||
(|x: String| {
|
||||
view! { <h1>{x}</h1> }
|
||||
})
|
||||
.into();
|
||||
rt.dispose();
|
||||
let _callback: Callback<(), String> = (|()| "test").into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_from() {
|
||||
let rt = create_runtime();
|
||||
let _callback: Callback<(), String> = (|()| "test").into();
|
||||
rt.dispose();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_callback_from_html() {
|
||||
use leptos::{
|
||||
html::{AnyElement, HtmlElement},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
let rt = create_runtime();
|
||||
|
||||
let _callback: Callback<String, HtmlElement<AnyElement>> =
|
||||
(|x: String| {
|
||||
view! { <h1>{x}</h1> }
|
||||
})
|
||||
.into();
|
||||
|
||||
rt.dispose();
|
||||
let _callback: UnsyncCallback<(), String> = (|()| "test").into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,70 +47,44 @@ type BoxedChildrenFn = Box<dyn Fn() -> AnyView<Dom> + Send>;
|
||||
/// to know exactly what children type the component expects. This is used internally by the
|
||||
/// `view!` macro implementation, and can also be used explicitly when using the builder syntax.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Without ToChildren
|
||||
/// Different component types take different types for their `children` prop, some of which cannot
|
||||
/// be directly constructed. Using `ToChildren` allows the component user to pass children without
|
||||
/// explicity constructing the correct type.
|
||||
///
|
||||
/// Without [ToChildren], consumers need to explicitly provide children using the type expected
|
||||
/// by the component. For example, [Provider][crate::Provider]'s children need to wrapped in
|
||||
/// a [Box], while [Show][crate::Show]'s children need to be wrapped in an [Rc].
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::{ProviderProps, ShowProps};
|
||||
/// # use leptos_dom::html::p;
|
||||
/// # use leptos_dom::IntoView;
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos::html::p;
|
||||
/// # use leptos::IntoView;
|
||||
/// # use leptos_macro::component;
|
||||
/// # use std::rc::Rc;
|
||||
/// #
|
||||
/// #[component]
|
||||
/// fn App() -> impl IntoView {
|
||||
/// (
|
||||
/// ProviderProps::builder()
|
||||
/// .children(Box::new(|| p().child("Foo").into_view().into()))
|
||||
/// // ...
|
||||
/// # .value("Foo")
|
||||
/// # .build(),
|
||||
/// ShowProps::builder()
|
||||
/// .children(Rc::new(|| p().child("Foo").into_view().into()))
|
||||
/// // ...
|
||||
/// # .when(|| true)
|
||||
/// # .fallback(|| p().child("foo"))
|
||||
/// # .build(),
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## With ToChildren
|
||||
///
|
||||
/// With [ToChildren], consumers don't need to know exactly which type a component uses for
|
||||
/// its children.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::{ProviderProps, ShowProps};
|
||||
/// # use leptos_dom::html::p;
|
||||
/// # use leptos_dom::IntoView;
|
||||
/// # use leptos_macro::component;
|
||||
/// # use std::rc::Rc;
|
||||
/// # use leptos::ToChildren;
|
||||
/// #
|
||||
/// # use leptos::children::ToChildren;
|
||||
/// use leptos::context::{Provider, ProviderProps};
|
||||
/// use leptos::control_flow::{Show, ShowProps};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App() -> impl IntoView {
|
||||
/// (
|
||||
/// Provider(
|
||||
/// ProviderProps::builder()
|
||||
/// .children(ToChildren::to_children(|| {
|
||||
/// p().child("Foo").into_view().into()
|
||||
/// p().child("Foo")
|
||||
/// }))
|
||||
/// // ...
|
||||
/// # .value("Foo")
|
||||
/// # .build(),
|
||||
/// ShowProps::builder()
|
||||
/// .value("Foo")
|
||||
/// .build(),
|
||||
/// ),
|
||||
/// Show(
|
||||
/// ShowProps::builder()
|
||||
/// .children(ToChildren::to_children(|| {
|
||||
/// p().child("Foo").into_view().into()
|
||||
/// p().child("Foo")
|
||||
/// }))
|
||||
/// // ...
|
||||
/// # .when(|| true)
|
||||
/// # .fallback(|| p().child("foo"))
|
||||
/// # .build(),
|
||||
/// .when(|| true)
|
||||
/// .fallback(|| p().child("foo"))
|
||||
/// .build(),
|
||||
/// )
|
||||
/// )
|
||||
/// }
|
||||
pub trait ToChildren<F> {
|
||||
|
||||
@@ -9,7 +9,7 @@ use reactive_graph::{
|
||||
traits::{Get, Update, With, WithUntracked},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
|
||||
use tachys::{
|
||||
html::attribute::Attribute,
|
||||
hydration::Cursor,
|
||||
@@ -22,6 +22,29 @@ use tachys::{
|
||||
};
|
||||
use throw_error::{Error, ErrorHook, ErrorId};
|
||||
|
||||
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
|
||||
/// render nothing, and search up through the view tree for an `<ErrorBoundary/>`.
|
||||
/// This component lets you define a fallback that should be rendered in that
|
||||
/// error case, allowing you to handle errors within a section of the interface.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// #[component]
|
||||
/// pub fn ErrorBoundaryExample() -> impl IntoView {
|
||||
/// let (value, set_value) = signal(Ok(0));
|
||||
/// let on_input =
|
||||
/// move |ev| set_value.set(event_target_value(&ev).parse::<i32>());
|
||||
///
|
||||
/// view! {
|
||||
/// <input type="text" on:input=on_input/>
|
||||
/// <ErrorBoundary
|
||||
/// fallback=move |_| view! { <p class="error">"Enter a valid number."</p>}
|
||||
/// >
|
||||
/// <p>"Value is: " {move || value.get()}</p>
|
||||
/// </ErrorBoundary>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Beginner's Tip: ErrorBoundary Requires Your Error To Implement std::error::Error.
|
||||
/// `ErrorBoundary` requires your `Result<T,E>` to implement [IntoView](https://docs.rs/leptos/latest/leptos/trait.IntoView.html).
|
||||
@@ -72,12 +95,11 @@ where
|
||||
});
|
||||
let hook = hook as Arc<dyn ErrorHook>;
|
||||
|
||||
// provide the error hook and render children
|
||||
// TODO unset this outside the ErrorBoundary
|
||||
throw_error::set_error_hook(Arc::clone(&hook));
|
||||
let _guard = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
let children = children.into_inner()();
|
||||
|
||||
ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children,
|
||||
@@ -87,8 +109,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ErrorBoundaryView<Chil, FalFn, Rndr> {
|
||||
hook: Arc<dyn ErrorHook>,
|
||||
boundary_id: SerializedDataId,
|
||||
errors_empty: ArcMemo<bool>,
|
||||
children: Chil,
|
||||
@@ -145,11 +167,14 @@ where
|
||||
type State = RenderEffect<ErrorBoundaryViewState<Chil::State, Fal::State>>;
|
||||
|
||||
fn build(mut self) -> Self::State {
|
||||
let hook = Arc::clone(&self.hook);
|
||||
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
let mut children = Some(self.children.build());
|
||||
RenderEffect::new(
|
||||
move |prev: Option<
|
||||
ErrorBoundaryViewState<Chil::State, Fal::State>,
|
||||
>| {
|
||||
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
if let Some(mut state) = prev {
|
||||
match (self.errors_empty.get(), &mut state.fallback) {
|
||||
// no errors, and was showing fallback
|
||||
@@ -216,6 +241,7 @@ where
|
||||
Self::Output<NewAttr>: RenderHtml<Rndr>,
|
||||
{
|
||||
let ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children,
|
||||
@@ -224,6 +250,7 @@ where
|
||||
rndr,
|
||||
} = self;
|
||||
ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children: children.add_any_attr(attr.into_cloneable_owned()),
|
||||
@@ -252,6 +279,7 @@ where
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
let ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children,
|
||||
@@ -260,6 +288,7 @@ where
|
||||
..
|
||||
} = self;
|
||||
ErrorBoundaryView {
|
||||
hook,
|
||||
boundary_id,
|
||||
errors_empty,
|
||||
children: children.resolve().await,
|
||||
@@ -277,6 +306,7 @@ where
|
||||
mark_branches: bool,
|
||||
) {
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let _hook = throw_error::set_error_hook(self.hook);
|
||||
let mut new_buf = String::with_capacity(Chil::MIN_LENGTH);
|
||||
let mut new_pos = *position;
|
||||
self.children.to_html_with_buf(
|
||||
@@ -309,6 +339,7 @@ where
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
let _hook = throw_error::set_error_hook(self.hook);
|
||||
// first, attempt to serialize the children to HTML, then check for errors
|
||||
let mut new_buf = StreamBuilder::new(buf.clone_id());
|
||||
let mut new_pos = *position;
|
||||
@@ -319,10 +350,6 @@ where
|
||||
mark_branches,
|
||||
);
|
||||
|
||||
if let Some(sc) = Owner::current_shared_context() {
|
||||
sc.seal_errors(&self.boundary_id);
|
||||
}
|
||||
|
||||
// any thrown errors would've been caught here
|
||||
if self.errors.with_untracked(|map| map.is_empty()) {
|
||||
buf.append(new_buf);
|
||||
@@ -345,12 +372,14 @@ where
|
||||
position: &PositionState,
|
||||
) -> Self::State {
|
||||
let mut children = Some(self.children);
|
||||
let hook = Arc::clone(&self.hook);
|
||||
let cursor = cursor.to_owned();
|
||||
let position = position.to_owned();
|
||||
RenderEffect::new(
|
||||
move |prev: Option<
|
||||
ErrorBoundaryViewState<Chil::State, Fal::State>,
|
||||
>| {
|
||||
let _hook = throw_error::set_error_hook(Arc::clone(&hook));
|
||||
if let Some(mut state) = prev {
|
||||
match (self.errors_empty.get(), &mut state.fallback) {
|
||||
// no errors, and was showing fallback
|
||||
@@ -424,7 +453,10 @@ impl ErrorBoundaryErrorHook {
|
||||
impl ErrorHook for ErrorBoundaryErrorHook {
|
||||
fn throw(&self, error: Error) -> ErrorId {
|
||||
// generate a unique ID
|
||||
let key = ErrorId::default(); // TODO unique ID...
|
||||
let key: ErrorId = Owner::current_shared_context()
|
||||
.map(|sc| sc.next_id())
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
|
||||
// register it with the shared context, so that it can be serialized from server to client
|
||||
// as needed
|
||||
|
||||
@@ -10,7 +10,7 @@ use tachys::{reactive_graph::OwnedView, view::keyed::keyed};
|
||||
/// as it avoids re-creating DOM nodes that are not being changed.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
/// struct Counter {
|
||||
@@ -80,25 +80,24 @@ where
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use leptos_macro::view;
|
||||
use tachys::{
|
||||
html::element::HtmlElement, prelude::ElementChild,
|
||||
renderer::mock_dom::MockDom, view::Render,
|
||||
};
|
||||
use tachys::{html::element::HtmlElement, prelude::ElementChild};
|
||||
|
||||
#[test]
|
||||
fn creates_list() {
|
||||
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
let list: HtmlElement<_, _, _, MockDom> = view! {
|
||||
<ol>
|
||||
<For each=move || values.get() key=|i| *i let:i>
|
||||
<li>{i}</li>
|
||||
</For>
|
||||
</ol>
|
||||
};
|
||||
let list = list.build();
|
||||
assert_eq!(
|
||||
list.el.to_debug_html(),
|
||||
"<ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ol>"
|
||||
);
|
||||
Owner::new().with(|| {
|
||||
let values = RwSignal::new(vec![1, 2, 3, 4, 5]);
|
||||
let list: HtmlElement<_, _, _, Dom> = view! {
|
||||
<ol>
|
||||
<For each=move || values.get() key=|i| *i let:i>
|
||||
<li>{i}</li>
|
||||
</For>
|
||||
</ol>
|
||||
};
|
||||
assert_eq!(
|
||||
list.to_html(),
|
||||
"<ol><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><!></\
|
||||
ol>"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ use web_sys::{
|
||||
/// should make use of indexing notation of `serde_qs`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_router::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// use leptos::form::ActionForm;
|
||||
///
|
||||
/// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
/// struct HeftyData {
|
||||
@@ -45,7 +45,7 @@ use web_sys::{
|
||||
///
|
||||
/// #[component]
|
||||
/// fn ComplexInput() -> impl IntoView {
|
||||
/// let submit = Action::<VeryImportantFn, _>::server();
|
||||
/// let submit = ServerAction::<VeryImportantFn>::new();
|
||||
///
|
||||
/// view! {
|
||||
/// <ActionForm action=submit>
|
||||
@@ -145,10 +145,6 @@ where
|
||||
} else {
|
||||
Either::Right(action_form)
|
||||
}
|
||||
// TODO add other attributes
|
||||
/*for (attr_name, attr_value) in attributes {
|
||||
action_form = action_form.attr(attr_name, attr_value);
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Automatically turns a server [MultiAction](leptos_server::MultiAction) into an HTML
|
||||
@@ -216,10 +212,6 @@ where
|
||||
} else {
|
||||
Either::Right(action_form)
|
||||
}
|
||||
// TODO add other attributes
|
||||
/*for (attr_name, attr_value) in attributes {
|
||||
action_form = action_form.attr(attr_name, attr_value);
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Resolves a redirect location to an (absolute) URL.
|
||||
|
||||
@@ -25,11 +25,7 @@ pub fn AutoReload(
|
||||
};
|
||||
|
||||
let script = include_str!("reload_script.js");
|
||||
view! {
|
||||
<script nonce=nonce>
|
||||
{format!("{script}({reload_port:?}, {protocol})")}
|
||||
</script>
|
||||
}
|
||||
view! { <script nonce=nonce>{format!("{script}({reload_port:?}, {protocol})")}</script> }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,6 +45,9 @@ pub fn HydrationScripts(
|
||||
#[cfg(not(feature = "nonce"))]
|
||||
let nonce = None::<String>;
|
||||
let script = if islands {
|
||||
if let Some(sc) = Owner::current_shared_context() {
|
||||
sc.set_is_hydrating(false);
|
||||
}
|
||||
include_str!("./island_script.js")
|
||||
} else {
|
||||
include_str!("./hydration_script.js")
|
||||
@@ -56,7 +55,13 @@ pub fn HydrationScripts(
|
||||
|
||||
view! {
|
||||
<link rel="modulepreload" href=format!("/{pkg_path}/{output_name}.js") nonce=nonce.clone()/>
|
||||
<link rel="preload" href=format!("/{pkg_path}/{wasm_output_name}.wasm") r#as="fetch" r#type="application/wasm" crossorigin=nonce.clone().unwrap_or_default()/>
|
||||
<link
|
||||
rel="preload"
|
||||
href=format!("/{pkg_path}/{wasm_output_name}.wasm")
|
||||
r#as="fetch"
|
||||
r#type="application/wasm"
|
||||
crossorigin=nonce.clone().unwrap_or_default()
|
||||
/>
|
||||
<script type="module" nonce=nonce>
|
||||
{format!("{script}({pkg_path:?}, {output_name:?}, {wasm_output_name:?})")}
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//#!rdeny(missing_docs)] // TODO restore
|
||||
#!rdeny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
//! # About Leptos
|
||||
//!
|
||||
@@ -98,25 +98,25 @@
|
||||
//! # A Simple Counter
|
||||
//!
|
||||
//! ```rust
|
||||
//! use leptos::*;
|
||||
//! use leptos::prelude::*;
|
||||
//!
|
||||
//! #[component]
|
||||
//! pub fn SimpleCounter( initial_value: i32) -> impl IntoView {
|
||||
//! pub fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
//! // create a reactive signal with the initial value
|
||||
//! let (value, set_value) = create_signal( initial_value);
|
||||
//! let (value, set_value) = signal( initial_value);
|
||||
//!
|
||||
//! // create event handlers for our buttons
|
||||
//! // note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures
|
||||
//! let clear = move |_| set_value.set(0);
|
||||
//! let decrement = move |_| set_value.update(|value| *value -= 1);
|
||||
//! let increment = move |_| set_value.update(|value| *value += 1);
|
||||
//! let decrement = move |_| *set_value.write() -= 1;
|
||||
//! let increment = move |_| *set_value.write() += 1;
|
||||
//!
|
||||
//! view! {
|
||||
//!
|
||||
//! <div>
|
||||
//! <button on:click=clear>"Clear"</button>
|
||||
//! <button on:click=decrement>"-1"</button>
|
||||
//! <span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
//! <span>"Value: " {value} "!"</span>
|
||||
//! <button on:click=increment>"+1"</button>
|
||||
//! </div>
|
||||
//! }
|
||||
@@ -125,18 +125,19 @@
|
||||
//!
|
||||
//! Leptos is easy to use with [Trunk](https://trunkrs.dev/) (or with a simple wasm-bindgen setup):
|
||||
//! ```
|
||||
//! # use leptos::*;
|
||||
//! # if false { // can't run in doctests
|
||||
//! use leptos::{mount::mount_to_body, prelude::*};
|
||||
//!
|
||||
//! #[component]
|
||||
//! fn SimpleCounter(initial_value: i32) -> impl IntoView {
|
||||
//! todo!()
|
||||
//! // ...
|
||||
//! # _ = initial_value;
|
||||
//! }
|
||||
//!
|
||||
//! pub fn main() {
|
||||
//! # if false { // can't run in doctest
|
||||
//! mount_to_body(|| view! { <SimpleCounter initial_value=3 /> })
|
||||
//! }
|
||||
//! # }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
@@ -296,10 +297,17 @@ pub mod spawn {
|
||||
}
|
||||
}
|
||||
|
||||
// these reexports are used in islands
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
#[doc(hidden)]
|
||||
pub use wasm_bindgen; // used in islands
|
||||
pub use serde;
|
||||
#[cfg(feature = "experimental-islands")]
|
||||
#[doc(hidden)]
|
||||
pub use web_sys; // used in islands
|
||||
pub use serde_json;
|
||||
#[doc(hidden)]
|
||||
pub use wasm_bindgen;
|
||||
#[doc(hidden)]
|
||||
pub use web_sys;
|
||||
|
||||
/*mod additional_attributes;
|
||||
pub use additional_attributes::*;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{logging, IntoView};
|
||||
use any_spawner::Executor;
|
||||
use reactive_graph::owner::Owner;
|
||||
#[cfg(debug_assertions)]
|
||||
use std::cell::Cell;
|
||||
use std::marker::PhantomData;
|
||||
use tachys::{
|
||||
dom::body,
|
||||
@@ -27,6 +29,11 @@ where
|
||||
owner.forget();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {
|
||||
static FIRST_CALL: Cell<bool> = const { Cell::new(true) };
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
/// Runs the provided closure and mounts the result to the provided element.
|
||||
pub fn hydrate_from<F, N>(
|
||||
@@ -45,15 +52,19 @@ where
|
||||
// already initialized, which is not an issue
|
||||
_ = Executor::init_wasm_bindgen();
|
||||
|
||||
if !cfg!(feature = "hydrate") {
|
||||
logging::warn!(
|
||||
"It seems like you're trying to use Leptos in hydration mode, but \
|
||||
the `hydrate` feature is not enabled on the `leptos` crate. Add \
|
||||
`features = [\"hydrate\"]` to your Cargo.toml for the crate to \
|
||||
work properly.\n\nNote that hydration and client-side rendering \
|
||||
now use separate functions from leptos::mount: you are calling a \
|
||||
hydration function."
|
||||
);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if !cfg!(feature = "hydrate") && FIRST_CALL.get() {
|
||||
logging::warn!(
|
||||
"It seems like you're trying to use Leptos in hydration mode, \
|
||||
but the `hydrate` feature is not enabled on the `leptos` \
|
||||
crate. Add `features = [\"hydrate\"]` to your Cargo.toml for \
|
||||
the crate to work properly.\n\nNote that hydration and \
|
||||
client-side rendering now use separate functions from \
|
||||
leptos::mount: you are calling a hydration function."
|
||||
);
|
||||
}
|
||||
FIRST_CALL.set(false);
|
||||
}
|
||||
|
||||
// create a new reactive owner and use it as the root node to run the app
|
||||
@@ -100,16 +111,20 @@ where
|
||||
// already initialized, which is not an issue
|
||||
_ = Executor::init_wasm_bindgen();
|
||||
|
||||
if !cfg!(feature = "csr") {
|
||||
logging::warn!(
|
||||
"It seems like you're trying to use Leptos in client-side \
|
||||
rendering mode, but the `csr` feature is not enabled on the \
|
||||
`leptos` crate. Add `features = [\"csr\"]` to your Cargo.toml \
|
||||
for the crate to work properly.\n\nNote that hydration and \
|
||||
client-side rendering now use different functions from \
|
||||
leptos::mount. You are using a client-side rendering mount \
|
||||
function."
|
||||
);
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if !cfg!(feature = "csr") && FIRST_CALL.get() {
|
||||
logging::warn!(
|
||||
"It seems like you're trying to use Leptos in client-side \
|
||||
rendering mode, but the `csr` feature is not enabled on the \
|
||||
`leptos` crate. Add `features = [\"csr\"]` to your \
|
||||
Cargo.toml for the crate to work properly.\n\nNote that \
|
||||
hydration and client-side rendering now use different \
|
||||
functions from leptos::mount. You are using a client-side \
|
||||
rendering mount function."
|
||||
);
|
||||
}
|
||||
FIRST_CALL.set(false);
|
||||
}
|
||||
|
||||
// create a new reactive owner and use it as the root node to run the app
|
||||
@@ -166,7 +181,7 @@ where
|
||||
/// Hydrates any islands that are currently present on the page.
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn hydrate_islands() {
|
||||
use hydration_context::HydrateSharedContext;
|
||||
use hydration_context::{HydrateSharedContext, SharedContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
// use wasm-bindgen-futures to drive the reactive system
|
||||
@@ -174,8 +189,13 @@ pub fn hydrate_islands() {
|
||||
// already initialized, which is not an issue
|
||||
_ = Executor::init_wasm_bindgen();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
FIRST_CALL.set(false);
|
||||
|
||||
// create a new reactive owner and use it as the root node to run the app
|
||||
let owner = Owner::new_root(Some(Arc::new(HydrateSharedContext::new())));
|
||||
let sc = HydrateSharedContext::new();
|
||||
sc.set_is_hydrating(false); // islands mode starts in "not hydrating"
|
||||
let owner = Owner::new_root(Some(Arc::new(sc)));
|
||||
owner.set();
|
||||
std::mem::forget(owner);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{children::TypedChildrenFn, mount, IntoView};
|
||||
use leptos_dom::helpers::document;
|
||||
use leptos_macro::component;
|
||||
use reactive_graph::{effect::Effect, owner::Owner};
|
||||
use reactive_graph::{effect::Effect, owner::Owner, untrack};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Renders components somewhere else in the DOM.
|
||||
@@ -62,12 +62,11 @@ where
|
||||
container.clone()
|
||||
};
|
||||
|
||||
// SendWrapper: this is only created in a single-threaded browser environment
|
||||
let _ = mount.append_child(&container);
|
||||
let handle = SendWrapper::new((
|
||||
mount::mount_to(render_root.unchecked_into(), {
|
||||
let children = Arc::clone(&children);
|
||||
move || children()
|
||||
move || untrack(|| children())
|
||||
}),
|
||||
mount.clone(),
|
||||
container,
|
||||
|
||||
@@ -9,7 +9,8 @@ use tachys::reactive_graph::OwnedView;
|
||||
/// This prevents issues related to “context shadowing.”
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::prelude::*;
|
||||
/// use leptos::{context::Provider, prelude::*};
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn App() -> impl IntoView {
|
||||
/// // each Provider will only provide the value to its children
|
||||
|
||||
@@ -31,6 +31,61 @@ use tachys::{
|
||||
};
|
||||
use throw_error::ErrorHookFuture;
|
||||
|
||||
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
/// it will render the `children`.
|
||||
///
|
||||
/// Each time one of the resources is loading again, it will fall back. To keep the current
|
||||
/// children instead, use [Transition](crate::Transition).
|
||||
///
|
||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
||||
/// those resources are read under the suspense), so you cannot assume that resources read
|
||||
/// synchronously have
|
||||
/// `Some` value in `children`. However, you can read resources asynchronously by using
|
||||
/// [Suspend](crate::prelude::Suspend).
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # if false { // don't run in doctests
|
||||
/// async fn fetch_cats(how_many: u32) -> Vec<String> { vec![] }
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = signal::<u32>(1);
|
||||
///
|
||||
/// let cats = Resource::new(move || cat_count.get(), |count| fetch_cats(count));
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
|
||||
/// // you can access a resource synchronously
|
||||
/// {move || {
|
||||
/// cats.get().map(|data| {
|
||||
/// data
|
||||
/// .into_iter()
|
||||
/// .map(|src| {
|
||||
/// view! {
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect_view()
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// // or you can use `Suspend` to read resources asynchronously
|
||||
/// {move || Suspend::new(async move {
|
||||
/// cats.await
|
||||
/// .into_iter()
|
||||
/// .map(|src| {
|
||||
/// view! {
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect_view()
|
||||
/// })}
|
||||
/// </Suspense>
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;}
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn Suspense<Chil>(
|
||||
#[prop(optional, into)] fallback: ViewFnOnce,
|
||||
|
||||
@@ -15,7 +15,61 @@ use reactive_graph::{
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
use tachys::reactive_graph::OwnedView;
|
||||
|
||||
/// TODO docs!
|
||||
/// If any [`Resource`](leptos_reactive::Resource) is read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
/// it will render the `children`.
|
||||
///
|
||||
/// Unlike [`Suspense`](crate::Suspense), this will not fall
|
||||
/// back to the `fallback` state if there are further changes after the initial load.
|
||||
///
|
||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
||||
/// those resources are read under the suspense), so you cannot assume that resources read
|
||||
/// synchronously have
|
||||
/// `Some` value in `children`. However, you can read resources asynchronously by using
|
||||
/// [Suspend](crate::prelude::Suspend).
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::prelude::*;
|
||||
/// # if false { // don't run in doctests
|
||||
/// async fn fetch_cats(how_many: u32) -> Vec<String> { vec![] }
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = signal::<u32>(1);
|
||||
///
|
||||
/// let cats = Resource::new(move || cat_count.get(), |count| fetch_cats(count));
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// <Transition fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }>
|
||||
/// // you can access a resource synchronously
|
||||
/// {move || {
|
||||
/// cats.get().map(|data| {
|
||||
/// data
|
||||
/// .into_iter()
|
||||
/// .map(|src| {
|
||||
/// view! {
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect_view()
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// // or you can use `Suspend` to read resources asynchronously
|
||||
/// {move || Suspend::new(async move {
|
||||
/// cats.await
|
||||
/// .into_iter()
|
||||
/// .map(|src| {
|
||||
/// view! {
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect_view()
|
||||
/// })}
|
||||
/// </Transition>
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;}
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn Transition<Chil>(
|
||||
/// Will be displayed while resources are pending. By default this is the empty view.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
use leptos::html::HtmlElement;
|
||||
|
||||
#[test]
|
||||
fn simple_ssr_test() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (value, set_value) = signal(0);
|
||||
let rendered = view! {
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
@@ -13,26 +13,13 @@ fn simple_ssr_test() {
|
||||
</div>
|
||||
};
|
||||
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().to_html(),
|
||||
"<div><button>-1</button><span>Value: \
|
||||
0!</span><button>+1</button></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().to_html().contains(
|
||||
"<div data-hk=\"0-0-0-1\"><button \
|
||||
data-hk=\"0-0-0-2\">-1</button><span data-hk=\"0-0-0-3\">Value: \
|
||||
<!--hk=0-0-0-4o|leptos-dyn-child-start-->0<!\
|
||||
--hk=0-0-0-4c|leptos-dyn-child-end-->!</span><button \
|
||||
data-hk=\"0-0-0-5\">+1</button></div>"
|
||||
));
|
||||
}
|
||||
|
||||
runtime.dispose();
|
||||
assert_eq!(
|
||||
rendered.to_html(),
|
||||
"<div><button>-1</button><span>Value: \
|
||||
<!>0<!>!</span><button>+1</button></div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_test_with_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -49,35 +36,21 @@ fn ssr_test_with_components() {
|
||||
}
|
||||
}
|
||||
|
||||
let runtime = create_runtime();
|
||||
let rendered = view! {
|
||||
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
<div class="counters">
|
||||
<Counter initial_value=1/>
|
||||
<Counter initial_value=2/>
|
||||
</div>
|
||||
};
|
||||
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().to_html(),
|
||||
"<div class=\"counters\"><div><button>-1</button><span>Value: \
|
||||
1!</span><button>+1</button></div><div><button>-1</\
|
||||
button><span>Value: 2!</span><button>+1</button></div></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().to_html().contains(
|
||||
"<div data-hk=\"0-0-0-3\"><button \
|
||||
data-hk=\"0-0-0-4\">-1</button><span data-hk=\"0-0-0-5\">Value: \
|
||||
<!--hk=0-0-0-6o|leptos-dyn-child-start-->1<!\
|
||||
--hk=0-0-0-6c|leptos-dyn-child-end-->!</span><button \
|
||||
data-hk=\"0-0-0-7\">+1</button></div>"
|
||||
));
|
||||
}
|
||||
runtime.dispose();
|
||||
assert_eq!(
|
||||
rendered.to_html(),
|
||||
"<div class=\"counters\"><div><button>-1</button><span>Value: \
|
||||
<!>1<!>!</span><button>+1</button></div><div><button>-1</\
|
||||
button><span>Value: <!>2<!>!</span><button>+1</button></div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_test_with_snake_case_components() {
|
||||
use leptos::prelude::*;
|
||||
@@ -86,7 +59,6 @@ fn ssr_test_with_snake_case_components() {
|
||||
fn snake_case_counter(initial_value: i32) -> impl IntoView {
|
||||
let (value, set_value) = signal(initial_value);
|
||||
view! {
|
||||
|
||||
<div>
|
||||
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<span>"Value: " {move || value.get().to_string()} "!"</span>
|
||||
@@ -94,111 +66,65 @@ fn ssr_test_with_snake_case_components() {
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
let runtime = create_runtime();
|
||||
let rendered = view! {
|
||||
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
<div class="counters">
|
||||
<SnakeCaseCounter initial_value=1/>
|
||||
<SnakeCaseCounter initial_value=2/>
|
||||
</div>
|
||||
};
|
||||
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().to_html(),
|
||||
"<div class=\"counters\"><div><button>-1</button><span>Value: \
|
||||
1!</span><button>+1</button></div><div><button>-1</\
|
||||
button><span>Value: 2!</span><button>+1</button></div></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().to_html().contains(
|
||||
"<div data-hk=\"0-0-0-3\"><button \
|
||||
data-hk=\"0-0-0-4\">-1</button><span data-hk=\"0-0-0-5\">Value: \
|
||||
<!--hk=0-0-0-6o|leptos-dyn-child-start-->1<!\
|
||||
--hk=0-0-0-6c|leptos-dyn-child-end-->!</span><button \
|
||||
data-hk=\"0-0-0-7\">+1</button></div>"
|
||||
));
|
||||
}
|
||||
|
||||
runtime.dispose();
|
||||
assert_eq!(
|
||||
rendered.to_html(),
|
||||
"<div class=\"counters\"><div><button>-1</button><span>Value: \
|
||||
<!>1<!>!</span><button>+1</button></div><div><button>-1</\
|
||||
button><span>Value: <!>2<!>!</span><button>+1</button></div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn test_classes() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (value, _set_value) = signal(5);
|
||||
let rendered = view! {
|
||||
|
||||
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! {
|
||||
<div
|
||||
class="my big"
|
||||
class:a=move || { value.get() > 10 }
|
||||
class:red=true
|
||||
class:car=move || { value.get() > 1 }
|
||||
></div>
|
||||
};
|
||||
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().to_html(),
|
||||
"<div class=\"my big red car\"></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().to_html().contains(
|
||||
"<div data-hk=\"0-0-0-1\" class=\"my big red car\"></div>"
|
||||
));
|
||||
}
|
||||
runtime.dispose();
|
||||
assert_eq!(rendered.to_html(), "<div class=\"my big red car\"></div>");
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_with_styles() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (_, set_value) = signal(0);
|
||||
let styles = "myclass";
|
||||
let rendered = view! {
|
||||
class = styles,
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! { class=styles,
|
||||
<div>
|
||||
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
|
||||
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>
|
||||
"-1"
|
||||
</button>
|
||||
</div>
|
||||
};
|
||||
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(
|
||||
rendered.into_view().to_html(),
|
||||
"<div class=\" myclass\"><button class=\"btn \
|
||||
myclass\">-1</button></div>"
|
||||
);
|
||||
} else {
|
||||
assert!(rendered.into_view().to_html().contains(
|
||||
"<div data-hk=\"0-0-0-1\" class=\" myclass\"><button \
|
||||
data-hk=\"0-0-0-2\" class=\"btn myclass\">-1</button></div>"
|
||||
));
|
||||
}
|
||||
runtime.dispose();
|
||||
assert_eq!(
|
||||
rendered.to_html(),
|
||||
"<div class=\"myclass\"><button class=\"btn \
|
||||
myclass\">-1</button></div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
#[test]
|
||||
fn ssr_option() {
|
||||
use leptos::prelude::*;
|
||||
|
||||
let runtime = create_runtime();
|
||||
let (_, _) = signal(0);
|
||||
let rendered = view! {
|
||||
let rendered: HtmlElement<_, _, _, Dom> = view! { <option></option> };
|
||||
|
||||
<option/>
|
||||
};
|
||||
|
||||
if cfg!(all(feature = "experimental-islands", feature = "ssr")) {
|
||||
assert_eq!(rendered.into_view().to_html(), "<option></option>");
|
||||
} else {
|
||||
assert!(rendered
|
||||
.into_view()
|
||||
.to_html()
|
||||
.contains("<option data-hk=\"0-0-0-1\"></option>"));
|
||||
}
|
||||
|
||||
runtime.dispose();
|
||||
assert_eq!(rendered.to_html(), "<option></option>");
|
||||
}
|
||||
|
||||
@@ -11,12 +11,8 @@ rust-version.workspace = true
|
||||
[dependencies]
|
||||
tachys = { workspace = true }
|
||||
reactive_graph = { workspace = true }
|
||||
hydration_context = { workspace = true }
|
||||
or_poisoned = { workspace = true }
|
||||
base64 = { version = "0.21", optional = true }
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
js-sys = "0.3"
|
||||
rand = { version = "0.8", optional = true }
|
||||
send_wrapper = "0.6"
|
||||
tracing = "0.1"
|
||||
wasm-bindgen = "0.2"
|
||||
@@ -30,11 +26,6 @@ features = ["Location"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
nightly = ["reactive_graph/nightly"]
|
||||
# TODO implement nonces
|
||||
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
|
||||
experimental-islands = []
|
||||
trace-component-props = ["tracing"]
|
||||
tracing = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
mod dyn_child;
|
||||
mod each;
|
||||
mod errors;
|
||||
mod fragment;
|
||||
mod unit;
|
||||
|
||||
use crate::{
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
|
||||
pub use dyn_child::*;
|
||||
pub use each::*;
|
||||
pub use errors::*;
|
||||
pub use fragment::*;
|
||||
use leptos_reactive::{untrack_with_diagnostics, Oco};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::OnceCell;
|
||||
use std::fmt;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use std::rc::Rc;
|
||||
pub use unit::*;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// The core foundational leptos components.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum CoreComponent {
|
||||
/// The [Unit] component.
|
||||
Unit(UnitRepr),
|
||||
/// The [DynChild] component.
|
||||
DynChild(DynChildRepr),
|
||||
/// The [Each] component.
|
||||
Each(EachRepr),
|
||||
}
|
||||
|
||||
impl Default for CoreComponent {
|
||||
fn default() -> Self {
|
||||
Self::Unit(UnitRepr::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for CoreComponent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unit(u) => u.fmt(f),
|
||||
Self::DynChild(dc) => dc.fmt(f),
|
||||
Self::Each(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom leptos component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct ComponentRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Rc<OnceCell<()>>,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
pub(crate) name: Oco<'static, str>,
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: Comment,
|
||||
/// The children of the component.
|
||||
pub children: Vec<View>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ComponentRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
if self.children.is_empty() {
|
||||
#[cfg(debug_assertions)]
|
||||
return write!(f, "<{} />", self.name);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return f.write_str("<Component />");
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
writeln!(f, "<{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("<Component>")?;
|
||||
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
|
||||
for child in &self.children {
|
||||
writeln!(pad_adapter, "{child:#?}")?;
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
write!(f, "</{}>", self.name)?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
f.write_str("</Component>")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for ComponentRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.mounted.get().is_none() {
|
||||
self.mounted.set(()).unwrap();
|
||||
|
||||
self.document_fragment
|
||||
.unchecked_ref::<web_sys::Node>()
|
||||
.to_owned()
|
||||
}
|
||||
// We need to prepare all children to move
|
||||
else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(
|
||||
&self.document_fragment,
|
||||
&opening,
|
||||
&self.closing.node,
|
||||
);
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self._opening.node.clone();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return if let Some(child) = self.children.get(0) {
|
||||
child.get_opening_node()
|
||||
} else {
|
||||
self.closing.node.clone()
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
}
|
||||
impl From<ComponentRepr> for View {
|
||||
fn from(value: ComponentRepr) -> Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
for child in &value.children {
|
||||
mount_child(MountKind::Before(&value.closing.node), child);
|
||||
}
|
||||
}
|
||||
|
||||
View::Component(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for ComponentRepr {
|
||||
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "trace", name = "<Component />", skip_all, fields(name = %self.name)))]
|
||||
fn into_view(self) -> View {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentRepr {
|
||||
/// Creates a new [`Component`].
|
||||
#[inline(always)]
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
Self::new_with_id_concrete(name.into(), HydrationCtx::id())
|
||||
}
|
||||
|
||||
/// Creates a new [`Component`] with the given hydration ID.
|
||||
#[inline(always)]
|
||||
pub fn new_with_id(
|
||||
name: impl Into<Oco<'static, str>>,
|
||||
id: Option<HydrationKey>,
|
||||
) -> Self {
|
||||
Self::new_with_id_concrete(name.into(), id)
|
||||
}
|
||||
|
||||
fn new_with_id_concrete(
|
||||
name: Oco<'static, str>,
|
||||
id: Option<HydrationKey>,
|
||||
) -> Self {
|
||||
let markers = (
|
||||
Comment::new(format!("</{name}>"), &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new(format!("<{name}>"), &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.expect("append to not err");
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment
|
||||
.append_with_node_1(&markers.0.node)
|
||||
.expect("append to not err");
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
mounted: Default::default(),
|
||||
#[cfg(debug_assertions)]
|
||||
_opening: markers.1,
|
||||
closing: markers.0,
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
name,
|
||||
children: Vec::with_capacity(1),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "ssr"))]
|
||||
/// Returns the name of the component.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
/// A user-defined `leptos` component.
|
||||
pub struct Component<F, V>
|
||||
where
|
||||
F: FnOnce() -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
id: Option<HydrationKey>,
|
||||
name: Oco<'static, str>,
|
||||
children_fn: F,
|
||||
}
|
||||
|
||||
impl<F, V> Component<F, V>
|
||||
where
|
||||
F: FnOnce() -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
/// Creates a new component.
|
||||
pub fn new(name: impl Into<Oco<'static, str>>, f: F) -> Self {
|
||||
Self {
|
||||
id: HydrationCtx::id(),
|
||||
name: name.into(),
|
||||
children_fn: f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, V> IntoView for Component<F, V>
|
||||
where
|
||||
F: FnOnce() -> V,
|
||||
V: IntoView,
|
||||
{
|
||||
#[track_caller]
|
||||
fn into_view(self) -> View {
|
||||
let Self {
|
||||
id,
|
||||
name,
|
||||
children_fn,
|
||||
} = self;
|
||||
|
||||
let mut repr = ComponentRepr::new_with_id(name, id);
|
||||
|
||||
// disposed automatically when the parent scope is disposed
|
||||
let child = untrack_with_diagnostics(|| children_fn().into_view());
|
||||
|
||||
repr.children.push(child);
|
||||
|
||||
repr.into_view()
|
||||
}
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
use crate::{
|
||||
hydration::{HydrationCtx, HydrationKey},
|
||||
Comment, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{cell::RefCell, fmt, ops::Deref, rc::Rc};
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text};
|
||||
use leptos_reactive::create_render_effect;
|
||||
use wasm_bindgen::JsCast;
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal representation of the [`DynChild`] core-component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment: web_sys::DocumentFragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: Comment,
|
||||
pub(crate) child: Rc<RefCell<Box<Option<View>>>>,
|
||||
closing: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for DynChildRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Write;
|
||||
|
||||
f.write_str("<DynChild>\n")?;
|
||||
|
||||
let mut pad_adapter = pad_adapter::PadAdapter::new(f);
|
||||
|
||||
writeln!(
|
||||
pad_adapter,
|
||||
"{:#?}",
|
||||
self.child.borrow().deref().deref().as_ref().unwrap()
|
||||
)?;
|
||||
|
||||
f.write_str("</DynChild>")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for DynChildRepr {
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
if self.document_fragment.child_nodes().length() != 0 {
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
} else {
|
||||
let opening = self.get_opening_node();
|
||||
|
||||
prepare_to_move(
|
||||
&self.document_fragment,
|
||||
&opening,
|
||||
&self.closing.node,
|
||||
);
|
||||
|
||||
self.document_fragment.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
#[cfg(debug_assertions)]
|
||||
return self.opening.node.clone();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
return self
|
||||
.child
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_opening_node();
|
||||
}
|
||||
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.closing.node.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl DynChildRepr {
|
||||
fn new_with_id(id: Option<HydrationKey>) -> Self {
|
||||
let markers = (
|
||||
Comment::new("</DynChild>", &id, true),
|
||||
#[cfg(debug_assertions)]
|
||||
Comment::new("<DynChild>", &id, false),
|
||||
);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let document_fragment = {
|
||||
let fragment = crate::document().create_document_fragment();
|
||||
|
||||
// Insert the comments into the document fragment
|
||||
// so they can serve as our references when inserting
|
||||
// future nodes
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
#[cfg(debug_assertions)]
|
||||
fragment
|
||||
.append_with_node_2(&markers.1.node, &markers.0.node)
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
fragment.append_with_node_1(&markers.0.node).unwrap();
|
||||
}
|
||||
|
||||
fragment
|
||||
};
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
document_fragment,
|
||||
#[cfg(debug_assertions)]
|
||||
opening: markers.1,
|
||||
child: Default::default(),
|
||||
closing: markers.0,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents any [`View`] that can change over time.
|
||||
pub struct DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
id: Option<HydrationKey>,
|
||||
child_fn: CF,
|
||||
}
|
||||
|
||||
impl<CF, N> DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
/// Creates a new dynamic child which will re-render whenever it's
|
||||
/// signal dependencies change.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn new(child_fn: CF) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), child_fn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub const fn new_with_id(id: Option<HydrationKey>, child_fn: CF) -> Self {
|
||||
Self { id, child_fn }
|
||||
}
|
||||
}
|
||||
|
||||
impl<CF, N> IntoView for DynChild<CF, N>
|
||||
where
|
||||
CF: Fn() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", name = "<DynChild />", skip_all)
|
||||
)]
|
||||
#[inline]
|
||||
fn into_view(self) -> View {
|
||||
// concrete inner function
|
||||
#[inline(never)]
|
||||
fn create_dyn_view(
|
||||
component: DynChildRepr,
|
||||
child_fn: Box<dyn Fn() -> View>,
|
||||
) -> DynChildRepr {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
let closing = component.closing.node.clone();
|
||||
|
||||
let child = component.child.clone();
|
||||
|
||||
#[cfg(all(
|
||||
debug_assertions,
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let span = tracing::Span::current();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
create_render_effect(
|
||||
move |prev_run: Option<Option<web_sys::Node>>| {
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = span.enter();
|
||||
|
||||
let new_child = child_fn().into_view();
|
||||
|
||||
let mut child_borrow = child.borrow_mut();
|
||||
|
||||
// Is this at least the second time we are loading a child?
|
||||
if let Some(prev_t) = prev_run {
|
||||
let child = child_borrow.take().unwrap();
|
||||
|
||||
// We need to know if our child wasn't moved elsewhere.
|
||||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
|
||||
// TODO check does this still detect moves correctly?
|
||||
let was_child_moved = prev_t.is_none()
|
||||
&& child
|
||||
.get_closing_node()
|
||||
.next_non_view_marker_sibling()
|
||||
.as_ref()
|
||||
!= Some(&closing);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
// node
|
||||
let ret = if let Some(prev_t) = prev_t {
|
||||
// Here, our child is also a text node
|
||||
|
||||
// nb: the match/ownership gymnastics here
|
||||
// are so that, if we can reuse the text node,
|
||||
// we can take ownership of new_t so we don't clone
|
||||
// the contents, which in O(n) on the length of the text
|
||||
if matches!(new_child, View::Text(_)) {
|
||||
if !was_child_moved && child != new_child {
|
||||
let mut new_t = match new_child {
|
||||
View::Text(t) => t,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
prev_t
|
||||
.unchecked_ref::<web_sys::Text>()
|
||||
.set_data(&new_t.content);
|
||||
|
||||
// replace new_t's text node with the prev node
|
||||
// see discussion: https://github.com/leptos-rs/leptos/pull/1472
|
||||
new_t.node = prev_t.clone();
|
||||
|
||||
let new_child = View::Text(new_t);
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
Some(prev_t)
|
||||
} else {
|
||||
let new_t = new_child.as_text().unwrap();
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child.clone());
|
||||
|
||||
Some(new_t.node.clone())
|
||||
}
|
||||
}
|
||||
// Child is not a text node, so we can remove the previous
|
||||
// text node
|
||||
else {
|
||||
if !was_child_moved && child != new_child {
|
||||
// Remove the text
|
||||
closing
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Element>()
|
||||
.remove();
|
||||
}
|
||||
|
||||
// Mount the new child, and we're done
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
// Otherwise, the new child can still be a text node,
|
||||
// but we know the previous child was not, so no special
|
||||
// treatment here
|
||||
else {
|
||||
// Technically, I think this check shouldn't be necessary, but
|
||||
// I can imagine some edge case that the child changes while
|
||||
// hydration is ongoing
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
let same_child = child == new_child;
|
||||
if !was_child_moved && !same_child {
|
||||
// Remove the child
|
||||
let start = child.get_opening_node();
|
||||
let end = &closing;
|
||||
|
||||
match child {
|
||||
View::CoreComponent(
|
||||
crate::CoreComponent::DynChild(
|
||||
child,
|
||||
),
|
||||
) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
View::Component(child) => {
|
||||
let start =
|
||||
child.get_opening_node();
|
||||
let end = child.closing.node;
|
||||
prepare_to_move(
|
||||
&child.document_fragment,
|
||||
&start,
|
||||
&end,
|
||||
);
|
||||
}
|
||||
_ => unmount_child(&start, end),
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the new child
|
||||
// If it's the same child, don't re-mount
|
||||
if !same_child {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We want to reuse text nodes, so hold onto it if
|
||||
// our child is one
|
||||
let t =
|
||||
new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
t
|
||||
};
|
||||
|
||||
ret
|
||||
}
|
||||
// Otherwise, we know for sure this is our first time
|
||||
else {
|
||||
// If it's a text node, we want to use the old text node
|
||||
// as the text node for the DynChild, rather than the new
|
||||
// text node being created during hydration
|
||||
let new_child = if HydrationCtx::is_hydrating()
|
||||
&& new_child.get_text().is_some()
|
||||
{
|
||||
let t = closing
|
||||
.previous_non_view_marker_sibling()
|
||||
.unwrap()
|
||||
.unchecked_into::<web_sys::Text>();
|
||||
|
||||
let new_child = match new_child {
|
||||
View::Text(text) => text,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
t.set_data(&new_child.content);
|
||||
View::Text(Text {
|
||||
node: t.unchecked_into(),
|
||||
content: new_child.content,
|
||||
})
|
||||
} else {
|
||||
new_child
|
||||
};
|
||||
|
||||
// If we are not hydrating, we simply mount the child
|
||||
if !HydrationCtx::is_hydrating() {
|
||||
mount_child(
|
||||
MountKind::Before(&closing),
|
||||
&new_child,
|
||||
);
|
||||
}
|
||||
|
||||
// We want to update text nodes, rather than replace them, so
|
||||
// make sure to hold onto the text node
|
||||
let t = new_child.get_text().map(|t| t.node.clone());
|
||||
|
||||
**child_borrow = Some(new_child);
|
||||
|
||||
t
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
{
|
||||
let new_child = child_fn().into_view();
|
||||
|
||||
**child.borrow_mut() = Some(new_child);
|
||||
}
|
||||
|
||||
component
|
||||
}
|
||||
|
||||
// monomorphized outer function
|
||||
let Self { id, child_fn } = self;
|
||||
|
||||
let component = DynChildRepr::new_with_id(id);
|
||||
let component = create_dyn_view(
|
||||
component,
|
||||
Box::new(move || child_fn().into_view()),
|
||||
);
|
||||
|
||||
View::CoreComponent(crate::CoreComponent::DynChild(component))
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use web_sys::Node;
|
||||
|
||||
pub(crate) trait NonViewMarkerSibling {
|
||||
fn next_non_view_marker_sibling(&self) -> Option<Node>;
|
||||
|
||||
fn previous_non_view_marker_sibling(&self) -> Option<Node>;
|
||||
}
|
||||
|
||||
impl NonViewMarkerSibling for web_sys::Node {
|
||||
#[cfg_attr(not(debug_assertions), inline(always))]
|
||||
fn next_non_view_marker_sibling(&self) -> Option<Node> {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
self.next_sibling().and_then(|node| {
|
||||
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
|
||||
node.next_sibling()
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.next_sibling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(debug_assertions), inline(always))]
|
||||
fn previous_non_view_marker_sibling(&self) -> Option<Node> {
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
self.previous_sibling().and_then(|node| {
|
||||
if node.text_content().unwrap_or_default().trim().starts_with("leptos-view") {
|
||||
node.previous_sibling()
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.previous_sibling()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,166 +0,0 @@
|
||||
use crate::{HydrationCtx, IntoView};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::{signal_prelude::*, use_context};
|
||||
use server_fn::error::Error;
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
/// A struct to hold all the possible errors that could be provided by child Views
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[repr(transparent)]
|
||||
pub struct Errors(HashMap<ErrorKey, Error>);
|
||||
|
||||
/// A unique key for an error that occurs at a particular location in the user interface.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct ErrorKey(Cow<'static, str>);
|
||||
|
||||
impl<T> From<T> for ErrorKey
|
||||
where
|
||||
T: Into<Cow<'static, str>>,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn from(key: T) -> ErrorKey {
|
||||
ErrorKey(key.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Errors {
|
||||
type Item = (ErrorKey, Error);
|
||||
type IntoIter = IntoIter;
|
||||
|
||||
#[inline(always)]
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
IntoIter(self.0.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
/// An owning iterator over all the errors contained in the [`Errors`] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct IntoIter(std::collections::hash_map::IntoIter<ErrorKey, Error>);
|
||||
|
||||
impl Iterator for IntoIter {
|
||||
type Item = (ErrorKey, Error);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over all the errors contained in the [`Errors`] struct.
|
||||
#[repr(transparent)]
|
||||
pub struct Iter<'a>(std::collections::hash_map::Iter<'a, ErrorKey, Error>);
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = (&'a ErrorKey, &'a Error);
|
||||
|
||||
#[inline(always)]
|
||||
fn next(
|
||||
&mut self,
|
||||
) -> std::option::Option<<Self as std::iter::Iterator>::Item> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> IntoView for Result<T, E>
|
||||
where
|
||||
T: IntoView + 'static,
|
||||
E: Into<Error>,
|
||||
{
|
||||
fn into_view(self) -> crate::View {
|
||||
let id = ErrorKey(
|
||||
HydrationCtx::peek()
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
);
|
||||
let errors = use_context::<RwSignal<Errors>>();
|
||||
match self {
|
||||
Ok(stuff) => {
|
||||
if let Some(errors) = errors {
|
||||
errors.update(|errors| {
|
||||
errors.0.remove(&id);
|
||||
});
|
||||
}
|
||||
stuff.into_view()
|
||||
}
|
||||
Err(error) => {
|
||||
let error = error.into();
|
||||
match errors {
|
||||
Some(errors) => {
|
||||
errors.update({
|
||||
#[cfg(all(
|
||||
target_arch = "wasm32",
|
||||
feature = "web"
|
||||
))]
|
||||
let id = id.clone();
|
||||
move |errors: &mut Errors| errors.insert(id, error)
|
||||
});
|
||||
|
||||
// remove the error from the list if this drops,
|
||||
// i.e., if it's in a DynChild that switches from Err to Ok
|
||||
// Only can run on the client, will panic on the server
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use leptos_reactive::{on_cleanup, queue_microtask};
|
||||
on_cleanup(move || {
|
||||
queue_microtask(move || {
|
||||
errors.update(|errors: &mut Errors| {
|
||||
errors.remove(&id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
warn!(
|
||||
"No ErrorBoundary components found! Returning \
|
||||
errors will not be handled and will silently \
|
||||
disappear"
|
||||
);
|
||||
}
|
||||
}
|
||||
().into_view()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Errors {
|
||||
/// Returns `true` if there are no errors.
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn insert<E>(&mut self, key: ErrorKey, error: E)
|
||||
where
|
||||
E: Into<Error>,
|
||||
{
|
||||
self.0.insert(key, error.into());
|
||||
}
|
||||
|
||||
/// Add an error with the default key for errors outside the reactive system
|
||||
pub fn insert_with_default_key<E>(&mut self, error: E)
|
||||
where
|
||||
E: Into<Error>,
|
||||
{
|
||||
self.0.insert(Default::default(), error.into());
|
||||
}
|
||||
|
||||
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
|
||||
pub fn remove(&mut self, key: &ErrorKey) -> Option<Error> {
|
||||
self.0.remove(key)
|
||||
}
|
||||
|
||||
/// An iterator over all the errors, in arbitrary order.
|
||||
#[inline(always)]
|
||||
pub fn iter(&self) -> Iter<'_> {
|
||||
Iter(self.0.iter())
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
use crate::{
|
||||
hydration::HydrationKey, ComponentRepr, HydrationCtx, IntoView, View,
|
||||
};
|
||||
|
||||
/// Trait for converting any iterable into a [`Fragment`].
|
||||
pub trait IntoFragment {
|
||||
/// Consumes this type, returning [`Fragment`].
|
||||
fn into_fragment(self) -> Fragment;
|
||||
}
|
||||
|
||||
impl<I, V> IntoFragment for I
|
||||
where
|
||||
I: IntoIterator<Item = V>,
|
||||
V: IntoView,
|
||||
{
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn into_fragment(self) -> Fragment {
|
||||
self.into_iter().map(|v| v.into_view()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a group of [`views`](View).
|
||||
#[must_use = "You are creating a Fragment but not using it. An unused view can \
|
||||
cause your view to be rendered as () unexpectedly, and it can \
|
||||
also cause issues with client-side hydration."]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Fragment {
|
||||
id: Option<HydrationKey>,
|
||||
/// The nodes contained in the fragment.
|
||||
pub nodes: Vec<View>,
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) view_marker: Option<String>,
|
||||
}
|
||||
|
||||
impl FromIterator<View> for Fragment {
|
||||
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
|
||||
Fragment::new(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<View> for Fragment {
|
||||
fn from(view: View) -> Self {
|
||||
Fragment::new(vec![view])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Fragment> for View {
|
||||
fn from(value: Fragment) -> Self {
|
||||
let mut frag = ComponentRepr::new_with_id("", value.id);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
frag.view_marker = value.view_marker;
|
||||
}
|
||||
|
||||
frag.children = value.nodes;
|
||||
|
||||
frag.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Fragment {
|
||||
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
|
||||
#[inline(always)]
|
||||
pub fn new(nodes: Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes)
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] from a function that returns [`Vec<Node>`].
|
||||
#[inline(always)]
|
||||
pub fn lazy(nodes: impl FnOnce() -> Vec<View>) -> Self {
|
||||
Self::new_with_id(HydrationCtx::id(), nodes())
|
||||
}
|
||||
|
||||
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
|
||||
#[inline(always)]
|
||||
pub const fn new_with_id(
|
||||
id: Option<HydrationKey>,
|
||||
nodes: Vec<View>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
nodes,
|
||||
#[cfg(debug_assertions)]
|
||||
view_marker: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gives access to the [`View`] children contained within the fragment.
|
||||
#[inline(always)]
|
||||
pub fn as_children(&self) -> &[View] {
|
||||
&self.nodes
|
||||
}
|
||||
|
||||
/// Returns the fragment's hydration ID.
|
||||
#[inline(always)]
|
||||
pub fn id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Adds an optional marker indicating the view macro source.
|
||||
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
|
||||
self.view_marker = Some(marker.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoView for Fragment {
|
||||
#[cfg_attr(debug_assertions, instrument(level = "trace", name = "</>", skip_all, fields(children = self.nodes.len())))]
|
||||
fn into_view(self) -> View {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
use std::fmt;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use crate::Mountable;
|
||||
use wasm_bindgen::JsCast;
|
||||
} else {
|
||||
use crate::hydration::HydrationKey;
|
||||
}
|
||||
}
|
||||
|
||||
use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View};
|
||||
|
||||
/// The internal representation of the [`Unit`] core-component.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct UnitRepr {
|
||||
comment: Comment,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub(crate) id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for UnitRepr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<() />")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UnitRepr {
|
||||
fn default() -> Self {
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
Self {
|
||||
comment: Comment::new("<() />", &id, true),
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
impl Mountable for UnitRepr {
|
||||
#[inline(always)]
|
||||
fn get_mountable_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_opening_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn get_closing_node(&self) -> web_sys::Node {
|
||||
self.comment.node.clone().unchecked_into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The unit `()` leptos counterpart.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Unit;
|
||||
|
||||
impl IntoView for Unit {
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", name = "<() />", skip_all)
|
||||
)]
|
||||
fn into_view(self) -> crate::View {
|
||||
let component = UnitRepr::default();
|
||||
|
||||
View::CoreComponent(CoreComponent::Unit(component))
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use crate::{html::AnyElement, HtmlElement};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Trait for a directive handler function.
|
||||
/// This is used so it's possible to use functions with one or two
|
||||
/// parameters as directive handlers.
|
||||
///
|
||||
/// You can use directives like the following.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::{*, html::AnyElement};
|
||||
///
|
||||
/// // This doesn't take an attribute value
|
||||
/// fn my_directive(el: HtmlElement<AnyElement>) {
|
||||
/// // do sth
|
||||
/// }
|
||||
///
|
||||
/// // This requires an attribute value
|
||||
/// fn another_directive(el: HtmlElement<AnyElement>, params: i32) {
|
||||
/// // do sth
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent() -> impl IntoView {
|
||||
/// view! {
|
||||
/// // no attribute value
|
||||
/// <div use:my_directive></div>
|
||||
///
|
||||
/// // with an attribute value
|
||||
/// <div use:another_directive=8></div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A directive is just syntactic sugar for
|
||||
///
|
||||
/// ```ignore
|
||||
/// let node_ref = create_node_ref();
|
||||
///
|
||||
/// create_effect(move |_| {
|
||||
/// if let Some(el) = node_ref.get() {
|
||||
/// directive_func(el, possibly_some_param);
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// A directive can be a function with one or two parameters.
|
||||
/// The first is the element the directive is added to and the optional
|
||||
/// second is the parameter that is provided in the attribute.
|
||||
pub trait Directive<T: ?Sized, P> {
|
||||
/// Calls the handler function
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P);
|
||||
}
|
||||
|
||||
impl<F> Directive<(HtmlElement<AnyElement>,), ()> for F
|
||||
where
|
||||
F: Fn(HtmlElement<AnyElement>),
|
||||
{
|
||||
fn run(&self, el: HtmlElement<AnyElement>, _: ()) {
|
||||
self(el)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, P> Directive<(HtmlElement<AnyElement>, P), P> for F
|
||||
where
|
||||
F: Fn(HtmlElement<AnyElement>, P),
|
||||
{
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
|
||||
self(el, param);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, P> Directive<T, P> for Rc<dyn Directive<T, P>> {
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
|
||||
(**self).run(el, param)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, P> Directive<T, P> for Box<dyn Directive<T, P>> {
|
||||
fn run(&self, el: HtmlElement<AnyElement>, param: P) {
|
||||
(**self).run(el, param);
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
pub mod typed;
|
||||
|
||||
use leptos_reactive::Oco;
|
||||
use std::{cell::RefCell, collections::HashSet};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::{
|
||||
convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue,
|
||||
UnwrapThrowExt,
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static GLOBAL_EVENTS: RefCell<HashSet<Oco<'static, str>>> = RefCell::new(HashSet::new());
|
||||
}
|
||||
|
||||
// Used in template macro
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(always)]
|
||||
pub fn add_event_helper<E: crate::ev::EventDescriptor + 'static>(
|
||||
target: &web_sys::Element,
|
||||
event: E,
|
||||
#[allow(unused_mut)] // used for tracing in debug
|
||||
mut event_handler: impl FnMut(E::EventType) + 'static,
|
||||
) {
|
||||
let event_name = event.name();
|
||||
let event_handler = Box::new(event_handler);
|
||||
|
||||
if E::BUBBLES {
|
||||
add_event_listener(
|
||||
target,
|
||||
event.event_delegation_key(),
|
||||
event_name,
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
} else {
|
||||
add_event_listener_undelegated(
|
||||
target,
|
||||
&event_name,
|
||||
event_handler,
|
||||
&None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an event listener to the target DOM element using implicit event delegation.
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub fn add_event_listener<E>(
|
||||
target: &web_sys::Element,
|
||||
key: Oco<'static, str>,
|
||||
event_name: Oco<'static, str>,
|
||||
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
|
||||
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = Box::new(move |e| {
|
||||
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
leptos_reactive::SpecialNonReactiveZone::exit(prev);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
|
||||
let key = intern(&key);
|
||||
debug_assert_eq!(
|
||||
Ok(false),
|
||||
js_sys::Reflect::has(target, &JsValue::from_str(&key)),
|
||||
"Error while adding {key} event listener, a listener of type {key} \
|
||||
already present."
|
||||
);
|
||||
_ = js_sys::Reflect::set(target, &JsValue::from_str(&key), &cb);
|
||||
add_delegated_event_listener(&key, event_name, options);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn add_event_listener_undelegated<E>(
|
||||
target: &web_sys::Element,
|
||||
event_name: &str,
|
||||
#[cfg(debug_assertions)] mut cb: Box<dyn FnMut(E)>,
|
||||
#[cfg(not(debug_assertions))] cb: Box<dyn FnMut(E)>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) where
|
||||
E: FromWasmAbi + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let cb = Box::new(move |e| {
|
||||
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
let _guard = span.enter();
|
||||
cb(e);
|
||||
leptos_reactive::SpecialNonReactiveZone::exit(prev);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let event_name = intern(event_name);
|
||||
let cb = Closure::wrap(cb as Box<dyn FnMut(E)>).into_js_value();
|
||||
if let Some(options) = options {
|
||||
_ = target
|
||||
.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
event_name,
|
||||
cb.unchecked_ref(),
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
_ = target
|
||||
.add_event_listener_with_callback(event_name, cb.unchecked_ref());
|
||||
}
|
||||
}
|
||||
|
||||
// cf eventHandler in ryansolid/dom-expressions
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
pub(crate) fn add_delegated_event_listener(
|
||||
key: &str,
|
||||
event_name: Oco<'static, str>,
|
||||
options: &Option<web_sys::AddEventListenerOptions>,
|
||||
) {
|
||||
GLOBAL_EVENTS.with(|global_events| {
|
||||
let mut events = global_events.borrow_mut();
|
||||
if !events.contains(&event_name) {
|
||||
// create global handler
|
||||
let key = JsValue::from_str(&key);
|
||||
let handler = move |ev: web_sys::Event| {
|
||||
let target = ev.target();
|
||||
let node = ev.composed_path().get(0);
|
||||
let mut node = if node.is_undefined() || node.is_null() {
|
||||
JsValue::from(target)
|
||||
} else {
|
||||
node
|
||||
};
|
||||
|
||||
// TODO reverse Shadow DOM retargetting
|
||||
|
||||
// TODO simulate currentTarget
|
||||
|
||||
while !node.is_null() {
|
||||
let node_is_disabled = js_sys::Reflect::get(
|
||||
&node,
|
||||
&JsValue::from_str("disabled"),
|
||||
)
|
||||
.unwrap_throw()
|
||||
.is_truthy();
|
||||
if !node_is_disabled {
|
||||
let maybe_handler =
|
||||
js_sys::Reflect::get(&node, &key).unwrap_throw();
|
||||
if !maybe_handler.is_undefined() {
|
||||
let f = maybe_handler
|
||||
.unchecked_ref::<js_sys::Function>();
|
||||
|
||||
if let Err(e) = f.call1(&node, &ev) {
|
||||
wasm_bindgen::throw_val(e);
|
||||
}
|
||||
|
||||
if ev.cancel_bubble() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// navigate up tree
|
||||
if let Some(parent) =
|
||||
node.unchecked_ref::<web_sys::Node>().parent_node()
|
||||
{
|
||||
node = parent.into()
|
||||
} else if let Some(root) = node.dyn_ref::<web_sys::ShadowRoot>() {
|
||||
node = root.host().unchecked_into();
|
||||
} else {
|
||||
node = JsValue::null()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let span = ::tracing::Span::current();
|
||||
let handler = move |e| {
|
||||
let _guard = span.enter();
|
||||
handler(e);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let handler = Box::new(handler) as Box<dyn FnMut(web_sys::Event)>;
|
||||
let handler = Closure::wrap(handler).into_js_value();
|
||||
if let Some(options) = options {
|
||||
_ = crate::window().add_event_listener_with_callback_and_add_event_listener_options(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
_ = crate::window().add_event_listener_with_callback(
|
||||
&event_name,
|
||||
handler.unchecked_ref(),
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// register that we've created handler
|
||||
events.insert(event_name);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,681 +0,0 @@
|
||||
//! Types for all DOM events.
|
||||
|
||||
use leptos_reactive::Oco;
|
||||
use std::marker::PhantomData;
|
||||
use wasm_bindgen::convert::FromWasmAbi;
|
||||
|
||||
/// A trait for converting types into [web_sys events](web_sys).
|
||||
pub trait EventDescriptor: Clone {
|
||||
/// The [`web_sys`] event type, such as [`web_sys::MouseEvent`].
|
||||
type EventType: FromWasmAbi;
|
||||
|
||||
/// Indicates if this event bubbles. For example, `click` bubbles,
|
||||
/// but `focus` does not.
|
||||
///
|
||||
/// If this is true, then the event will be delegated globally,
|
||||
/// otherwise, event listeners will be directly attached to the element.
|
||||
const BUBBLES: bool;
|
||||
|
||||
/// The name of the event, such as `click` or `mouseover`.
|
||||
fn name(&self) -> Oco<'static, str>;
|
||||
|
||||
/// The key used for event delegation.
|
||||
fn event_delegation_key(&self) -> Oco<'static, str>;
|
||||
|
||||
/// Return the options for this type. This is only used when you create a [`Custom`] event
|
||||
/// handler.
|
||||
#[inline(always)]
|
||||
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
|
||||
&None
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides the [`EventDescriptor::BUBBLES`] value to always return
|
||||
/// `false`, which forces the event to not be globally delegated.
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct undelegated<Ev: EventDescriptor>(pub Ev);
|
||||
|
||||
impl<Ev: EventDescriptor> EventDescriptor for undelegated<Ev> {
|
||||
type EventType = Ev::EventType;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
self.0.event_delegation_key()
|
||||
}
|
||||
|
||||
const BUBBLES: bool = false;
|
||||
}
|
||||
|
||||
/// A custom event.
|
||||
#[derive(Debug)]
|
||||
pub struct Custom<E: FromWasmAbi = web_sys::Event> {
|
||||
name: Oco<'static, str>,
|
||||
options: Option<web_sys::AddEventListenerOptions>,
|
||||
_event_type: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Clone for Custom<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
options: self.options.clone(),
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
||||
type EventType = E;
|
||||
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
format!("$$${}", self.name).into()
|
||||
}
|
||||
|
||||
const BUBBLES: bool = false;
|
||||
|
||||
#[inline(always)]
|
||||
fn options(&self) -> &Option<web_sys::AddEventListenerOptions> {
|
||||
&self.options
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Custom<E> {
|
||||
/// Creates a custom event type that can be used within
|
||||
/// [`HtmlElement::on`](crate::HtmlElement::on), for events
|
||||
/// which are not covered in the [`ev`](crate::ev) module.
|
||||
pub fn new(name: impl Into<Oco<'static, str>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
options: None,
|
||||
_event_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the [`AddEventListenerOptions`] used for this event listener.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # let canvas_ref: NodeRef<html::Canvas> = create_node_ref();
|
||||
/// # if false {
|
||||
/// let mut non_passive_wheel = ev::Custom::<ev::WheelEvent>::new("wheel");
|
||||
/// let options = non_passive_wheel.options_mut();
|
||||
/// options.passive(false);
|
||||
/// canvas_ref.on_load(move |canvas: HtmlElement<html::Canvas>| {
|
||||
/// canvas.on(non_passive_wheel, move |_event| {
|
||||
/// // Handle _event
|
||||
/// });
|
||||
/// });
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// [`AddEventListenerOptions`]: web_sys::AddEventListenerOptions
|
||||
pub fn options_mut(&mut self) -> &mut web_sys::AddEventListenerOptions {
|
||||
self.options
|
||||
.get_or_insert_with(web_sys::AddEventListenerOptions::new)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type that can respond to DOM events
|
||||
pub trait DOMEventResponder: Sized {
|
||||
/// Adds handler to specified event
|
||||
fn add<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self;
|
||||
/// Same as [add](DOMEventResponder::add), but with [`EventHandler`]
|
||||
#[inline]
|
||||
fn add_handler(self, handler: impl EventHandler) -> Self {
|
||||
handler.attach(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DOMEventResponder for crate::HtmlElement<T>
|
||||
where
|
||||
T: crate::html::ElementDescriptor + 'static,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn add<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self {
|
||||
self.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
impl DOMEventResponder for crate::View {
|
||||
#[inline(always)]
|
||||
fn add<E: EventDescriptor + 'static>(
|
||||
self,
|
||||
event: E,
|
||||
handler: impl FnMut(E::EventType) + 'static,
|
||||
) -> Self {
|
||||
self.on(event, handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// A statically typed event handler.
|
||||
pub enum EventHandlerFn {
|
||||
/// `keydown` event handler.
|
||||
Keydown(Box<dyn FnMut(KeyboardEvent)>),
|
||||
/// `keyup` event handler.
|
||||
Keyup(Box<dyn FnMut(KeyboardEvent)>),
|
||||
/// `keypress` event handler.
|
||||
Keypress(Box<dyn FnMut(KeyboardEvent)>),
|
||||
|
||||
/// `click` event handler.
|
||||
Click(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `dblclick` event handler.
|
||||
Dblclick(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `mousedown` event handler.
|
||||
Mousedown(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `mouseup` event handler.
|
||||
Mouseup(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `mouseenter` event handler.
|
||||
Mouseenter(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `mouseleave` event handler.
|
||||
Mouseleave(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `mouseout` event handler.
|
||||
Mouseout(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `mouseover` event handler.
|
||||
Mouseover(Box<dyn FnMut(MouseEvent)>),
|
||||
/// `mousemove` event handler.
|
||||
Mousemove(Box<dyn FnMut(MouseEvent)>),
|
||||
|
||||
/// `wheel` event handler.
|
||||
Wheel(Box<dyn FnMut(WheelEvent)>),
|
||||
|
||||
/// `touchstart` event handler.
|
||||
Touchstart(Box<dyn FnMut(TouchEvent)>),
|
||||
/// `touchend` event handler.
|
||||
Touchend(Box<dyn FnMut(TouchEvent)>),
|
||||
/// `touchcancel` event handler.
|
||||
Touchcancel(Box<dyn FnMut(TouchEvent)>),
|
||||
/// `touchmove` event handler.
|
||||
Touchmove(Box<dyn FnMut(TouchEvent)>),
|
||||
|
||||
/// `pointerenter` event handler.
|
||||
Pointerenter(Box<dyn FnMut(PointerEvent)>),
|
||||
/// `pointerleave` event handler.
|
||||
Pointerleave(Box<dyn FnMut(PointerEvent)>),
|
||||
/// `pointerdown` event handler.
|
||||
Pointerdown(Box<dyn FnMut(PointerEvent)>),
|
||||
/// `pointerup` event handler.
|
||||
Pointerup(Box<dyn FnMut(PointerEvent)>),
|
||||
/// `pointercancel` event handler.
|
||||
Pointercancel(Box<dyn FnMut(PointerEvent)>),
|
||||
/// `pointerout` event handler.
|
||||
Pointerout(Box<dyn FnMut(PointerEvent)>),
|
||||
/// `pointerover` event handler.
|
||||
Pointerover(Box<dyn FnMut(PointerEvent)>),
|
||||
/// `pointermove` event handler.
|
||||
Pointermove(Box<dyn FnMut(PointerEvent)>),
|
||||
|
||||
/// `drag` event handler.
|
||||
Drag(Box<dyn FnMut(DragEvent)>),
|
||||
/// `dragend` event handler.
|
||||
Dragend(Box<dyn FnMut(DragEvent)>),
|
||||
/// `dragenter` event handler.
|
||||
Dragenter(Box<dyn FnMut(DragEvent)>),
|
||||
/// `dragleave` event handler.
|
||||
Dragleave(Box<dyn FnMut(DragEvent)>),
|
||||
/// `dragstart` event handler.
|
||||
Dragstart(Box<dyn FnMut(DragEvent)>),
|
||||
/// `drop` event handler.
|
||||
Drop(Box<dyn FnMut(DragEvent)>),
|
||||
|
||||
/// `blur` event handler.
|
||||
Blur(Box<dyn FnMut(FocusEvent)>),
|
||||
/// `focusout` event handler.
|
||||
Focusout(Box<dyn FnMut(FocusEvent)>),
|
||||
/// `focus` event handler.
|
||||
Focus(Box<dyn FnMut(FocusEvent)>),
|
||||
/// `focusin` event handler.
|
||||
Focusin(Box<dyn FnMut(FocusEvent)>),
|
||||
}
|
||||
|
||||
/// Type that can be used to handle DOM events
|
||||
pub trait EventHandler {
|
||||
/// Attaches event listener to any target that can respond to DOM events
|
||||
fn attach<T: DOMEventResponder>(self, target: T) -> T;
|
||||
}
|
||||
|
||||
impl<T, const N: usize> EventHandler for [T; N]
|
||||
where
|
||||
T: EventHandler,
|
||||
{
|
||||
#[inline]
|
||||
fn attach<R: DOMEventResponder>(self, target: R) -> R {
|
||||
let mut target = target;
|
||||
for item in self {
|
||||
target = item.attach(target);
|
||||
}
|
||||
target
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EventHandler for Option<T>
|
||||
where
|
||||
T: EventHandler,
|
||||
{
|
||||
#[inline]
|
||||
fn attach<R: DOMEventResponder>(self, target: R) -> R {
|
||||
match self {
|
||||
Some(event_handler) => event_handler.attach(target),
|
||||
None => target,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! tc {
|
||||
($($ty:ident),*) => {
|
||||
impl<$($ty),*> EventHandler for ($($ty,)*)
|
||||
where
|
||||
$($ty: EventHandler),*
|
||||
{
|
||||
#[inline]
|
||||
fn attach<RES: DOMEventResponder>(self, target: RES) -> RES {
|
||||
::paste::paste! {
|
||||
let (
|
||||
$(
|
||||
[<$ty:lower>],)*
|
||||
) = self;
|
||||
$(
|
||||
let target = [<$ty:lower>].attach(target);
|
||||
)*
|
||||
target
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tc!(A);
|
||||
tc!(A, B);
|
||||
tc!(A, B, C);
|
||||
tc!(A, B, C, D);
|
||||
tc!(A, B, C, D, E);
|
||||
tc!(A, B, C, D, E, F);
|
||||
tc!(A, B, C, D, E, F, G);
|
||||
tc!(A, B, C, D, E, F, G, H);
|
||||
tc!(A, B, C, D, E, F, G, H, I);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
|
||||
tc!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
|
||||
tc!(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);
|
||||
#[rustfmt::skip]
|
||||
tc!(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, Z);
|
||||
|
||||
macro_rules! collection_callback {
|
||||
{$(
|
||||
$collection:ident
|
||||
),* $(,)?} => {
|
||||
$(
|
||||
impl<T> EventHandler for $collection<T>
|
||||
where
|
||||
T: EventHandler
|
||||
{
|
||||
#[inline]
|
||||
fn attach<R: DOMEventResponder>(self, target: R) -> R {
|
||||
let mut target = target;
|
||||
for item in self {
|
||||
target = item.attach(target);
|
||||
}
|
||||
target
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque};
|
||||
|
||||
collection_callback! {
|
||||
Vec,
|
||||
BTreeSet,
|
||||
BinaryHeap,
|
||||
HashSet,
|
||||
LinkedList,
|
||||
VecDeque,
|
||||
}
|
||||
|
||||
macro_rules! generate_event_types {
|
||||
{$(
|
||||
$( #[$does_not_bubble:ident] )?
|
||||
$( $event:ident )+ : $web_event:ident
|
||||
),* $(,)?} => {
|
||||
::paste::paste! {
|
||||
$(
|
||||
#[doc = "The `" [< $($event)+ >] "` event, which receives [" $web_event "](web_sys::" $web_event ") as its argument."]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct [<$( $event )+ >];
|
||||
|
||||
impl EventDescriptor for [< $($event)+ >] {
|
||||
type EventType = web_sys::$web_event;
|
||||
|
||||
#[inline(always)]
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!([< $($event)+ >]).into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn event_delegation_key(&self) -> Oco<'static, str> {
|
||||
concat!("$$$", stringify!([< $($event)+ >])).into()
|
||||
}
|
||||
|
||||
const BUBBLES: bool = true $(&& generate_event_types!($does_not_bubble))?;
|
||||
}
|
||||
)*
|
||||
|
||||
/// An enum holding all basic event types with their respective handlers.
|
||||
///
|
||||
/// It currently omits [`Custom`] and [`undelegated`] variants.
|
||||
#[non_exhaustive]
|
||||
pub enum GenericEventHandler {
|
||||
$(
|
||||
#[doc = "Variant mapping [`struct@" [< $($event)+ >] "`] to its event handler type."]
|
||||
[< $($event:camel)+ >]([< $($event)+ >], Box<dyn FnMut($web_event) + 'static>),
|
||||
)*
|
||||
}
|
||||
|
||||
impl ::core::fmt::Debug for GenericEventHandler {
|
||||
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
|
||||
match self {
|
||||
$(
|
||||
Self::[< $($event:camel)+ >](event, _) => f
|
||||
.debug_tuple(stringify!([< $($event:camel)+ >]))
|
||||
.field(&event)
|
||||
.field(&::std::any::type_name::<Box<dyn FnMut($web_event) + 'static>>())
|
||||
.finish(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventHandler for GenericEventHandler {
|
||||
fn attach<T: DOMEventResponder>(self, target: T) -> T {
|
||||
match self {
|
||||
$(
|
||||
Self::[< $($event:camel)+ >](event, handler) => target.add(event, handler),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
impl<F> From<([< $($event)+ >], F)> for GenericEventHandler
|
||||
where
|
||||
F: FnMut($web_event) + 'static
|
||||
{
|
||||
fn from(value: ([< $($event)+ >], F)) -> Self {
|
||||
Self::[< $($event:camel)+ >](value.0, Box::new(value.1))
|
||||
}
|
||||
}
|
||||
// NOTE: this could become legal in future and would save us from useless allocations
|
||||
//impl<F> From<([< $($event)+ >], Box<F>)> for GenericEventHandler
|
||||
//where
|
||||
// F: FnMut($web_event) + 'static
|
||||
//{
|
||||
// fn from(value: ([< $($event)+ >], Box<F>)) -> Self {
|
||||
// Self::[< $($event:camel)+ >](value.0, value.1)
|
||||
// }
|
||||
//}
|
||||
impl<F> EventHandler for ([< $($event)+ >], F)
|
||||
where
|
||||
F: FnMut($web_event) + 'static
|
||||
{
|
||||
fn attach<L: DOMEventResponder>(self, target: L) -> L {
|
||||
target.add(self.0, self.1)
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
|
||||
(does_not_bubble) => { false }
|
||||
}
|
||||
|
||||
generate_event_types! {
|
||||
// =========================================================
|
||||
// WindowEventHandlersEventMap
|
||||
// =========================================================
|
||||
#[does_not_bubble]
|
||||
after print: Event,
|
||||
#[does_not_bubble]
|
||||
before print: Event,
|
||||
#[does_not_bubble]
|
||||
before unload: BeforeUnloadEvent,
|
||||
#[does_not_bubble]
|
||||
gamepad connected: GamepadEvent,
|
||||
#[does_not_bubble]
|
||||
gamepad disconnected: GamepadEvent,
|
||||
hash change: HashChangeEvent,
|
||||
#[does_not_bubble]
|
||||
language change: Event,
|
||||
#[does_not_bubble]
|
||||
message: MessageEvent,
|
||||
#[does_not_bubble]
|
||||
message error: MessageEvent,
|
||||
#[does_not_bubble]
|
||||
offline: Event,
|
||||
#[does_not_bubble]
|
||||
online: Event,
|
||||
#[does_not_bubble]
|
||||
page hide: PageTransitionEvent,
|
||||
#[does_not_bubble]
|
||||
page show: PageTransitionEvent,
|
||||
pop state: PopStateEvent,
|
||||
rejection handled: PromiseRejectionEvent,
|
||||
#[does_not_bubble]
|
||||
storage: StorageEvent,
|
||||
#[does_not_bubble]
|
||||
unhandled rejection: PromiseRejectionEvent,
|
||||
#[does_not_bubble]
|
||||
unload: Event,
|
||||
|
||||
// =========================================================
|
||||
// GlobalEventHandlersEventMap
|
||||
// =========================================================
|
||||
#[does_not_bubble]
|
||||
abort: UiEvent,
|
||||
animation cancel: AnimationEvent,
|
||||
animation end: AnimationEvent,
|
||||
animation iteration: AnimationEvent,
|
||||
animation start: AnimationEvent,
|
||||
aux click: MouseEvent,
|
||||
before input: InputEvent,
|
||||
#[does_not_bubble]
|
||||
blur: FocusEvent,
|
||||
#[does_not_bubble]
|
||||
can play: Event,
|
||||
#[does_not_bubble]
|
||||
can play through: Event,
|
||||
change: Event,
|
||||
click: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
close: Event,
|
||||
composition end: CompositionEvent,
|
||||
composition start: CompositionEvent,
|
||||
composition update: CompositionEvent,
|
||||
context menu: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
cue change: Event,
|
||||
dbl click: MouseEvent,
|
||||
drag: DragEvent,
|
||||
drag end: DragEvent,
|
||||
drag enter: DragEvent,
|
||||
drag leave: DragEvent,
|
||||
drag over: DragEvent,
|
||||
drag start: DragEvent,
|
||||
drop: DragEvent,
|
||||
#[does_not_bubble]
|
||||
duration change: Event,
|
||||
#[does_not_bubble]
|
||||
emptied: Event,
|
||||
#[does_not_bubble]
|
||||
ended: Event,
|
||||
#[does_not_bubble]
|
||||
error: ErrorEvent,
|
||||
#[does_not_bubble]
|
||||
focus: FocusEvent,
|
||||
#[does_not_bubble]
|
||||
focus in: FocusEvent,
|
||||
#[does_not_bubble]
|
||||
focus out: FocusEvent,
|
||||
form data: Event, // web_sys does not include `FormDataEvent`
|
||||
#[does_not_bubble]
|
||||
got pointer capture: PointerEvent,
|
||||
input: Event,
|
||||
#[does_not_bubble]
|
||||
invalid: Event,
|
||||
key down: KeyboardEvent,
|
||||
key press: KeyboardEvent,
|
||||
key up: KeyboardEvent,
|
||||
#[does_not_bubble]
|
||||
load: Event,
|
||||
#[does_not_bubble]
|
||||
loaded data: Event,
|
||||
#[does_not_bubble]
|
||||
loaded metadata: Event,
|
||||
#[does_not_bubble]
|
||||
load start: Event,
|
||||
lost pointer capture: PointerEvent,
|
||||
mouse down: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
mouse enter: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
mouse leave: MouseEvent,
|
||||
mouse move: MouseEvent,
|
||||
mouse out: MouseEvent,
|
||||
mouse over: MouseEvent,
|
||||
mouse up: MouseEvent,
|
||||
#[does_not_bubble]
|
||||
pause: Event,
|
||||
#[does_not_bubble]
|
||||
play: Event,
|
||||
#[does_not_bubble]
|
||||
playing: Event,
|
||||
pointer cancel: PointerEvent,
|
||||
pointer down: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
pointer enter: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
pointer leave: PointerEvent,
|
||||
pointer move: PointerEvent,
|
||||
pointer out: PointerEvent,
|
||||
pointer over: PointerEvent,
|
||||
pointer up: PointerEvent,
|
||||
#[does_not_bubble]
|
||||
progress: ProgressEvent,
|
||||
#[does_not_bubble]
|
||||
rate change: Event,
|
||||
reset: Event,
|
||||
#[does_not_bubble]
|
||||
resize: UiEvent,
|
||||
#[does_not_bubble]
|
||||
scroll: Event,
|
||||
#[does_not_bubble]
|
||||
scroll end: Event,
|
||||
security policy violation: SecurityPolicyViolationEvent,
|
||||
#[does_not_bubble]
|
||||
seeked: Event,
|
||||
#[does_not_bubble]
|
||||
seeking: Event,
|
||||
select: Event,
|
||||
#[does_not_bubble]
|
||||
selection change: Event,
|
||||
select start: Event,
|
||||
slot change: Event,
|
||||
#[does_not_bubble]
|
||||
stalled: Event,
|
||||
submit: SubmitEvent,
|
||||
#[does_not_bubble]
|
||||
suspend: Event,
|
||||
#[does_not_bubble]
|
||||
time update: Event,
|
||||
#[does_not_bubble]
|
||||
toggle: Event,
|
||||
touch cancel: TouchEvent,
|
||||
touch end: TouchEvent,
|
||||
touch move: TouchEvent,
|
||||
touch start: TouchEvent,
|
||||
transition cancel: TransitionEvent,
|
||||
transition end: TransitionEvent,
|
||||
transition run: TransitionEvent,
|
||||
transition start: TransitionEvent,
|
||||
#[does_not_bubble]
|
||||
volume change: Event,
|
||||
#[does_not_bubble]
|
||||
waiting: Event,
|
||||
webkit animation end: Event,
|
||||
webkit animation iteration: Event,
|
||||
webkit animation start: Event,
|
||||
webkit transition end: Event,
|
||||
wheel: WheelEvent,
|
||||
|
||||
// =========================================================
|
||||
// WindowEventMap
|
||||
// =========================================================
|
||||
D O M Content Loaded: Event, // Hack for correct casing
|
||||
#[does_not_bubble]
|
||||
device motion: DeviceMotionEvent,
|
||||
#[does_not_bubble]
|
||||
device orientation: DeviceOrientationEvent,
|
||||
#[does_not_bubble]
|
||||
orientation change: Event,
|
||||
|
||||
// =========================================================
|
||||
// DocumentAndElementEventHandlersEventMap
|
||||
// =========================================================
|
||||
copy: Event, // ClipboardEvent is unstable
|
||||
cut: Event, // ClipboardEvent is unstable
|
||||
paste: Event, // ClipboardEvent is unstable
|
||||
|
||||
// =========================================================
|
||||
// DocumentEventMap
|
||||
// =========================================================
|
||||
fullscreen change: Event,
|
||||
fullscreen error: Event,
|
||||
pointer lock change: Event,
|
||||
pointer lock error: Event,
|
||||
#[does_not_bubble]
|
||||
ready state change: Event,
|
||||
visibility change: Event,
|
||||
}
|
||||
|
||||
// Export `web_sys` event types
|
||||
pub use web_sys::{
|
||||
AnimationEvent, BeforeUnloadEvent, CompositionEvent, CustomEvent,
|
||||
DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, Event,
|
||||
FocusEvent, GamepadEvent, HashChangeEvent, InputEvent, KeyboardEvent,
|
||||
MessageEvent, MouseEvent, PageTransitionEvent, PointerEvent, PopStateEvent,
|
||||
ProgressEvent, PromiseRejectionEvent, SecurityPolicyViolationEvent,
|
||||
StorageEvent, SubmitEvent, TouchEvent, TransitionEvent, UiEvent,
|
||||
WheelEvent,
|
||||
};
|
||||
@@ -141,7 +141,7 @@ pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
|
||||
// Closure::once_into_js only frees the callback when it's actually
|
||||
// called, so this instead uses into_js_value, which can be freed by
|
||||
// the host JS engine's GC if it supports weak references (which all
|
||||
// modern brower engines do). The way this works is that the provided
|
||||
// modern browser engines do). The way this works is that the provided
|
||||
// callback's captured data is dropped immediately after being called,
|
||||
// as before, but it leaves behind a small stub closure rust-side that
|
||||
// will be freed "eventually" by the JS GC. If the function is never
|
||||
@@ -318,7 +318,7 @@ pub fn set_timeout_with_handle(
|
||||
/// listeners to prevent them from firing constantly as you type.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::{leptos_dom::helpers::debounce, logging::log, *};
|
||||
/// use leptos::{leptos_dom::helpers::debounce, logging::log, prelude::*, *};
|
||||
///
|
||||
/// #[component]
|
||||
/// fn DebouncedButton() -> impl IntoView {
|
||||
@@ -507,7 +507,10 @@ pub fn window_event_listener_untyped(
|
||||
/// Creates a window event listener from a typed event, returning a
|
||||
/// cancelable handle.
|
||||
/// ```
|
||||
/// use leptos::{leptos_dom::helpers::window_event_listener, logging::log, *};
|
||||
/// use leptos::{
|
||||
/// ev, leptos_dom::helpers::window_event_listener, logging::log,
|
||||
/// prelude::*,
|
||||
/// };
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App() -> impl IntoView {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,312 +0,0 @@
|
||||
#[cfg(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
))]
|
||||
use leptos_reactive::SharedContext;
|
||||
use std::{cell::RefCell, fmt::Display};
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
mod hydrate_only {
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use std::{cell::Cell, collections::HashMap};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
/// See ["createTreeWalker"](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker)
|
||||
#[allow(unused)]
|
||||
const FILTER_SHOW_COMMENT: u32 = 0b10000000;
|
||||
|
||||
thread_local! {
|
||||
pub static HYDRATION_COMMENTS: LazyCell<HashMap<String, web_sys::Comment>> = LazyCell::new(|| {
|
||||
let document = crate::document();
|
||||
let body = document.body().unwrap();
|
||||
let walker = document
|
||||
.create_tree_walker_with_what_to_show(&body, FILTER_SHOW_COMMENT)
|
||||
.unwrap();
|
||||
let mut map = HashMap::new();
|
||||
while let Ok(Some(node)) = walker.next_node() {
|
||||
if let Some(content) = node.text_content() {
|
||||
if let Some(hk) = content.strip_prefix("hk=") {
|
||||
if let Some(hk) = hk.split('|').next() {
|
||||
map.insert(hk.into(), node.unchecked_into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
pub static HYDRATION_ELEMENTS: LazyCell<HashMap<String, web_sys::HtmlElement>> = LazyCell::new(|| {
|
||||
let document = crate::document();
|
||||
let els = document.query_selector_all("[data-hk]");
|
||||
if let Ok(list) = els {
|
||||
let len = list.length();
|
||||
let mut map = HashMap::with_capacity(len as usize);
|
||||
for idx in 0..len {
|
||||
let el = list.item(idx).unwrap().unchecked_into::<web_sys::HtmlElement>();
|
||||
let dataset = el.dataset();
|
||||
let hk = dataset.get(wasm_bindgen::intern("hk")).unwrap();
|
||||
map.insert(hk, el);
|
||||
}
|
||||
map
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
pub static IS_HYDRATING: Cell<bool> = const { Cell::new(true) };
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_marker(id: &str) -> Option<web_sys::Comment> {
|
||||
HYDRATION_COMMENTS.with(|comments| comments.get(id).cloned())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn get_element(hk: &str) -> Option<web_sys::HtmlElement> {
|
||||
HYDRATION_ELEMENTS.with(|els| els.get(hk).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub(crate) use hydrate_only::*;
|
||||
|
||||
/// A stable identifier within the server-rendering or hydration process.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct HydrationKey {
|
||||
/// ID of the current outlet
|
||||
pub outlet: usize,
|
||||
/// ID of the current fragment.
|
||||
pub fragment: usize,
|
||||
/// ID of the current error boundary.
|
||||
pub error: usize,
|
||||
/// ID of the current key.
|
||||
pub id: usize,
|
||||
}
|
||||
|
||||
impl Display for HydrationKey {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}-{}-{}-{}",
|
||||
self.outlet, self.fragment, self.error, self.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for HydrationKey {
|
||||
type Err = (); // TODO better error
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut pieces = s.splitn(4, '-');
|
||||
let first = pieces.next().ok_or(())?;
|
||||
let second = pieces.next().ok_or(())?;
|
||||
let third = pieces.next().ok_or(())?;
|
||||
let fourth = pieces.next().ok_or(())?;
|
||||
let outlet = usize::from_str(first).map_err(|_| ())?;
|
||||
let fragment = usize::from_str(second).map_err(|_| ())?;
|
||||
let error = usize::from_str(third).map_err(|_| ())?;
|
||||
let id = usize::from_str(fourth).map_err(|_| ())?;
|
||||
Ok(HydrationKey {
|
||||
outlet,
|
||||
fragment,
|
||||
error,
|
||||
id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn parse_hydration_key() {
|
||||
use crate::HydrationKey;
|
||||
use std::str::FromStr;
|
||||
assert_eq!(
|
||||
HydrationKey::from_str("0-1-2-3"),
|
||||
Ok(HydrationKey {
|
||||
outlet: 0,
|
||||
fragment: 1,
|
||||
error: 2,
|
||||
id: 3
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
thread_local!(static ID: RefCell<HydrationKey> = const {RefCell::new(HydrationKey { outlet: 0, fragment: 0, error: 0, id: 0 })});
|
||||
|
||||
/// Control and utility methods for hydration.
|
||||
pub struct HydrationCtx;
|
||||
|
||||
impl HydrationCtx {
|
||||
/// If you're in an hydration context, get the next `id` without incrementing it.
|
||||
pub fn peek() -> Option<HydrationKey> {
|
||||
#[cfg(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
))]
|
||||
let no_hydrate = SharedContext::no_hydrate();
|
||||
#[cfg(not(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
)))]
|
||||
let no_hydrate = false;
|
||||
if no_hydrate {
|
||||
None
|
||||
} else {
|
||||
Some(ID.with(|id| *id.borrow()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next `id` without incrementing it.
|
||||
pub fn peek_always() -> HydrationKey {
|
||||
ID.with(|id| *id.borrow())
|
||||
}
|
||||
|
||||
/// Increments the current hydration `id` and returns it
|
||||
pub fn id() -> Option<HydrationKey> {
|
||||
#[cfg(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
))]
|
||||
let no_hydrate = SharedContext::no_hydrate();
|
||||
#[cfg(not(all(
|
||||
feature = "experimental-islands",
|
||||
any(feature = "hydrate", feature = "ssr")
|
||||
)))]
|
||||
let no_hydrate = false;
|
||||
|
||||
if no_hydrate {
|
||||
None
|
||||
} else {
|
||||
Some(ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.id = id.id.wrapping_add(1);
|
||||
*id
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
pub fn next_component() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.fragment = id.fragment.wrapping_add(1);
|
||||
id.id = 0;
|
||||
*id
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next outlet, and returns it
|
||||
pub fn next_outlet() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.outlet = id.outlet.wrapping_add(1);
|
||||
id.id = 0;
|
||||
*id
|
||||
})
|
||||
}
|
||||
|
||||
/// Resets the hydration `id` for the next component, and returns it
|
||||
pub fn next_error() -> HydrationKey {
|
||||
ID.with(|id| {
|
||||
let mut id = id.borrow_mut();
|
||||
id.error = id.error.wrapping_add(1);
|
||||
id.id = 0;
|
||||
*id
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
pub fn reset_id() {
|
||||
ID.with(|id| {
|
||||
*id.borrow_mut() = HydrationKey {
|
||||
outlet: 0,
|
||||
fragment: 0,
|
||||
error: 0,
|
||||
id: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Resumes hydration from the provided `id`. Useful for
|
||||
/// `Suspense` and other fancy things.
|
||||
pub fn continue_from(id: HydrationKey) {
|
||||
ID.with(|i| *i.borrow_mut() = id);
|
||||
}
|
||||
|
||||
/// Resumes hydration after the provided `id`. Useful for
|
||||
/// islands and other fancy things.
|
||||
pub fn continue_after(id: HydrationKey) {
|
||||
ID.with(|i| {
|
||||
*i.borrow_mut() = HydrationKey {
|
||||
outlet: id.outlet,
|
||||
fragment: id.fragment,
|
||||
error: id.error,
|
||||
id: id.id + 1,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn stop_hydrating() {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
IS_HYDRATING.with(|is_hydrating| {
|
||||
is_hydrating.set(false);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn with_hydration_on<T>(f: impl FnOnce() -> T) -> T {
|
||||
let prev = IS_HYDRATING.with(|is_hydrating| {
|
||||
let prev = is_hydrating.get();
|
||||
is_hydrating.set(true);
|
||||
prev
|
||||
});
|
||||
let value = f();
|
||||
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
|
||||
value
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn with_hydration_off<T>(f: impl FnOnce() -> T) -> T {
|
||||
let prev = IS_HYDRATING.with(|is_hydrating| {
|
||||
let prev = is_hydrating.get();
|
||||
is_hydrating.set(false);
|
||||
prev
|
||||
});
|
||||
let value = f();
|
||||
IS_HYDRATING.with(|is_hydrating| is_hydrating.set(prev));
|
||||
value
|
||||
}
|
||||
|
||||
/// Whether the UI is currently in the process of hydrating from the server-sent HTML.
|
||||
#[inline(always)]
|
||||
pub fn is_hydrating() -> bool {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
IS_HYDRATING.with(|is_hydrating| is_hydrating.get())
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn to_string(id: &HydrationKey, closing: bool) -> String {
|
||||
#[cfg(debug_assertions)]
|
||||
return format!("{id}{}", if closing { 'c' } else { 'o' });
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
id.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,421 +0,0 @@
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
use leptos_reactive::{Oco, TextProp};
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
/// Represents the different possible values an attribute node could have.
|
||||
///
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly, but it can be useful for defining
|
||||
/// permissive APIs for certain components.
|
||||
#[derive(Clone)]
|
||||
pub enum Attribute {
|
||||
/// A plain string value.
|
||||
String(Oco<'static, str>),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute.
|
||||
Fn(Rc<dyn Fn() -> Attribute>),
|
||||
/// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`.
|
||||
Option(Option<Oco<'static, str>>),
|
||||
/// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`.
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
/// Converts the attribute to its HTML value at that moment, including the attribute name,
|
||||
/// so it can be rendered on the server.
|
||||
pub fn as_value_string(
|
||||
&self,
|
||||
attr_name: &'static str,
|
||||
) -> Oco<'static, str> {
|
||||
match self {
|
||||
Attribute::String(value) => {
|
||||
format!("{attr_name}=\"{value}\"").into()
|
||||
}
|
||||
Attribute::Fn(f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_value_string(attr_name)
|
||||
}
|
||||
Attribute::Option(value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{attr_name}=\"{value}\"").into())
|
||||
.unwrap_or_default(),
|
||||
Attribute::Bool(include) => {
|
||||
Oco::Borrowed(if *include { attr_name } else { "" })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the attribute to its HTML value at that moment, not including
|
||||
/// the attribute name, so it can be rendered on the server.
|
||||
pub fn as_nameless_value_string(&self) -> Option<Oco<'static, str>> {
|
||||
match self {
|
||||
Attribute::String(value) => Some(value.clone()),
|
||||
Attribute::Fn(f) => {
|
||||
let mut value = f();
|
||||
while let Attribute::Fn(f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_nameless_value_string()
|
||||
}
|
||||
Attribute::Option(value) => value.as_ref().cloned(),
|
||||
Attribute::Bool(include) => {
|
||||
if *include {
|
||||
Some("".into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Attribute {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::String(l0), Self::String(r0)) => l0 == r0,
|
||||
(Self::Fn(_), Self::Fn(_)) => false,
|
||||
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Attribute {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(),
|
||||
Self::Fn(_) => f.debug_tuple("Fn").finish(),
|
||||
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
|
||||
Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts some type into an [`Attribute`].
|
||||
///
|
||||
/// This is implemented by default for Rust primitive and string types.
|
||||
pub trait IntoAttribute {
|
||||
/// Converts the object into an [`Attribute`].
|
||||
fn into_attribute(self) -> Attribute;
|
||||
|
||||
/// Helper function for dealing with `Box<dyn IntoAttribute>`.
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute;
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute + 'static> From<T> for Box<dyn IntoAttribute> {
|
||||
#[inline(always)]
|
||||
fn from(value: T) -> Self {
|
||||
Box::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Attribute {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_into_attr_boxed {
|
||||
() => {
|
||||
#[inline(always)]
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
|
||||
self.into_attribute()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IntoAttribute for String {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Oco::Owned(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Oco::Borrowed(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Cow<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(self.into())
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Oco<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Rc<str> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(Oco::Counted(self))
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for bool {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::Bool(self)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl<T: IntoAttribute> IntoAttribute for Option<T> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
self.map_or(Attribute::Option(None), IntoAttribute::into_attribute)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl<T, U> IntoAttribute for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: IntoAttribute,
|
||||
{
|
||||
fn into_attribute(self) -> Attribute {
|
||||
let modified_fn = Rc::new(move || (self)().into_attribute());
|
||||
Attribute::Fn(modified_fn)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Box<dyn IntoAttribute>> {
|
||||
fn into_attribute(self) -> Attribute {
|
||||
match self {
|
||||
Some(bx) => bx.into_attribute_boxed(),
|
||||
None => Attribute::Option(None),
|
||||
}
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for TextProp {
|
||||
fn into_attribute(self) -> Attribute {
|
||||
(move || self.get()).into_attribute()
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
impl IntoAttribute for core::fmt::Arguments<'_> {
|
||||
fn into_attribute(self) -> Attribute {
|
||||
match self.as_str() {
|
||||
Some(s) => s.into_attribute(),
|
||||
None => self.to_string().into_attribute(),
|
||||
}
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
|
||||
/* impl IntoAttribute for Box<dyn IntoAttribute> {
|
||||
#[inline(always)]
|
||||
fn into_attribute(self) -> Attribute {
|
||||
self.into_attribute_boxed()
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
} */
|
||||
|
||||
macro_rules! attr_type {
|
||||
($attr_type:ty) => {
|
||||
impl IntoAttribute for $attr_type {
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(self.to_string().into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
|
||||
self.into_attribute()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! attr_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoAttribute for $signal_type
|
||||
where
|
||||
T: IntoAttribute + Clone,
|
||||
{
|
||||
fn into_attribute(self) -> Attribute {
|
||||
let modified_fn = Rc::new(move || self.get().into_attribute());
|
||||
Attribute::Fn(modified_fn)
|
||||
}
|
||||
|
||||
impl_into_attr_boxed! {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
attr_type!(&String);
|
||||
attr_type!(usize);
|
||||
attr_type!(u8);
|
||||
attr_type!(u16);
|
||||
attr_type!(u32);
|
||||
attr_type!(u64);
|
||||
attr_type!(u128);
|
||||
attr_type!(isize);
|
||||
attr_type!(i8);
|
||||
attr_type!(i16);
|
||||
attr_type!(i32);
|
||||
attr_type!(i64);
|
||||
attr_type!(i128);
|
||||
attr_type!(f32);
|
||||
attr_type!(f64);
|
||||
attr_type!(char);
|
||||
|
||||
attr_signal_type!(ReadSignal<T>);
|
||||
attr_signal_type!(RwSignal<T>);
|
||||
attr_signal_type!(Memo<T>);
|
||||
attr_signal_type!(Signal<T>);
|
||||
attr_signal_type!(MaybeSignal<T>);
|
||||
attr_signal_type!(MaybeProp<T>);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
#[track_caller]
|
||||
pub fn attribute_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Oco<'static, str>,
|
||||
value: Attribute,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
let called_at = std::panic::Location::caller();
|
||||
use leptos_reactive::create_render_effect;
|
||||
match value {
|
||||
Attribute::Fn(f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) {
|
||||
attribute_expression(
|
||||
&el,
|
||||
&name,
|
||||
new.clone(),
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
called_at,
|
||||
);
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
_ => attribute_expression(
|
||||
el,
|
||||
&name,
|
||||
value,
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
called_at,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn attribute_expression(
|
||||
el: &web_sys::Element,
|
||||
attr_name: &str,
|
||||
value: Attribute,
|
||||
force: bool,
|
||||
#[cfg(debug_assertions)] called_at: &'static std::panic::Location<'static>,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
|
||||
if force || !HydrationCtx::is_hydrating() {
|
||||
match value {
|
||||
Attribute::String(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(value);
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
}
|
||||
Attribute::Option(value) => {
|
||||
if attr_name == "inner_html" {
|
||||
el.set_inner_html(&value.unwrap_or_default());
|
||||
} else {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
match value {
|
||||
Some(value) => {
|
||||
let value = wasm_bindgen::intern(&value);
|
||||
el.set_attribute(attr_name, value).unwrap_throw();
|
||||
}
|
||||
None => el.remove_attribute(attr_name).unwrap_throw(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Attribute::Bool(value) => {
|
||||
let attr_name = wasm_bindgen::intern(attr_name);
|
||||
if value {
|
||||
el.set_attribute(attr_name, attr_name).unwrap_throw();
|
||||
} else {
|
||||
el.remove_attribute(attr_name).unwrap_throw();
|
||||
}
|
||||
}
|
||||
Attribute::Fn(f) => {
|
||||
let mut v = f();
|
||||
crate::debug_warn!(
|
||||
"At {called_at}, you are providing a dynamic attribute \
|
||||
with a nested function. For example, you might have a \
|
||||
closure that returns another function instead of a \
|
||||
value. This creates some added overhead. If possible, \
|
||||
you should instead provide a function that returns a \
|
||||
value instead.",
|
||||
);
|
||||
while let Attribute::Fn(f) = v {
|
||||
v = f();
|
||||
}
|
||||
attribute_expression(
|
||||
el,
|
||||
attr_name,
|
||||
v,
|
||||
force,
|
||||
#[cfg(debug_assertions)]
|
||||
called_at,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
|
||||
/// Represents the different possible values a single class on an element could have,
|
||||
/// allowing you to do fine-grained updates to single items
|
||||
/// in [`Element.classList`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
|
||||
///
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly, but it can be useful for defining
|
||||
/// permissive APIs for certain components.
|
||||
pub enum Class {
|
||||
/// Whether the class is present.
|
||||
Value(bool),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to toggle the class.
|
||||
Fn(Box<dyn Fn() -> bool>),
|
||||
}
|
||||
|
||||
/// Converts some type into a [`Class`].
|
||||
pub trait IntoClass {
|
||||
/// Converts the object into a [`Class`].
|
||||
fn into_class(self) -> Class;
|
||||
|
||||
/// Helper function for dealing with `Box<dyn IntoClass>`.
|
||||
fn into_class_boxed(self: Box<Self>) -> Class;
|
||||
}
|
||||
|
||||
impl IntoClass for bool {
|
||||
#[inline(always)]
|
||||
fn into_class(self) -> Class {
|
||||
Class::Value(self)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoClass for T
|
||||
where
|
||||
T: Fn() -> bool + 'static,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn into_class(self) -> Class {
|
||||
let modified_fn = Box::new(self);
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
|
||||
impl Class {
|
||||
/// Converts the class to its HTML value at that moment so it can be rendered on the server.
|
||||
pub fn as_value_string(&self, class_name: &'static str) -> &'static str {
|
||||
match self {
|
||||
Class::Value(value) => {
|
||||
if *value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
Class::Fn(f) => {
|
||||
let value = f();
|
||||
if value {
|
||||
class_name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use leptos_reactive::Oco;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn class_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Oco<'static, str>,
|
||||
value: Class,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
let class_list = el.class_list();
|
||||
match value {
|
||||
Class::Fn(f) => {
|
||||
create_render_effect(move |old| {
|
||||
let new = f();
|
||||
if old.as_ref() != Some(&new) && (old.is_some() || new) {
|
||||
class_expression(&class_list, &name, new, true)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Class::Value(value) => {
|
||||
class_expression(&class_list, &name, value, false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn class_expression(
|
||||
class_list: &web_sys::DomTokenList,
|
||||
class_name: &str,
|
||||
value: bool,
|
||||
force: bool,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
|
||||
if force || !HydrationCtx::is_hydrating() {
|
||||
let class_name = wasm_bindgen::intern(class_name);
|
||||
|
||||
if value {
|
||||
if let Err(e) = class_list.add_1(class_name) {
|
||||
crate::error!("[HtmlElement::class()] {e:?}");
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = class_list.remove_1(class_name) {
|
||||
crate::error!("[HtmlElement::class()] {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! class_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl IntoClass for $signal_type {
|
||||
#[inline(always)]
|
||||
fn into_class(self) -> Class {
|
||||
let modified_fn = Box::new(move || self.get());
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! class_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl IntoClass for $signal_type {
|
||||
#[inline(always)]
|
||||
fn into_class(self) -> Class {
|
||||
let modified_fn = Box::new(move || self.get().unwrap_or(false));
|
||||
Class::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_class_boxed(self: Box<Self>) -> Class {
|
||||
(*self).into_class()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class_signal_type!(ReadSignal<bool>);
|
||||
class_signal_type!(RwSignal<bool>);
|
||||
class_signal_type!(Memo<bool>);
|
||||
class_signal_type!(Signal<bool>);
|
||||
class_signal_type!(MaybeSignal<bool>);
|
||||
class_signal_type_optional!(MaybeProp<bool>);
|
||||
@@ -1,178 +0,0 @@
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
/// Represents the different possible values an element property could have,
|
||||
/// allowing you to do fine-grained updates to single fields.
|
||||
///
|
||||
/// This mostly exists for the [`view`](https://docs.rs/leptos_macro/latest/leptos_macro/macro.view.html)
|
||||
/// macro’s use. You usually won't need to interact with it directly, but it can be useful for defining
|
||||
/// permissive APIs for certain components.
|
||||
pub enum Property {
|
||||
/// A static JavaScript value.
|
||||
Value(JsValue),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to update the property.
|
||||
Fn(Box<dyn Fn() -> JsValue>),
|
||||
}
|
||||
|
||||
/// Converts some type into a [`Property`].
|
||||
///
|
||||
/// This is implemented by default for Rust primitive types, [`String`] and friends, and [`JsValue`].
|
||||
pub trait IntoProperty {
|
||||
/// Converts the object into a [`Property`].
|
||||
fn into_property(self) -> Property;
|
||||
|
||||
/// Helper function for dealing with `Box<dyn IntoProperty>`.
|
||||
fn into_property_boxed(self: Box<Self>) -> Property;
|
||||
}
|
||||
|
||||
impl<T, U> IntoProperty for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: Into<JsValue>,
|
||||
{
|
||||
fn into_property(self) -> Property {
|
||||
let modified_fn = Box::new(move || self().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! prop_type {
|
||||
($prop_type:ty) => {
|
||||
impl IntoProperty for $prop_type {
|
||||
#[inline(always)]
|
||||
fn into_property(self) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoProperty for Option<$prop_type> {
|
||||
#[inline(always)]
|
||||
fn into_property(self) -> Property {
|
||||
Property::Value(self.into())
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! prop_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoProperty for $signal_type
|
||||
where
|
||||
T: Into<JsValue> + Clone,
|
||||
{
|
||||
fn into_property(self) -> Property {
|
||||
let modified_fn = Box::new(move || self.get().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! prop_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoProperty for $signal_type
|
||||
where
|
||||
T: Clone,
|
||||
Option<T>: Into<JsValue>,
|
||||
{
|
||||
fn into_property(self) -> Property {
|
||||
let modified_fn = Box::new(move || self.get().into());
|
||||
Property::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_property_boxed(self: Box<Self>) -> Property {
|
||||
(*self).into_property()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
prop_type!(JsValue);
|
||||
prop_type!(String);
|
||||
prop_type!(&String);
|
||||
prop_type!(&str);
|
||||
prop_type!(usize);
|
||||
prop_type!(u8);
|
||||
prop_type!(u16);
|
||||
prop_type!(u32);
|
||||
prop_type!(u64);
|
||||
prop_type!(u128);
|
||||
prop_type!(isize);
|
||||
prop_type!(i8);
|
||||
prop_type!(i16);
|
||||
prop_type!(i32);
|
||||
prop_type!(i64);
|
||||
prop_type!(i128);
|
||||
prop_type!(f32);
|
||||
prop_type!(f64);
|
||||
prop_type!(bool);
|
||||
|
||||
prop_signal_type!(ReadSignal<T>);
|
||||
prop_signal_type!(RwSignal<T>);
|
||||
prop_signal_type!(Memo<T>);
|
||||
prop_signal_type!(Signal<T>);
|
||||
prop_signal_type!(MaybeSignal<T>);
|
||||
prop_signal_type_optional!(MaybeProp<T>);
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use leptos_reactive::Oco;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn property_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Oco<'static, str>,
|
||||
value: Property,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
|
||||
match value {
|
||||
Property::Fn(f) => {
|
||||
let el = el.clone();
|
||||
create_render_effect(move |_| {
|
||||
let new = f();
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(&el, prop_name, new.clone());
|
||||
new
|
||||
});
|
||||
}
|
||||
Property::Value(value) => {
|
||||
let prop_name = wasm_bindgen::intern(&name);
|
||||
property_expression(el, prop_name, value)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn property_expression(
|
||||
el: &web_sys::Element,
|
||||
prop_name: &str,
|
||||
value: JsValue,
|
||||
) {
|
||||
js_sys::Reflect::set(el, &JsValue::from_str(prop_name), &value)
|
||||
.unwrap_throw();
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
use leptos_reactive::{
|
||||
MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet,
|
||||
};
|
||||
use std::{borrow::Cow, rc::Rc};
|
||||
|
||||
/// todo docs
|
||||
#[derive(Clone)]
|
||||
pub enum Style {
|
||||
/// A plain string value.
|
||||
Value(Oco<'static, str>),
|
||||
/// An optional string value, which sets the property to the value if `Some` and removes the property if `None`.
|
||||
Option(Option<Oco<'static, str>>),
|
||||
/// A (presumably reactive) function, which will be run inside an effect to update the style.
|
||||
Fn(Rc<dyn Fn() -> Style>),
|
||||
}
|
||||
|
||||
impl PartialEq for Style {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Value(l0), Self::Value(r0)) => l0 == r0,
|
||||
(Self::Fn(_), Self::Fn(_)) => false,
|
||||
(Self::Option(l0), Self::Option(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Style {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Value(arg0) => f.debug_tuple("Value").field(arg0).finish(),
|
||||
Self::Fn(_) => f.debug_tuple("Fn").finish(),
|
||||
Self::Option(arg0) => f.debug_tuple("Option").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts some type into a [`Style`].
|
||||
pub trait IntoStyle {
|
||||
/// Converts the object into a [`Style`].
|
||||
fn into_style(self) -> Style;
|
||||
|
||||
/// Helper function for dealing with `Box<dyn IntoStyle>`.
|
||||
fn into_style_boxed(self: Box<Self>) -> Style;
|
||||
}
|
||||
|
||||
impl IntoStyle for Style {
|
||||
fn into_style(self) -> Style {
|
||||
self
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for &'static str {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(Oco::Borrowed(self))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for String {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(Oco::Owned(self))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Rc<str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(Oco::Counted(self))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Cow<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(self.into())
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Oco<'static, str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(self)
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<&'static str> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Oco::Borrowed))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<String> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Oco::Owned))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<Rc<str>> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Oco::Counted))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<Cow<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(Oco::from))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<Oco<'static, str>> {
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self)
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> IntoStyle for T
|
||||
where
|
||||
T: Fn() -> U + 'static,
|
||||
U: IntoStyle,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn into_style(self) -> Style {
|
||||
let modified_fn = Rc::new(move || (self)().into_style());
|
||||
Style::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Converts the style to its HTML value at that moment so it can be rendered on the server.
|
||||
pub fn as_value_string(
|
||||
&self,
|
||||
style_name: &str,
|
||||
) -> Option<Oco<'static, str>> {
|
||||
match self {
|
||||
Style::Value(value) => {
|
||||
Some(format!("{style_name}: {value};").into())
|
||||
}
|
||||
Style::Option(value) => value
|
||||
.as_ref()
|
||||
.map(|value| format!("{style_name}: {value};").into()),
|
||||
Style::Fn(f) => {
|
||||
let mut value = f();
|
||||
while let Style::Fn(f) = value {
|
||||
value = f();
|
||||
}
|
||||
value.as_value_string(style_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
pub fn style_helper(
|
||||
el: &web_sys::Element,
|
||||
name: Oco<'static, str>,
|
||||
value: Style,
|
||||
) {
|
||||
use leptos_reactive::create_render_effect;
|
||||
use std::ops::Deref;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let el = el.unchecked_ref::<web_sys::HtmlElement>();
|
||||
let style_list = el.style();
|
||||
match value {
|
||||
Style::Fn(f) => {
|
||||
create_render_effect(move |old| {
|
||||
let mut new = f();
|
||||
while let Style::Fn(f) = new {
|
||||
new = f();
|
||||
}
|
||||
let new = match new {
|
||||
Style::Value(value) => Some(value),
|
||||
Style::Option(value) => value,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if old.as_ref() != Some(&new) {
|
||||
style_expression(&style_list, &name, new.as_deref(), true)
|
||||
}
|
||||
new
|
||||
});
|
||||
}
|
||||
Style::Value(value) => {
|
||||
style_expression(&style_list, &name, Some(value.deref()), false)
|
||||
}
|
||||
Style::Option(value) => {
|
||||
style_expression(&style_list, &name, value.as_deref(), false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
#[inline(never)]
|
||||
pub(crate) fn style_expression(
|
||||
style_list: &web_sys::CssStyleDeclaration,
|
||||
style_name: &str,
|
||||
value: Option<&str>,
|
||||
force: bool,
|
||||
) {
|
||||
use crate::HydrationCtx;
|
||||
|
||||
if force || !HydrationCtx::is_hydrating() {
|
||||
let style_name = wasm_bindgen::intern(style_name);
|
||||
|
||||
if let Some(value) = value {
|
||||
if let Err(e) = style_list.set_property(style_name, value) {
|
||||
crate::error!("[HtmlElement::style()] {e:?}");
|
||||
}
|
||||
} else {
|
||||
if let Err(e) = style_list.remove_property(style_name) {
|
||||
crate::error!("[HtmlElement::style()] {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! style_type {
|
||||
($style_type:ty) => {
|
||||
impl IntoStyle for $style_type {
|
||||
fn into_style(self) -> Style {
|
||||
Style::Value(self.to_string().into())
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoStyle for Option<$style_type> {
|
||||
fn into_style(self) -> Style {
|
||||
Style::Option(self.map(|n| n.to_string().into()))
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! style_signal_type {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoStyle for $signal_type
|
||||
where
|
||||
T: IntoStyle + Clone,
|
||||
{
|
||||
fn into_style(self) -> Style {
|
||||
let modified_fn = Rc::new(move || self.get().into_style());
|
||||
Style::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! style_signal_type_optional {
|
||||
($signal_type:ty) => {
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
impl<T> IntoStyle for $signal_type
|
||||
where
|
||||
T: Clone,
|
||||
Option<T>: IntoStyle,
|
||||
{
|
||||
fn into_style(self) -> Style {
|
||||
let modified_fn = Rc::new(move || self.get().into_style());
|
||||
Style::Fn(modified_fn)
|
||||
}
|
||||
|
||||
fn into_style_boxed(self: Box<Self>) -> Style {
|
||||
(*self).into_style()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
style_type!(&String);
|
||||
style_type!(usize);
|
||||
style_type!(u8);
|
||||
style_type!(u16);
|
||||
style_type!(u32);
|
||||
style_type!(u64);
|
||||
style_type!(u128);
|
||||
style_type!(isize);
|
||||
style_type!(i8);
|
||||
style_type!(i16);
|
||||
style_type!(i32);
|
||||
style_type!(i64);
|
||||
style_type!(i128);
|
||||
style_type!(f32);
|
||||
style_type!(f64);
|
||||
style_type!(char);
|
||||
|
||||
style_signal_type!(ReadSignal<T>);
|
||||
style_signal_type!(RwSignal<T>);
|
||||
style_signal_type!(Memo<T>);
|
||||
style_signal_type!(Signal<T>);
|
||||
style_signal_type!(MaybeSignal<T>);
|
||||
style_signal_type_optional!(MaybeProp<T>);
|
||||
@@ -1,11 +0,0 @@
|
||||
mod into_attribute;
|
||||
mod into_class;
|
||||
mod into_property;
|
||||
mod into_style;
|
||||
#[cfg(feature = "trace-component-props")]
|
||||
#[doc(hidden)]
|
||||
pub mod tracing_property;
|
||||
pub use into_attribute::*;
|
||||
pub use into_class::*;
|
||||
pub use into_property::*;
|
||||
pub use into_style::*;
|
||||
@@ -1,176 +0,0 @@
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
#[macro_export]
|
||||
/// Use for tracing property
|
||||
macro_rules! tracing_props {
|
||||
() => {
|
||||
::leptos::leptos_dom::tracing::span!(
|
||||
::leptos::leptos_dom::tracing::Level::TRACE,
|
||||
"leptos_dom::tracing_props",
|
||||
props = String::from("[]")
|
||||
);
|
||||
};
|
||||
($($prop:tt),+ $(,)?) => {
|
||||
{
|
||||
use ::leptos::leptos_dom::tracing_property::{Match, SerializeMatch, DefaultMatch};
|
||||
let mut props = String::from('[');
|
||||
$(
|
||||
let prop = (&&Match {
|
||||
name: stringify!{$prop},
|
||||
value: std::cell::Cell::new(Some(&$prop))
|
||||
}).spez();
|
||||
props.push_str(&format!("{prop},"));
|
||||
)*
|
||||
props.pop();
|
||||
props.push(']');
|
||||
::leptos::leptos_dom::tracing::span!(
|
||||
::leptos::leptos_dom::tracing::Level::TRACE,
|
||||
"leptos_dom::tracing_props",
|
||||
props
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Implementation based on spez
|
||||
// see https://github.com/m-ou-se/spez
|
||||
|
||||
pub struct Match<T> {
|
||||
pub name: &'static str,
|
||||
pub value: std::cell::Cell<Option<T>>,
|
||||
}
|
||||
|
||||
pub trait SerializeMatch {
|
||||
type Return;
|
||||
fn spez(&self) -> Self::Return;
|
||||
}
|
||||
impl<T: serde::Serialize> SerializeMatch for &Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
|
||||
// suppresses warnings when serializing signals into props
|
||||
#[cfg(debug_assertions)]
|
||||
let prev = leptos_reactive::SpecialNonReactiveZone::enter();
|
||||
|
||||
let value = serde_json::to_string(self.value.get().unwrap_throw())
|
||||
.map_or_else(
|
||||
|err| format!(r#"{{"name": "{name}", "error": "{err}"}}"#),
|
||||
|value| format!(r#"{{"name": "{name}", "value": {value}}}"#),
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
leptos_reactive::SpecialNonReactiveZone::exit(prev);
|
||||
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DefaultMatch {
|
||||
type Return;
|
||||
fn spez(&self) -> Self::Return;
|
||||
}
|
||||
impl<T> DefaultMatch for Match<&T> {
|
||||
type Return = String;
|
||||
fn spez(&self) -> Self::Return {
|
||||
let name = self.name;
|
||||
format!(r#"{{"name": "{name}", "value": "[unserializable value]"}}"#)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_primitive() {
|
||||
// String
|
||||
let test = String::from("string");
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
|
||||
|
||||
// &str
|
||||
let test = "string";
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": "string"}"#);
|
||||
|
||||
// u128
|
||||
let test: u128 = 1;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 1}"#);
|
||||
|
||||
// i128
|
||||
let test: i128 = -1;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": -1}"#);
|
||||
|
||||
// f64
|
||||
let test = 3.25;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": 3.25}"#);
|
||||
|
||||
// bool
|
||||
let test = true;
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": true}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_serialize() {
|
||||
use serde::Serialize;
|
||||
#[derive(Serialize)]
|
||||
struct CustomStruct {
|
||||
field: &'static str,
|
||||
}
|
||||
|
||||
let test = CustomStruct { field: "field" };
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(prop, r#"{"name": "test", "value": {"field":"field"}}"#);
|
||||
// Verification of ownership
|
||||
assert_eq!(test.field, "field");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::needless_borrow)]
|
||||
fn match_no_serialize() {
|
||||
struct CustomStruct {
|
||||
field: &'static str,
|
||||
}
|
||||
|
||||
let test = CustomStruct { field: "field" };
|
||||
let prop = (&&Match {
|
||||
name: stringify! {test},
|
||||
value: std::cell::Cell::new(Some(&test)),
|
||||
})
|
||||
.spez();
|
||||
assert_eq!(
|
||||
prop,
|
||||
r#"{"name": "test", "value": "[unserializable value]"}"#
|
||||
);
|
||||
// Verification of ownership
|
||||
assert_eq!(test.field, "field");
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
//! Exports types for working with MathML elements.
|
||||
|
||||
use super::{AnyElement, ElementDescriptor, HtmlElement};
|
||||
use crate::HydrationCtx;
|
||||
use cfg_if::cfg_if;
|
||||
use leptos_reactive::Oco;
|
||||
cfg_if! {
|
||||
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
use wasm_bindgen::JsCast;
|
||||
} else {
|
||||
use super::{HydrationKey, html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG};
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! generate_math_tags {
|
||||
(
|
||||
$(
|
||||
#[$meta:meta]
|
||||
$(#[$void:ident])?
|
||||
$tag:ident $(- $second:ident $(- $third:ident)?)? $(@ $trailing_:pat)?
|
||||
),* $(,)?
|
||||
) => {
|
||||
paste::paste! {
|
||||
$(
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
thread_local! {
|
||||
static [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]: LazyCell<web_sys::HtmlElement> = LazyCell::new(|| {
|
||||
crate::document()
|
||||
.create_element_ns(
|
||||
Some(wasm_bindgen::intern("http://www.w3.org/1998/Math/MathML")),
|
||||
concat![
|
||||
stringify!($tag),
|
||||
$(
|
||||
"-", stringify!($second),
|
||||
$(
|
||||
"-", stringify!($third)
|
||||
)?
|
||||
)?
|
||||
],
|
||||
)
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[$meta]
|
||||
pub struct [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn default() -> Self {
|
||||
#[allow(unused)]
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
|
||||
let element = if HydrationCtx::is_hydrating() && id.is_some() {
|
||||
let id = id.unwrap();
|
||||
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
|
||||
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
|
||||
let element = [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
);
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
type Target = web_sys::Element;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
use wasm_bindgen::JsCast;
|
||||
return &self.element.unchecked_ref();
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::AsRef<web_sys::HtmlElement> for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn as_ref(&self) -> &web_sys::HtmlElement {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return &self.element;
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!($tag).into()
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
fn hydration_id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
generate_math_tags! { @void $($void)? }
|
||||
}
|
||||
|
||||
impl From<HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>> for HtmlElement<AnyElement> {
|
||||
fn from(element: HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>) -> Self {
|
||||
element.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
#[$meta]
|
||||
pub fn [<$tag $(_ $second $(_ $third)?)? $($trailing_)?>]() -> HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]> {
|
||||
HtmlElement::new([<$tag:camel $($second:camel $($third:camel)?)?>]::default())
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
(@void) => {};
|
||||
(@void void) => {
|
||||
fn is_void(&self) -> bool {
|
||||
true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
generate_math_tags![
|
||||
/// MathML element.
|
||||
math,
|
||||
/// MathML element.
|
||||
mi,
|
||||
/// MathML element.
|
||||
mn,
|
||||
/// MathML element.
|
||||
mo,
|
||||
/// MathML element.
|
||||
ms,
|
||||
/// MathML element.
|
||||
mspace,
|
||||
/// MathML element.
|
||||
mtext,
|
||||
/// MathML element.
|
||||
menclose,
|
||||
/// MathML element.
|
||||
merror,
|
||||
/// MathML element.
|
||||
mfenced,
|
||||
/// MathML element.
|
||||
mfrac,
|
||||
/// MathML element.
|
||||
mpadded,
|
||||
/// MathML element.
|
||||
mphantom,
|
||||
/// MathML element.
|
||||
mroot,
|
||||
/// MathML element.
|
||||
mrow,
|
||||
/// MathML element.
|
||||
msqrt,
|
||||
/// MathML element.
|
||||
mstyle,
|
||||
/// MathML element.
|
||||
mmultiscripts,
|
||||
/// MathML element.
|
||||
mover,
|
||||
/// MathML element.
|
||||
mprescripts,
|
||||
/// MathML element.
|
||||
msub,
|
||||
/// MathML element.
|
||||
msubsup,
|
||||
/// MathML element.
|
||||
msup,
|
||||
/// MathML element.
|
||||
munder,
|
||||
/// MathML element.
|
||||
munderover,
|
||||
/// MathML element.
|
||||
mtable,
|
||||
/// MathML element.
|
||||
mtd,
|
||||
/// MathML element.
|
||||
mtr,
|
||||
/// MathML element.
|
||||
maction,
|
||||
/// MathML element.
|
||||
annotation,
|
||||
/// MathML element.
|
||||
annotation
|
||||
- xml,
|
||||
/// MathML element.
|
||||
semantics,
|
||||
];
|
||||
@@ -1,226 +0,0 @@
|
||||
use crate::{html::ElementDescriptor, HtmlElement};
|
||||
use leptos_reactive::{create_render_effect, signal_prelude::*};
|
||||
use std::cell::Cell;
|
||||
|
||||
/// Contains a shared reference to a DOM node created while using the `view`
|
||||
/// macro to create your UI.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::{*, logging::log};
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent() -> impl IntoView {
|
||||
/// let input_ref = create_node_ref::<Input>();
|
||||
///
|
||||
/// let on_click = move |_| {
|
||||
/// let node =
|
||||
/// input_ref.get().expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg_attr(not(debug_assertions), repr(transparent))]
|
||||
pub struct NodeRef<T: ElementDescriptor + 'static> {
|
||||
element: RwSignal<Option<HtmlElement<T>>>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
/// Creates a shared reference to a DOM node created while using the `view`
|
||||
/// macro to create your UI.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::{*, logging::log};
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent() -> impl IntoView {
|
||||
/// let input_ref = create_node_ref::<Input>();
|
||||
///
|
||||
/// let on_click = move |_| {
|
||||
/// let node =
|
||||
/// input_ref.get().expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn create_node_ref<T: ElementDescriptor + 'static>() -> NodeRef<T> {
|
||||
NodeRef {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: std::panic::Location::caller(),
|
||||
element: create_rw_signal(None),
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor + 'static> NodeRef<T> {
|
||||
/// Creates a shared reference to a DOM node created while using the `view`
|
||||
/// macro to create your UI.
|
||||
///
|
||||
/// This is identical to [`create_node_ref`].
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::{*, logging::log};
|
||||
///
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent() -> impl IntoView {
|
||||
/// let input_ref = NodeRef::<Input>::new();
|
||||
///
|
||||
/// let on_click = move |_| {
|
||||
/// let node =
|
||||
/// input_ref.get().expect("input_ref should be loaded by now");
|
||||
/// // `node` is strongly typed
|
||||
/// // it is dereferenced to an `HtmlInputElement` automatically
|
||||
/// log!("value is {:?}", node.value())
|
||||
/// };
|
||||
///
|
||||
/// view! {
|
||||
/// <div>
|
||||
/// // `node_ref` loads the input
|
||||
/// <input _ref=input_ref type="text"/>
|
||||
/// // the button consumes it
|
||||
/// <button on:click=on_click>"Click me"</button>
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
create_node_ref()
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This tracks reactively, so that node references can be used in effects.
|
||||
/// Initially, the value will be `None`, but once it is loaded the effect
|
||||
/// will rerun and its value will be `Some(Element)`.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> Option<HtmlElement<T>>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.element.get()
|
||||
}
|
||||
|
||||
/// Gets the element that is currently stored in the reference.
|
||||
///
|
||||
/// This **does not** track reactively.
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn get_untracked(&self) -> Option<HtmlElement<T>>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.element.get_untracked()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Loads an element into the reference. This tracks reactively,
|
||||
/// so that effects that use the node reference will rerun once it is loaded,
|
||||
/// i.e., effects can be forward-declared.
|
||||
#[track_caller]
|
||||
pub fn load(&self, node: &HtmlElement<T>)
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.element.update(|current| {
|
||||
if current.is_some() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::debug_warn!(
|
||||
"You are setting the NodeRef defined at {}, which has \
|
||||
already been filled It’s possible this is intentional, \
|
||||
but it’s also possible that you’re accidentally using \
|
||||
the same NodeRef for multiple _ref attributes.",
|
||||
self.defined_at
|
||||
);
|
||||
}
|
||||
*current = Some(node.clone());
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs the provided closure when the `NodeRef` has been connected
|
||||
/// with it's [`HtmlElement`].
|
||||
#[inline(always)]
|
||||
pub fn on_load<F>(self, f: F)
|
||||
where
|
||||
T: Clone,
|
||||
F: FnOnce(HtmlElement<T>) + 'static,
|
||||
{
|
||||
let f = Cell::new(Some(f));
|
||||
|
||||
create_render_effect(move |_| {
|
||||
if let Some(node_ref) = self.get() {
|
||||
f.take().unwrap()(node_ref);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor> Clone for NodeRef<T> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ElementDescriptor + 'static> Copy for NodeRef<T> {}
|
||||
|
||||
impl<T: ElementDescriptor + 'static> Default for NodeRef<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "nightly")] {
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnOnce<()> for NodeRef<T> {
|
||||
type Output = Option<HtmlElement<T>>;
|
||||
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + ElementDescriptor + 'static> FnMut<()> for NodeRef<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + ElementDescriptor + Clone + 'static> Fn<()> for NodeRef<T> {
|
||||
#[inline(always)]
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
use crate::{Attribute, IntoAttribute};
|
||||
use leptos_reactive::use_context;
|
||||
use std::{fmt::Display, ops::Deref};
|
||||
|
||||
/// A nonce a cryptographic nonce ("number used once") which can be
|
||||
/// used by Content Security Policy to determine whether or not a given
|
||||
/// resource will be allowed to load.
|
||||
///
|
||||
/// When the `nonce` feature is enabled on one of the server integrations,
|
||||
/// a nonce is generated during server rendering and added to all inline
|
||||
/// scripts used for HTML streaming and resource loading.
|
||||
///
|
||||
/// The nonce being used during the current server response can be
|
||||
/// accessed using [`use_nonce`].
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// pub fn App() -> impl IntoView {
|
||||
/// provide_meta_context;
|
||||
///
|
||||
/// view! {
|
||||
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||
/// <Meta
|
||||
/// http_equiv="Content-Security-Policy"
|
||||
/// content=move || {
|
||||
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||
/// use_nonce()
|
||||
/// .map(|nonce| {
|
||||
/// format!(
|
||||
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||
/// )
|
||||
/// })
|
||||
/// .unwrap_or_default()
|
||||
/// }
|
||||
/// />
|
||||
/// // manually insert nonce during SSR on inline script
|
||||
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
|
||||
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||
/// <Style>"body { color: blue; }"</Style>
|
||||
/// <p>"Test"</p>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Nonce(pub(crate) String);
|
||||
|
||||
impl Deref for Nonce {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Nonce {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Nonce {
|
||||
fn into_attribute(self) -> Attribute {
|
||||
Attribute::String(self.0.into())
|
||||
}
|
||||
|
||||
fn into_attribute_boxed(self: Box<Self>) -> Attribute {
|
||||
Attribute::String(self.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Accesses the nonce that has been generated during the current
|
||||
/// server response. This can be added to inline `<script>` and
|
||||
/// `<style>` tags for compatibility with a Content Security Policy.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// pub fn App() -> impl IntoView {
|
||||
/// provide_meta_context;
|
||||
///
|
||||
/// view! {
|
||||
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||
/// <Meta
|
||||
/// http_equiv="Content-Security-Policy"
|
||||
/// content=move || {
|
||||
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||
/// use_nonce()
|
||||
/// .map(|nonce| {
|
||||
/// format!(
|
||||
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||
/// )
|
||||
/// })
|
||||
/// .unwrap_or_default()
|
||||
/// }
|
||||
/// />
|
||||
/// // manually insert nonce during SSR on inline script
|
||||
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
|
||||
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||
/// <Style>"body { color: blue; }"</Style>
|
||||
/// <p>"Test"</p>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_nonce() -> Option<Nonce> {
|
||||
use_context::<Nonce>()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "nonce"))]
|
||||
pub use generate::*;
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "nonce"))]
|
||||
mod generate {
|
||||
use super::Nonce;
|
||||
use base64::{
|
||||
alphabet,
|
||||
engine::{self, general_purpose},
|
||||
Engine,
|
||||
};
|
||||
use leptos_reactive::provide_context;
|
||||
use rand::{thread_rng, RngCore};
|
||||
|
||||
const NONCE_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
|
||||
&alphabet::URL_SAFE,
|
||||
general_purpose::NO_PAD,
|
||||
);
|
||||
|
||||
impl Nonce {
|
||||
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||
pub fn new() -> Self {
|
||||
let mut thread_rng = thread_rng();
|
||||
let mut bytes = [0; 16];
|
||||
thread_rng.fill_bytes(&mut bytes);
|
||||
Nonce(NONCE_ENGINE.encode(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Nonce {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a nonce and provides it during server rendering.
|
||||
pub fn provide_nonce() {
|
||||
provide_context(Nonce::new())
|
||||
}
|
||||
}
|
||||
@@ -1,789 +0,0 @@
|
||||
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
|
||||
//! Server-side HTML rendering utilities.
|
||||
|
||||
use crate::{
|
||||
html::{ElementChildren, StringOrView},
|
||||
CoreComponent, HydrationCtx, HydrationKey, IntoView, View,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::*;
|
||||
use std::pin::Pin;
|
||||
|
||||
type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
||||
|
||||
/// Renders the given function to a static HTML string.
|
||||
///
|
||||
/// ```
|
||||
/// # cfg_if::cfg_if! { if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
|
||||
/// # use leptos::*;
|
||||
/// let html = leptos::ssr::render_to_string(|| view! {
|
||||
/// <p>"Hello, world!"</p>
|
||||
/// });
|
||||
/// // trim off the beginning, which has a bunch of hydration info, for comparison
|
||||
/// assert!(html.contains("Hello, world!</p>"));
|
||||
/// # }}
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string<F, N>(f: F) -> Oco<'static, str>
|
||||
where
|
||||
F: FnOnce() -> N + 'static,
|
||||
N: IntoView,
|
||||
{
|
||||
HydrationCtx::reset_id();
|
||||
let runtime = leptos_reactive::create_runtime();
|
||||
|
||||
let html = f().into_view().render_to_string();
|
||||
|
||||
runtime.dispose();
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 2) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 3) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
render_to_stream_with_prefix(view, || "".into())
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the prefix
|
||||
/// 2) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
let (stream, runtime) =
|
||||
render_to_stream_with_prefix_undisposed(view, prefix);
|
||||
runtime.dispose();
|
||||
stream
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so
|
||||
/// it can be disposed when appropriate. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the prefix
|
||||
/// 2) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context(view, prefix, || {})
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so
|
||||
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the prefix
|
||||
/// 2) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
additional_context: impl FnOnce() + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view,
|
||||
prefix,
|
||||
additional_context,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders a function to a stream of HTML strings and returns the [RuntimeId] that was created, so
|
||||
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
|
||||
/// the same scope. This can be used to generate additional HTML that has access to the same reactive graph.
|
||||
///
|
||||
/// If `replace_blocks` is true, this will wait for any fragments with blocking resources and
|
||||
/// actually replace them in the initial HTML. This is slower to render (as it requires walking
|
||||
/// back over the HTML for string replacement) but has the advantage of never including those fallbacks
|
||||
/// in the HTML.
|
||||
///
|
||||
/// This renders:
|
||||
/// 1) the prefix
|
||||
/// 2) the application shell
|
||||
/// a) HTML for everything that is not under a `<Suspense/>`,
|
||||
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
|
||||
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
|
||||
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
|
||||
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
|
||||
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
|
||||
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
|
||||
/// read under that `<Suspense/>` resolve.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
additional_context: impl FnOnce() + 'static,
|
||||
replace_blocks: bool,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
// Add additional context items
|
||||
additional_context();
|
||||
|
||||
// the actual app body/template code
|
||||
// this does NOT contain any of the data being loaded asynchronously in resources
|
||||
let shell = view().render_to_string();
|
||||
|
||||
let resources = SharedContext::pending_resources();
|
||||
let pending_resources = serde_json::to_string(&resources).unwrap();
|
||||
let pending_fragments = SharedContext::pending_fragments();
|
||||
let serializers = SharedContext::serialization_resolvers();
|
||||
let nonce_str = crate::nonce::use_nonce()
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
let local_only = SharedContext::fragments_with_local_resources();
|
||||
let local_only = serde_json::to_string(&local_only).unwrap();
|
||||
|
||||
let mut blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
|
||||
for (fragment_id, data) in pending_fragments {
|
||||
if data.should_block {
|
||||
blocking_fragments
|
||||
.push(async move { (fragment_id, data.out_of_order.await) });
|
||||
} else {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id, data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
}
|
||||
}
|
||||
|
||||
let stream = futures::stream::once(
|
||||
// HTML for the view function and script to store resources
|
||||
{
|
||||
let nonce_str = nonce_str.clone();
|
||||
async move {
|
||||
let resolvers = format!(
|
||||
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new \
|
||||
Map();__LEPTOS_LOCAL_ONLY = {local_only};</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks =
|
||||
Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix();
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close =
|
||||
format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) =
|
||||
shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell =
|
||||
format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments = fragments_to_chunks(
|
||||
nonce_str.clone(),
|
||||
blocking_fragments,
|
||||
);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix();
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.chain(ooo_body_stream_recurse(nonce_str, fragments, serializers));
|
||||
|
||||
(stream, runtime)
|
||||
}
|
||||
|
||||
fn ooo_body_stream_recurse(
|
||||
nonce_str: String,
|
||||
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> Pin<Box<dyn Stream<Item = String>>> {
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
let fragments = fragments_to_chunks(nonce_str.clone(), fragments);
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(nonce_str.clone(), serializers);
|
||||
|
||||
Box::pin(
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
fragments.chain(resources).chain(
|
||||
futures::stream::once(async move {
|
||||
let pending = SharedContext::pending_fragments();
|
||||
|
||||
if !pending.is_empty() {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = SharedContext::serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
}
|
||||
Box::pin(ooo_body_stream_recurse(
|
||||
nonce_str,
|
||||
fragments,
|
||||
serializers,
|
||||
))
|
||||
as Pin<Box<dyn Stream<Item = String>>>
|
||||
} else {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn fragments_to_chunks(
|
||||
nonce_str: String,
|
||||
fragments: impl Stream<Item = (String, String)>,
|
||||
) -> impl Stream<Item = String> {
|
||||
fragments.map(move |(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script{nonce_str}>
|
||||
(function() {{ let id = "{fragment_id}";
|
||||
let open = undefined;
|
||||
let close = undefined;
|
||||
let walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT);
|
||||
while(walker.nextNode()) {{
|
||||
if(walker.currentNode.textContent == `suspense-open-${{id}}`) {{
|
||||
open = walker.currentNode;
|
||||
}} else if(walker.currentNode.textContent == `suspense-close-${{id}}`) {{
|
||||
close = walker.currentNode;
|
||||
}}
|
||||
}}
|
||||
let range = new Range();
|
||||
range.setStartAfter(open);
|
||||
range.setEndBefore(close);
|
||||
range.deleteContents();
|
||||
let tpl = document.getElementById("{fragment_id}f");
|
||||
close.parentNode.insertBefore(tpl.content.cloneNode(true), close);}})()
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Consumes the node and renders it into an HTML string.
|
||||
///
|
||||
/// This is __NOT__ the same as [`render_to_string`]. This
|
||||
/// functions differs in that it assumes a runtime is in scope.
|
||||
/// [`render_to_string`] creates, and disposes of a runtime for you.
|
||||
///
|
||||
/// # Panics
|
||||
/// When called in a scope without a runtime. Use [`render_to_string`] instead.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn render_to_string(self) -> Oco<'static, str> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::logging::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently.\n",
|
||||
);
|
||||
|
||||
self.render_to_string_helper(false)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn render_to_string_helper(
|
||||
self,
|
||||
dont_escape_text: bool,
|
||||
) -> Oco<'static, str> {
|
||||
match self {
|
||||
View::Text(node) => {
|
||||
if dont_escape_text {
|
||||
node.content
|
||||
} else {
|
||||
html_escape::encode_safe(&node.content).to_string().into()
|
||||
}
|
||||
}
|
||||
View::Component(node) => {
|
||||
let content = || {
|
||||
node.children
|
||||
.into_iter()
|
||||
.map(|node| {
|
||||
node.render_to_string_helper(dont_escape_text)
|
||||
})
|
||||
.join("")
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
let name = to_kebab_case(&node.name);
|
||||
let content = format!(r#"{}{}{}"#,
|
||||
node.id.to_marker(false, &name),
|
||||
content(),
|
||||
node.id.to_marker(true, &name),
|
||||
);
|
||||
if let Some(id) = node.view_marker {
|
||||
format!("<!--leptos-view|{id}|open-->{content}<!--leptos-view|{id}|close-->").into()
|
||||
} else {
|
||||
content.into()
|
||||
}
|
||||
} else {
|
||||
format!(
|
||||
r#"{}{}"#,
|
||||
content(),
|
||||
node.id.to_marker(true)
|
||||
).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Suspense(id, node) => format!(
|
||||
"<!--suspense-open-{id}-->{}<!--suspense-close-{id}-->",
|
||||
View::CoreComponent(node)
|
||||
.render_to_string_helper(dont_escape_text)
|
||||
)
|
||||
.into(),
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id,
|
||||
"",
|
||||
false,
|
||||
Box::new(move || {
|
||||
u.id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
"unit",
|
||||
)
|
||||
})
|
||||
as Box<dyn FnOnce() -> Oco<'static, str>>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
(
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(move || {
|
||||
if let Some(child) = *child {
|
||||
if let View::Text(t) = child {
|
||||
// if we don't check if the string is empty,
|
||||
// the HTML is an empty string; but an empty string
|
||||
// is not a text node in HTML, so can't be updated
|
||||
// in the future. so we put a one-space text node instead
|
||||
let was_empty = t.content.is_empty();
|
||||
let content = if was_empty {
|
||||
" ".into()
|
||||
} else {
|
||||
t.content
|
||||
};
|
||||
// escape content unless we're in a <script> or <style>
|
||||
let content = if dont_escape_text {
|
||||
content
|
||||
} else {
|
||||
html_escape::encode_safe(&content)
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
if !cfg!(debug_assertions) {
|
||||
format!("<!>{content}",).into()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
} else {
|
||||
child.render_to_string_helper(
|
||||
dont_escape_text,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
})
|
||||
as Box<dyn FnOnce() -> Oco<'static, str>>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(move || {
|
||||
children
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|node| {
|
||||
let id = node.id;
|
||||
let is_el = matches!(
|
||||
node.child,
|
||||
View::Element(_)
|
||||
);
|
||||
|
||||
let content = || {
|
||||
node.child.render_to_string_helper(
|
||||
dont_escape_text,
|
||||
)
|
||||
};
|
||||
|
||||
if is_el {
|
||||
content()
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
id.to_marker(
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
"each-item",
|
||||
),
|
||||
content(),
|
||||
id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
"each-item",
|
||||
)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
})
|
||||
as Box<dyn FnOnce() -> Oco<'static, str>>,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if wrap {
|
||||
format!(
|
||||
r#"{}{}{}"#,
|
||||
id.to_marker(
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
),
|
||||
content(),
|
||||
id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
),
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
View::Element(el) => {
|
||||
let is_script_or_style =
|
||||
el.name == "script" || el.name == "style";
|
||||
let el_html = if let ElementChildren::Chunks(chunks) =
|
||||
el.children
|
||||
{
|
||||
chunks
|
||||
.into_iter()
|
||||
.map(|chunk| match chunk {
|
||||
StringOrView::String(string) => string,
|
||||
StringOrView::View(view) => view()
|
||||
.render_to_string_helper(is_script_or_style),
|
||||
})
|
||||
.join("")
|
||||
.into()
|
||||
} else {
|
||||
let tag_name: Oco<'_, str> = el.name;
|
||||
|
||||
let mut inner_html: Option<Oco<'_, str>> = None;
|
||||
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, value)| -> Option<Oco<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
inner_html = Some(value);
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.join("");
|
||||
|
||||
if el.is_void {
|
||||
format!("<{tag_name}{attrs}/>").into()
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
format!("<{tag_name}{attrs}>{inner_html}</{tag_name}>")
|
||||
.into()
|
||||
} else {
|
||||
let children = match el.children {
|
||||
ElementChildren::Empty => "".into(),
|
||||
ElementChildren::Children(c) => c
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
v.render_to_string_helper(
|
||||
is_script_or_style,
|
||||
)
|
||||
})
|
||||
.join("")
|
||||
.into(),
|
||||
ElementChildren::InnerHtml(h) => h,
|
||||
// already handled this case above
|
||||
ElementChildren::Chunks(_) => unreachable!(),
|
||||
};
|
||||
|
||||
format!("<{tag_name}{attrs}>{children}</{tag_name}>")
|
||||
.into()
|
||||
}
|
||||
};
|
||||
cfg_if! {
|
||||
if #[cfg(debug_assertions)] {
|
||||
if let Some(id) = el.view_marker {
|
||||
format!("<!--leptos-view|{id}|open-->{el_html}<!--leptos-view|{id}|close-->").into()
|
||||
} else {
|
||||
el_html
|
||||
}
|
||||
} else {
|
||||
el_html
|
||||
}
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn to_kebab_case(name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut new_name = String::with_capacity(name.len() + 8);
|
||||
|
||||
let mut chars = name.chars();
|
||||
|
||||
new_name.push(
|
||||
chars
|
||||
.next()
|
||||
.map(|mut c| {
|
||||
if c.is_ascii() {
|
||||
c.make_ascii_lowercase();
|
||||
}
|
||||
|
||||
c
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for mut char in chars {
|
||||
if char.is_ascii_uppercase() {
|
||||
char.make_ascii_lowercase();
|
||||
|
||||
new_name.push('-');
|
||||
}
|
||||
|
||||
new_name.push(char);
|
||||
}
|
||||
|
||||
new_name
|
||||
}
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn render_serializers(
|
||||
nonce_str: String,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> impl Stream<Item = String> {
|
||||
serializers.map(move |(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
let json = json.replace('<', "\\u003c");
|
||||
|
||||
format!(
|
||||
r#"<script{nonce_str}>
|
||||
(function() {{ let val = {json:?};
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
|
||||
}} else {{
|
||||
__LEPTOS_RESOLVED_RESOURCES.set({id}, val);
|
||||
}} }})();
|
||||
</script>"#,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn escape_attr<T>(value: &T) -> Oco<'_, str>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
html_escape::encode_double_quoted_attribute(value).into()
|
||||
}
|
||||
|
||||
pub(crate) trait ToMarker {
|
||||
fn to_marker(
|
||||
&self,
|
||||
closing: bool,
|
||||
#[cfg(debug_assertions)] component_name: &str,
|
||||
) -> Oco<'static, str>;
|
||||
}
|
||||
|
||||
impl ToMarker for HydrationKey {
|
||||
#[inline(always)]
|
||||
fn to_marker(
|
||||
&self,
|
||||
closing: bool,
|
||||
#[cfg(debug_assertions)] mut component_name: &str,
|
||||
) -> Oco<'static, str> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if component_name.is_empty() {
|
||||
// NOTE:
|
||||
// If the name is left empty, this will lead to invalid comments,
|
||||
// so a placeholder is used here.
|
||||
component_name = "<>";
|
||||
}
|
||||
if closing || component_name == "unit" {
|
||||
format!("<!--hk={self}c|leptos-{component_name}-end-->").into()
|
||||
} else {
|
||||
format!("<!--hk={self}o|leptos-{component_name}-start-->")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
if closing {
|
||||
format!("<!--hk={self}-->").into()
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToMarker for Option<HydrationKey> {
|
||||
#[inline(always)]
|
||||
fn to_marker(
|
||||
&self,
|
||||
closing: bool,
|
||||
#[cfg(debug_assertions)] component_name: &str,
|
||||
) -> Oco<'static, str> {
|
||||
self.map(|key| {
|
||||
key.to_marker(
|
||||
closing,
|
||||
#[cfg(debug_assertions)]
|
||||
component_name,
|
||||
)
|
||||
})
|
||||
.unwrap_or("".into())
|
||||
}
|
||||
}
|
||||
@@ -1,531 +0,0 @@
|
||||
#![cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
|
||||
//! Server-side HTML rendering utilities for in-order streaming and async rendering.
|
||||
|
||||
use crate::{
|
||||
html::{ElementChildren, StringOrView},
|
||||
ssr::{render_serializers, ToMarker},
|
||||
CoreComponent, HydrationCtx, View,
|
||||
};
|
||||
use async_recursion::async_recursion;
|
||||
use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use leptos_reactive::{
|
||||
create_runtime, suspense::StreamChunk, Oco, RuntimeId, SharedContext,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Renders a view to HTML, waiting to return until all `async` [Resource](leptos_reactive::Resource)s
|
||||
/// loaded in `<Suspense/>` elements have finished loading.
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn render_to_string_async(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
) -> String {
|
||||
let mut buf = String::new();
|
||||
let (stream, runtime) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view,
|
||||
|| "".into(),
|
||||
|| {},
|
||||
);
|
||||
let mut stream = Box::pin(stream);
|
||||
while let Some(chunk) = stream.next().await {
|
||||
buf.push_str(&chunk);
|
||||
}
|
||||
runtime.dispose();
|
||||
buf
|
||||
}
|
||||
|
||||
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
|
||||
/// in order:
|
||||
/// 1. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
|
||||
/// 2. any serialized [Resource](leptos_reactive::Resource)s
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn render_to_stream_in_order(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
render_to_stream_in_order_with_prefix(view, || "".into())
|
||||
}
|
||||
|
||||
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
|
||||
/// in order:
|
||||
/// 1. `prefix`
|
||||
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
|
||||
/// 3. any serialized [Resource](leptos_reactive::Resource)s
|
||||
///
|
||||
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
|
||||
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn render_to_stream_in_order_with_prefix(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
) -> impl Stream<Item = String> {
|
||||
#[cfg(all(feature = "web", feature = "ssr"))]
|
||||
crate::logging::console_error(
|
||||
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
|
||||
enabled as features, which may cause issues like <Suspense/>` \
|
||||
failing to work silently.\n",
|
||||
);
|
||||
|
||||
let (stream, runtime) =
|
||||
render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view,
|
||||
prefix,
|
||||
|| {},
|
||||
);
|
||||
runtime.dispose();
|
||||
stream
|
||||
}
|
||||
|
||||
/// Renders an in-order HTML stream, pausing at `<Suspense/>` components. The stream contains,
|
||||
/// in order:
|
||||
/// 1. `prefix`
|
||||
/// 2. HTML from the `view` in order, pausing to wait for each `<Suspense/>`
|
||||
/// 3. any serialized [Resource](leptos_reactive::Resource)s
|
||||
///
|
||||
/// `additional_context` is injected before the `view` is rendered. The `prefix` is generated
|
||||
/// after the `view` is rendered, but before `<Suspense/>` nodes have resolved.
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
||||
view: impl FnOnce() -> View + 'static,
|
||||
prefix: impl FnOnce() -> Oco<'static, str> + 'static,
|
||||
additional_context: impl FnOnce() + 'static,
|
||||
) -> (impl Stream<Item = String>, RuntimeId) {
|
||||
HydrationCtx::reset_id();
|
||||
|
||||
// create the runtime
|
||||
let runtime = create_runtime();
|
||||
|
||||
// add additional context
|
||||
additional_context();
|
||||
|
||||
// render view and return chunks
|
||||
let view = view();
|
||||
|
||||
let blocking_fragments_ready = SharedContext::blocking_fragments_ready();
|
||||
let chunks = view.into_stream_chunks();
|
||||
let pending_resources =
|
||||
serde_json::to_string(&SharedContext::pending_resources()).unwrap();
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||
let (prefix_tx, prefix_rx) = futures::channel::oneshot::channel();
|
||||
leptos_reactive::spawn_local(async move {
|
||||
blocking_fragments_ready.await;
|
||||
|
||||
let remaining_chunks = handle_blocking_chunks(tx.clone(), chunks).await;
|
||||
|
||||
let prefix = prefix();
|
||||
prefix_tx.send(prefix).expect("to send prefix");
|
||||
handle_chunks(tx, remaining_chunks).await;
|
||||
});
|
||||
|
||||
let nonce = crate::nonce::use_nonce();
|
||||
let nonce_str = nonce
|
||||
.as_ref()
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
let local_only = SharedContext::fragments_with_local_resources();
|
||||
let local_only = serde_json::to_string(&local_only).unwrap();
|
||||
|
||||
let stream = futures::stream::once({
|
||||
let nonce_str = nonce_str.clone();
|
||||
async move {
|
||||
let prefix = prefix_rx.await.expect("to receive prefix");
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
<script{nonce_str}>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
__LEPTOS_LOCAL_ONLY = {local_only};
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
}
|
||||
})
|
||||
.chain(rx)
|
||||
.chain(
|
||||
futures::stream::once(async move {
|
||||
let serializers = SharedContext::serialization_resolvers();
|
||||
render_serializers(nonce_str, serializers)
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
(stream, runtime)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_blocking_chunks(
|
||||
tx: UnboundedSender<String>,
|
||||
mut queued_chunks: VecDeque<StreamChunk>,
|
||||
) -> VecDeque<StreamChunk> {
|
||||
let mut buffer = String::new();
|
||||
while let Some(chunk) = queued_chunks.pop_front() {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async {
|
||||
chunks,
|
||||
should_block,
|
||||
} => {
|
||||
if should_block {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send async HTML chunk");
|
||||
|
||||
// send the inner stream
|
||||
let suspended = chunks.await;
|
||||
handle_blocking_chunks(tx.clone(), suspended).await;
|
||||
} else {
|
||||
// TODO: should probably first check if there are any *other* blocking chunks
|
||||
queued_chunks.push_front(StreamChunk::Async {
|
||||
chunks,
|
||||
should_block: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send final sync chunk
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send final HTML chunk");
|
||||
|
||||
queued_chunks
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[async_recursion(?Send)]
|
||||
async fn handle_chunks(
|
||||
tx: UnboundedSender<String>,
|
||||
chunks: VecDeque<StreamChunk>,
|
||||
) {
|
||||
let mut buffer = String::new();
|
||||
for chunk in chunks {
|
||||
match chunk {
|
||||
StreamChunk::Sync(sync) => buffer.push_str(&sync),
|
||||
StreamChunk::Async { chunks, .. } => {
|
||||
// add static HTML before the Suspense and stream it down
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send async HTML chunk");
|
||||
|
||||
// send the inner stream
|
||||
|
||||
let suspended = chunks.await;
|
||||
|
||||
handle_chunks(tx.clone(), suspended).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
// send final sync chunk
|
||||
tx.unbounded_send(std::mem::take(&mut buffer))
|
||||
.expect("failed to send final HTML chunk");
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Renders the view into a set of HTML chunks that can be streamed.
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn into_stream_chunks(self) -> VecDeque<StreamChunk> {
|
||||
let mut chunks = VecDeque::new();
|
||||
self.into_stream_chunks_helper(&mut chunks, false);
|
||||
chunks
|
||||
}
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
fn into_stream_chunks_helper(
|
||||
self,
|
||||
chunks: &mut VecDeque<StreamChunk>,
|
||||
dont_escape_text: bool,
|
||||
) {
|
||||
match self {
|
||||
View::Suspense(id, view) => {
|
||||
let id = id.to_string();
|
||||
if let Some(data) = SharedContext::take_pending_fragment(&id) {
|
||||
chunks.push_back(StreamChunk::Async {
|
||||
chunks: data.in_order,
|
||||
should_block: data.should_block,
|
||||
});
|
||||
} else {
|
||||
// if not registered, means it was already resolved
|
||||
View::CoreComponent(view)
|
||||
.into_stream_chunks_helper(chunks, dont_escape_text);
|
||||
}
|
||||
}
|
||||
View::Text(node) => {
|
||||
chunks.push_back(StreamChunk::Sync(node.content))
|
||||
}
|
||||
View::Component(node) => {
|
||||
#[cfg(debug_assertions)]
|
||||
let name = crate::ssr::to_kebab_case(&node.name);
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
chunks.push_back(StreamChunk::Sync(node.id.to_marker(
|
||||
false,
|
||||
#[cfg(debug_assertions)]
|
||||
&name,
|
||||
)));
|
||||
}
|
||||
|
||||
for child in node.children {
|
||||
child.into_stream_chunks_helper(chunks, dont_escape_text);
|
||||
}
|
||||
chunks.push_back(StreamChunk::Sync(node.id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
&name,
|
||||
)));
|
||||
}
|
||||
View::Element(el) => {
|
||||
let is_script_or_style =
|
||||
el.name == "script" || el.name == "style";
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|open-->").into(),
|
||||
));
|
||||
}
|
||||
if let ElementChildren::Chunks(el_chunks) = el.children {
|
||||
for chunk in el_chunks {
|
||||
match chunk {
|
||||
StringOrView::String(string) => {
|
||||
chunks.push_back(StreamChunk::Sync(string))
|
||||
}
|
||||
StringOrView::View(view) => view()
|
||||
.into_stream_chunks_helper(
|
||||
chunks,
|
||||
is_script_or_style,
|
||||
),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let tag_name = el.name;
|
||||
|
||||
let mut inner_html = None;
|
||||
|
||||
let attrs = el
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|(name, value)| -> Option<Oco<'static, str>> {
|
||||
if value.is_empty() {
|
||||
Some(format!(" {name}").into())
|
||||
} else if name == "inner_html" {
|
||||
inner_html = Some(value);
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
format!(
|
||||
" {name}=\"{}\"",
|
||||
html_escape::encode_double_quoted_attribute(&value)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
.join("");
|
||||
|
||||
if el.is_void {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}/>").into(),
|
||||
));
|
||||
} else if let Some(inner_html) = inner_html {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!(
|
||||
"<{tag_name}{attrs}>{inner_html}</{tag_name}>"
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
} else {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<{tag_name}{attrs}>").into(),
|
||||
));
|
||||
|
||||
match el.children {
|
||||
ElementChildren::Empty => {}
|
||||
ElementChildren::Children(children) => {
|
||||
for child in children {
|
||||
child.into_stream_chunks_helper(
|
||||
chunks,
|
||||
is_script_or_style,
|
||||
);
|
||||
}
|
||||
}
|
||||
ElementChildren::InnerHtml(inner_html) => {
|
||||
chunks.push_back(StreamChunk::Sync(inner_html))
|
||||
}
|
||||
// handled above
|
||||
ElementChildren::Chunks(_) => unreachable!(),
|
||||
}
|
||||
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("</{tag_name}>").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(id) = &el.view_marker {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
format!("<!--leptos-view|{id}|close-->").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
View::Transparent(_) => {}
|
||||
View::CoreComponent(node) => {
|
||||
let (id, name, wrap, content) = match node {
|
||||
CoreComponent::Unit(u) => (
|
||||
u.id,
|
||||
"",
|
||||
false,
|
||||
Box::new(move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
u.id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
"unit",
|
||||
),
|
||||
));
|
||||
})
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
),
|
||||
CoreComponent::DynChild(node) => {
|
||||
let child = node.child.take();
|
||||
(
|
||||
node.id,
|
||||
"dyn-child",
|
||||
true,
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
if let Some(child) = *child {
|
||||
if let View::Text(t) = child {
|
||||
// if we don't check if the string is empty,
|
||||
// the HTML is an empty string; but an empty string
|
||||
// is not a text node in HTML, so can't be updated
|
||||
// in the future. so we put a one-space text node instead
|
||||
let was_empty =
|
||||
t.content.is_empty();
|
||||
let content = if was_empty {
|
||||
" ".into()
|
||||
} else {
|
||||
t.content
|
||||
};
|
||||
// escape content unless we're in a <script> or <style>
|
||||
let content = if dont_escape_text {
|
||||
content
|
||||
} else {
|
||||
html_escape::encode_safe(
|
||||
&content,
|
||||
)
|
||||
.to_string()
|
||||
.into()
|
||||
};
|
||||
// On debug builds, `DynChild` has two marker nodes,
|
||||
// so there is no way for the text to be merged with
|
||||
// surrounding text when the browser parses the HTML,
|
||||
// but in release, `DynChild` only has a trailing marker,
|
||||
// and the browser automatically merges the dynamic text
|
||||
// into one single node, so we need to artificially make the
|
||||
// browser create the dynamic text as it's own text node
|
||||
chunks.push_back(
|
||||
if !cfg!(debug_assertions) {
|
||||
StreamChunk::Sync(
|
||||
format!(
|
||||
"<!>{}",
|
||||
html_escape::encode_safe(
|
||||
&content
|
||||
)
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
StreamChunk::Sync(html_escape::encode_safe(
|
||||
&content
|
||||
).to_string().into())
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child.into_stream_chunks_helper(
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
CoreComponent::Each(node) => {
|
||||
let children = node.children.take();
|
||||
(
|
||||
node.id,
|
||||
"each",
|
||||
true,
|
||||
Box::new(
|
||||
move |chunks: &mut VecDeque<StreamChunk>| {
|
||||
for node in children.into_iter().flatten() {
|
||||
let id = node.id;
|
||||
let is_el = matches!(
|
||||
node.child,
|
||||
View::Element(_)
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if !is_el {
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
id.to_marker(
|
||||
false,
|
||||
"each-item",
|
||||
),
|
||||
))
|
||||
};
|
||||
node.child.into_stream_chunks_helper(
|
||||
chunks,
|
||||
dont_escape_text,
|
||||
);
|
||||
|
||||
if !is_el {
|
||||
chunks.push_back(
|
||||
StreamChunk::Sync(
|
||||
id.to_marker(
|
||||
true,
|
||||
#[cfg(
|
||||
debug_assertions
|
||||
)]
|
||||
"each-item",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
as Box<dyn FnOnce(&mut VecDeque<StreamChunk>)>,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if wrap {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
chunks.push_back(StreamChunk::Sync(
|
||||
id.to_marker(false, name),
|
||||
));
|
||||
}
|
||||
content(chunks);
|
||||
chunks.push_back(StreamChunk::Sync(id.to_marker(
|
||||
true,
|
||||
#[cfg(debug_assertions)]
|
||||
name,
|
||||
)));
|
||||
} else {
|
||||
content(chunks);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
//! Exports types for working with SVG elements.
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey};
|
||||
use super::{AnyElement, ElementDescriptor, HtmlElement};
|
||||
use crate::HydrationCtx;
|
||||
use leptos_reactive::Oco;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use once_cell::unsync::Lazy as LazyCell;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
macro_rules! generate_svg_tags {
|
||||
(
|
||||
$(
|
||||
#[$meta:meta]
|
||||
$(#[$void:ident])?
|
||||
$tag:ident $(- $second:ident $(- $third:ident)?)? $(@ $trailing_:pat)?
|
||||
),* $(,)?
|
||||
) => {
|
||||
paste::paste! {
|
||||
$(
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
thread_local! {
|
||||
static [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]: LazyCell<web_sys::HtmlElement> = LazyCell::new(|| {
|
||||
crate::document()
|
||||
.create_element_ns(
|
||||
Some(wasm_bindgen::intern("http://www.w3.org/2000/svg")),
|
||||
concat![
|
||||
stringify!($tag),
|
||||
$(
|
||||
"-", stringify!($second),
|
||||
$(
|
||||
"-", stringify!($third)
|
||||
)?
|
||||
)?
|
||||
],
|
||||
)
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[$meta]
|
||||
pub struct [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element: web_sys::HtmlElement,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id: Option<HydrationKey>,
|
||||
}
|
||||
|
||||
impl Default for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn default() -> Self {
|
||||
#[allow(unused)]
|
||||
let id = HydrationCtx::id();
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "hydrate"))]
|
||||
let element = if HydrationCtx::is_hydrating() && id.is_some() {
|
||||
let id = id.unwrap();
|
||||
if let Some(el) = crate::hydration::get_element(&id.to_string()) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(
|
||||
el.node_name().to_ascii_uppercase(),
|
||||
stringify!([<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]),
|
||||
"SSR and CSR elements have the same hydration key but \
|
||||
different node kinds. Check out the docs for information \
|
||||
about this kind of hydration bug: https://leptos-rs.github.io/leptos/ssr/24_hydration_bugs.html"
|
||||
);
|
||||
el.unchecked_into()
|
||||
} else {
|
||||
crate::warn!(
|
||||
"element with id {id} not found, ignoring it for hydration"
|
||||
);
|
||||
|
||||
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
[<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web", not(feature = "hydrate")))]
|
||||
let element = [<$tag:upper $(_ $second:upper $(_ $third:upper)?)?>]
|
||||
.with(|el|
|
||||
el.clone_node()
|
||||
.unwrap()
|
||||
.unchecked_into()
|
||||
);
|
||||
|
||||
Self {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
element,
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
type Target = web_sys::SvgElement;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
{
|
||||
use wasm_bindgen::JsCast;
|
||||
return &self.element.unchecked_ref();
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::AsRef<web_sys::HtmlElement> for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn as_ref(&self) -> &web_sys::HtmlElement {
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return &self.element;
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
unimplemented!("{HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG}");
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] {
|
||||
fn name(&self) -> Oco<'static, str> {
|
||||
stringify!($tag).into()
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
|
||||
fn hydration_id(&self) -> &Option<HydrationKey> {
|
||||
&self.id
|
||||
}
|
||||
|
||||
generate_svg_tags! { @void $($void)? }
|
||||
}
|
||||
|
||||
impl From<HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>> for HtmlElement<AnyElement> {
|
||||
fn from(element: HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]>) -> Self {
|
||||
element.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
#[$meta]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn [<$tag $(_ $second $(_ $third)?)? $($trailing_)?>]() -> HtmlElement<[<$tag:camel $($second:camel $($third:camel)?)?>]> {
|
||||
HtmlElement::new([<$tag:camel $($second:camel $($third:camel)?)?>]::default())
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
(@void) => {};
|
||||
(@void void) => {
|
||||
fn is_void(&self) -> bool {
|
||||
true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
generate_svg_tags![
|
||||
/// SVG Element.
|
||||
a,
|
||||
/// SVG Element.
|
||||
animate,
|
||||
/// SVG Element.
|
||||
animateMotion,
|
||||
/// SVG Element.
|
||||
animateTransform,
|
||||
/// SVG Element.
|
||||
circle,
|
||||
/// SVG Element.
|
||||
clipPath,
|
||||
/// SVG Element.
|
||||
defs,
|
||||
/// SVG Element.
|
||||
desc,
|
||||
/// SVG Element.
|
||||
discard,
|
||||
/// SVG Element.
|
||||
ellipse,
|
||||
/// SVG Element.
|
||||
feBlend,
|
||||
/// SVG Element.
|
||||
feColorMatrix,
|
||||
/// SVG Element.
|
||||
feComponentTransfer,
|
||||
/// SVG Element.
|
||||
feComposite,
|
||||
/// SVG Element.
|
||||
feConvolveMatrix,
|
||||
/// SVG Element.
|
||||
feDiffuseLighting,
|
||||
/// SVG Element.
|
||||
feDisplacementMap,
|
||||
/// SVG Element.
|
||||
feDistantLight,
|
||||
/// SVG Element.
|
||||
feDropShadow,
|
||||
/// SVG Element.
|
||||
feFlood,
|
||||
/// SVG Element.
|
||||
feFuncA,
|
||||
/// SVG Element.
|
||||
feFuncB,
|
||||
/// SVG Element.
|
||||
feFuncG,
|
||||
/// SVG Element.
|
||||
feFuncR,
|
||||
/// SVG Element.
|
||||
feGaussianBlur,
|
||||
/// SVG Element.
|
||||
feImage,
|
||||
/// SVG Element.
|
||||
feMerge,
|
||||
/// SVG Element.
|
||||
feMergeNode,
|
||||
/// SVG Element.
|
||||
feMorphology,
|
||||
/// SVG Element.
|
||||
feOffset,
|
||||
/// SVG Element.
|
||||
fePointLight,
|
||||
/// SVG Element.
|
||||
feSpecularLighting,
|
||||
/// SVG Element.
|
||||
feSpotLight,
|
||||
/// SVG Element.
|
||||
feTile,
|
||||
/// SVG Element.
|
||||
feTurbulence,
|
||||
/// SVG Element.
|
||||
filter,
|
||||
/// SVG Element.
|
||||
foreignObject,
|
||||
/// SVG Element.
|
||||
g,
|
||||
/// SVG Element.
|
||||
hatch,
|
||||
/// SVG Element.
|
||||
hatchpath,
|
||||
/// SVG Element.
|
||||
image,
|
||||
/// SVG Element.
|
||||
line,
|
||||
/// SVG Element.
|
||||
linearGradient,
|
||||
/// SVG Element.
|
||||
marker,
|
||||
/// SVG Element.
|
||||
mask,
|
||||
/// SVG Element.
|
||||
metadata,
|
||||
/// SVG Element.
|
||||
mpath,
|
||||
/// SVG Element.
|
||||
path,
|
||||
/// SVG Element.
|
||||
pattern,
|
||||
/// SVG Element.
|
||||
polygon,
|
||||
/// SVG Element.
|
||||
polyline,
|
||||
/// SVG Element.
|
||||
radialGradient,
|
||||
/// SVG Element.
|
||||
rect,
|
||||
/// SVG Element.
|
||||
script,
|
||||
/// SVG Element.
|
||||
set,
|
||||
/// SVG Element.
|
||||
stop,
|
||||
/// SVG Element.
|
||||
style,
|
||||
/// SVG Element.
|
||||
svg,
|
||||
/// SVG Element.
|
||||
switch,
|
||||
/// SVG Element.
|
||||
symbol,
|
||||
/// SVG Element.
|
||||
text,
|
||||
/// SVG Element.
|
||||
textPath,
|
||||
/// SVG Element.
|
||||
title,
|
||||
/// SVG Element.
|
||||
tspan,
|
||||
/// SVG Element.
|
||||
use @_,
|
||||
/// SVG Element.
|
||||
view,
|
||||
];
|
||||
@@ -1,49 +0,0 @@
|
||||
use crate::{IntoView, View};
|
||||
use std::{any::Any, fmt, rc::Rc};
|
||||
|
||||
/// Wrapper for arbitrary data that can be passed through the view.
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct Transparent(Rc<dyn Any>);
|
||||
|
||||
impl Transparent {
|
||||
/// Creates a new wrapper for this data.
|
||||
#[inline(always)]
|
||||
pub fn new<T>(value: T) -> Self
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
Self(Rc::new(value))
|
||||
}
|
||||
|
||||
/// Returns some reference to the inner value if it is of type `T`, or `None` if it isn't.
|
||||
#[inline(always)]
|
||||
pub fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.0.downcast_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Transparent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("Transparent").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Transparent {
|
||||
#[inline(always)]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
std::ptr::eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Transparent {}
|
||||
|
||||
impl IntoView for Transparent {
|
||||
#[inline(always)]
|
||||
fn into_view(self) -> View {
|
||||
View::Transparent(self)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_macro"
|
||||
version = "0.7.0-alpha2"
|
||||
version = "0.7.0-beta"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
@@ -34,13 +34,14 @@ log = "0.4"
|
||||
typed-builder = "0.18"
|
||||
trybuild = "1"
|
||||
leptos = { path = "../leptos" }
|
||||
server_fn = { path = "../server_fn", features = ["cbor"] }
|
||||
insta = "1.29"
|
||||
serde = "1"
|
||||
|
||||
[features]
|
||||
csr = []
|
||||
hydrate = []
|
||||
ssr = ["server_fn_macro/ssr"]
|
||||
ssr = ["server_fn_macro/ssr", "leptos/ssr"]
|
||||
nightly = ["server_fn_macro/nightly"]
|
||||
tracing = []
|
||||
experimental-islands = []
|
||||
|
||||
@@ -11,13 +11,13 @@ dependencies = [
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2024-04-14", "test", "--doc"]
|
||||
args = ["+nightly-2024-08-01", "test", "--doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2024-04-14", "doc"]
|
||||
args = ["+nightly-2024-08-01", "doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn TestComponent(
|
||||
@@ -38,7 +38,7 @@ pub fn TestComponent(
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TestMutCallback<F>(mut callback: F, value: &'static str) -> impl IntoView
|
||||
pub fn TestMutCallback<F>(mut callback: F, value: &'static str) -> impl IntoView
|
||||
where
|
||||
F: FnMut(u32) + 'static,
|
||||
{
|
||||
@@ -46,9 +46,7 @@ where
|
||||
view! {
|
||||
<button on:click=move |_| {
|
||||
callback(5);
|
||||
}>
|
||||
{value}
|
||||
</button>
|
||||
}>{value}</button>
|
||||
<TestComponent key="test"/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ impl ToTokens for Model {
|
||||
};
|
||||
let island_serialized_props = if is_island_with_other_props {
|
||||
quote! {
|
||||
.attr("data-props", _leptos_ser_props)
|
||||
.with_props( _leptos_ser_props)
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
@@ -260,11 +260,21 @@ impl ToTokens for Model {
|
||||
let component = if *is_island {
|
||||
quote! {
|
||||
{
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
#component_id,
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
if ::leptos::reactive_graph::owner::Owner::current_shared_context()
|
||||
.map(|sc| sc.get_is_hydrating())
|
||||
.unwrap_or(false) {
|
||||
::leptos::either::Either::Left(
|
||||
#component
|
||||
)
|
||||
} else {
|
||||
::leptos::either::Either::Right(
|
||||
::leptos::tachys::html::islands::Island::new(
|
||||
#component_id,
|
||||
#component
|
||||
)
|
||||
#island_serialized_props
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -285,7 +295,15 @@ impl ToTokens for Model {
|
||||
let wrapped_children = if is_island_with_children {
|
||||
quote! {
|
||||
use leptos::tachys::view::any_view::IntoAny;
|
||||
let children = Box::new(|| ::leptos::tachys::html::islands::IslandChildren::new(children()).into_any());
|
||||
let children = Box::new(|| {
|
||||
let sc = ::leptos::reactive_graph::owner::Owner::current_shared_context().unwrap();
|
||||
let prev = sc.get_is_hydrating();
|
||||
let value = ::leptos::reactive_graph::owner::Owner::with_no_hydration(||
|
||||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
|
||||
);
|
||||
sc.set_is_hydrating(prev);
|
||||
value
|
||||
});
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
@@ -375,16 +393,15 @@ impl ToTokens for Model {
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
let deserialize_island_props = quote! {}; /*if is_island_with_other_props {
|
||||
quote! {
|
||||
let props = el.dataset().get("props") // TODO ::leptos::wasm_bindgen::intern("props"))
|
||||
.and_then(|data| ::leptos::serde_json::from_str::<#props_serialized_name>(&data).ok())
|
||||
.expect("could not deserialize props");
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};*/
|
||||
// TODO
|
||||
let deserialize_island_props = if is_island_with_other_props {
|
||||
quote! {
|
||||
let props = el.dataset().get(::leptos::wasm_bindgen::intern("props"))
|
||||
.and_then(|data| ::leptos::serde_json::from_str::<#props_serialized_name>(&data).ok())
|
||||
.expect("could not deserialize props");
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[::leptos::wasm_bindgen::prelude::wasm_bindgen(wasm_bindgen = ::leptos::wasm_bindgen)]
|
||||
@@ -608,8 +625,6 @@ impl Docs {
|
||||
let mut quote_ws = "".to_string();
|
||||
let mut view_code_fence_state = ViewCodeFenceState::Outside;
|
||||
// todo fix docs stuff
|
||||
const RUST_START: &str = "# let runtime = ::leptos::create_runtime();";
|
||||
const RUST_END: &str = "# runtime.dispose();";
|
||||
const RSX_START: &str = "# ::leptos::view! {";
|
||||
const RSX_END: &str = "# };";
|
||||
|
||||
@@ -637,15 +652,12 @@ impl Docs {
|
||||
.trim_start();
|
||||
vec![
|
||||
format!("{leading_ws}{quotes}{rust_options}"),
|
||||
format!("{leading_ws}{RUST_START}"),
|
||||
format!("{leading_ws}"),
|
||||
]
|
||||
}
|
||||
ViewCodeFenceState::Rust if trimmed_doc == quotes => {
|
||||
view_code_fence_state = ViewCodeFenceState::Outside;
|
||||
vec![
|
||||
format!("{leading_ws}{RUST_END}"),
|
||||
doc.to_owned(),
|
||||
]
|
||||
vec![format!("{leading_ws}"), doc.to_owned()]
|
||||
}
|
||||
ViewCodeFenceState::Rust
|
||||
if trimmed_doc.starts_with('<') =>
|
||||
@@ -694,7 +706,7 @@ impl Docs {
|
||||
|
||||
if view_code_fence_state != ViewCodeFenceState::Outside {
|
||||
if view_code_fence_state == ViewCodeFenceState::Rust {
|
||||
attrs.push((format!("{quote_ws}{RUST_END}"), Span::call_site()))
|
||||
attrs.push((quote_ws.clone(), Span::call_site()))
|
||||
} else {
|
||||
attrs.push((format!("{quote_ws}{RSX_END}"), Span::call_site()))
|
||||
}
|
||||
|
||||
166
leptos_macro/src/custom_view.rs
Normal file
166
leptos_macro/src/custom_view.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{parse::Parse, parse_macro_input, ImplItem, ItemImpl};
|
||||
|
||||
pub fn custom_view_impl(tokens: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(tokens as CustomViewMacroInput);
|
||||
input.into_token_stream().into()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CustomViewMacroInput {
|
||||
impl_block: ItemImpl,
|
||||
}
|
||||
|
||||
impl Parse for CustomViewMacroInput {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let impl_block = input.parse()?;
|
||||
Ok(CustomViewMacroInput { impl_block })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CustomViewMacroInput {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let ItemImpl {
|
||||
impl_token,
|
||||
generics,
|
||||
self_ty,
|
||||
items,
|
||||
..
|
||||
} = &self.impl_block;
|
||||
let impl_span = &impl_token;
|
||||
let view_ty = items
|
||||
.iter()
|
||||
.find_map(|item| match item {
|
||||
ImplItem::Type(ty) => (ty.ident == "View").then_some(&ty.ty),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
proc_macro_error::abort!(
|
||||
impl_span,
|
||||
"You must include `type View = ...;` to specify the type. \
|
||||
In most cases, this will be `type View = AnyView<Rndr>;"
|
||||
)
|
||||
});
|
||||
|
||||
let view_fn = items
|
||||
.iter()
|
||||
.find_map(|item| match item {
|
||||
ImplItem::Fn(f) => {
|
||||
(f.sig.ident == "into_view").then_some(&f.block)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
proc_macro_error::abort!(
|
||||
impl_span,
|
||||
"You must include `fn into_view(self) -> Self::View` to \
|
||||
specify the view function."
|
||||
)
|
||||
});
|
||||
let generic_params = &generics.params;
|
||||
let where_preds =
|
||||
&generics.where_clause.as_ref().map(|wc| &wc.predicates);
|
||||
|
||||
tokens.extend(quote! {
|
||||
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::Render<Rndr> for #self_ty
|
||||
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
|
||||
type State = <#view_ty as ::leptos::tachys::view::Render<Rndr>>::State;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
let view = #view_fn;
|
||||
view.build()
|
||||
}
|
||||
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
let view = #view_fn;
|
||||
view.rebuild(state);
|
||||
}
|
||||
}
|
||||
|
||||
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::add_attr::AddAnyAttr<Rndr> for #self_ty
|
||||
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
|
||||
type Output<SomeNewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>> =
|
||||
<#view_ty as ::leptos::tachys::view::add_attr::AddAnyAttr<Rndr>>::Output<SomeNewAttr>;
|
||||
|
||||
fn add_any_attr<NewAttr: ::leptos::tachys::html::attribute::Attribute<Rndr>>(
|
||||
self,
|
||||
attr: NewAttr,
|
||||
) -> Self::Output<NewAttr>
|
||||
where
|
||||
Self::Output<NewAttr>: ::leptos::tachys::view::RenderHtml<Rndr>,
|
||||
{
|
||||
let view = #view_fn;
|
||||
view.add_any_attr(attr)
|
||||
}
|
||||
}
|
||||
|
||||
#impl_token<#generic_params, Rndr> ::leptos::tachys::view::RenderHtml<Rndr> for #self_ty
|
||||
where Rndr: ::leptos::tachys::renderer::Renderer, #where_preds {
|
||||
type AsyncOutput = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::AsyncOutput;
|
||||
const MIN_LENGTH: usize = <#view_ty as ::leptos::tachys::view::RenderHtml<Rndr>>::MIN_LENGTH;
|
||||
|
||||
async fn resolve(self) -> Self::AsyncOutput {
|
||||
let view = #view_fn;
|
||||
::leptos::tachys::view::RenderHtml::<Rndr>::resolve(view).await
|
||||
}
|
||||
|
||||
fn dry_resolve(&mut self) {
|
||||
// TODO... The problem is that view_fn expects to take self
|
||||
// dry_resolve is the only one that takes &mut self
|
||||
// this can only have an effect if walking over the view would read from
|
||||
// resources that are not otherwise read synchronously, which is an interesting
|
||||
// edge case to handle but probably (?) irrelevant for most actual use cases of
|
||||
// this macro
|
||||
}
|
||||
|
||||
fn to_html_with_buf(
|
||||
self,
|
||||
buf: &mut String,
|
||||
position: &mut ::leptos::tachys::view::Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
) {
|
||||
let view = #view_fn;
|
||||
::leptos::tachys::view::RenderHtml::<Rndr>::to_html_with_buf(
|
||||
view,
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches
|
||||
);
|
||||
}
|
||||
|
||||
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||
self,
|
||||
buf: &mut ::leptos::tachys::ssr::StreamBuilder,
|
||||
position: &mut ::leptos::tachys::view::Position,
|
||||
escape: bool,
|
||||
mark_branches: bool,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
let view = #view_fn;
|
||||
::leptos::tachys::view::RenderHtml::<Rndr>::to_html_async_with_buf::<OUT_OF_ORDER>(
|
||||
view,
|
||||
buf,
|
||||
position,
|
||||
escape,
|
||||
mark_branches
|
||||
);
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
cursor: &::leptos::tachys::hydration::Cursor<Rndr>,
|
||||
position: &::leptos::tachys::view::PositionState,
|
||||
) -> Self::State {
|
||||
let view = #view_fn;
|
||||
::leptos::tachys::view::RenderHtml::<Rndr>::hydrate::<FROM_SERVER>(
|
||||
view, cursor, position
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ mod params;
|
||||
mod view;
|
||||
use crate::component::unmodified_fn_name_from_fn_name;
|
||||
mod component;
|
||||
mod custom_view;
|
||||
mod slice;
|
||||
mod slot;
|
||||
|
||||
@@ -27,48 +28,41 @@ mod slot;
|
||||
///
|
||||
/// 1. Text content should be provided as a Rust string, i.e., double-quoted:
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// view! { <p>"Here’s some text"</p> };
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// view! { <p>"Here’s some text"</p> }
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// 2. Self-closing tags need an explicit `/` as in XML/XHTML
|
||||
/// ```rust,compile_fail
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// // ❌ not like this
|
||||
/// view! { <input type="text" name="name"> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// // ✅ add that slash
|
||||
/// view! { <input type="text" name="name" /> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// 3. Components (functions annotated with `#[component]`) can be inserted as camel-cased tags. (Generics
|
||||
/// on components are specified as `<Component<T>/>`, not the turbofish `<Component::<T>/>`.)
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// # #[component]
|
||||
/// # fn Counter(initial_value: i32) -> impl IntoView { view! { <p></p>} }
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// view! { <div><Counter initial_value=3 /></div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// 4. Dynamic content can be wrapped in curly braces (`{ }`) to insert text nodes, elements, or set attributes.
|
||||
@@ -80,9 +74,9 @@ mod slot;
|
||||
/// take an `Option`, in which case `Some` sets the attribute and `None` removes the attribute.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let (count, set_count) = create_signal(0);
|
||||
///
|
||||
/// view! {
|
||||
@@ -95,15 +89,13 @@ mod slot;
|
||||
/// }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// 5. Event handlers can be added with `on:` attributes. In most cases, the events are given the correct type
|
||||
/// based on the event name.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// view! {
|
||||
/// <button on:click=|ev| {
|
||||
/// log::debug!("click event: {ev:#?}");
|
||||
@@ -111,18 +103,15 @@ mod slot;
|
||||
/// "Click me"
|
||||
/// </button>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 6. DOM properties can be set with `prop:` attributes, which take any primitive type or `JsValue` (or a signal
|
||||
/// that returns a primitive or JsValue). They can also take an `Option`, in which case `Some` sets the property
|
||||
/// and `None` deletes the property.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let (name, set_name) = create_signal("Alice".to_string());
|
||||
///
|
||||
/// view! {
|
||||
@@ -134,53 +123,41 @@ mod slot;
|
||||
/// on:click=move |ev| set_name.set(event_target_value(&ev)) // `event_target_value` is a useful little Leptos helper
|
||||
/// />
|
||||
/// }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 7. Classes can be toggled with `class:` attributes, which take a `bool` (or a signal that returns a `bool`).
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let (count, set_count) = create_signal(2);
|
||||
/// view! { <div class:hidden-div={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// Class names can include dashes, and since v0.5.0 can include a dash-separated segment of only numbers.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let (count, set_count) = create_signal(2);
|
||||
/// view! { <div class:hidden-div-25={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Class names cannot include special symbols.
|
||||
/// ```rust,compile_fail
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let (count, set_count) = create_signal(2);
|
||||
/// // class:hidden-[div]-25 is invalid attribute name
|
||||
/// view! { <div class:hidden-[div]-25={move || count.get() < 3}>"Now you see me, now you don’t."</div> }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// However, you can pass arbitrary class names using the syntax `class=("name", value)`.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let (count, set_count) = create_signal(2);
|
||||
/// // this allows you to use CSS frameworks that include complex class names
|
||||
/// view! {
|
||||
@@ -190,16 +167,14 @@ mod slot;
|
||||
/// "Now you see me, now you don’t."
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 8. Individual styles can also be set with `style:` or `style=("property-name", value)` syntax.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let (x, set_x) = create_signal(0);
|
||||
/// let (y, set_y) = create_signal(0);
|
||||
/// view! {
|
||||
@@ -212,67 +187,57 @@ mod slot;
|
||||
/// "Moves when coordinates change"
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 9. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](https://docs.rs/leptos/latest/leptos/struct.NodeRef.html) to use later.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// use leptos::html::Input;
|
||||
///
|
||||
/// let (value, set_value) = create_signal(0);
|
||||
/// let my_input = create_node_ref::<Input>();
|
||||
/// view! { <input type="text" _ref=my_input/> }
|
||||
/// let (value, set_value) = signal(0);
|
||||
/// let my_input = NodeRef::<Input>::new();
|
||||
/// view! { <input type="text" node_ref=my_input/> }
|
||||
/// // `my_input` now contains an `Element` that we can use anywhere
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// ```
|
||||
///
|
||||
/// 10. You can add the same class to every element in the view by passing in a special
|
||||
/// `class = {/* ... */},` argument after ``. This is useful for injecting a class
|
||||
/// provided by a scoped styling library.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let class = "mycustomclass";
|
||||
/// view! { class = class,
|
||||
/// <div> // will have class="mycustomclass"
|
||||
/// <p>"Some text"</p> // will also have class "mycustomclass"
|
||||
/// </div>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// 11. You can set any HTML element’s `innerHTML` with the `inner_html` attribute on an
|
||||
/// element. Be careful: this HTML will not be escaped, so you should ensure that it
|
||||
/// only contains trusted input.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # let runtime = create_runtime();
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// # fn test() -> impl IntoView {
|
||||
/// let html = "<p>This HTML will be injected.</p>";
|
||||
/// view! {
|
||||
/// <div inner_html=html/>
|
||||
/// }
|
||||
/// # ;
|
||||
/// # };
|
||||
/// # runtime.dispose();
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Here’s a simple example that shows off several of these features, put together
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
///
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// # use leptos::prelude::*;
|
||||
/// pub fn SimpleCounter() -> impl IntoView {
|
||||
/// // create a reactive signal with the initial value
|
||||
/// let (value, set_value) = create_signal(0);
|
||||
@@ -292,8 +257,6 @@ mod slot;
|
||||
/// </div>
|
||||
/// }
|
||||
/// }
|
||||
/// # ;
|
||||
/// # }
|
||||
/// ```
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro]
|
||||
@@ -364,7 +327,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
///
|
||||
/// Here’s how you would define and use a simple Leptos component which can accept custom properties for a name and age:
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// #[component]
|
||||
@@ -411,7 +374,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// a particular tag is a component, not an HTML element.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// // PascalCase: Generated component will be called MyComponent
|
||||
/// #[component]
|
||||
@@ -422,48 +385,15 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// fn my_snake_case_component() -> impl IntoView {}
|
||||
/// ```
|
||||
///
|
||||
/// * You can pass generic arguments, and they can either be defined in a `where` clause
|
||||
/// or inline in the generic block, but not in an `impl` in function argument position.
|
||||
///
|
||||
/// ```compile_error
|
||||
/// // ❌ This won't work.
|
||||
/// # use leptos::*;
|
||||
/// use leptos::html::Div;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyComponent(render_prop: impl Fn() -> HtmlElement<Div>) -> impl IntoView {
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// // ✅ Do this instead
|
||||
/// # use leptos::*;
|
||||
/// use leptos::html::Div;
|
||||
///
|
||||
/// #[component]
|
||||
/// fn MyComponent<T>(render_prop: T) -> impl IntoView
|
||||
/// where
|
||||
/// T: Fn() -> HtmlElement<Div>,
|
||||
/// {
|
||||
/// }
|
||||
///
|
||||
/// // or
|
||||
/// #[component]
|
||||
/// fn MyComponent2<T: Fn() -> HtmlElement<Div>>(
|
||||
/// render_prop: T,
|
||||
/// ) -> impl IntoView {
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 5. You can access the children passed into the component with the `children` property, which takes
|
||||
/// an argument of the type `Children`. This is an alias for `Box<dyn FnOnce() -> Fragment>`.
|
||||
/// an argument of the type `Children`. This is an alias for `Box<dyn FnOnce() -> AnyView<_>>`.
|
||||
/// If you need `children` to be a `Fn` or `FnMut`, you can use the `ChildrenFn` or `ChildrenFnMut`
|
||||
/// type aliases.
|
||||
/// type aliases. If you want to iterate over the children, you can take `ChildrenFragment`.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// #[component]
|
||||
/// fn ComponentWithChildren(children: Children) -> impl IntoView {
|
||||
/// fn ComponentWithChildren(children: ChildrenFragment) -> impl IntoView {
|
||||
/// view! {
|
||||
/// <ul>
|
||||
/// {children()
|
||||
@@ -502,7 +432,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
/// `Some(T)` explicitly. This means that the optional property can be omitted (and be `None`), or explicitly
|
||||
/// specified as either `None` or `Some(T)`.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn MyComponent(
|
||||
@@ -570,7 +500,7 @@ pub fn component(
|
||||
///
|
||||
/// ## Example
|
||||
/// ```rust,ignore
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
///
|
||||
/// #[component]
|
||||
/// pub fn App() -> impl IntoView {
|
||||
@@ -656,7 +586,7 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
|
||||
///
|
||||
/// Here’s how you would define and use a simple Leptos component which can accept a custom slot:
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// #[slot]
|
||||
@@ -668,16 +598,10 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
|
||||
///
|
||||
/// #[component]
|
||||
/// fn HelloComponent(
|
||||
///
|
||||
/// /// Component slot, should be passed through the <HelloSlot slot> syntax.
|
||||
/// hello_slot: HelloSlot,
|
||||
/// ) -> impl IntoView {
|
||||
/// // mirror the children from the slot, if any were passed
|
||||
/// if let Some(children) = hello_slot.children {
|
||||
/// (children)().into_view()
|
||||
/// } else {
|
||||
/// ().into_view()
|
||||
/// }
|
||||
/// hello_slot.children.map(|children| children())
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
@@ -702,7 +626,7 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
|
||||
///
|
||||
/// ```compile_error
|
||||
/// // ❌ This won't work
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// #[slot]
|
||||
/// struct SlotWithChildren {
|
||||
@@ -728,7 +652,7 @@ fn component_macro(s: TokenStream, island: bool) -> TokenStream {
|
||||
///
|
||||
/// ```
|
||||
/// // ✅ Do this instead
|
||||
/// # use leptos::*;
|
||||
/// # use leptos::prelude::*;
|
||||
///
|
||||
/// #[slot]
|
||||
/// struct SlotWithChildren {
|
||||
@@ -908,9 +832,8 @@ pub fn params_derive(
|
||||
/// Can be used to access deeply nested fields within a global state object.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use leptos::{create_runtime, create_rw_signal};
|
||||
/// # use leptos::prelude::*;
|
||||
/// # use leptos_macro::slice;
|
||||
/// # let runtime = create_runtime();
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// pub struct Outer {
|
||||
@@ -924,7 +847,7 @@ pub fn params_derive(
|
||||
/// inner_name: String,
|
||||
/// }
|
||||
///
|
||||
/// let outer_signal = create_rw_signal(Outer::default());
|
||||
/// let outer_signal = RwSignal::new(Outer::default());
|
||||
///
|
||||
/// let (count, set_count) = slice!(outer_signal.count);
|
||||
///
|
||||
@@ -935,3 +858,36 @@ pub fn params_derive(
|
||||
pub fn slice(input: TokenStream) -> TokenStream {
|
||||
slice::slice_impl(input)
|
||||
}
|
||||
|
||||
/// Implements the traits needed to make something [`IntoView`] on some type.
|
||||
///
|
||||
/// The renderer relies on the implementation of several traits, implementing which for a custom
|
||||
/// struct involves significant boilerplate. This macro is intended to make it easier to implement
|
||||
/// a view type for custom data, by allowing you to provide some custom view logic for the type.
|
||||
///
|
||||
/// ```rust
|
||||
/// use leptos::custom_view;
|
||||
///
|
||||
/// struct Foo<T>(T);
|
||||
///
|
||||
/// #[custom_view]
|
||||
/// impl<T> CustomView for Foo<T>
|
||||
/// where
|
||||
/// T: ToString + Send + 'static,
|
||||
/// {
|
||||
/// // this will usually be `AnyView<Rndr>`, but for simple types
|
||||
/// // you may be able to specify the output type easily
|
||||
/// type View = String;
|
||||
///
|
||||
/// fn into_view(self) -> Self::View {
|
||||
/// self.0.to_string().to_ascii_uppercase()
|
||||
/// }
|
||||
/// }
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn custom_view(
|
||||
_args: proc_macro::TokenStream,
|
||||
s: TokenStream,
|
||||
) -> TokenStream {
|
||||
custom_view::custom_view_impl(s)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl ToTokens for SliceMacroInput {
|
||||
let path = &self.path;
|
||||
|
||||
tokens.extend(quote! {
|
||||
::leptos::create_slice(
|
||||
::leptos::reactive_graph::computed::create_slice(
|
||||
#root,
|
||||
|st: &_| st.#path.clone(),
|
||||
|st: &mut _, n| st.#path = n
|
||||
|
||||
@@ -6,7 +6,7 @@ use self::{
|
||||
slot_helper::{get_slot, slot_to_tokens},
|
||||
};
|
||||
use convert_case::{Case::Snake, Casing};
|
||||
use leptos_hot_reload::parsing::is_component_node;
|
||||
use leptos_hot_reload::parsing::{is_component_node, value_to_string};
|
||||
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
|
||||
use proc_macro_error::abort;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
@@ -319,6 +319,10 @@ pub(crate) fn element_to_tokens(
|
||||
})
|
||||
};
|
||||
|
||||
let global_class_expr = global_class.map(|class| {
|
||||
quote! { .class((#class, true)) }
|
||||
});
|
||||
|
||||
let self_closing = is_self_closing(node);
|
||||
let children = if !self_closing {
|
||||
element_children_to_tokens(
|
||||
@@ -348,6 +352,7 @@ pub(crate) fn element_to_tokens(
|
||||
#name
|
||||
#children
|
||||
#attributes
|
||||
#global_class_expr
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -373,8 +378,7 @@ fn is_spread_marker(node: &NodeElement) -> bool {
|
||||
fn attribute_to_tokens(
|
||||
tag_type: TagType,
|
||||
node: &NodeAttribute,
|
||||
// TODO global_class support
|
||||
_global_class: Option<&TokenTree>,
|
||||
global_class: Option<&TokenTree>,
|
||||
is_custom: bool,
|
||||
) -> TokenStream {
|
||||
match node {
|
||||
@@ -454,7 +458,7 @@ fn attribute_to_tokens(
|
||||
(name.contains('-') && !name.starts_with("aria-"))
|
||||
// TODO check: do we actually provide SVG attributes?
|
||||
// we don't provide statically-checked methods for SVG attributes
|
||||
|| tag_type == TagType::Svg
|
||||
|| (tag_type == TagType::Svg && name != "inner_html")
|
||||
{
|
||||
let value = attribute_value(node);
|
||||
quote! {
|
||||
@@ -463,6 +467,19 @@ fn attribute_to_tokens(
|
||||
} else {
|
||||
let key = attribute_name(&node.key);
|
||||
let value = attribute_value(node);
|
||||
|
||||
// special case of global_class and class attribute
|
||||
if &node.key.to_string() == "class"
|
||||
&& global_class.is_some()
|
||||
&& node.value().and_then(value_to_string).is_none()
|
||||
{
|
||||
let span = node.key.span();
|
||||
proc_macro_error::emit_error!(span, "Combining a global class (view! { class = ... }) \
|
||||
and a dynamic `class=` attribute on an element causes runtime inconsistencies. You can \
|
||||
toggle individual classes dynamically with the `class:name=value` syntax. \n\nSee this issue \
|
||||
for more information and an example: https://github.com/leptos-rs/leptos/issues/773")
|
||||
};
|
||||
|
||||
quote! {
|
||||
.#key(#value)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use core::num::NonZeroUsize;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
fn Component(
|
||||
|
||||
@@ -1,98 +1,131 @@
|
||||
#[cfg(test)]
|
||||
use cfg_if::cfg_if;
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub mod tests {
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "ssr"))] {
|
||||
use leptos::{server, server_fn::{codec, ServerFn}, ServerFnError};
|
||||
use std::any::TypeId;
|
||||
use leptos::{
|
||||
server,
|
||||
server_fn::{codec, ServerFn, ServerFnError},
|
||||
};
|
||||
use std::any::TypeId;
|
||||
|
||||
#[test]
|
||||
fn server_default() {
|
||||
#[server]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric),
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
|
||||
#[test]
|
||||
fn server_default() {
|
||||
#[server]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<MyServerAction as ServerFn>::PATH
|
||||
.trim_end_matches(char::is_numeric),
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_full_legacy() {
|
||||
#[server(FooBar, "/foo/bar", "Cbor", "my_path")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::Cbor>());
|
||||
#[test]
|
||||
fn server_full_legacy() {
|
||||
#[server(FooBar, "/foo/bar", "Cbor", "my_path")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_all_keywords() {
|
||||
#[server(endpoint = "my_path", encoding = "Cbor", prefix = "/foo/bar", name = FooBar)]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::Cbor>());
|
||||
#[test]
|
||||
fn server_all_keywords() {
|
||||
#[server(endpoint = "my_path", encoding = "Cbor", prefix = "/foo/bar", name = FooBar)]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::Cbor>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_mix() {
|
||||
#[server(FooBar, endpoint = "my_path")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
|
||||
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
|
||||
#[test]
|
||||
fn server_mix() {
|
||||
#[server(FooBar, endpoint = "my_path")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_name() {
|
||||
#[server(name = FooBar)]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<FooBar as ServerFn>::PATH.trim_end_matches(char::is_numeric),
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(TypeId::of::<<FooBar as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
|
||||
#[test]
|
||||
fn server_name() {
|
||||
#[server(name = FooBar)]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<FooBar as ServerFn>::PATH.trim_end_matches(char::is_numeric),
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_prefix() {
|
||||
#[server(prefix = "/foo/bar")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric), "/foo/bar/my_server_action");
|
||||
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
|
||||
#[test]
|
||||
fn server_prefix() {
|
||||
#[server(prefix = "/foo/bar")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<MyServerAction as ServerFn>::PATH
|
||||
.trim_end_matches(char::is_numeric),
|
||||
"/foo/bar/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_encoding() {
|
||||
#[server(encoding = "GetJson")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<MyServerAction as ServerFn>::PATH.trim_end_matches(char::is_numeric),
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::GetUrl>());
|
||||
#[test]
|
||||
fn server_encoding() {
|
||||
#[server(encoding = "GetJson")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<MyServerAction as ServerFn>::PATH
|
||||
.trim_end_matches(char::is_numeric),
|
||||
"/api/my_server_action"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::GetUrl>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_endpoint() {
|
||||
#[server(endpoint = "/path/to/my/endpoint")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(<MyServerAction as ServerFn>::PATH, "/api/path/to/my/endpoint");
|
||||
assert_eq!(TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(), TypeId::of::<codec::PostUrl>());
|
||||
#[test]
|
||||
fn server_endpoint() {
|
||||
#[server(endpoint = "/path/to/my/endpoint")]
|
||||
pub async fn my_server_action() -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(
|
||||
<MyServerAction as ServerFn>::PATH,
|
||||
"/api/path/to/my/endpoint"
|
||||
);
|
||||
assert_eq!(
|
||||
TypeId::of::<<MyServerAction as ServerFn>::InputEncoding>(),
|
||||
TypeId::of::<codec::PostUrl>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::{create_runtime, create_rw_signal};
|
||||
use leptos::prelude::RwSignal;
|
||||
use leptos_macro::slice;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -18,9 +18,7 @@ pub struct InnerTuple(String);
|
||||
|
||||
#[test]
|
||||
fn green() {
|
||||
let _ = create_runtime();
|
||||
|
||||
let outer_signal = create_rw_signal(OuterState::default());
|
||||
let outer_signal = RwSignal::new(OuterState::default());
|
||||
|
||||
let (_, _) = slice!(outer_signal.count);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use leptos::{create_runtime, create_rw_signal};
|
||||
use leptos_macro::slice;
|
||||
use leptos::prelude::RwSignal;
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
pub struct OuterState {
|
||||
@@ -14,9 +14,7 @@ pub struct InnerState {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = create_runtime();
|
||||
|
||||
let outer_signal = create_rw_signal(OuterState::default());
|
||||
let outer_signal = RwSignal::new(OuterState::default());
|
||||
|
||||
let (_, _) = slice!();
|
||||
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
error: unexpected end of input, expected identifier
|
||||
--> tests/slice/red.rs:21:18
|
||||
--> tests/slice/red.rs:19:18
|
||||
|
|
||||
21 | let (_, _) = slice!();
|
||||
19 | let (_, _) = slice!();
|
||||
| ^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error: expected `.`
|
||||
--> tests/slice/red.rs:21:18
|
||||
|
|
||||
21 | let (_, _) = slice!(outer_signal);
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error: unexpected end of input, expected identifier or integer
|
||||
--> tests/slice/red.rs:23:18
|
||||
|
|
||||
23 | let (_, _) = slice!(outer_signal);
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
23 | let (_, _) = slice!(outer_signal.);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error: unexpected end of input, expected identifier or integer
|
||||
--> tests/slice/red.rs:25:18
|
||||
|
|
||||
25 | let (_, _) = slice!(outer_signal.);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error: unexpected end of input, expected identifier or integer
|
||||
--> tests/slice/red.rs:27:18
|
||||
|
|
||||
27 | let (_, _) = slice!(outer_signal.inner.);
|
||||
25 | let (_, _) = slice!(outer_signal.inner.);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the macro `slice` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
fn missing_scope() {}
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
error: return type is incorrect
|
||||
--> tests/ui/component.rs:4:1
|
||||
|
|
||||
4 | fn missing_scope() {}
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: return signature must be `-> impl IntoView`
|
||||
|
||||
error: return type is incorrect
|
||||
--> tests/ui/component.rs:7:1
|
||||
|
|
||||
7 | fn missing_return_type() {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: return signature must be `-> impl IntoView`
|
||||
|
||||
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
|
||||
--> tests/ui/component.rs:10:31
|
||||
|
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
error: return type is incorrect
|
||||
--> tests/ui/component_absolute.rs:2:1
|
||||
|
|
||||
2 | fn missing_return_type() {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: return signature must be `-> impl IntoView`
|
||||
|
||||
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default`, `into` and `attrs`
|
||||
--> tests/ui/component_absolute.rs:5:31
|
||||
|
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[server(endpoint = "my_path", FooBar)]
|
||||
pub async fn positional_argument_follows_keyword_argument() -> Result<(), ServerFnError> {
|
||||
pub async fn positional_argument_follows_keyword_argument(
|
||||
) -> Result<(), ServerFnError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,43 +5,43 @@ error: positional argument follows keyword argument
|
||||
| ^^^^^^
|
||||
|
||||
error: keyword argument repeated: `endpoint`
|
||||
--> tests/ui/server.rs:8:30
|
||||
--> tests/ui/server.rs:9:30
|
||||
|
|
||||
8 | #[server(endpoint = "first", endpoint = "second")]
|
||||
9 | #[server(endpoint = "first", endpoint = "second")]
|
||||
| ^^^^^^^^
|
||||
|
||||
error: expected string literal
|
||||
--> tests/ui/server.rs:13:15
|
||||
--> tests/ui/server.rs:14:15
|
||||
|
|
||||
13 | #[server(Foo, Bar)]
|
||||
14 | #[server(Foo, Bar)]
|
||||
| ^^^
|
||||
|
||||
error: expected string literal
|
||||
--> tests/ui/server.rs:17:15
|
||||
--> tests/ui/server.rs:18:15
|
||||
|
|
||||
17 | #[server(Foo, Bar, bazz)]
|
||||
18 | #[server(Foo, Bar, bazz)]
|
||||
| ^^^
|
||||
|
||||
error: expected identifier
|
||||
--> tests/ui/server.rs:22:10
|
||||
--> tests/ui/server.rs:23:10
|
||||
|
|
||||
22 | #[server("Foo")]
|
||||
23 | #[server("Foo")]
|
||||
| ^^^^^
|
||||
|
||||
error: expected `,`
|
||||
--> tests/ui/server.rs:27:14
|
||||
--> tests/ui/server.rs:28:14
|
||||
|
|
||||
27 | #[server(Foo Bar)]
|
||||
28 | #[server(Foo Bar)]
|
||||
| ^^^
|
||||
|
||||
error: unexpected extra argument
|
||||
--> tests/ui/server.rs:32:49
|
||||
--> tests/ui/server.rs:33:49
|
||||
|
|
||||
32 | #[server(FooBar, "/foo/bar", "Cbor", "my_path", "extra")]
|
||||
33 | #[server(FooBar, "/foo/bar", "Cbor", "my_path", "extra")]
|
||||
| ^^^^^^^
|
||||
|
||||
error: Encoding not found.
|
||||
--> tests/ui/server.rs:37:21
|
||||
--> tests/ui/server.rs:38:21
|
||||
|
|
||||
37 | #[server(encoding = "wrong")]
|
||||
38 | #[server(encoding = "wrong")]
|
||||
| ^^^^^^^
|
||||
|
||||
@@ -18,6 +18,7 @@ server_fn = { workspace = true }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
futures = "0.3"
|
||||
|
||||
any_spawner = { workspace = true }
|
||||
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
|
||||
|
||||
# serialization formats
|
||||
|
||||
@@ -43,7 +43,7 @@ mod shared;
|
||||
////! crate that is enabled).
|
||||
////!
|
||||
////! ```rust,ignore
|
||||
////! use leptos::*;
|
||||
////! use leptos::prelude::*;
|
||||
////! #[server(ReadFromDB)]
|
||||
////! async fn read_posts(how_many: usize, query: String) -> Result<Vec<Posts>, ServerFnError> {
|
||||
////! // do some server-only work here to access the database
|
||||
@@ -118,7 +118,7 @@ mod shared;
|
||||
////! HTML forms don’t support `PUT` or `DELETE`, and they don’t support sending JSON. This means that if you use anything
|
||||
////! but a `GET` or `POST` request with URL-encoded data, it can only work once WASM has loaded.
|
||||
////!
|
||||
////! The CBOR encoding is suported for historical reasons; an earlier version of server functions used a URL encoding that
|
||||
////! The CBOR encoding is supported for historical reasons; an earlier version of server functions used a URL encoding that
|
||||
////! didn’t support nested objects like structs or vectors as server function arguments, which CBOR did. But note that the
|
||||
////! CBOR forms encounter the same issue as `PUT`, `DELETE`, or JSON: they do not degrade gracefully if the WASM version of
|
||||
////! your app is not available.
|
||||
|
||||
@@ -48,6 +48,12 @@ impl<T> ArcLocalResource<T> {
|
||||
if cfg!(feature = "ssr") {
|
||||
pending().await
|
||||
} else {
|
||||
// LocalResources that are immediately available can cause a hydration error,
|
||||
// because the future *looks* like it is alredy ready (and therefore would
|
||||
// already have been rendered to html on the server), but in fact was ignored
|
||||
// on the server. the simplest way to avoid this is to ensure that we always
|
||||
// wait a tick before resolving any value for a localresource.
|
||||
any_spawner::Executor::tick().await;
|
||||
fut.await
|
||||
}
|
||||
}
|
||||
@@ -198,12 +204,23 @@ impl<T> LocalResource<T> {
|
||||
if cfg!(feature = "ssr") {
|
||||
pending().await
|
||||
} else {
|
||||
// LocalResources that are immediately available can cause a hydration error,
|
||||
// because the future *looks* like it is alredy ready (and therefore would
|
||||
// already have been rendered to html on the server), but in fact was ignored
|
||||
// on the server. the simplest way to avoid this is to ensure that we always
|
||||
// wait a tick before resolving any value for a localresource.
|
||||
any_spawner::Executor::tick().await;
|
||||
fut.await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
data: AsyncDerived::new_unsync(fetcher),
|
||||
data: if cfg!(feature = "ssr") {
|
||||
AsyncDerived::new_mock_unsync(fetcher)
|
||||
} else {
|
||||
AsyncDerived::new_unsync(fetcher)
|
||||
},
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "leptos_meta"
|
||||
version = "0.7.0-alpha"
|
||||
version = "0.7.0-beta"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -23,7 +23,7 @@ use leptos::{
|
||||
/// following the `{..}` operator.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
|
||||
@@ -23,7 +23,7 @@ use leptos::{
|
||||
/// following the `{..}` operator.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! HTML that should be injected into the `<head>` of the HTML document being rendered.
|
||||
//!
|
||||
//! ```
|
||||
//! use leptos::*;
|
||||
//! use leptos::prelude::*;
|
||||
//! use leptos_meta::*;
|
||||
//!
|
||||
//! #[component]
|
||||
@@ -173,6 +173,7 @@ pub struct ServerMetaContext {
|
||||
/// Attributes for the `<body>` element.
|
||||
pub(crate) body: Sender<String>,
|
||||
/// Arbitrary elements to be added to the `<head>` as HTML.
|
||||
#[allow(unused)] // used in SSR
|
||||
pub(crate) elements: Sender<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use leptos::{
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
|
||||
@@ -11,7 +11,7 @@ use leptos::{
|
||||
/// head to set metadata
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
|
||||
@@ -7,7 +7,7 @@ use leptos::{
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
|
||||
@@ -7,7 +7,7 @@ use leptos::{
|
||||
/// head, accepting any of the valid attributes for that tag.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
|
||||
@@ -8,7 +8,7 @@ use leptos::{
|
||||
/// head that loads a stylesheet from the URL given by the `href` property.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
|
||||
@@ -73,7 +73,7 @@ where
|
||||
/// `<Title formatter=.../>` that will wrap each of the text values of `<Title/>` components created lower in the tree.
|
||||
///
|
||||
/// ```
|
||||
/// use leptos::*;
|
||||
/// use leptos::prelude::*;
|
||||
/// use leptos_meta::*;
|
||||
///
|
||||
/// #[component]
|
||||
@@ -181,20 +181,24 @@ impl TitleView {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // TODO these should be used to rebuild the attributes, I guess
|
||||
struct TitleViewState {
|
||||
el: HtmlTitleElement,
|
||||
formatter: Option<Formatter>,
|
||||
text: Option<TextProp>,
|
||||
// effect is stored in the view state to keep it alive until rebuild
|
||||
#[allow(dead_code)]
|
||||
effect: RenderEffect<Oco<'static, str>>,
|
||||
}
|
||||
|
||||
impl Render<Dom> for TitleView {
|
||||
type State = TitleViewState;
|
||||
|
||||
fn build(self) -> Self::State {
|
||||
fn build(mut self) -> Self::State {
|
||||
let el = self.el();
|
||||
let meta = self.meta;
|
||||
if let Some(formatter) = self.formatter.take() {
|
||||
*meta.title.formatter.write().or_poisoned() = Some(formatter);
|
||||
}
|
||||
if let Some(text) = self.text.take() {
|
||||
*meta.title.text.write().or_poisoned() = Some(text);
|
||||
}
|
||||
let effect = RenderEffect::new({
|
||||
let el = el.clone();
|
||||
move |prev| {
|
||||
@@ -207,16 +211,11 @@ impl Render<Dom> for TitleView {
|
||||
text
|
||||
}
|
||||
});
|
||||
TitleViewState {
|
||||
el,
|
||||
formatter: self.formatter,
|
||||
text: self.text,
|
||||
effect,
|
||||
}
|
||||
TitleViewState { effect }
|
||||
}
|
||||
|
||||
fn rebuild(self, _state: &mut Self::State) {
|
||||
// TODO should this rebuild?
|
||||
fn rebuild(self, state: &mut Self::State) {
|
||||
*state = self.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,12 +256,18 @@ impl RenderHtml<Dom> for TitleView {
|
||||
}
|
||||
|
||||
fn hydrate<const FROM_SERVER: bool>(
|
||||
self,
|
||||
mut self,
|
||||
_cursor: &Cursor<Dom>,
|
||||
_position: &PositionState,
|
||||
) -> Self::State {
|
||||
let el = self.el();
|
||||
let meta = self.meta;
|
||||
if let Some(formatter) = self.formatter.take() {
|
||||
*meta.title.formatter.write().or_poisoned() = Some(formatter);
|
||||
}
|
||||
if let Some(text) = self.text.take() {
|
||||
*meta.title.text.write().or_poisoned() = Some(text);
|
||||
}
|
||||
let effect = RenderEffect::new({
|
||||
let el = el.clone();
|
||||
move |prev| {
|
||||
@@ -276,12 +281,7 @@ impl RenderHtml<Dom> for TitleView {
|
||||
text
|
||||
}
|
||||
});
|
||||
TitleViewState {
|
||||
el,
|
||||
formatter: self.formatter,
|
||||
text: self.text,
|
||||
effect,
|
||||
}
|
||||
TitleViewState { effect }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "next_tuple"
|
||||
version = "0.1.0-alpha"
|
||||
version = "0.1.0-beta"
|
||||
edition = "2021"
|
||||
authors = ["Greg Johnston"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -8,9 +8,9 @@ codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
leptos = { version = "0.6.12", features = ["csr"] }
|
||||
leptos_meta = { version = "0.6.12", features = ["csr"] }
|
||||
leptos_router = { version = "0.6.12", features = ["csr"] }
|
||||
leptos = { version = "0.6.13", features = ["csr"] }
|
||||
leptos_meta = { version = "0.6.13", features = ["csr"] }
|
||||
leptos_router = { version = "0.6.13", features = ["csr"] }
|
||||
console_log = "1"
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -14,7 +14,7 @@ leptos_router = { path = "../../../router", features = ["csr"] }
|
||||
log = "0.4"
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1"
|
||||
gloo-net = "0.5"
|
||||
gloo-net = "0.6"
|
||||
gloo-storage = "0.3"
|
||||
serde = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn make_openapi_call_via_gpt(message:String) -> ChatCompletionParameters {
|
||||
// This name will be given to the OpenAI API as part of our functions
|
||||
let name = operation.operation_id.clone().expect("Each operation to have an operation id");
|
||||
|
||||
// we'll use the descrition
|
||||
// we'll use the description
|
||||
let desc = operation.description.clone().expect("Each operation to have a description, this is how GPT knows what the functiond does and it is helpful for calling it.");
|
||||
let mut required_list = vec![];
|
||||
let mut properties = serde_json::Map::new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["app", "frontend", "ids", "server","e2e"]
|
||||
members = ["app", "frontend", "ids", "server", "e2e"]
|
||||
|
||||
# need to be applied only to wasm build
|
||||
[profile.release]
|
||||
@@ -13,34 +13,38 @@ leptos = { version = "0.6.9", features = ["nightly"] }
|
||||
leptos_meta = { version = "0.6.9", features = ["nightly"] }
|
||||
leptos_router = { version = "0.6.9", features = ["nightly"] }
|
||||
leptos_axum = { version = "0.6.9" }
|
||||
leptos-use = {version = "0.10.5"}
|
||||
leptos-use = { version = "0.10.5" }
|
||||
|
||||
axum = "0.7"
|
||||
axum-server = {version = "0.6", features = ["tls-rustls"]}
|
||||
axum-extra = { version = "0.9.2", features=["cookie"]}
|
||||
axum-server = { version = "0.6", features = ["tls-rustls"] }
|
||||
axum-extra = { version = "0.9.2", features = ["cookie"] }
|
||||
cfg-if = "1"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
console_log = "1"
|
||||
http = "1"
|
||||
ids = {path="./ids"}
|
||||
ids = { path = "./ids" }
|
||||
# this goes to this personal branch because of https://github.com/ory/sdk/issues/325#issuecomment-1960834676
|
||||
ory-kratos-client = {git="https://github.com/sjud/kratos-client-rust"}
|
||||
ory-keto-client = {version = "0.11.0-alpha.0"}
|
||||
reqwest = { version = "0.11.24", features = ["json","cookies"] }
|
||||
ory-kratos-client = { git = "https://github.com/sjud/kratos-client-rust" }
|
||||
ory-keto-client = { version = "0.11.0-alpha.0" }
|
||||
reqwest = { version = "0.12", features = ["json", "cookies"] }
|
||||
serde = "1.0.197"
|
||||
serde_json = "1.0.114"
|
||||
sqlx = {version= "0.7.3", features=["runtime-tokio","sqlite","macros"]}
|
||||
sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite", "macros"] }
|
||||
thiserror = "1"
|
||||
time = "0.3.34"
|
||||
tokio = { version = "1.33.0", features = ["full"] }
|
||||
tower = { version = "0.4.13", features = ["full"] }
|
||||
tower-http = { version = "0.5", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = {version="0.3.18", features=["env-filter"]}
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
url = "2.5.0"
|
||||
uuid = {version = "1.7.0", features=["v4","serde"]}
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||
wasm-bindgen = "0.2.92"
|
||||
web-sys = {version = "0.3.69", features=["HtmlDocument","HtmlFormElement","FormData"]}
|
||||
web-sys = { version = "0.3.69", features = [
|
||||
"HtmlDocument",
|
||||
"HtmlFormElement",
|
||||
"FormData",
|
||||
] }
|
||||
|
||||
|
||||
# See https://github.com/akesson/cargo-leptos for documentation of all the parameters.
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn kratos_html(node: UiNode, body: RwSignal<HashMap<String, String>>) -> imp
|
||||
body.update(|map| {
|
||||
_ = map.insert(name.clone(), value.clone());
|
||||
});
|
||||
// this expects the identifer to be an email, but it could be telelphone etc so code is extra fragile
|
||||
// this expects the identifier to be an email, but it could be telephone etc so code is extra fragile
|
||||
view! {<input type="hidden" value=value name=name /> }.into_view()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,21 @@ edition = "2021"
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.72"
|
||||
async-trait = "0.1.72"
|
||||
cucumber = {version="0.20.2",features=["tracing","macros"]}
|
||||
cucumber = { version = "0.20.2", features = ["tracing", "macros"] }
|
||||
pretty_assertions = "1.4.0"
|
||||
serde_json = "1.0.104"
|
||||
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
url = "2.4.0"
|
||||
reqwest = "0.11.25"
|
||||
reqwest = "0.12"
|
||||
tracing = "0.1.40"
|
||||
chromiumoxide = {version = "0.5.7", default-features = false, features=["tokio-runtime"]}
|
||||
chromiumoxide = { version = "0.5.7", default-features = false, features = [
|
||||
"tokio-runtime",
|
||||
] }
|
||||
ids.workspace = true
|
||||
fake = "2.9.2"
|
||||
tokio-tungstenite = "0.21.0"
|
||||
futures-util = "0.3.30"
|
||||
uuid = {version="1.7.0",features=["serde"]}
|
||||
uuid = { version = "1.7.0", features = ["serde"] }
|
||||
once_cell = "1.19.0"
|
||||
futures = "0.3.30"
|
||||
|
||||
@@ -28,13 +30,9 @@ harness = false # Allow Cucumber to print output instead of libtest
|
||||
|
||||
[features]
|
||||
#vscode thing to get autocomplete
|
||||
ssr=[]
|
||||
ssr = []
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.19.0"
|
||||
regex = "1.10.3"
|
||||
serde.workspace = true
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ pub static LOGOUT_BUTTON_ID: &'static str = "logout_button_id";
|
||||
pub static LOGIN_BUTTON_ID: &'static str = "login_button_id";
|
||||
/// This function is for use in kratos_html, it takes the name of the input node and it
|
||||
/// matches it according to what we've specified in the kratos schema file. If we change the schema.
|
||||
/// I.e use a phone instead of an email, the identifer id will change and break tests that expect an email.
|
||||
/// I.e use a phone instead of an email, the identifier id will change and break tests that expect an email.
|
||||
/// i.e use oidc instead of password, as auth method... that will break tests too.
|
||||
/// Which is good.
|
||||
pub fn match_name_to_id(name: String) -> &'static str {
|
||||
|
||||
@@ -27,39 +27,39 @@ tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "1" }
|
||||
sqlx = { version = "0.7", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
thiserror = "1.0.38"
|
||||
wasm-bindgen = "0.2"
|
||||
axum_session_auth = { version = "0.12", features = [
|
||||
"sqlite-rustls",
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
axum_session = { version = "0.12", features = [
|
||||
"sqlite-rustls",
|
||||
"sqlite-rustls",
|
||||
], optional = true }
|
||||
async-trait = { version = "0.1.64", optional = true }
|
||||
reqwest = { version = "0.11", optional = true, features = ["json"] }
|
||||
reqwest = { version = "0.12", optional = true, features = ["json"] }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:serde_json",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:reqwest",
|
||||
"dep:oauth2",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session",
|
||||
"dep:async-trait",
|
||||
"dep:sqlx",
|
||||
"dep:rand",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
"dep:serde_json",
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:reqwest",
|
||||
"dep:oauth2",
|
||||
"dep:axum_session_auth",
|
||||
"dep:axum_session",
|
||||
"dep:async-trait",
|
||||
"dep:sqlx",
|
||||
"dep:rand",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user