Compare commits
992 Commits
web-transp
...
develop
Author | SHA1 | Date |
---|---|---|
![]() |
7830ea7bae | |
![]() |
9b76b0ae44 | |
![]() |
f1fa1cba71 | |
![]() |
3afa2c098e | |
![]() |
47a095f04a | |
![]() |
3fe8157a09 | |
![]() |
104e77e1e8 | |
![]() |
6be5ad2fbc | |
![]() |
10b29846ae | |
![]() |
6b2d488ae6 | |
![]() |
83c120a3b1 | |
![]() |
d81ea1aef9 | |
![]() |
3c5974c2e1 | |
![]() |
b758fb3e06 | |
![]() |
a705eb7044 | |
![]() |
a4145207f0 | |
![]() |
439c71341b | |
![]() |
c4a99869cb | |
![]() |
aa138d5a19 | |
![]() |
3869f09f5a | |
![]() |
686d12ad2a | |
![]() |
5a894a0139 | |
![]() |
7b706e126b | |
![]() |
16fa3cdd90 | |
![]() |
8b84986410 | |
![]() |
3b18b23a1f | |
![]() |
c780ded239 | |
![]() |
72a3e28fea | |
![]() |
23da089235 | |
![]() |
ffc98aa905 | |
![]() |
44bd907f44 | |
![]() |
83b4af8299 | |
![]() |
7f7b555998 | |
![]() |
3379e822fb | |
![]() |
f1e0f80922 | |
![]() |
ccc7ea427a | |
![]() |
9c36bae809 | |
![]() |
5f10252adb | |
![]() |
aed0b3a228 | |
![]() |
5ba9759fba | |
![]() |
05c751770b | |
![]() |
024111f67e | |
![]() |
4cc46524d8 | |
![]() |
e422bbdecc | |
![]() |
00d4e87a01 | |
![]() |
f754ada278 | |
![]() |
48e7a124a4 | |
![]() |
dba88cb2fb | |
![]() |
3c34e03b3d | |
![]() |
41f87ae7b2 | |
![]() |
67ed84ca2a | |
![]() |
b676dea6eb | |
![]() |
a18dbfc539 | |
![]() |
d7fba2787e | |
![]() |
e28bb58eee | |
![]() |
09bd26ccd2 | |
![]() |
ea74aa41a5 | |
![]() |
2534471565 | |
![]() |
433bb6e5ab | |
![]() |
c6b1eaca30 | |
![]() |
e991742383 | |
![]() |
90b8b1cf35 | |
![]() |
d8dacf01d7 | |
![]() |
59c5221b7a | |
![]() |
db376dfbe4 | |
![]() |
55a3f01318 | |
![]() |
a98f3b78ad | |
![]() |
07fd37c9e5 | |
![]() |
6311779c40 | |
![]() |
56011be1ce | |
![]() |
08b5ac5e3a | |
![]() |
5a3ee74bf5 | |
![]() |
666a45827e | |
![]() |
1bd119caf6 | |
![]() |
c35b85ce4e | |
![]() |
39cb56db3e | |
![]() |
9fbd96caed | |
![]() |
0ada59177b | |
![]() |
1cb3d0118a | |
![]() |
3a561c2faa | |
![]() |
92a7a92939 | |
![]() |
ab1d5f11b1 | |
![]() |
d898f92cb9 | |
![]() |
2d1cf0ce54 | |
![]() |
0201fc6a2e | |
![]() |
70376416fc | |
![]() |
be186f3849 | |
![]() |
f17c99d50f | |
![]() |
1c1e52ccce | |
![]() |
d3aac79e98 | |
![]() |
986ed894c1 | |
![]() |
65ea56a2e2 | |
![]() |
100e4ac64c | |
![]() |
bf1a64d697 | |
![]() |
bceefa2122 | |
![]() |
685c033cf4 | |
![]() |
eb825e2b7d | |
![]() |
4322b99559 | |
![]() |
4c0bf6dfbd | |
![]() |
750883a2d1 | |
![]() |
94a691e059 | |
![]() |
bebd836f45 | |
![]() |
d60f326230 | |
![]() |
7d20db71f6 | |
![]() |
42acbff4a2 | |
![]() |
182610e046 | |
![]() |
72e1c2c07e | |
![]() |
69bf92b634 | |
![]() |
256dad1ba6 | |
![]() |
750cd44740 | |
![]() |
2925230e34 | |
![]() |
d8b22b58fa | |
![]() |
3d16cabd5a | |
![]() |
45db7d9d29 | |
![]() |
978598f52d | |
![]() |
8f1a8d6b75 | |
![]() |
0d07c77ef4 | |
![]() |
42a2384666 | |
![]() |
edc0b5b8d3 | |
![]() |
641774ea66 | |
![]() |
e40db72ed5 | |
![]() |
a7df17505f | |
![]() |
cab1456bd1 | |
![]() |
8685255430 | |
![]() |
14dab8a652 | |
![]() |
e44f446eea | |
![]() |
ff56adcc40 | |
![]() |
57a0f468ac | |
![]() |
116a1362f9 | |
![]() |
a454c450c6 | |
![]() |
73d86f27b7 | |
![]() |
c6567508e3 | |
![]() |
87b200f764 | |
![]() |
624d24b2f6 | |
![]() |
3cf9802bae | |
![]() |
1e5c238690 | |
![]() |
41ee5b7de5 | |
![]() |
2c0cc510e1 | |
![]() |
9e12b9076c | |
![]() |
391de46e08 | |
![]() |
a4c2e87473 | |
![]() |
2db7468cd5 | |
![]() |
32d4f920f6 | |
![]() |
0942459599 | |
![]() |
17f22018ba | |
![]() |
0c085c320d | |
![]() |
e77a68f6a6 | |
![]() |
657c1c8492 | |
![]() |
440ac29fb2 | |
![]() |
4f84035efc | |
![]() |
b7caf5e41c | |
![]() |
1e7f0dd8d2 | |
![]() |
8f17404aae | |
![]() |
2012286f8f | |
![]() |
821bdfd7ee | |
![]() |
bc395f4932 | |
![]() |
d245ab10a5 | |
![]() |
7a24033982 | |
![]() |
7ee8d30e11 | |
![]() |
3bc1f28019 | |
![]() |
b85a6b1241 | |
![]() |
c86c69d072 | |
![]() |
a9c8e6ed11 | |
![]() |
03d36e849c | |
![]() |
de567de237 | |
![]() |
f500e93183 | |
![]() |
d20737a75c | |
![]() |
9cda19db84 | |
![]() |
47afc9e01e | |
![]() |
3cd4a5d079 | |
![]() |
23134b4b8e | |
![]() |
0d544431ed | |
![]() |
fbea7b1d71 | |
![]() |
e90319dad3 | |
![]() |
5e06342a3a | |
![]() |
a44f86d051 | |
![]() |
1e9c7a820b | |
![]() |
2a16577829 | |
![]() |
2f8cfea9ba | |
![]() |
ee1b2e460d | |
![]() |
e822f2c416 | |
![]() |
8b1c46bd7b | |
![]() |
f36327c36d | |
![]() |
8b4e750986 | |
![]() |
6b2130b1bf | |
![]() |
18a69f44ce | |
![]() |
54dab9a2a7 | |
![]() |
6e677ad352 | |
![]() |
faaf52f5ee | |
![]() |
b9dc373e63 | |
![]() |
f87279552a | |
![]() |
2fd150f56c | |
![]() |
752b91d9b9 | |
![]() |
e4ad61c5c5 | |
![]() |
defedfa44d | |
![]() |
5c890ecf09 | |
![]() |
7c696b15a3 | |
![]() |
0e3a4c980b | |
![]() |
5ea8fd7fc4 | |
![]() |
781ea7764b | |
![]() |
14b2dd351a | |
![]() |
dcf1d00810 | |
![]() |
2b5e59c418 | |
![]() |
a685ec5d5b | |
![]() |
8a9a7f2630 | |
![]() |
f29f3fb98c | |
![]() |
b3a5dbbfdb | |
![]() |
a76d3ecb2d | |
![]() |
fa679aa40c | |
![]() |
e89639050d | |
![]() |
cc75d774e9 | |
![]() |
86e07b3a6b | |
![]() |
1acef4efc3 | |
![]() |
ad47bdfb85 | |
![]() |
ea71691061 | |
![]() |
0480644851 | |
![]() |
dc3c406346 | |
![]() |
c8a7e58b50 | |
![]() |
31b0c51a76 | |
![]() |
afa40c83aa | |
![]() |
2e9570f0ae | |
![]() |
0a00bed483 | |
![]() |
bbdb8d6652 | |
![]() |
4042dc0e25 | |
![]() |
860aa9e46d | |
![]() |
c89da30a6d | |
![]() |
1004107c93 | |
![]() |
9945394a79 | |
![]() |
8a0fedf30b | |
![]() |
ed9f5748ac | |
![]() |
65b017d65f | |
![]() |
c90c09e741 | |
![]() |
ef05be33a8 | |
![]() |
6d16a23999 | |
![]() |
16714ed0ad | |
![]() |
91f5f07413 | |
![]() |
305b8eca89 | |
![]() |
4fb2aa5b6a | |
![]() |
bd17ab4809 | |
![]() |
01702f0fde | |
![]() |
18ea30972e | |
![]() |
467aeb4312 | |
![]() |
b874aa16e2 | |
![]() |
e1fb273cd8 | |
![]() |
7314357d2e | |
![]() |
39a4e1553c | |
![]() |
229183f136 | |
![]() |
3e5f1aaa09 | |
![]() |
0d08df81ec | |
![]() |
ead432fe04 | |
![]() |
b6319e9b1d | |
![]() |
c0adef6eea | |
![]() |
84a31ca6a0 | |
![]() |
d97cd5f40a | |
![]() |
4aafd4262f | |
![]() |
d45517ffbb | |
![]() |
32a14d985a | |
![]() |
5d46096dc2 | |
![]() |
ca5414c358 | |
![]() |
5a4b4d699e | |
![]() |
bc21b05164 | |
![]() |
d3b50bc3a9 | |
![]() |
7a994bdff3 | |
![]() |
1d74d2328a | |
![]() |
552c55939d | |
![]() |
437f8bcf53 | |
![]() |
0c33439fa6 | |
![]() |
b4a11683fb | |
![]() |
71b95c8949 | |
![]() |
0975e24224 | |
![]() |
b00c6d160e | |
![]() |
65a301da9e | |
![]() |
bca69f6a57 | |
![]() |
ff52c94fb5 | |
![]() |
41198f3479 | |
![]() |
77700d2e2f | |
![]() |
9b61ee4f67 | |
![]() |
284c42d981 | |
![]() |
6b726a340e | |
![]() |
9901ae7f4d | |
![]() |
fe409baf4c | |
![]() |
cbbb640533 | |
![]() |
97c0760a48 | |
![]() |
1d8105af11 | |
![]() |
38c0547dfa | |
![]() |
93852fc1ad | |
![]() |
539152a127 | |
![]() |
ed980b3d4c | |
![]() |
854ce6ed30 | |
![]() |
16161d8309 | |
![]() |
652fd1e130 | |
![]() |
2ee3ad21a4 | |
![]() |
9b2d620499 | |
![]() |
dbff015dc9 | |
![]() |
fb8325c583 | |
![]() |
3f95087783 | |
![]() |
eb8b3b1c5b | |
![]() |
c25cbec470 | |
![]() |
701370246f | |
![]() |
d7b527f39e | |
![]() |
51671c666d | |
![]() |
a5c2e8624b | |
![]() |
31d2080741 | |
![]() |
93e8e1405c | |
![]() |
e5ccf1242d | |
![]() |
34556125ae | |
![]() |
62fe77741b | |
![]() |
65a84c6602 | |
![]() |
2234f3926f | |
![]() |
d1fe079520 | |
![]() |
805993a9b7 | |
![]() |
ba6fba2b25 | |
![]() |
0e241426e9 | |
![]() |
1e55dcf6d5 | |
![]() |
f333707293 | |
![]() |
50472e2782 | |
![]() |
ca6c591aef | |
![]() |
39a93a4d50 | |
![]() |
afa6e930fa | |
![]() |
c8e77a619d | |
![]() |
d4f11e77aa | |
![]() |
a9cfaf8a78 | |
![]() |
c40f43bc9a | |
![]() |
a2e4181450 | |
![]() |
7385cb0abf | |
![]() |
490a26fa78 | |
![]() |
a1fcf54752 | |
![]() |
820f1795fa | |
![]() |
4708c24da0 | |
![]() |
ea1c1326fe | |
![]() |
433c2e0cac | |
![]() |
929c32af4e | |
![]() |
420559dd66 | |
![]() |
78f1adeab9 | |
![]() |
ee5150fa4e | |
![]() |
5244040041 | |
![]() |
9b5a788ada | |
![]() |
7036bffb0f | |
![]() |
65beaaab41 | |
![]() |
4bafc8b250 | |
![]() |
3850a0cb84 | |
![]() |
1177a2d3ab | |
![]() |
44d758a1ca | |
![]() |
46773b5602 | |
![]() |
cf7807b39b | |
![]() |
081178321f | |
![]() |
43449e1e3a | |
![]() |
c825bacb3c | |
![]() |
b51853d464 | |
![]() |
efd38eaf8d | |
![]() |
f082567d8e | |
![]() |
2b767131cf | |
![]() |
29e7f9fcab | |
![]() |
b91d183b8c | |
![]() |
72375ae91c | |
![]() |
30743468dd | |
![]() |
f57703254d | |
![]() |
451b62ef1b | |
![]() |
937c7ccec4 | |
![]() |
0fa34d99ce | |
![]() |
c78114b960 | |
![]() |
5992480a97 | |
![]() |
55ec85bc9e | |
![]() |
3992f254d7 | |
![]() |
1ac2d8ac79 | |
![]() |
44004a89e5 | |
![]() |
0f2d428cbc | |
![]() |
07b01321f7 | |
![]() |
fdb41f3a0d | |
![]() |
ceabb783e6 | |
![]() |
9244e530ae | |
![]() |
326f039e03 | |
![]() |
3a65035b0a | |
![]() |
c4b2c2364b | |
![]() |
7c8e2551b4 | |
![]() |
2e2ef7ddec | |
![]() |
91483ab6ff | |
![]() |
e99c9e0654 | |
![]() |
aa45410ed1 | |
![]() |
a79fb963b3 | |
![]() |
b218e2115d | |
![]() |
8b43f29224 | |
![]() |
4d6ca33b23 | |
![]() |
e9d0885c6d | |
![]() |
ea19292f95 | |
![]() |
48151b593c | |
![]() |
723fca3395 | |
![]() |
0a67e27077 | |
![]() |
76da175516 | |
![]() |
1ea565a34f | |
![]() |
d1f3e19acb | |
![]() |
7824c07223 | |
![]() |
ffbec7025d | |
![]() |
fa24bd44e4 | |
![]() |
9572b867a3 | |
![]() |
161d9fedc7 | |
![]() |
c36a7227b8 | |
![]() |
6b59265dc1 | |
![]() |
eee4e3c981 | |
![]() |
9f88878f04 | |
![]() |
9a31383e2f | |
![]() |
e149f38be1 | |
![]() |
34d95080f3 | |
![]() |
39849bf23a | |
![]() |
12273efe44 | |
![]() |
ddcc835b8a | |
![]() |
5736b4ae75 | |
![]() |
337b81a8d8 | |
![]() |
e2c24d20c3 | |
![]() |
ae4b836431 | |
![]() |
1dcbab9558 | |
![]() |
f6bc599b85 | |
![]() |
e43fe54b83 | |
![]() |
11c2c86a30 | |
![]() |
60db7e2959 | |
![]() |
81a9b4cf31 | |
![]() |
753412539d | |
![]() |
953df608fe | |
![]() |
0ed8abf34d | |
![]() |
e1d30aebdb | |
![]() |
13da358729 | |
![]() |
755a5db9b8 | |
![]() |
cf60cab386 | |
![]() |
a706cebb2d | |
![]() |
d851e049eb | |
![]() |
192900c7eb | |
![]() |
beef6b3602 | |
![]() |
3c6c29f3b9 | |
![]() |
c0bdb00f39 | |
![]() |
0875d74810 | |
![]() |
19ad9ceada | |
![]() |
5defce5cc1 | |
![]() |
9db6d03dc8 | |
![]() |
1aeb613970 | |
![]() |
e5c6ec4b3d | |
![]() |
ac9a44b803 | |
![]() |
3cf498e171 | |
![]() |
d12241a812 | |
![]() |
182765419c | |
![]() |
05eaa113cd | |
![]() |
0483fa80c2 | |
![]() |
4f70926469 | |
![]() |
f02da62231 | |
![]() |
4d27ea199b | |
![]() |
891764569c | |
![]() |
6fa8900f68 | |
![]() |
5030f1428e | |
![]() |
f72e78ba6f | |
![]() |
7934512a99 | |
![]() |
ae2f033926 | |
![]() |
dfb650011c | |
![]() |
c47574f70c | |
![]() |
bcf016b538 | |
![]() |
e96e4a216c | |
![]() |
0e6c6baeb6 | |
![]() |
f72d906ba2 | |
![]() |
f8bb417405 | |
![]() |
b74e2bfbb8 | |
![]() |
fdb00bdcb4 | |
![]() |
ba3c10fde7 | |
![]() |
a22dafc057 | |
![]() |
daf00c49c0 | |
![]() |
843e5848ef | |
![]() |
abff1d05de | |
![]() |
ff6cdce6d1 | |
![]() |
ff09c67a0e | |
![]() |
cac77adf27 | |
![]() |
6e11ff8f74 | |
![]() |
05121d96f2 | |
![]() |
1d7a2009fd | |
![]() |
5d1b59a2b4 | |
![]() |
07c21e92ed | |
![]() |
bffbcf47d6 | |
![]() |
9a21366110 | |
![]() |
86b65595e2 | |
![]() |
72fefafe7a | |
![]() |
2243144a52 | |
![]() |
6a999c2d7e | |
![]() |
ae5791fb5f | |
![]() |
b3f9a18a4d | |
![]() |
30e47767a5 | |
![]() |
51c79816dc | |
![]() |
9dbece08f5 | |
![]() |
f7fb1c8af0 | |
![]() |
161c2dbfc9 | |
![]() |
49e7153131 | |
![]() |
945c213320 | |
![]() |
1a0b9d29e5 | |
![]() |
702a81af58 | |
![]() |
1325eec587 | |
![]() |
c8fc7fd238 | |
![]() |
48fce4f01c | |
![]() |
170824e772 | |
![]() |
5d52df0ba2 | |
![]() |
47043504ab | |
![]() |
72b28b2a97 | |
![]() |
519db9a954 | |
![]() |
56ae15a56f | |
![]() |
e446b3eedc | |
![]() |
44f114c53f | |
![]() |
b1de35a4f6 | |
![]() |
d2c56ef730 | |
![]() |
6412661082 | |
![]() |
949e56f3e0 | |
![]() |
6b501be16c | |
![]() |
a7aabb49ff | |
![]() |
de45cdfabf | |
![]() |
66c0aa4fcf | |
![]() |
f1bfa080dc | |
![]() |
b411e9ed72 | |
![]() |
dc41488154 | |
![]() |
2103f602ae | |
![]() |
46960ec505 | |
![]() |
2ad2457c93 | |
![]() |
31e3648cc0 | |
![]() |
685da484ad | |
![]() |
b3750b832a | |
![]() |
1f0a93fe00 | |
![]() |
910c604045 | |
![]() |
c3370186b5 | |
![]() |
761a0fb3d6 | |
![]() |
b4f6372c84 | |
![]() |
82369bc516 | |
![]() |
b42c8c2c88 | |
![]() |
8c5edc7ee7 | |
![]() |
d11108f933 | |
![]() |
67a05b0779 | |
![]() |
56059c5019 | |
![]() |
192c27c59b | |
![]() |
77e71c75f5 | |
![]() |
f8e93f1ac9 | |
![]() |
ba6ef70d40 | |
![]() |
040cfe3059 | |
![]() |
0ea0f3cf37 | |
![]() |
daa5c0496c | |
![]() |
634e28b594 | |
![]() |
fed87f584b | |
![]() |
d341409758 | |
![]() |
c02f056d96 | |
![]() |
d99bdc3c39 | |
![]() |
94e81ec04e | |
![]() |
2f8533cd0b | |
![]() |
7b9e60a834 | |
![]() |
c6dbc96153 | |
![]() |
1f1cd4bd4c | |
![]() |
e594204813 | |
![]() |
12953172ac | |
![]() |
2b64f9da03 | |
![]() |
062753e1f5 | |
![]() |
35aaa72c64 | |
![]() |
bb76da59cc | |
![]() |
494f0d4191 | |
![]() |
636b203455 | |
![]() |
dbfdd77a94 | |
![]() |
a2fe65a9a1 | |
![]() |
9d6443921b | |
![]() |
d4a7d4af44 | |
![]() |
cd9ea6d4ce | |
![]() |
f649ab1bd3 | |
![]() |
2f3bb704ca | |
![]() |
0c7ab4f921 | |
![]() |
dc2768d44f | |
![]() |
9ca42b340e | |
![]() |
4c73f53690 | |
![]() |
9069f510a8 | |
![]() |
dcf1a39092 | |
![]() |
72fbff06ec | |
![]() |
791651073a | |
![]() |
6fca8a7c72 | |
![]() |
dcffccd3ae | |
![]() |
f2db306540 | |
![]() |
40ba9f1268 | |
![]() |
d9a2e6cef0 | |
![]() |
0568bd7214 | |
![]() |
777fb9b41f | |
![]() |
66dd1c1d7b | |
![]() |
c302246cd9 | |
![]() |
76789e8f0d | |
![]() |
e33cd26f45 | |
![]() |
c5ba1a9c8e | |
![]() |
dbdfffc1c1 | |
![]() |
57c7f2d8ac | |
![]() |
fe67174373 | |
![]() |
35a4f55562 | |
![]() |
dc06f31b77 | |
![]() |
2c87ee3bfa | |
![]() |
34a04770be | |
![]() |
2c02121ed4 | |
![]() |
4b7993d409 | |
![]() |
941109e55b | |
![]() |
1c15dc1453 | |
![]() |
940f09b3c4 | |
![]() |
418cc3a80f | |
![]() |
f15bd3b87a | |
![]() |
436e4ddbd8 | |
![]() |
6ccc9e9c94 | |
![]() |
9a0e74112a | |
![]() |
f0327340fa | |
![]() |
1f823de4a1 | |
![]() |
424cfd87ba | |
![]() |
edf9ec6143 | |
![]() |
79041854e0 | |
![]() |
80abc3a667 | |
![]() |
6a591acdcf | |
![]() |
f8780010ae | |
![]() |
2a14ac7ebe | |
![]() |
8b7fe09c1f | |
![]() |
0b4725caec | |
![]() |
b59db9e980 | |
![]() |
ebd3e23575 | |
![]() |
8624f45276 | |
![]() |
e6138a2698 | |
![]() |
6a1c061715 | |
![]() |
e8671d8c88 | |
![]() |
2be4e5ef7b | |
![]() |
20ad2ff9ac | |
![]() |
326445dc43 | |
![]() |
fffdc0fe64 | |
![]() |
0a5e19eaa8 | |
![]() |
35bfc89a42 | |
![]() |
0551551c62 | |
![]() |
c5389419ae | |
![]() |
6f0293dec4 | |
![]() |
0892104623 | |
![]() |
6b8110671a | |
![]() |
9ca90807ad | |
![]() |
b3ec534a5d | |
![]() |
82d669a42e | |
![]() |
e3dd27e10b | |
![]() |
b26e4c1415 | |
![]() |
6172fbd43d | |
![]() |
d6826ca92d | |
![]() |
4c20476b47 | |
![]() |
2fe3713f39 | |
![]() |
255a699f7d | |
![]() |
41fb2df3a8 | |
![]() |
14839abfb5 | |
![]() |
75701f70d6 | |
![]() |
208b94aefb | |
![]() |
afcd51126c | |
![]() |
09a5c9017c | |
![]() |
cbdae3de93 | |
![]() |
7c0c37fb0f | |
![]() |
21749ef0d3 | |
![]() |
1000e2e95a | |
![]() |
80728114b8 | |
![]() |
39268eda93 | |
![]() |
f50a04ae4f | |
![]() |
2759382e7c | |
![]() |
3595708a40 | |
![]() |
26d0e5ca20 | |
![]() |
06fb18fb3a | |
![]() |
b6128b5dde | |
![]() |
57dd46aaad | |
![]() |
eeb0e03d0a | |
![]() |
328577fc2a | |
![]() |
bd34006c42 | |
![]() |
bac182f5d6 | |
![]() |
5d7afc0245 | |
![]() |
fff41075d4 | |
![]() |
5708dfb20f | |
![]() |
a6e9fe6e71 | |
![]() |
0b7fbcf341 | |
![]() |
c104da0371 | |
![]() |
885830304b | |
![]() |
1a129e3951 | |
![]() |
ce895445e0 | |
![]() |
378468ee56 | |
![]() |
abd96a9139 | |
![]() |
48a8ff572f | |
![]() |
8caecab717 | |
![]() |
bb3a0a718a | |
![]() |
2546b3a624 | |
![]() |
78de4e3014 | |
![]() |
887b9f7293 | |
![]() |
ccdd0d79db | |
![]() |
df109f91d2 | |
![]() |
093aeac21a | |
![]() |
ee215e5fc0 | |
![]() |
d5350b0b5b | |
![]() |
fdc73d5d9c | |
![]() |
57be12a943 | |
![]() |
d165916b49 | |
![]() |
c2c7dea7cd | |
![]() |
42a596f753 | |
![]() |
75068e68e4 | |
![]() |
2ff246c7d2 | |
![]() |
5cfec4ea7a | |
![]() |
559436c092 | |
![]() |
d6e744632e | |
![]() |
38d038dd4d | |
![]() |
bdac0f31e8 | |
![]() |
d61381f7c4 | |
![]() |
6899db0c19 | |
![]() |
093b4bdcfa | |
![]() |
e1874e3bfc | |
![]() |
66d44bc7f3 | |
![]() |
23aafbaa56 | |
![]() |
c0d8fdb583 | |
![]() |
0fa2405728 | |
![]() |
3fe8e6ce2e | |
![]() |
1b541e5e35 | |
![]() |
26b947b8d1 | |
![]() |
1b4b228f48 | |
![]() |
ca1e3b5888 | |
![]() |
837832f3d0 | |
![]() |
b3d8e50edc | |
![]() |
f0eea03171 | |
![]() |
63482a9912 | |
![]() |
f3d04d5385 | |
![]() |
37b201622b | |
![]() |
1679ad2d45 | |
![]() |
7934b385bd | |
![]() |
206a3c13f8 | |
![]() |
4219f2ed58 | |
![]() |
030f93048b | |
![]() |
99f8ef2a05 | |
![]() |
1714295db7 | |
![]() |
ba10b96f10 | |
![]() |
a74e754d96 | |
![]() |
12f8b4d5db | |
![]() |
42e061cb1b | |
![]() |
c496647ebe | |
![]() |
3a6eee09a1 | |
![]() |
93679364d0 | |
![]() |
c303ac0da0 | |
![]() |
8380a7224b | |
![]() |
5b14f9f174 | |
![]() |
2cce474a85 | |
![]() |
a69f019731 | |
![]() |
18c9a457e1 | |
![]() |
99cc5c9640 | |
![]() |
17c01af5e2 | |
![]() |
387bbd8c40 | |
![]() |
2d53463b6e | |
![]() |
fee32181cc | |
![]() |
f87ad44151 | |
![]() |
fbc5eeaae3 | |
![]() |
0fa9d93913 | |
![]() |
600fca2ff0 | |
![]() |
a9bc6f5aa0 | |
![]() |
a02c0f15a9 | |
![]() |
303beaf7da | |
![]() |
8ebedce27f | |
![]() |
07d8628f4a | |
![]() |
207f1205b2 | |
![]() |
5283522909 | |
![]() |
e1c3590698 | |
![]() |
636e87fe14 | |
![]() |
836de8b2f3 | |
![]() |
8aac732c28 | |
![]() |
8a91477cae | |
![]() |
1e99ba988a | |
![]() |
acba200de8 | |
![]() |
b0fde462de | |
![]() |
29282a7983 | |
![]() |
4d29fdf78b | |
![]() |
37444e2305 | |
![]() |
91b58933e7 | |
![]() |
b9bee9623e | |
![]() |
4c78e43460 | |
![]() |
95b31b0453 | |
![]() |
5eaac19ec4 | |
![]() |
9af0ffae44 | |
![]() |
8895df60c3 | |
![]() |
cdbbf6af92 | |
![]() |
bca5eba9d7 | |
![]() |
321079e3ae | |
![]() |
35ff45604a | |
![]() |
3a55047a08 | |
![]() |
b481d60313 | |
![]() |
269fdfd131 | |
![]() |
b027422eaf | |
![]() |
3b1a79b82e | |
![]() |
48134fe0ff | |
![]() |
787bb7044f | |
![]() |
afa0ec5bd6 | |
![]() |
00107ab323 | |
![]() |
ddbd0804d1 | |
![]() |
f696d693ac | |
![]() |
9e61275e1c | |
![]() |
e1ecf57198 | |
![]() |
f4d0ecf328 | |
![]() |
299424b0f4 | |
![]() |
7b798fe7c4 | |
![]() |
c5d024bde5 | |
![]() |
f2f8c1271d | |
![]() |
b09cacee33 | |
![]() |
150a961d33 | |
![]() |
5764b53eb9 | |
![]() |
802bf0309f | |
![]() |
345d417041 | |
![]() |
55d68879f5 | |
![]() |
21857de541 | |
![]() |
a19821ff36 | |
![]() |
94b8c6a494 | |
![]() |
41b8d05dff | |
![]() |
af1205a298 | |
![]() |
6b5b3a62a6 | |
![]() |
379730c21a | |
![]() |
e3da825170 | |
![]() |
53431df523 | |
![]() |
df7cd11f13 | |
![]() |
3fea94bbb6 | |
![]() |
b6b0593891 | |
![]() |
909fd4cdaf | |
![]() |
706344fa14 | |
![]() |
a623df49bd | |
![]() |
8ccc8bd33a | |
![]() |
4fc8a74b86 | |
![]() |
c6b711177e | |
![]() |
67ef5a0211 | |
![]() |
78cd6d94df | |
![]() |
5ddb55b0ad | |
![]() |
839446db67 | |
![]() |
559a10bfea | |
![]() |
1e6558168e | |
![]() |
816e3410a1 | |
![]() |
1c4eff6e21 | |
![]() |
d3a939ed8c | |
![]() |
b7baab20b7 | |
![]() |
fe71830ffc | |
![]() |
3ba49ebe18 | |
![]() |
25940275fd | |
![]() |
3d6ec86881 | |
![]() |
2b7be994e0 | |
![]() |
dcf572def2 | |
![]() |
2c752ac328 | |
![]() |
f8f18494c9 | |
![]() |
d35a053cea | |
![]() |
978411f619 | |
![]() |
28f0aa3933 | |
![]() |
b61bcea05e | |
![]() |
ce0c4e9e4f | |
![]() |
b27e7a105f | |
![]() |
acebb6977f | |
![]() |
4a91f8eb20 | |
![]() |
0be1ed6057 | |
![]() |
975752cff1 | |
![]() |
784af7543a | |
![]() |
d8a3e5d5da | |
![]() |
74b637889d | |
![]() |
34fffab63d | |
![]() |
ddf281fd16 | |
![]() |
a92aa3f6b9 | |
![]() |
38d51ab355 | |
![]() |
a74f4be540 | |
![]() |
3da57f1c30 | |
![]() |
afec038131 | |
![]() |
3643084b1b | |
![]() |
714e4bca20 | |
![]() |
4f92bc7545 | |
![]() |
0a577e3e8e | |
![]() |
c21d9d390d | |
![]() |
00b8e6ba8f | |
![]() |
91e5e432ea | |
![]() |
59e70fc7bd | |
![]() |
0dc938e4bb | |
![]() |
45223e9728 | |
![]() |
37cc8555a7 | |
![]() |
39df52f344 | |
![]() |
d15c821ceb | |
![]() |
d9552e5143 | |
![]() |
686f4ad4ae | |
![]() |
7e4cbd91c6 | |
![]() |
fd20dd9869 | |
![]() |
54dcd69a36 | |
![]() |
59f1ce8732 | |
![]() |
322ddbf646 | |
![]() |
fe46056a18 | |
![]() |
740eef306e | |
![]() |
311b13779d | |
![]() |
aedbf553c9 | |
![]() |
1350755474 | |
![]() |
dcf7dbabdd | |
![]() |
352cdc3e23 | |
![]() |
79c2e3a397 | |
![]() |
9a07618ac4 | |
![]() |
9f3ffc685f | |
![]() |
d84c653e84 | |
![]() |
ad644574f8 | |
![]() |
a9ae426c13 | |
![]() |
880e44f1b8 | |
![]() |
1ad6a7c4c6 | |
![]() |
823ce74401 | |
![]() |
1d274b181e | |
![]() |
75d2bfc416 | |
![]() |
defeddc9b2 | |
![]() |
e15b0cfa44 | |
![]() |
ca2e064206 | |
![]() |
797c4b6bd2 | |
![]() |
0bad12cc5f | |
![]() |
a6d1e6a914 | |
![]() |
3e54e533d1 | |
![]() |
85285df468 | |
![]() |
3eb2eb210b | |
![]() |
63f164685b | |
![]() |
4cb5ca5f63 | |
![]() |
8fc53b14a9 | |
![]() |
4276889448 | |
![]() |
ab0bc92946 | |
![]() |
805748209a | |
![]() |
28719283c0 | |
![]() |
f16163080d | |
![]() |
0bce22f9c2 | |
![]() |
578cb14922 | |
![]() |
9c9340e76e | |
![]() |
b32804ebf5 | |
![]() |
72680008f0 | |
![]() |
3951381f08 | |
![]() |
ba6edcf359 | |
![]() |
c4605ef411 | |
![]() |
e5bf5be13c | |
![]() |
1eb5b5d8c8 | |
![]() |
13d11ece39 | |
![]() |
92837cacac | |
![]() |
38e301e9f2 | |
![]() |
89b3228c87 | |
![]() |
1cbdb6e985 | |
![]() |
4914e9d4bf | |
![]() |
b3bd304321 | |
![]() |
44ef446c33 | |
![]() |
962e8fe0b9 | |
![]() |
5c0865d4a6 | |
![]() |
2dcecfcca8 | |
![]() |
e721a5261d | |
![]() |
c53bc221e1 | |
![]() |
64aea9aba3 | |
![]() |
8a3db108e5 | |
![]() |
6add96537e | |
![]() |
e1b6bdcdbc | |
![]() |
e1c164ba9a | |
![]() |
d0a396136a | |
![]() |
effc4edaa3 | |
![]() |
434189464a | |
![]() |
71cac90d9e | |
![]() |
cd51af92a2 | |
![]() |
bebbdcf556 | |
![]() |
7a06c65939 | |
![]() |
b0243e99c7 | |
![]() |
1cd9f396a7 | |
![]() |
6ca0a5b3d4 | |
![]() |
7222691493 | |
![]() |
f1392fc6f0 | |
![]() |
3c85a1321c | |
![]() |
98d177630f | |
![]() |
27ee6aa5d7 | |
![]() |
c66bd8b5fb | |
![]() |
f608cc65e1 | |
![]() |
c084402921 | |
![]() |
94838262af | |
![]() |
457c90204a | |
![]() |
350ceb9585 | |
![]() |
be67208a72 | |
![]() |
e98e951342 | |
![]() |
688d136740 | |
![]() |
0c9a00c2bf | |
![]() |
3b71ac7af9 | |
![]() |
1bcb4564f8 | |
![]() |
14c55a225f | |
![]() |
2e7f28f88f | |
![]() |
61db33f779 | |
![]() |
4a65bef0ed | |
![]() |
496c94a3aa | |
![]() |
a3af7bf07d | |
![]() |
d1e7d7ab18 | |
![]() |
8dce4ff728 | |
![]() |
9c315d7b47 | |
![]() |
657fc04149 | |
![]() |
3b2b54b479 | |
![]() |
78c63320d5 | |
![]() |
d30a0a9f64 | |
![]() |
9b53eac285 | |
![]() |
2b0a8f05ec | |
![]() |
c3c69ff9f8 | |
![]() |
9622e659d8 | |
![]() |
440ee21dd6 | |
![]() |
9cf9615bac | |
![]() |
3b22e2612a | |
![]() |
b4d011c9d3 | |
![]() |
9c34156fda | |
![]() |
953396d4d5 | |
![]() |
12b0b69fe5 | |
![]() |
58222160bb | |
![]() |
d64041fc5d | |
![]() |
8d2826365e | |
![]() |
789ebb1f61 | |
![]() |
6153d6aff2 | |
![]() |
27074e88cb | |
![]() |
12bc908f79 | |
![]() |
11460ff360 | |
![]() |
14d70e7713 |
|
@ -1,36 +0,0 @@
|
||||||
# flyctl launch added from .gitignore
|
|
||||||
**/debug_bin*
|
|
||||||
**/gen
|
|
||||||
**/node_modules
|
|
||||||
|
|
||||||
# flyctl launch added from library/.gitignore
|
|
||||||
# Logs
|
|
||||||
library/**/logs
|
|
||||||
library/**/*.log
|
|
||||||
library/**/npm-debug.log*
|
|
||||||
library/**/yarn-debug.log*
|
|
||||||
library/**/yarn-error.log*
|
|
||||||
library/**/pnpm-debug.log*
|
|
||||||
library/**/lerna-debug.log*
|
|
||||||
|
|
||||||
library/**/node_modules
|
|
||||||
library/**/dist
|
|
||||||
library/**/dist-ssr
|
|
||||||
library/**/*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
library/**/stats.html
|
|
||||||
|
|
||||||
# flyctl launch added from library/node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/.gitignore
|
|
||||||
!library/node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/**/*
|
|
||||||
|
|
||||||
# flyctl launch added from library/node_modules/.pnpm/vite-plugin-webfont-dl@3.7.6_vite@4.4.5/node_modules/vite-plugin-webfont-dl/dist/.gitignore
|
|
||||||
library/node_modules/.pnpm/vite-plugin-webfont-dl@3.7.6_vite@4.4.5/node_modules/vite-plugin-webfont-dl/dist/**/*
|
|
||||||
!library/node_modules/.pnpm/vite-plugin-webfont-dl@3.7.6_vite@4.4.5/node_modules/vite-plugin-webfont-dl/dist/**/.gitignore
|
|
||||||
|
|
||||||
# flyctl launch added from node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/.gitignore
|
|
||||||
!node_modules/.pnpm/tailwindcss@3.3.3/node_modules/tailwindcss/stubs/**/*
|
|
||||||
|
|
||||||
# flyctl launch added from webui/assetgen/css/node_modules/.pnpm/tailwindcss@3.3.2/node_modules/tailwindcss/stubs/.gitignore
|
|
||||||
!webui/assetgen/css/node_modules/.pnpm/tailwindcss@3.3.2/node_modules/tailwindcss/stubs/**/*
|
|
||||||
fly.toml
|
|
|
@ -1,8 +1,2 @@
|
||||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.webp filter=lfs diff=lfs merge=lfs -text
|
bundles linguist-generated=true
|
||||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.blend* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.hdr* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.wav* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
*.ttf* filter=lfs diff=lfs merge=lfs -text
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
* @delaneyj @bencroker
|
|
@ -0,0 +1,31 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: Create a bug report for Datastar.
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
So you think you’ve discovered a bug in Datastar? To ensure this hasn’t already been reported, please first search [existing issues](https://github.com/starfederation/datastar/issues?q=is%3Aissue) and ensure you’re running the [latest release](https://github.com/starfederation/datastar/releases). The `#bug-report` channel in the [Discord server](https://discord.gg/bnRNgZjgPh) is also a great place to report bugs.
|
||||||
|
|
||||||
|
To rule out that this isn’t a setup or configuration issue, please read the [guide](https://data-star.dev/guide) and the [reference](https://data-star.dev/reference/attribute_plugins) docs.
|
||||||
|
|
||||||
|
If you still believe you’ve found a bug, please provide a clear and concise description, including:
|
||||||
|
- What is happening and what you expected to happen.
|
||||||
|
- Steps to reproduce the issue.
|
||||||
|
- Screenshots, if applicable.
|
||||||
|
- type: textarea
|
||||||
|
id: body
|
||||||
|
attributes:
|
||||||
|
label: Bug Report
|
||||||
|
description: Please provide a clear and concise description of the bug.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Datastar Version
|
||||||
|
description: Provide the Datastar version that this relates to, ideally the [latest release](https://github.com/starfederation/datastar/releases).
|
||||||
|
placeholder: v1.0.0
|
||||||
|
validations:
|
||||||
|
required: true
|
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: false
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature for Datastar.
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
So you have a feature request for Datastar? To ensure this isn’t already on our radar, please first search [existing issues](https://github.com/starfederation/datastar/issues?q=is%3Aissue). The `#general` channel in the [Discord server](https://discord.gg/bnRNgZjgPh) is also a great place to suggest features.
|
||||||
|
|
||||||
|
If you still believe you have a new suggestion, please provide a clear and concise description, including:
|
||||||
|
- What problem you believe your feature request can solve.
|
||||||
|
- Any alternative solutions or features you have already considered.
|
||||||
|
- type: textarea
|
||||||
|
id: body
|
||||||
|
attributes:
|
||||||
|
label: Feature Request
|
||||||
|
description: Please provide a clear and concise suggestion for a feature.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Datastar Version
|
||||||
|
description: Provide the Datastar version that this relates to, ideally the [latest release](https://github.com/starfederation/datastar/releases).
|
||||||
|
placeholder: v1.0.0
|
|
@ -0,0 +1,24 @@
|
||||||
|
name: Support Request
|
||||||
|
description: Ask a question about Datastar.
|
||||||
|
labels:
|
||||||
|
- question
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
So you have a question about Datastar? To ensure your question hasn’t already been answered, please first search [existing issues](https://github.com/starfederation/datastar/issues?q=is%3Aissue). The `#help` channel in the [Discord server](https://discord.gg/bnRNgZjgPh) is also a great place to ask questions.
|
||||||
|
|
||||||
|
If your question is still unanswered, please provide a clear and concise description. Note that we are diligent about documentation, so please check whether your question is answered in the [guide](https://data-star.dev/guide) or the [reference](https://data-star.dev/reference/attribute_plugins) docs before submitting.
|
||||||
|
- type: textarea
|
||||||
|
id: body
|
||||||
|
attributes:
|
||||||
|
label: Support Request
|
||||||
|
description: Please provide a clear and concise question.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Datastar Version
|
||||||
|
description: Provide the Datastar version that this relates to, ideally the [latest release](https://github.com/starfederation/datastar/releases).
|
||||||
|
placeholder: v1.0.0
|
|
@ -0,0 +1,34 @@
|
||||||
|
name: Build Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: develop
|
||||||
|
paths:
|
||||||
|
- 'Dockerfile-dev'
|
||||||
|
- '.github/workflows/build-docker-images.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}-dev
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: ./.github
|
||||||
|
file: ./Dockerfile-dev
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
build-args: |
|
||||||
|
ENVIRONMENT=development
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Deploy Site
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy app
|
||||||
|
if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[deploy-site]')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency: deploy-group
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Deploy to fly.io
|
||||||
|
uses: superfly/flyctl-actions/setup-flyctl@master
|
||||||
|
- run: |
|
||||||
|
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task tools'
|
||||||
|
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task support'
|
||||||
|
flyctl deploy --local-only
|
||||||
|
env:
|
||||||
|
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: Enforce Branch Policy
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
enforce-branch-policy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Enforce main branch pull request policy
|
||||||
|
if: github.event_name == 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
echo "Pull requests to the main branch are not allowed."
|
||||||
|
exit 1
|
||||||
|
- name: Enforce main branch push policy
|
||||||
|
run: |
|
||||||
|
echo "Push events to the main branch are allowed."
|
||||||
|
exit 0
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: Build & Publish NuGet to GitHub Registry
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
NuGetDirectory: ${{ github.workspace }}/nuget
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 9.0.x
|
||||||
|
|
||||||
|
- name: Build and Pack NuGet package with versioning
|
||||||
|
run: dotnet pack sdk/dotnet/src/Datastar.fsproj --configuration Release --output ${{ env.NuGetDirectory }}
|
||||||
|
|
||||||
|
- name: Upload NuGet package to GitHub
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nugetPackage
|
||||||
|
path: ${{ env.NuGetDirectory }}/*.nupkg
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Download nuget package artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nugetPackage
|
||||||
|
path: nugetPackage
|
||||||
|
|
||||||
|
- name: Publish to NuGet.org
|
||||||
|
run: dotnet nuget push nugetPackage/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
|
@ -0,0 +1,30 @@
|
||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'bundles/*.js'
|
||||||
|
- 'library/src/**/*.ts'
|
||||||
|
- 'site/smoketests/*.go'
|
||||||
|
- 'site/static/md/tests/*.md'
|
||||||
|
- '.github/workflows/tests.yml'
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Run CI tests
|
||||||
|
run: |
|
||||||
|
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task tools'
|
||||||
|
docker container run --rm -t -v "${{ github.workspace }}":/app -v go-modules:/go/pkg/mod ghcr.io/starfederation/datastar-dev -c 'go tool task test'
|
|
@ -1,6 +1,17 @@
|
||||||
debug_bin*
|
tailwindcli
|
||||||
node_modules
|
datastar_site
|
||||||
|
data
|
||||||
site_bin
|
|
||||||
.task
|
.task
|
||||||
*_templ.go
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
datastar-website
|
||||||
|
*_bin
|
||||||
|
*.qtpl.go
|
||||||
|
*/java/*/target/
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
__debug_bin*
|
||||||
|
|
||||||
|
# search index
|
||||||
|
/data-star.bleve
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"jdinabox.quicktemplate-vscode",
|
||||||
|
"golang.go",
|
||||||
|
"a-h.templ"
|
||||||
|
]
|
||||||
|
}
|
|
@ -9,14 +9,16 @@
|
||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/backends/go/cmd/site/main.go"
|
"program": "${workspaceFolder}/site/cmd/site/main.go",
|
||||||
|
"preLaunchTask": "build datastar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Go test example",
|
"name": "Build Library",
|
||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/backends/example/go/main.go"
|
"program": "${workspaceFolder}/build/cmd/build/main.go",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"go.testTimeout": "200s",
|
||||||
"datastar",
|
"go.coverOnSingleTestFile": true,
|
||||||
"HTMX",
|
"go.coverOnSingleTest": true,
|
||||||
"hyperscript",
|
"editor.foldingStrategy": "indentation",
|
||||||
"idiomorph",
|
"makefile.configureOnOpen": false,
|
||||||
"Neato",
|
"editor.formatOnSave": true,
|
||||||
"sudodevnull",
|
"[typescript]": {
|
||||||
"TLDR",
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
"Websockets"
|
},
|
||||||
]
|
"[json]": {
|
||||||
}
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.formatOnSave": false
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.formatOnSave": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"label": "build datastar",
|
||||||
|
"command": "task",
|
||||||
|
"args": [
|
||||||
|
"support"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Building Datastar using Docker
|
||||||
|
|
||||||
|
Datastar comes with a development environment for developing Datastar via a Docker container.
|
||||||
|
|
||||||
|
It takes care of the setup listed here for you in a Dockerized environment, which allows you to get it up and running quickly & easily.
|
||||||
|
|
||||||
|
See [DOCKER.md](DOCKER.md) for detailed instructions on using it.
|
||||||
|
|
||||||
|
# Building Datastar Natively
|
||||||
|
|
||||||
|
To run the examples website you will need the following:
|
||||||
|
|
||||||
|
- [Go](https://go.dev/)
|
||||||
|
- [Taskfile](https://taskfile.dev/)
|
||||||
|
|
||||||
|
To publish the library or if you're using a BSD operating system, you will also need:
|
||||||
|
|
||||||
|
- [pnpm](https://pnpm.io/)
|
||||||
|
- [Node and npm](https://nodejs.org/)
|
||||||
|
|
||||||
|
`task tools` will install all required tools
|
||||||
|
|
||||||
|
`task build` will build the library as well as sdk constant templates
|
||||||
|
|
||||||
|
The compiled files can be found in `./bundles`
|
||||||
|
|
||||||
|
Finally, `task` will build the library and serve the reference documentation website.
|
||||||
|
|
||||||
|
The website should be available on http://localhost:8080 with hot-reloading enabled.
|
||||||
|
|
||||||
|
## End to end tests
|
||||||
|
|
||||||
|
Once you have the website running, you should be able to run the
|
||||||
|
[rod](https://go-rod.github.io) tests.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ task test
|
||||||
|
```
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Release Notes for Datastar
|
||||||
|
|
||||||
|
Each tagged version of Datastar is accompanied by a release note. Read the [release notes »](https://github.com/starfederation/datastar/releases)
|
||||||
|
|
||||||
|
# WIP Release Notes
|
||||||
|
|
||||||
|
## v1.0.0-beta.11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the `__debounce`, `__throttle` and `__viewtransition` modifiers to `data-on-intersect`.
|
||||||
|
- Added the `__debounce`, `__throttle` and `__viewtransition` modifiers to `data-on-signal-change`.
|
||||||
|
- Added the `__viewtransition` modifier to `data-on-interval`.
|
||||||
|
- Added the `__viewtransition` modifier to `data-on-load`.
|
||||||
|
- Added the `__viewtransition` modifier to `data-on-raf`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The `datastar-sse` event is now dispatched on the `document` element, and using `data-on-datastar-sse` automatically listens for the event on the `document` ([#802](https://github.com/starfederation/datastar/issues/802)).
|
||||||
|
- The `data-on-signals-change-*` attribute key now accepts a path in which `*` matches a single path segment and `**` matches multiple path segments (`data-on-signals-change-foo.*.baz`, `data-on-signals-change-foo.**`).
|
||||||
|
- The `data-persist` attribute now accepts one or more space-separated paths in which `*` matches a single path segment and `**` matches multiple path segments (`data-persist="foo.*.baz"`, `data-persist="foo.**"`).
|
||||||
|
- The `@setAll` action now accepts one or more space-separated paths in which `*` matches a single path segment and `**` matches multiple path segments (`@setAll('foo.*.baz', true)`, `@setAll('foo.**', true)`) ([#793](https://github.com/starfederation/datastar/issues/793)).
|
||||||
|
- The `@toggleAll` action now accepts one or more space-separated paths in which `*` matches a single path segment and `**` matches multiple path segments (`@toggleAll('foo.*.baz')`, `@toggleAll('foo.**')`) ([#793](https://github.com/starfederation/datastar/issues/793)).
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Contributing to Datastar
|
||||||
|
|
||||||
|
Thanks for thinking about contributing to Datastar 🚀
|
||||||
|
|
||||||
|
## Before You Contribute
|
||||||
|
|
||||||
|
Before you contribute, please consider that Datastar is a lightweight JavaScript framework that aims to simplify reactive web apps by using a minimalist, attribute-based approach for state management and event handling directly in HTML.
|
||||||
|
|
||||||
|
Anything outside of this scope may not be accepted. If you have a need for a feature that is not within the scope of Datastar, consider using a Web Component or writing your own Datastar plugin.
|
||||||
|
|
||||||
|
## Bug Reports & Feature Requests
|
||||||
|
|
||||||
|
Before submitting bug reports and feature requests, please search the [open issues](https://github.com/starfederation/datastar/issues) and the _#help_ channel in the [Discord server](https://discord.gg/bnRNgZjgPh) to see if it has already been addressed. When submitting a [new issue](https://github.com/starfederation/datastar/issues/new), please use a descriptive title and include a clear description and as much relevant information as possible.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
Always open an issue to discuss your idea _before_ submitting a pull request. Pull requests must be made to the `develop` branch, have a descriptive title and clearly describe the problem and solution. If the pull request adds or changes behavior, documentation and examples should be updated accordingly.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Datastar’s documentation is written in markdown files that live in [this folder](https://github.com/starfederation/datastar/tree/develop/site/static/md). Improvements to them can be submitted via pull requests.
|
||||||
|
|
||||||
|
## SDKs
|
||||||
|
|
||||||
|
We are currently only accepting SDKs from people willing to contribute _and_ maintain them.
|
||||||
|
|
||||||
|
SDKs must follow the [Architecture Decision Record](https://github.com/starfederation/datastar/blob/develop/sdk/README.md). Pull requests for new SDKs must also include the following (use alphabetical order, where appropriate):
|
||||||
|
|
||||||
|
- A README file that explains how to use the SDK.
|
||||||
|
- An entry in the [SDKs reference guide](https://github.com/starfederation/datastar/blob/develop/site/static/md/reference/sdks.md).
|
||||||
|
- A value in `SDKLanguages` in [`consts.go`](https://github.com/starfederation/datastar/blob/develop/build/consts.go).
|
||||||
|
- A `consts_[language].qtpl` file for generating [constants and defaults](https://github.com/starfederation/datastar/blob/develop/build).
|
||||||
|
- Sample [code snippets](https://github.com/starfederation/datastar/tree/develop/site/static/code_snippets) in the language of your SDK.
|
||||||
|
- An implementation of each of the [examples](https://github.com/starfederation/datastar/tree/develop/examples).
|
||||||
|
- Template values for consts and examples in [`run.go`](https://github.com/starfederation/datastar/blob/develop/build/run.go).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Datastar comes with instructions for building, either manually or via Docker. Read the [Building Guidelines »](BUILDING.md)
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Building Datastar using Docker
|
||||||
|
|
||||||
|
Datastar comes with a development environment for developing Datastar via a Docker container.
|
||||||
|
|
||||||
|
It takes care of the setup listed in the for you in a Dockerized environment, which allows you to get it up and running quickly & easily.
|
||||||
|
|
||||||
|
The only requirement is that you have [Docker](https://www.docker.com/products/docker-desktop) installed (you do not need `golang`, `tmpl`, `npm`, `pnpm` or any other project dependencies installed locally).
|
||||||
|
|
||||||
|
## Why Docker?
|
||||||
|
|
||||||
|
Developers who have adopted [Docker](https://www.docker.com/) for a containerized approach to development are used to not having to install a matching development infrastructure each time they approach a project.
|
||||||
|
|
||||||
|
This allows you to "shrink-wrap" the devops needed to run a project in a container, which will run anywhere, on any machine, without having to do any meticulous setup.
|
||||||
|
|
||||||
|
It also allows you to easily swap between basic things like Go versions, without affecting your local computer.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
In terminal, `cd` to the `datastar/` directory, and then type:
|
||||||
|
|
||||||
|
```
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The first time you run this command, it may take a bit of time to build the Docker image, and download all of the appropriate packages, and cache them locally.
|
||||||
|
|
||||||
|
Then just navigate to `http://localhost:8080` in your browser, and Datastar site from `site` will be up and running.
|
||||||
|
|
||||||
|
You can freely make changes to the `library` Datastar codebase, and the changes will be rebuilt and reload automatically.
|
||||||
|
|
||||||
|
You can also make changes to the `site` Datastar website backend, and the backend will be rebuilt and reload automatically.
|
||||||
|
|
||||||
|
## Using Datastar Docker Dev
|
||||||
|
|
||||||
|
Datastar Docker Dev uses the venerable `make` command to automate setup and access to the Docker containers used.
|
||||||
|
|
||||||
|
It uses `make` because it's available pre-installed on any development machine.
|
||||||
|
|
||||||
|
See the [Using Make & Makefiles to Automate your Frontend Workflow](https://nystudio107.com/blog/using-make-makefiles-to-automate-your-frontend-workflow) article for more on `make`.
|
||||||
|
|
||||||
|
The make tool is available for just about every platform you can imagine, and is installed with the [XCode CLI Tools](https://www.embarcadero.com/starthere/xe5/mobdevsetup/ios/en/installing_the_commandline_tools.html) on the Mac, and [WSL2](https://docs.microsoft.com/en-us/windows/wsl/install-win10) on Windows. Probably you have these installed already if you’re doing development.
|
||||||
|
|
||||||
|
Below are details and options available in each of the provided `make` commands:
|
||||||
|
|
||||||
|
* `make dev` - starts up the Go website server for the backend with hot reloading as you make changes
|
||||||
|
* `make test` - run the project tests inside the Docker container
|
||||||
|
* `make task xxx` - runs the specified task from the `Taskfile.yml` inside of the Docker container
|
||||||
|
* `make ssh` - gives you a shell inside of the Docker container
|
||||||
|
* `make image-build` - rebuilds the Docker image from scratch (you will probably never need this)
|
||||||
|
* `make clean` - removes all build artifacts, including the Docker image & volumes, to start from scratch
|
||||||
|
|
||||||
|
### CLI Arguments
|
||||||
|
|
||||||
|
You can pass in optional CLI arguments to override the default settings Datastar dev uses:
|
||||||
|
|
||||||
|
* `TAG=` (default is defined in `Dockerfile-dev`) - allows you to specify the official [golang Docker image](https://hub.docker.com/_/golang) tag that should be used. Using this, you can change the version of Go the container runs, e.g.: `make image-build TAG="1.24"` will use the latest patch version of Go 1.24 official Docker image.
|
||||||
|
|
||||||
|
### Terminating
|
||||||
|
|
||||||
|
To terminate the `datastar-dev` Docker container, enter `Control-C` in the terminal.
|
13
Dockerfile
13
Dockerfile
|
@ -1,16 +1,17 @@
|
||||||
FROM docker.io/golang:1.22.1-alpine AS build
|
FROM docker.io/golang:1.24.2-alpine AS build
|
||||||
|
|
||||||
RUN apk add --no-cache upx
|
RUN apk add --no-cache upx
|
||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.* *.go ./
|
COPY . .
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY backends/go/. ./backends/go/
|
COPY site ./site
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
go build -o /out/site backends/go/cmd/site/main.go
|
go build -ldflags="-s" -o /out/site site/cmd/site/main.go
|
||||||
RUN upx /out/site
|
RUN upx -9 -k /out/site
|
||||||
|
|
||||||
FROM scratch
|
FROM alpine
|
||||||
|
RUN chmod a=rwx,u+t /tmp
|
||||||
COPY --from=build /out/site /
|
COPY --from=build /out/site /
|
||||||
ENTRYPOINT ["/site"]
|
ENTRYPOINT ["/site"]
|
|
@ -0,0 +1,48 @@
|
||||||
|
ARG TAG=1.24
|
||||||
|
|
||||||
|
FROM golang:$TAG
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
RUN apt update && sudo apt upgrade \
|
||||||
|
&& \
|
||||||
|
set -eux; \
|
||||||
|
# Packages to install
|
||||||
|
apt install -y \
|
||||||
|
git \
|
||||||
|
jq \
|
||||||
|
rsync \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
# Needed for headless chrome/tests
|
||||||
|
libglib2.0-dev \
|
||||||
|
libnss3-dev \
|
||||||
|
libdbus-1-dev \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libxcomposite-dev \
|
||||||
|
libxdamage1 \
|
||||||
|
libxrandr2 \
|
||||||
|
libgbm-dev \
|
||||||
|
libxkbcommon-x11-0 \
|
||||||
|
libpangocairo-1.0-0 \
|
||||||
|
libasound2 \
|
||||||
|
&& \
|
||||||
|
# Clean out directories that don't need to be part of the image
|
||||||
|
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||||
|
&& \
|
||||||
|
# Install node packages \
|
||||||
|
npm install -g npm@^10.0.0 \
|
||||||
|
npm install -g pnpm \
|
||||||
|
&& \
|
||||||
|
# Install flyctl cli \
|
||||||
|
curl -L https://fly.io/install.sh | sh \
|
||||||
|
&& \
|
||||||
|
ln -s /root/.fly/bin/flyctl /usr/local/bin/fly \
|
||||||
|
&& \
|
||||||
|
# Make this a safe .git directory
|
||||||
|
git config --global --add safe.directory /app
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/sh"]
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) Delaney Gillilan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,41 @@
|
||||||
|
TAG?=1.24
|
||||||
|
CONTAINER?=$(shell basename $(CURDIR))-dev
|
||||||
|
DEV_PORT?=8080
|
||||||
|
IMAGE_INFO=$(shell docker image inspect $(CONTAINER):$(TAG))
|
||||||
|
IMAGE_NAME=${CONTAINER}:${TAG}
|
||||||
|
DOCKER_RUN=docker container run --rm -it -v "${CURDIR}":/app -v go-modules:/go/pkg/mod
|
||||||
|
ARCH=$(shell uname -m)
|
||||||
|
|
||||||
|
.PHONY: build clean dev image-build task test ssh
|
||||||
|
|
||||||
|
# Perform a dist build
|
||||||
|
build: image-check
|
||||||
|
${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} build
|
||||||
|
# Clean up all build artifacts to start from scratch
|
||||||
|
clean:
|
||||||
|
docker image rm ${IMAGE_NAME}
|
||||||
|
docker volume rm go-modules
|
||||||
|
# Run the development server
|
||||||
|
dev: --image-check
|
||||||
|
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task -w'
|
||||||
|
# Build the Docker image
|
||||||
|
image-build:
|
||||||
|
docker build -f Dockerfile-dev . -t ${IMAGE_NAME} --build-arg TAG=${TAG} --no-cache
|
||||||
|
${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} -c 'go tool task tools'
|
||||||
|
# Run the passed in task command
|
||||||
|
task: --image-check
|
||||||
|
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task $(filter-out $@,$(MAKECMDGOALS)) $(MAKEFLAGS)'
|
||||||
|
# Run the test suite
|
||||||
|
test: --image-check
|
||||||
|
${DOCKER_RUN} --name ${CONTAINER}-$@ -e DEV_PORT="${DEV_PORT}" -p ${DEV_PORT}:${DEV_PORT} ${IMAGE_NAME} -c 'go tool task test'
|
||||||
|
# Open a shell inside of the container
|
||||||
|
ssh: --image-check
|
||||||
|
${DOCKER_RUN} --name ${CONTAINER}-$@ --entrypoint=/bin/sh ${IMAGE_NAME}
|
||||||
|
# Ensure the image has been created
|
||||||
|
--image-check:
|
||||||
|
ifeq ($(IMAGE_INFO), [])
|
||||||
|
--image-check: image-build
|
||||||
|
endif
|
||||||
|
%:
|
||||||
|
@:
|
||||||
|
# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line
|
|
@ -0,0 +1,62 @@
|
||||||
|
[](https://github.com/starfederation/datastar/releases)
|
||||||
|
[](https://github.com/starfederation/datastar/blob/main/LICENSE)
|
||||||
|
[](https://github.com/starfederation/datastar/stargazers)
|
||||||
|
|
||||||
|
<p align="center"><img width="200" src="https://data-star.dev/static/images/rocket.webp"></p>
|
||||||
|
|
||||||
|
# Datastar
|
||||||
|
|
||||||
|
### The hypermedia framework.
|
||||||
|
|
||||||
|
Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.
|
||||||
|
|
||||||
|
Getting started is as easy as adding a single 14.5 KiB script tag to your HTML.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start adding frontend reactivity using declarative <code>data-*</code> attributes.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input data-bind-title />
|
||||||
|
<div data-text="$title.toUpperCase()"></div>
|
||||||
|
<button data-on-click="@post('/endpoint')">Save</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit the [Datastar Website »](https://data-star.dev/)
|
||||||
|
|
||||||
|
Watch the [Videos »](https://www.youtube.com/@data-star)
|
||||||
|
|
||||||
|
Join the [Discord Server »](https://discord.com/invite/bnRNgZjgPh)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Read the [Getting Started Guide »](https://data-star.dev/guide/getting_started)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Read the [Contribution Guidelines »](https://github.com/starfederation/datastar/blob/develop/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Custom Plugins
|
||||||
|
|
||||||
|
You can manually add your own plugins to the core:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script type="module">
|
||||||
|
import { load } from 'datastar'
|
||||||
|
|
||||||
|
load(
|
||||||
|
// Look ma’, I made a plugin!
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#starfederation/datastar&Date)
|
213
Taskfile.yml
213
Taskfile.yml
|
@ -2,120 +2,203 @@
|
||||||
|
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
interval: 100ms
|
interval: 1000ms
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
NAME: "site"
|
|
||||||
BIN_NAME: "{{.NAME}}_bin"
|
|
||||||
VERSION:
|
VERSION:
|
||||||
sh: cat library/package.json| jq -r .version
|
sh: cat VERSION
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
tools:
|
tools:
|
||||||
|
platforms:
|
||||||
|
[
|
||||||
|
windows,
|
||||||
|
linux,
|
||||||
|
darwin/arm64,
|
||||||
|
darwin/amd64,
|
||||||
|
openbsd,
|
||||||
|
dragonfly,
|
||||||
|
freebsd,
|
||||||
|
netbsd,
|
||||||
|
]
|
||||||
cmds:
|
cmds:
|
||||||
- go install github.com/a-h/templ/cmd/templ@latest
|
- platforms: [linux/amd64]
|
||||||
- go install github.com/go-task/task/v3/cmd/task@latest
|
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-x64
|
||||||
|
|
||||||
|
- platforms: [linux/arm64]
|
||||||
|
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-linux-arm64
|
||||||
|
|
||||||
|
- platforms: [darwin/arm64]
|
||||||
|
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-arm64
|
||||||
|
|
||||||
|
- platforms: [darwin/amd64]
|
||||||
|
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-macos-x64
|
||||||
|
|
||||||
|
- platforms: [windows]
|
||||||
|
cmd: test -f site/tailwindcli || wget -O site/tailwindcli https://github.com/dobicinaitis/tailwind-cli-extra/releases/download/v1.7.21/tailwindcss-extra-windows-x64.exe
|
||||||
|
|
||||||
|
- platforms: [openbsd, dragonfly, freebsd, netbsd]
|
||||||
|
cmd: pnpm add tailwindcss @tailwindcss/container-queries @tailwindcss/typography daisyui
|
||||||
|
|
||||||
|
- platforms: [openbsd, dragonfly, freebsd, netbsd]
|
||||||
|
cmd: test -f site/tailwindcli || (echo "#!/bin/sh" > site/tailwindcli && echo "tailwindcss $@" >> site/tailwindcli)
|
||||||
|
|
||||||
|
- chmod +x site/tailwindcli
|
||||||
|
|
||||||
version:
|
version:
|
||||||
cmds:
|
cmds:
|
||||||
- echo {{.VERSION}}
|
- echo {{.VERSION}}
|
||||||
libpub:
|
|
||||||
dir: ./library
|
qtc:
|
||||||
|
sources:
|
||||||
|
- "**/*.qtpl"
|
||||||
|
generates:
|
||||||
|
- "**/*.qtpl.go"
|
||||||
|
cmds:
|
||||||
|
- go tool qtc
|
||||||
|
|
||||||
|
build:
|
||||||
deps:
|
deps:
|
||||||
|
- qtc
|
||||||
|
sources:
|
||||||
|
- VERSION
|
||||||
|
- "**/*.qtpl"
|
||||||
|
- "**/package.json"
|
||||||
|
- "**/*.ts"
|
||||||
|
generates:
|
||||||
|
- "bundles/**/*"
|
||||||
|
cmds:
|
||||||
|
- go run build/cmd/build/main.go
|
||||||
|
- cp -r bundles/* site/static/js/
|
||||||
|
- mkdir -p library/dist/
|
||||||
|
- cp -r bundles/* library/dist/
|
||||||
|
- mkdir -p site/static/librarySource
|
||||||
|
- rm -rf site/static/librarySource/*
|
||||||
|
- cp -r library/src/* site/static/librarySource/
|
||||||
|
|
||||||
|
library:
|
||||||
|
dir: library
|
||||||
|
requires:
|
||||||
|
vars: [VERSION]
|
||||||
|
sources:
|
||||||
|
- "**/*.ts"
|
||||||
|
- "**/*.js"
|
||||||
|
- "**/*.json"
|
||||||
|
generates:
|
||||||
|
- "dist/**/*"
|
||||||
|
cmds:
|
||||||
|
- pnpm i
|
||||||
|
- pnpm build
|
||||||
|
|
||||||
|
libpub:
|
||||||
|
dir: library
|
||||||
|
requires:
|
||||||
|
vars: [VERSION]
|
||||||
|
deps:
|
||||||
|
- build
|
||||||
- library
|
- library
|
||||||
cmds:
|
cmds:
|
||||||
- git push origin
|
- git push origin
|
||||||
- git tag v{{.VERSION}}
|
- git tag v{{.VERSION}}
|
||||||
- git push origin --tags
|
- git push origin --tags
|
||||||
- npm publish --access public
|
- npm publish --access public
|
||||||
- git push --tags
|
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/bundles/datastar.js
|
||||||
- GOPROXY=proxy.golang.org go list -m github.com/delaneyj/datastar@v{{.VERSION}}
|
- git push origin --tags
|
||||||
|
- GOPROXY=proxy.golang.org go list -m github.com/starfederation/datastar@v{{.VERSION}}
|
||||||
- task: deploy
|
- task: deploy
|
||||||
|
|
||||||
library:
|
sdktspub:
|
||||||
vars:
|
dir: sdk/typescript
|
||||||
BACKEND_STATIC_DIR: "../backends/go/{{.NAME}}/static/library"
|
requires:
|
||||||
dir: library
|
vars: [VERSION]
|
||||||
generates:
|
|
||||||
- "dist/**/*"
|
|
||||||
sources:
|
|
||||||
- "src/**/*.ts"
|
|
||||||
- "package.json"
|
|
||||||
cmds:
|
cmds:
|
||||||
# - echo "{{.BACKEND_STATIC_DIR}}"
|
- git push origin
|
||||||
- pnpm i
|
- git tag sdktsv{{.VERSION}}
|
||||||
# - pnpm vitest --watch=false
|
- git push origin --tags
|
||||||
- pnpm prettier -w .
|
- deno run -A build.ts {{.VERSION}}
|
||||||
- pnpm build
|
- cd npm && npm publish --access public
|
||||||
- rsync -av dist/ {{.BACKEND_STATIC_DIR}}
|
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/web/serverSentEventGenerator.js
|
||||||
- rsync -av package.json {{.BACKEND_STATIC_DIR}}
|
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/abstractServerSentEventGenerator.js
|
||||||
|
- curl https://purge.jsdelivr.net/gh/starfederation/datastar/sdk/typescript/npm/esm/node/serverSentEventGenerator.js
|
||||||
|
- git push origin --tags
|
||||||
|
|
||||||
css:
|
idiomorph:
|
||||||
dir: backends/go/site/css
|
dir: library/src/vendored
|
||||||
generates:
|
|
||||||
- "../static/css/site.css"
|
|
||||||
sources:
|
|
||||||
- "../**/*.md"
|
|
||||||
# - "../**/*.go"
|
|
||||||
- "../**/*.templ"
|
|
||||||
cmds:
|
cmds:
|
||||||
- pnpm tailwindcss build -o ../static/css/site.css
|
- wget -O idiomorph.esm.js https://raw.githubusercontent.com/bigskysoftware/idiomorph/refs/heads/main/dist/idiomorph.esm.js
|
||||||
|
- npx -p typescript tsc *.js --declaration --allowJs --emitDeclarationOnly --outDir .
|
||||||
|
css:
|
||||||
|
dir: site
|
||||||
|
sources:
|
||||||
|
- "**/*.templ"
|
||||||
|
- "**/*.md"
|
||||||
|
- "**/*.go"
|
||||||
|
generates:
|
||||||
|
- "static/css/site.css"
|
||||||
|
|
||||||
|
cmds:
|
||||||
|
- ./tailwindcli build -i src/css/site.css -o static/css/site.css
|
||||||
|
|
||||||
templ:
|
templ:
|
||||||
env:
|
env:
|
||||||
TEMPL_EXPERIMENT: rawgo
|
TEMPL_EXPERIMENT: rawgo
|
||||||
|
# deps:
|
||||||
|
# - library
|
||||||
generates:
|
generates:
|
||||||
- "**/*_templ.go"
|
- "**/*_templ.go"
|
||||||
sources:
|
sources:
|
||||||
- "**/*.templ"
|
- "**/*.templ"
|
||||||
cmds:
|
cmds:
|
||||||
- templ generate .
|
- go tool templ generate .
|
||||||
|
|
||||||
kill:
|
kill:
|
||||||
|
method: none
|
||||||
cmds:
|
cmds:
|
||||||
- killall -q {{.BIN_NAME}} || echo "Process was not running."
|
- fuser -k 8080/tcp > /dev/null 2>&1 || true
|
||||||
|
|
||||||
tests:
|
support:
|
||||||
dir: playwright
|
|
||||||
cmds:
|
|
||||||
- pnpm i
|
|
||||||
- pnpm playwright
|
|
||||||
|
|
||||||
hot:
|
|
||||||
desc: Server hot reload
|
|
||||||
dir: backends/go
|
|
||||||
env:
|
|
||||||
ENV: dev
|
|
||||||
sources:
|
sources:
|
||||||
|
- "**/*.templ"
|
||||||
- "**/*.go"
|
- "**/*.go"
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
- "{{.NAME}}/static/**/*"
|
- site/static/**/*
|
||||||
|
generates:
|
||||||
|
- ./datastar-website
|
||||||
deps:
|
deps:
|
||||||
- library
|
|
||||||
- kill
|
- kill
|
||||||
|
- build
|
||||||
- templ
|
- templ
|
||||||
# - css
|
- css
|
||||||
|
|
||||||
|
site:
|
||||||
|
method: none
|
||||||
|
desc: build and run site
|
||||||
|
deps:
|
||||||
|
- support
|
||||||
cmds:
|
cmds:
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
- go build -o ./{{.BIN_NAME}} cmd/{{.NAME}}/main.go
|
- go build -o ./datastar-website "site/cmd/site/main.go"
|
||||||
- ./{{.BIN_NAME}}
|
|
||||||
|
|
||||||
test:
|
|
||||||
dir: playwright
|
|
||||||
cmds:
|
|
||||||
- curl -o - -I http://localhost:8080
|
|
||||||
- pnpm i
|
|
||||||
- pnpm playwright install
|
|
||||||
- pnpm playwright test --ui
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
|
method: none
|
||||||
deps:
|
deps:
|
||||||
- library
|
- support
|
||||||
cmds:
|
cmds:
|
||||||
- fly deploy --local-only
|
- fly deploy --local-only
|
||||||
|
|
||||||
default:
|
test:
|
||||||
deps:
|
deps:
|
||||||
- hot
|
- support
|
||||||
silent: true
|
cmds:
|
||||||
|
- go test -v github.com/starfederation/datastar/site/smoketests -run '^TestUnit' -count=1 -timeout=1m
|
||||||
|
|
||||||
|
test-all:
|
||||||
|
deps:
|
||||||
|
- support
|
||||||
|
cmds:
|
||||||
|
- go test -v github.com/starfederation/datastar/site/smoketests -count=1 -timeout=1m
|
||||||
|
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- task: site
|
||||||
|
- ./datastar-website
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
static/library/*
|
|
|
@ -1 +0,0 @@
|
||||||
static/library
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"name": "datastar-css",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"autoprefixer": "^10.4.19",
|
|
||||||
"daisyui": "^4.11.1",
|
|
||||||
"postcss": "^8.4.38",
|
|
||||||
"prettier": "^3.3.0",
|
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
|
||||||
"tailwindcss": "^3.4.3",
|
|
||||||
"vite": "^5.2.12"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
|
||||||
"@tailwindcss/typography": "^0.5.13"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,29 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import "github.com/goccy/go-json"
|
|
||||||
|
|
||||||
func UnmarshalPackageJSON(data []byte) (PackageJSON, error) {
|
|
||||||
var r PackageJSON
|
|
||||||
err := json.Unmarshal(data, &r)
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PackageJSON) Marshal() ([]byte, error) {
|
|
||||||
return json.Marshal(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
type PackageJSON struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Repository string `json:"repository"`
|
|
||||||
Files []string `json:"files"`
|
|
||||||
Private bool `json:"private"`
|
|
||||||
Main string `json:"main"`
|
|
||||||
Module string `json:"module"`
|
|
||||||
Types string `json:"types"`
|
|
||||||
DevDependencies map[string]string
|
|
||||||
Dependencies map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
var packageJSON PackageJSON
|
|
|
@ -1,155 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"embed"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
"github.com/alecthomas/chroma"
|
|
||||||
"github.com/alecthomas/chroma/formatters/html"
|
|
||||||
"github.com/alecthomas/chroma/lexers"
|
|
||||||
"github.com/alecthomas/chroma/styles"
|
|
||||||
"github.com/benbjohnson/hashfs"
|
|
||||||
"github.com/delaneyj/toolbelt"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
|
||||||
"github.com/gomarkdown/markdown/ast"
|
|
||||||
mdhtml "github.com/gomarkdown/markdown/html"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed static/*
|
|
||||||
var staticFS embed.FS
|
|
||||||
|
|
||||||
var (
|
|
||||||
staticSys = hashfs.NewFS(staticFS)
|
|
||||||
highlightCSS templ.Component
|
|
||||||
mdRenderer func() *mdhtml.Renderer
|
|
||||||
)
|
|
||||||
|
|
||||||
func staticPath(path string) string {
|
|
||||||
return "/" + staticSys.HashName("static/"+path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunBlocking(port int) toolbelt.CtxErrFunc {
|
|
||||||
upsertIIfeBuildSize()
|
|
||||||
return func(ctx context.Context) error {
|
|
||||||
|
|
||||||
b, err := staticFS.ReadFile("static/library/package.json")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error reading package.json: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
packageJSON, err = UnmarshalPackageJSON(b)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error unmarshaling package.json: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
router := chi.NewRouter()
|
|
||||||
|
|
||||||
router.Use(
|
|
||||||
middleware.Logger,
|
|
||||||
middleware.Recoverer,
|
|
||||||
// toolbelt.CompressMiddleware(),
|
|
||||||
)
|
|
||||||
|
|
||||||
setupRoutes(router)
|
|
||||||
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: fmt.Sprintf(":%d", port),
|
|
||||||
Handler: router,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
srv.Shutdown(context.Background())
|
|
||||||
}()
|
|
||||||
|
|
||||||
return srv.ListenAndServe()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRoutes(router chi.Router) error {
|
|
||||||
defer router.Handle("/static/*", hashfs.FileServer(staticSys))
|
|
||||||
// defer router.Get("/hotreload", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// sse := datastar.NewSSE(w, r)
|
|
||||||
// <-r.Context().Done()
|
|
||||||
// sse.Send("reload", datastar.WithSSERetry(250))
|
|
||||||
// })
|
|
||||||
|
|
||||||
htmlFormatter := html.New(html.WithClasses(true), html.TabWidth(2))
|
|
||||||
if htmlFormatter == nil {
|
|
||||||
return fmt.Errorf("couldn't create html formatter")
|
|
||||||
}
|
|
||||||
styleName := "nord"
|
|
||||||
highlightStyle := styles.Get(styleName)
|
|
||||||
if highlightStyle == nil {
|
|
||||||
return fmt.Errorf("couldn't find style %s", styleName)
|
|
||||||
}
|
|
||||||
highlightCSSBuffer := &bytes.Buffer{}
|
|
||||||
if err := htmlFormatter.WriteCSS(highlightCSSBuffer, highlightStyle); err != nil {
|
|
||||||
return fmt.Errorf("error writing highlight css: %w", err)
|
|
||||||
}
|
|
||||||
highlightCSS = templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
|
||||||
_, err := io.WriteString(w, fmt.Sprintf(`<style>%s</style>`, highlightCSSBuffer.String()))
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
|
|
||||||
mdRenderer = func() *mdhtml.Renderer {
|
|
||||||
return mdhtml.NewRenderer(mdhtml.RendererOptions{
|
|
||||||
Flags: mdhtml.CommonFlags | mdhtml.HrefTargetBlank,
|
|
||||||
RenderNodeHook: func(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
|
||||||
|
|
||||||
// based on https://github.com/alecthomas/chroma/blob/master/quick/quick.go
|
|
||||||
htmlHighlight := func(w io.Writer, source, lang, defaultLang string) error {
|
|
||||||
if lang == "" {
|
|
||||||
lang = defaultLang
|
|
||||||
}
|
|
||||||
l := lexers.Get(lang)
|
|
||||||
if l == nil {
|
|
||||||
l = lexers.Analyse(source)
|
|
||||||
}
|
|
||||||
if l == nil {
|
|
||||||
l = lexers.Fallback
|
|
||||||
}
|
|
||||||
l = chroma.Coalesce(l)
|
|
||||||
|
|
||||||
it, err := l.Tokenise(nil, source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return htmlFormatter.Format(w, highlightStyle, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderCode := func(w io.Writer, codeBlock *ast.CodeBlock) {
|
|
||||||
defaultLang := ""
|
|
||||||
lang := string(codeBlock.Info)
|
|
||||||
htmlHighlight(w, string(codeBlock.Literal), lang, defaultLang)
|
|
||||||
}
|
|
||||||
|
|
||||||
if code, ok := node.(*ast.CodeBlock); ok {
|
|
||||||
renderCode(w, code)
|
|
||||||
return ast.GoToNext, true
|
|
||||||
}
|
|
||||||
return ast.GoToNext, false
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := errors.Join(
|
|
||||||
setupHome(router),
|
|
||||||
setupGuide(router),
|
|
||||||
setupReferenceRoutes(router),
|
|
||||||
setupExamples(router),
|
|
||||||
setupEssays(router),
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("error setting up routes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,339 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/go-sanitize/sanitize"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
sanitizer *sanitize.Sanitizer
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupExamples(router chi.Router) (err error) {
|
|
||||||
mdElementRenderers, _, err := markdownRenders("examples")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sanitizer, err = sanitize.New()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error creating sanitizer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sidebarGroups := []*SidebarGroup{
|
|
||||||
{
|
|
||||||
Label: "Ported HTMX Examples",
|
|
||||||
Links: []*SidebarLink{
|
|
||||||
{ID: "click_to_edit"},
|
|
||||||
{ID: "bulk_update"},
|
|
||||||
{ID: "click_to_load"},
|
|
||||||
{ID: "delete_row"},
|
|
||||||
{ID: "edit_row"},
|
|
||||||
{ID: "lazy_load"},
|
|
||||||
{ID: "fetch_indicator"},
|
|
||||||
{ID: "inline_validation"},
|
|
||||||
{ID: "infinite_scroll"},
|
|
||||||
{ID: "active_search"},
|
|
||||||
{ID: "progress_bar"},
|
|
||||||
{ID: "value_select"},
|
|
||||||
{ID: "animations"},
|
|
||||||
{ID: "file_upload"},
|
|
||||||
{ID: "dialogs_browser"},
|
|
||||||
{ID: "lazy_tabs"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Web Components Examples",
|
|
||||||
Links: []*SidebarLink{
|
|
||||||
{ID: "shoelace_kitchensink"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Reactive Examples",
|
|
||||||
Links: []*SidebarLink{
|
|
||||||
{ID: "multiline_fragments"},
|
|
||||||
{ID: "scroll_into_view"},
|
|
||||||
{ID: "on_load"},
|
|
||||||
{ID: "model_binding"},
|
|
||||||
{ID: "disable_button"},
|
|
||||||
{ID: "merge_options"},
|
|
||||||
{ID: "redirects"},
|
|
||||||
{ID: "view_transition_api"},
|
|
||||||
{ID: "title_update_backend"},
|
|
||||||
{ID: "store_changed"},
|
|
||||||
{ID: "raf_update"},
|
|
||||||
{ID: "update_store"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Backend Examples",
|
|
||||||
Links: []*SidebarLink{
|
|
||||||
{ID: "node"},
|
|
||||||
{ID: "python"},
|
|
||||||
{ID: "quick_primer_go"},
|
|
||||||
{ID: "templ_counter"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
lo.ForEach(sidebarGroups, func(group *SidebarGroup, grpIdx int) {
|
|
||||||
lo.ForEach(group.Links, func(link *SidebarLink, linkIdx int) {
|
|
||||||
link.URL = templ.SafeURL("/examples/" + link.ID)
|
|
||||||
link.Label = strings.ToUpper(strings.ReplaceAll(link.ID, "_", " "))
|
|
||||||
|
|
||||||
if linkIdx > 0 {
|
|
||||||
link.Prev = group.Links[linkIdx-1]
|
|
||||||
} else if grpIdx > 0 {
|
|
||||||
prvGrp := sidebarGroups[grpIdx-1]
|
|
||||||
link.Prev = prvGrp.Links[len(prvGrp.Links)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if linkIdx < len(group.Links)-1 {
|
|
||||||
link.Next = group.Links[linkIdx+1]
|
|
||||||
} else if grpIdx < len(sidebarGroups)-1 {
|
|
||||||
nxtGrp := sidebarGroups[grpIdx+1]
|
|
||||||
link.Next = nxtGrp.Links[0]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Route("/examples", func(examplesRouter chi.Router) {
|
|
||||||
examplesRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(w, r, string(sidebarGroups[0].Links[0].URL), http.StatusFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
examplesRouter.Get("/{name}", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
name := chi.URLParam(r, "name")
|
|
||||||
contents, ok := mdElementRenderers[name]
|
|
||||||
if !ok {
|
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentLink *SidebarLink
|
|
||||||
for _, group := range sidebarGroups {
|
|
||||||
for _, link := range group.Links {
|
|
||||||
if link.ID == name {
|
|
||||||
currentLink = link
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SidebarPage(r, sidebarGroups, currentLink, contents).Render(r.Context(), w)
|
|
||||||
})
|
|
||||||
|
|
||||||
// mdElementRenderers, _, err := markdownRenders("examples")
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// type Example struct {
|
|
||||||
// URL string
|
|
||||||
// Label string
|
|
||||||
// Description string
|
|
||||||
// Prev, Next *Example
|
|
||||||
// }
|
|
||||||
// type ExampleGroup struct {
|
|
||||||
// Label string
|
|
||||||
// Examples []*Example
|
|
||||||
// }
|
|
||||||
// var (
|
|
||||||
// prevExample *Example
|
|
||||||
// examplesByURL = map[string]*Example{}
|
|
||||||
// )
|
|
||||||
// examples := lo.Map([]ExampleGroup{
|
|
||||||
// {
|
|
||||||
// Label: "Ported HTMX Examples",
|
|
||||||
// Examples: []*Example{
|
|
||||||
// {Label: "Click to Edit", Description: "inline editing of a data object"},
|
|
||||||
// {Label: "Bulk Update", Description: "bulk updating of multiple rows of data"},
|
|
||||||
// {Label: "Click to Load", Description: "loading data on demand"},
|
|
||||||
// {Label: "Delete Row", Description: "row deletion in a table"},
|
|
||||||
// {Label: "Edit Row", Description: "how to edit rows in a table"},
|
|
||||||
// {Label: "Lazy Load", Description: "how to lazy load content"},
|
|
||||||
// {Label: "Fetch Indicator", Description: "show a loading indicator when fetching data"},
|
|
||||||
// {Label: "Inline Validation", Description: "how to do inline field validation"},
|
|
||||||
// {Label: "Infinite Scroll", Description: "infinite scrolling of a page"},
|
|
||||||
// {Label: "Active Search", Description: "the active search box pattern"},
|
|
||||||
// {Label: "Progress Bar", Description: "a job-runner like progress bar"},
|
|
||||||
// {Label: "Value Select", Description: "making the values of a select dependent on another select"},
|
|
||||||
// {Label: "Animations", Description: "various animation techniques"},
|
|
||||||
// {Label: "File Upload", Description: "how to upload a file via ajax with a progress bar"},
|
|
||||||
// {Label: "Dialogs Browser", Description: "the prompt and confirm dialogs"},
|
|
||||||
// {Label: "Lazy Tabs", Description: "how to lazy load tabs"},
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// Label: "Web Components Examples",
|
|
||||||
// Examples: []*Example{
|
|
||||||
// {Label: "Shoelace Kitchensink", Description: "the Shoelace Web Components library"},
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// Label: "Reactive Examples",
|
|
||||||
// Examples: []*Example{
|
|
||||||
// {Label: "Multiline Fragments", Description: "multiline fragments"},
|
|
||||||
// {Label: "Scroll Into View", Description: "scrolling an element into view"},
|
|
||||||
// {Label: "On Load", Description: "how to load data on page load"},
|
|
||||||
// {Label: "Model Binding", Description: "two-way data binding to signals"},
|
|
||||||
// {Label: "Disable Button", Description: "how to disable a button while processing"},
|
|
||||||
// {Label: "Merge Options", Description: "how to merge options in a select"},
|
|
||||||
// {Label: "Redirects", Description: "how to redirect to another page"},
|
|
||||||
// {Label: "View Transition API", Description: "using the view transition API"},
|
|
||||||
// {Label: "Title Update Backend", Description: "target a specific element for updates"},
|
|
||||||
// {Label: "Store Changed", Description: "detect when a store has changed"},
|
|
||||||
// {Label: "RAF Update", Description: "update a signal on requestAnimationFrame"},
|
|
||||||
// {Label: "Update Store", Description: "update a store from an SSE event"},
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// Label: "Backend Examples",
|
|
||||||
// Examples: []*Example{
|
|
||||||
// {Label: "Node", Description: "example backend in node"},
|
|
||||||
// {Label: "Python", Description: "example backend in python"},
|
|
||||||
// {Label: "Quick Primer Go", Description: "The getting started guide in Go"},
|
|
||||||
// {Label: "Templ Counter", Description: "a simple counter example for Templ"},
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }, func(g ExampleGroup, i int) ExampleGroup {
|
|
||||||
// for j, example := range g.Examples {
|
|
||||||
// g.Examples[j].URL = "/examples/" + toolbelt.Cased(g.Examples[j].Label, toolbelt.Snake, toolbelt.Lower)
|
|
||||||
// if prevExample != nil {
|
|
||||||
// example.Prev = prevExample
|
|
||||||
// prevExample.Next = example
|
|
||||||
// }
|
|
||||||
// prevExample = example
|
|
||||||
// examplesByURL[example.URL] = example
|
|
||||||
// }
|
|
||||||
// return g
|
|
||||||
// })
|
|
||||||
|
|
||||||
// router.Route("/examples", func(examplesRouter chi.Router) {
|
|
||||||
|
|
||||||
// examplesRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// http.Redirect(w, r, examples[0].Examples[0].URL, http.StatusFound)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// examplesRouter.Get("/{exampleName}", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// exampleName := chi.URLParam(r, "exampleName")
|
|
||||||
// contents, ok := mdElementRenderers[exampleName]
|
|
||||||
// if !ok {
|
|
||||||
// http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// example, ok := examplesByURL[r.URL.Path]
|
|
||||||
// if !ok {
|
|
||||||
// http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// contentGroup := []ElementRenderer{}
|
|
||||||
// if example.Prev != nil {
|
|
||||||
// contentGroup = append(contentGroup,
|
|
||||||
// buttonLink().
|
|
||||||
// CLASS("w-full").
|
|
||||||
// HREF(example.Prev.URL).
|
|
||||||
// Text("Back to "+example.Prev.Label).
|
|
||||||
// CLASS("flex flex-col justify-center items-center no-underline"))
|
|
||||||
// }
|
|
||||||
// contentGroup = append(contentGroup, contents)
|
|
||||||
|
|
||||||
// nextHREF := "/reference"
|
|
||||||
// nextLabel := "Dive deeper"
|
|
||||||
|
|
||||||
// if example.Next != nil {
|
|
||||||
// nextHREF = example.Next.URL
|
|
||||||
// nextLabel = "Next " + example.Next.Label
|
|
||||||
// }
|
|
||||||
// contentGroup = append(contentGroup,
|
|
||||||
// buttonLink().
|
|
||||||
// CLASS("w-full").
|
|
||||||
// HREF(nextHREF).
|
|
||||||
// Text(nextLabel).
|
|
||||||
// CLASS("flex flex-col justify-center items-center no-underline"))
|
|
||||||
|
|
||||||
// sidebarContents := Group(
|
|
||||||
// Range(examples, func(g ExampleGroup) ElementRenderer {
|
|
||||||
// return DIV(
|
|
||||||
// DIV(
|
|
||||||
// DIV().CLASS("text-2xl font-bold text-primary").Text(g.Label),
|
|
||||||
// HR().CLASS("divider border-primary"),
|
|
||||||
// ),
|
|
||||||
// TABLE().
|
|
||||||
// CLASS("table w-full").
|
|
||||||
// Children(
|
|
||||||
// THEAD(
|
|
||||||
// TR(
|
|
||||||
// TH().Text("Pattern"),
|
|
||||||
// TH().Text("Description"),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// TBODY(
|
|
||||||
// Range(g.Examples, func(e *Example) ElementRenderer {
|
|
||||||
// return TR().
|
|
||||||
// CLASS("hover").
|
|
||||||
// Children(
|
|
||||||
// TD(link(e.URL, e.Label, e.URL == r.URL.Path)),
|
|
||||||
// TD().CLASS("text-xs").Text(e.Description),
|
|
||||||
// )
|
|
||||||
// }),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// }),
|
|
||||||
// )
|
|
||||||
|
|
||||||
// pp := prosePage(r, sidebarContents, Group(contentGroup...), nil)
|
|
||||||
// pp.Render(w)
|
|
||||||
// })
|
|
||||||
|
|
||||||
examplesSessionStore := sessions.NewCookieStore([]byte("ExampleSession"))
|
|
||||||
|
|
||||||
if err := errors.Join(
|
|
||||||
setupExamplesClickToEdit(examplesRouter),
|
|
||||||
setupExamplesBulkUpdate(examplesRouter),
|
|
||||||
setupExamplesClickToLoad(examplesRouter),
|
|
||||||
setupExamplesEditRow(examplesRouter),
|
|
||||||
setupExamplesDeleteRow(examplesRouter),
|
|
||||||
setupExamplesLazyLoad(examplesRouter),
|
|
||||||
setupExamplesFetchIndicator(examplesRouter),
|
|
||||||
setupExamplesOnLoad(examplesRouter, examplesSessionStore),
|
|
||||||
setupExamplesDisableButton(examplesRouter),
|
|
||||||
setupExampleInlineValidation(examplesRouter),
|
|
||||||
setupExamplesInfiniteScroll(examplesRouter),
|
|
||||||
setupExamplesActiveSearch(examplesRouter),
|
|
||||||
setupExamplesProgressBar(examplesRouter),
|
|
||||||
setupExamplesValueSelect(examplesRouter),
|
|
||||||
setupExamplesAnimations(examplesRouter),
|
|
||||||
setupExamplesFileUpload(examplesRouter),
|
|
||||||
setupExamplesDialogsBrowser(examplesRouter),
|
|
||||||
setupExamplesLazyTabs(examplesRouter),
|
|
||||||
setupExamplesMergeOptions(examplesRouter),
|
|
||||||
setupExamplesRedirects(examplesRouter),
|
|
||||||
setupExamplesMultilineFragments(examplesRouter),
|
|
||||||
setupExamplesUpdateStore(examplesRouter),
|
|
||||||
//
|
|
||||||
setupExamplesShoelaceKitchensink(examplesRouter),
|
|
||||||
//
|
|
||||||
setupExamplesViewTransitionAPI(examplesRouter),
|
|
||||||
setupExamplesModelBinding(examplesRouter),
|
|
||||||
setupExamplesTitleUpdateBackend(examplesRouter),
|
|
||||||
setupExamplesStoreChanged(examplesRouter, examplesSessionStore),
|
|
||||||
setupExamplesScrollIntoView(examplesRouter),
|
|
||||||
setupExamplesQuickPrimerGo(examplesRouter),
|
|
||||||
setupExamplesTemplCounter(examplesRouter, examplesSessionStore),
|
|
||||||
); err != nil {
|
|
||||||
panic(fmt.Sprintf("error setting up examples routes: %s", err))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
goaway "github.com/TwiN/go-away"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ClickToEditContactStore struct {
|
|
||||||
FirstName string `json:"firstName,omitempty" san:"trim,xss,max=128"`
|
|
||||||
LastName string `json:"lastName,omitempty" san:"trim,xss,max=128"`
|
|
||||||
Email string `json:"email,omitempty" san:"trim,xss,max=128"`
|
|
||||||
}
|
|
||||||
|
|
||||||
templ setupExamplesClickToEditUserComponent(store *ClickToEditContactStore) {
|
|
||||||
<div id="contact_1" class="flex flex-col max-w-sm gap-2">
|
|
||||||
<label>First Name: { goaway.Censor(store.FirstName) }</label>
|
|
||||||
<label>Last Name: { goaway.Censor(store.LastName) }</label>
|
|
||||||
<label>Email: { goaway.Censor(store.Email) }</label>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
data-on-click="$$get('/examples/click_to_edit/contact/1/edit')"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
data-on-click="$$patch('/examples/click_to_edit/contact/1/reset')"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ setupExamplesClickToEditUserEdit(store *ClickToEditContactStore) {
|
|
||||||
<div id="contact_1" class="flex flex-col gap-2" data-store={ templ.JSONString(store) }>
|
|
||||||
<label class="flex items-center gap-2 input input-bordered">
|
|
||||||
First Name
|
|
||||||
<input type="text" class="grow" data-model="firstName"/>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2 input input-bordered">
|
|
||||||
Last Name
|
|
||||||
<input type="text" class="grow" data-model="lastName"/>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2 input input-bordered">
|
|
||||||
Email
|
|
||||||
<input type="text" class="grow" data-model="email"/>
|
|
||||||
</label>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
data-on-click="$$put('/examples/click_to_edit/contact/1')"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
data-on-click="$$get('/examples/click_to_edit/contact/1')"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupExamplesClickToLoad(examplesRouter chi.Router) error {
|
|
||||||
|
|
||||||
examplesRouter.Get("/click_to_load/data", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
store := &ClickToLoadStore{}
|
|
||||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
if store.Limit < 1 {
|
|
||||||
store.Limit = 10
|
|
||||||
} else if store.Limit > 100 {
|
|
||||||
store.Limit = 100
|
|
||||||
}
|
|
||||||
if store.Offset < 0 {
|
|
||||||
store.Offset = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
|
|
||||||
if store.Offset == 0 {
|
|
||||||
datastar.RenderFragmentTempl(sse, ClickToEditAgentsTable(store))
|
|
||||||
} else {
|
|
||||||
datastar.RenderFragmentTempl(sse, ClickToLoadMoreButton(store))
|
|
||||||
for i := 0; i < store.Limit; i++ {
|
|
||||||
log.Printf("ClickToLoadAgentRow: %d", store.Offset+i)
|
|
||||||
datastar.RenderFragmentTempl(
|
|
||||||
sse,
|
|
||||||
ClickToLoadAgentRow(store.Offset+i),
|
|
||||||
datastar.WithQuerySelectorID("click_to_load_rows"),
|
|
||||||
datastar.WithMergeAppendElement(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ fetchIndicatorEmpty() {
|
|
||||||
<div id="greeting">No data</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ fetchIndicatorGreeting() {
|
|
||||||
<div id="greeting">Hello, the time is { time.Now().Format(time.RFC3339) }</div>
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupExamplesInfiniteScroll(examplesRouter chi.Router) error {
|
|
||||||
|
|
||||||
examplesRouter.Get("/infinite_scroll/data", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
store := &infiniteScrollStore{}
|
|
||||||
if err := datastar.QueryStringUnmarshal(r, store); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if store.Limit < 1 || store.Limit > 100 {
|
|
||||||
store.Limit = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
|
|
||||||
if store.Offset == 0 {
|
|
||||||
datastar.RenderFragmentTempl(sse, infiniteScrollAgents(store))
|
|
||||||
} else {
|
|
||||||
datastar.RenderFragmentTempl(sse, infiniteScrollMore(store))
|
|
||||||
for i := 0; i < store.Limit; i++ {
|
|
||||||
|
|
||||||
datastar.RenderFragmentTempl(
|
|
||||||
sse, infiniteScrollAgent(store.Offset+i),
|
|
||||||
datastar.WithQuerySelectorID("click_to_load_rows"),
|
|
||||||
datastar.WithMergeAppendElement(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/delaneyj/toolbelt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type infiniteScrollStore struct {
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
templ infiniteScrollMore(store *infiniteScrollStore) {
|
|
||||||
<div
|
|
||||||
id="loading_message"
|
|
||||||
class="alert alert-info"
|
|
||||||
data-intersects={ fmt.Sprintf(
|
|
||||||
"$offset=%d;$limit=%d;$$get('/examples/infinite_scroll/data')",
|
|
||||||
store.Offset+store.Limit,
|
|
||||||
store.Limit,
|
|
||||||
) }
|
|
||||||
>
|
|
||||||
@icon("svg-spinners:blocks-wave")
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ infiniteScrollAgent(i int) {
|
|
||||||
<tr id={ fmt.Sprintf("agent_%d", i) }>
|
|
||||||
<td>Agent Smith { fmt.Sprint(i) }</td>
|
|
||||||
<td>{ fmt.Sprintf("void%d@null.org", i+1) }</td>
|
|
||||||
<td class="uppercase">{ fmt.Sprintf("%x", toolbelt.AliasHash(fmt.Sprint(i))) }</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ infiniteScrollAgents(store *infiniteScrollStore) {
|
|
||||||
<div
|
|
||||||
id="infinite_scroll"
|
|
||||||
data-store={ templ.JSONString(store) }
|
|
||||||
class="flex flex-col gap-2"
|
|
||||||
>
|
|
||||||
<table class="table w-full table-zebra">
|
|
||||||
<caption>Agents</caption>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>ID</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="click_to_load_rows">
|
|
||||||
for i := 0; i < store.Limit; i++ {
|
|
||||||
@infiniteScrollAgent(store.Offset + i)
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
@infiniteScrollMore(store)
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ModelBindingStore struct {
|
|
||||||
BindText string `json:"bindText"`
|
|
||||||
BindNumber int `json:"bindNumber"`
|
|
||||||
BindBool bool `json:"bindBool"`
|
|
||||||
BindSelection int `json:"bindSelection"`
|
|
||||||
}
|
|
||||||
|
|
||||||
templ ModelBindingView(optionCount int, store *ModelBindingStore) {
|
|
||||||
<div
|
|
||||||
id="container"
|
|
||||||
class="flex flex-col gap-4"
|
|
||||||
data-store={ templ.JSONString(store) }
|
|
||||||
>
|
|
||||||
<input class="input input-bordered" data-model="bindText"/>
|
|
||||||
<input class="input input-bordered" type="number" data-model="bindNumber"/>
|
|
||||||
<textarea class="textarea textarea-bordered" data-model="bindText"></textarea>
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label cursor-pointer">
|
|
||||||
<span class="label-text">Checkbox</span>
|
|
||||||
<input type="checkbox" checked="checked" class="checkbox" data-model="bindBool"/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<select class="select select-bordered" data-model="bindSelection">
|
|
||||||
for i :=1 ; i <= optionCount; i++ {
|
|
||||||
{{ str := fmt.Sprint( i) }}
|
|
||||||
<option value={ str }>Option { str }</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
for i := 1; i <= optionCount; i++ {
|
|
||||||
{{ str := fmt.Sprint( i) }}
|
|
||||||
<div class="form-control">
|
|
||||||
<label class="label cursor-pointer">
|
|
||||||
<span class="label-text">Option { str }</span>
|
|
||||||
<input type="radio" class="radio" data-model="bindSelection" value={ str }/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@datastar.TemplSignalStoreView()
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupExamplesMultilineFragments(examplesRouter chi.Router) error {
|
|
||||||
examplesRouter.Get("/multiline_fragments/data", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
|
|
||||||
datastar.RenderFragmentString(sse, `
|
|
||||||
<div id="replaceMe">
|
|
||||||
<pre>
|
|
||||||
This is a multiline fragment.
|
|
||||||
|
|
||||||
Used when you are writing a lot of text by hand
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
type RedirectsStore struct {
|
|
||||||
RedirectTo string `json:"redirectTo"`
|
|
||||||
}
|
|
||||||
|
|
||||||
templ redirectsView(store *RedirectsStore) {
|
|
||||||
<div id="demo" class="flex w-full gap-4" data-store={ templ.JSONString(store) }>
|
|
||||||
<label class="flex items-center flex-1 gap-2">
|
|
||||||
<span>Redirect to: </span>
|
|
||||||
<input data-model="redirectTo" class="flex-1 input input-bordered"/>
|
|
||||||
</label>
|
|
||||||
<button class="btn btn-success" data-on-click="$$post('/examples/redirects/data')">Redirect</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
"github.com/delaneyj/toolbelt"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupExamplesShoelaceKitchensink(examplesRouter chi.Router) error {
|
|
||||||
examplesRouter.Route("/shoelace_kitchensink/data", func(dataRouter chi.Router) {
|
|
||||||
|
|
||||||
options := lo.Map(lo.Range(7), func(i, index int) ShoelaceKitchensinkOption {
|
|
||||||
offset := toolbelt.NextID()
|
|
||||||
return ShoelaceKitchensinkOption{
|
|
||||||
Label: fmt.Sprintf("Option %d", i),
|
|
||||||
Value: uint32(offset % math.MaxUint32),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
dataRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
store := &ShoelaceKitchensinkStore{
|
|
||||||
Nested: &ShoelaceKitchensinkNested{
|
|
||||||
Label: fmt.Sprintf("Hello World %d", rand.Intn(100)),
|
|
||||||
Selection: options[rand.Intn(len(options))].Value,
|
|
||||||
IsChecked: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
datastar.RenderFragmentTempl(sse, ShoelaceKitchensinkView(r, options, store))
|
|
||||||
})
|
|
||||||
|
|
||||||
dataRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var res any
|
|
||||||
if err := datastar.BodyUnmarshal(r, &res); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("res: %v", res)
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
datastar.RenderFragmentString(sse, "<div></div>")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ShoelaceKitchensinkStore struct {
|
|
||||||
Nested *ShoelaceKitchensinkNested `json:"nested"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShoelaceKitchensinkNested struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
Selection uint32 `json:"selection"`
|
|
||||||
IsChecked bool `json:"isChecked"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShoelaceKitchensinkOption struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
Value uint32 `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
templ ShoelaceKitchensinkView(r *http.Request, options []ShoelaceKitchensinkOption, store *ShoelaceKitchensinkStore) {
|
|
||||||
<div
|
|
||||||
id="shoelace_kitchensink"
|
|
||||||
class="flex flex-col gap-4 sl-theme-dark"
|
|
||||||
data-store={ templ.JSONString(store) }
|
|
||||||
>
|
|
||||||
<sl-input label="Label" data-model="nested.label"></sl-input>
|
|
||||||
<sl-select
|
|
||||||
label="Select"
|
|
||||||
data-model="nested.selection"
|
|
||||||
data-on-sl-change="console.log('change')"
|
|
||||||
>
|
|
||||||
for _, o := range options {
|
|
||||||
<sl-option value={ fmt.Sprint(o.Value) }>{ o.Label } ({ fmt.Sprint(o.Value) })</sl-option>
|
|
||||||
}
|
|
||||||
</sl-select>
|
|
||||||
<sl-radio-group label="Radio Group" data-bind-value="$nested.selection" data-on-sl-change="$nested.selection = ctx.el.value">
|
|
||||||
for _, o := range options {
|
|
||||||
<sl-radio value={ fmt.Sprint(o.Value) }>{ o.Label } ({ fmt.Sprint(o.Value) })</sl-radio>
|
|
||||||
}
|
|
||||||
</sl-radio-group>
|
|
||||||
<sl-checkbox data-model="nested.isChecked">Checkbox</sl-checkbox>
|
|
||||||
<sl-button
|
|
||||||
variant="primary"
|
|
||||||
data-on-click={ fmt.Sprintf("$$post('%s')", r.URL.Path) }
|
|
||||||
>Submit</sl-button>
|
|
||||||
@datastar.TemplSignalStoreView()
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
"github.com/delaneyj/toolbelt"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupExamplesUpdateStore(examplesRouter chi.Router) error {
|
|
||||||
|
|
||||||
examplesRouter.Route("/update_store/data", func(dataRouter chi.Router) {
|
|
||||||
dataRouter.Post("/patch", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
store := map[string]any{}
|
|
||||||
if err := datastar.BodyUnmarshal(r, &store); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
randKey := toolbelt.NextEncodedID()
|
|
||||||
store[randKey] = time.Now().Format(time.RFC3339Nano)
|
|
||||||
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
datastar.PatchStore(sse, store)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
"github.com/delaneyj/datastar"
|
|
||||||
"github.com/delaneyj/toolbelt"
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var homePageChartSVG string
|
|
||||||
|
|
||||||
func setupHome(router chi.Router) error {
|
|
||||||
|
|
||||||
chartWidth := 480
|
|
||||||
graph := chart.BarChart{
|
|
||||||
Title: "File Size (Hello World) first load",
|
|
||||||
Width: chartWidth,
|
|
||||||
Height: chartWidth,
|
|
||||||
Background: chart.Style{
|
|
||||||
FillColor: drawing.Color{R: 1, G: 1, B: 1, A: 0},
|
|
||||||
FontColor: drawing.ColorWhite,
|
|
||||||
},
|
|
||||||
Canvas: chart.Style{
|
|
||||||
FillColor: drawing.Color{R: 1, G: 1, B: 1, A: 0},
|
|
||||||
FontColor: drawing.ColorWhite,
|
|
||||||
FontSize: 6,
|
|
||||||
},
|
|
||||||
TitleStyle: chart.Style{
|
|
||||||
FontColor: drawing.ColorWhite,
|
|
||||||
},
|
|
||||||
XAxis: chart.Style{
|
|
||||||
FontColor: drawing.ColorWhite,
|
|
||||||
},
|
|
||||||
YAxis: chart.YAxis{
|
|
||||||
Style: chart.Style{
|
|
||||||
FontColor: drawing.ColorWhite,
|
|
||||||
},
|
|
||||||
ValueFormatter: func(v any) string {
|
|
||||||
return humanize.Bytes(uint64(v.(float64)))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Bars: []chart.Value{
|
|
||||||
{Label: "Next.js", Value: 86221},
|
|
||||||
{Label: "SvelteKit", Value: 81920},
|
|
||||||
{Label: "HTMX+\nhyperscript", Value: 40653},
|
|
||||||
{Label: "HTMX+\nAlpine", Value: 37980},
|
|
||||||
{Label: "Datastar", Value: 10445},
|
|
||||||
{Label: "Datastar Core", Value: 4526},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer := bytes.NewBuffer([]byte{})
|
|
||||||
err := graph.Render(chart.SVG, buffer)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
homePageChartSVG = buffer.String()
|
|
||||||
|
|
||||||
var globalCount = new(int32)
|
|
||||||
c := int32(toolbelt.Fit(rand.Float32(), 0, 1, -100, 100))
|
|
||||||
globalCount = &c
|
|
||||||
|
|
||||||
globalCountExample := func() templ.Component {
|
|
||||||
store := &GlobalCountStore{
|
|
||||||
Count: atomic.LoadInt32(globalCount),
|
|
||||||
}
|
|
||||||
return HomeGlobalCountExample(*store)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
Home().Render(r.Context(), w)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Route("/api", func(apiRouter chi.Router) {
|
|
||||||
apiRouter.Route("/globalCount", func(globalCountRouter chi.Router) {
|
|
||||||
globalCountRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
datastar.RenderFragmentTempl(sse, globalCountExample())
|
|
||||||
})
|
|
||||||
|
|
||||||
globalCountRouter.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
store := &GlobalCountStore{}
|
|
||||||
if err := datastar.BodyUnmarshal(r, store); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.StoreInt32(globalCount, store.Count)
|
|
||||||
sse := datastar.NewSSE(w, r)
|
|
||||||
datastar.RenderFragmentTempl(sse, globalCountExample())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ Home() {
|
|
||||||
{{ cdnText := `<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar" ></script>` }}
|
|
||||||
@Page() {
|
|
||||||
<div class="flex flex-col items-center min-h-screen gap-4 p-16 bg-gradient-to-br from-base-300 to-base-100">
|
|
||||||
<div class="flex flex-col items-center max-w-lg gap-8">
|
|
||||||
<img
|
|
||||||
class="w-24 border-4 rounded-full shadow-xl md:w-96 border-primary"
|
|
||||||
src={ staticPath("/images/rocket.png") }
|
|
||||||
/>
|
|
||||||
<div class="text-4xl font-bold uppercase font-brand md:text-6xl text-primary">Datastar</div>
|
|
||||||
<div class="text-center font-brand">
|
|
||||||
<div class="text-xl">Real-time hypermedia framework</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Using a single
|
|
||||||
<span class="text-lg font-bold text-primary">{ iifeBuildSize }</span>
|
|
||||||
CDN link and have access to everything needed to rival a full-stack SPA framework; all in the language of your choice.
|
|
||||||
</p>
|
|
||||||
<div class="flex flex-wrap w-full gap-4">
|
|
||||||
<a
|
|
||||||
class="flex items-center justify-center flex-1 btn btn-secondary"
|
|
||||||
href="https://discord.gg/CHvPMrAp6F"
|
|
||||||
>
|
|
||||||
@icon("simple-icons:discord")
|
|
||||||
Join the conversation
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="flex items-center justify-center flex-1 btn btn-accent"
|
|
||||||
href="https://github.com/delaneyj/datastar/tree/main/library/src/lib"
|
|
||||||
>
|
|
||||||
@icon("simple-icons:github")
|
|
||||||
View the source
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="w-full shadow-xl card bg-base-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-ghost"
|
|
||||||
data-on-click={ fmt.Sprintf("$$clipboard('%s')", cdnText) }
|
|
||||||
>
|
|
||||||
@icon("material-symbols:content-copy")
|
|
||||||
</button>
|
|
||||||
<code
|
|
||||||
class="flex-1 overflow-hidden text-xs text-primary text-ellipsis"
|
|
||||||
>
|
|
||||||
{ cdnText }
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@templ.Raw(homePageChartSVG)
|
|
||||||
<div class="w-full shadow-xl card bg-base-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Example of a dynamically loaded area of page with shared global state</h2>
|
|
||||||
<div
|
|
||||||
id="global-count-example"
|
|
||||||
class="flex items-center justify-center gap-4 p-4"
|
|
||||||
data-on-load="$$get('/api/globalCount')"
|
|
||||||
data-fetch-indicator="'#spinner'"
|
|
||||||
>
|
|
||||||
<span class="text-2xl">Loading example on delay...</span>
|
|
||||||
@icon("svg-spinners:eclipse", "id", "spinner")
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
class="flex items-center w-full gap-1 btn btn-primary btn-outline btn-lg"
|
|
||||||
href={ templ.SafeURL("/guide") }
|
|
||||||
>
|
|
||||||
@icon("simple-icons:rocket")
|
|
||||||
Let's Get Started!
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type GlobalCountStore struct {
|
|
||||||
Count int32 `json:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
templ HomeGlobalCountExample(store GlobalCountStore) {
|
|
||||||
<div
|
|
||||||
id="global-count-example"
|
|
||||||
class="flex flex-col gap-4"
|
|
||||||
data-store={ templ.JSONString(store) }
|
|
||||||
>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<a class="flex-1 btn btn-success" data-on-click="$count++">Increment Local +</a>
|
|
||||||
<a class="flex-1 btn btn-error" data-on-click="$count--">Decrement Local -</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div data-text="`Count is ${$count % 2 === 0 ? 'even' : 'odd'}`"></div>
|
|
||||||
<input
|
|
||||||
class="flex-1 input input-bordered"
|
|
||||||
type="number"
|
|
||||||
name="count"
|
|
||||||
data-model="count"
|
|
||||||
data-testid="localcount_input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<a class="flex-1 btn btn-info" data-on-click="$$get('/api/globalCount')">Load global</a>
|
|
||||||
<a class="flex-1 btn btn-warning" data-on-click="$$post('/api/globalCount')">Store global</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,227 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
templ Page() {
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>DATASTAR</title>
|
|
||||||
<link rel="icon" href={ staticPath("images/datastar_icon.svg") }/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
|
|
||||||
/>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Inter:wght@100..900&family=Gideon+Roman:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&display=swap" rel="stylesheet"/>
|
|
||||||
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-QZ4RYHJW6X"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', 'G-QZ4RYHJW6X');
|
|
||||||
</script>
|
|
||||||
<script type="module" defer src={ staticPath("library/datastar.js") }></script>
|
|
||||||
// <script defer>
|
|
||||||
// function initHotReload() {
|
|
||||||
// console.log("Hot reload initializing")
|
|
||||||
// if (typeof(EventSource) !== "undefined") {
|
|
||||||
// const es = new EventSource("/hotreload");
|
|
||||||
// es.onmessage = function(event) {
|
|
||||||
// location.reload();
|
|
||||||
// }
|
|
||||||
// es.onerror = function(err) {
|
|
||||||
// console.log("lost connection to server, reloading");
|
|
||||||
// setTimeout(() => {
|
|
||||||
// location.reload();
|
|
||||||
// }, 500);
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// initHotReload();
|
|
||||||
// </script>
|
|
||||||
<link href={ staticPath("css/site.css") } rel="stylesheet" type="text/css"/>
|
|
||||||
</head>
|
|
||||||
<body class="flex flex-col w-full h-full min-h-screen overflow-y-scroll scrollbar scrollbar-thumb-primary scrollbar-track-accent">
|
|
||||||
{ children... }
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ icon(icon string, attrs ...string) {
|
|
||||||
<iconify-icon icon={ icon } { KVPairsAttrs(attrs...)... }></iconify-icon>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ headerIconLink(iconName, href string) {
|
|
||||||
<a target="_blank" rel="noopener noreferrer" href={ templ.SafeURL(href) }>
|
|
||||||
@icon(iconName)
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ headerTopLevelLink(r *http.Request, text string) {
|
|
||||||
{{ url := templ.SafeURL("/"+ strings.ToLower(text)) }}
|
|
||||||
<a
|
|
||||||
href={ url }
|
|
||||||
class={
|
|
||||||
"font-bold uppercase link-hover",
|
|
||||||
templ.KV("link-primary", strings.HasPrefix(r.URL.Path, string(url))),
|
|
||||||
}
|
|
||||||
>{ text }</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ headerExternalLinks() {
|
|
||||||
@headerIconLink("simple-icons:discord", "https://discord.gg/CHvPMrAp6F")
|
|
||||||
@headerIconLink("simple-icons:github", "https://github.com/delaneyj/datastar/tree/main/library")
|
|
||||||
@headerIconLink("simple-icons:npm", "https://www.npmjs.com/package/@sudodevnull/datastar")
|
|
||||||
@headerIconLink("simple-icons:twitter", "https://twitter.com/delaneyj")
|
|
||||||
}
|
|
||||||
|
|
||||||
templ headerTopLevelLinks(r *http.Request) {
|
|
||||||
@headerTopLevelLink(r, "Guide")
|
|
||||||
@headerTopLevelLink(r, "Reference")
|
|
||||||
@headerTopLevelLink(r, "Examples")
|
|
||||||
@headerTopLevelLink(r, "Essays")
|
|
||||||
}
|
|
||||||
|
|
||||||
templ header(r *http.Request) {
|
|
||||||
<div class="navbar bg-base-200">
|
|
||||||
<div class="flex justify-between w-full gap-4">
|
|
||||||
<div class="flex items-baseline gap-1">
|
|
||||||
<a
|
|
||||||
class="flex gap-1 text-2xl font-bold uppercase font-brand"
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
<span>Datastar</span>
|
|
||||||
<img src={ staticPath("images/datastar_icon.svg") } class="h-8"/>
|
|
||||||
</a>
|
|
||||||
<div class="font-mono text-xs text-accent">v{ packageJSON.Version }</div>
|
|
||||||
</div>
|
|
||||||
<div class="hidden md:text-md lg:text-lg md:flex md:gap-4 md:visible ">
|
|
||||||
@headerTopLevelLinks(r)
|
|
||||||
</div>
|
|
||||||
<div class="hidden text-xl md:flex md:gap-4 md:visible">
|
|
||||||
@headerExternalLinks()
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap justify-around visible gap-2 pb-8 text-sm bg-base-200 md:hidden">
|
|
||||||
@headerTopLevelLinks(r)
|
|
||||||
</div>
|
|
||||||
<div class="visible navbar bg-base-300 md:hidden">
|
|
||||||
<div class="navbar-start">
|
|
||||||
<label for="sidebar-drawer" class="btn btn-ghost drawer-button">
|
|
||||||
@icon("material-symbols:menu")
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="gap-4 navbar-end">
|
|
||||||
@headerExternalLinks()
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
type SidebarLink struct {
|
|
||||||
ID string
|
|
||||||
Label string
|
|
||||||
URL templ.SafeURL
|
|
||||||
Prev *SidebarLink
|
|
||||||
Next *SidebarLink
|
|
||||||
IsDisabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type SidebarGroup struct {
|
|
||||||
Label string
|
|
||||||
Links []*SidebarLink
|
|
||||||
}
|
|
||||||
|
|
||||||
templ SidebarPage(r *http.Request, sidebarGroups []*SidebarGroup, current *SidebarLink, contents string) {
|
|
||||||
@Page() {
|
|
||||||
@highlightCSS
|
|
||||||
<div class="drawer">
|
|
||||||
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle"/>
|
|
||||||
<div class="flex flex-col min-h-screen drawer-content">
|
|
||||||
@header(r)
|
|
||||||
<div class="flex flex-1">
|
|
||||||
<aside class="flex-col hidden gap-4 px-4 py-8 overflow-y-auto md:flex min-w-64 bg-base-300 md:visible">
|
|
||||||
@SidebarContents(sidebarGroups, current)
|
|
||||||
</aside>
|
|
||||||
<div class="flex flex-col items-center w-full gap-16 p-4 md:p-16">
|
|
||||||
@SidebarPrevNextLinks(sidebarGroups, current)
|
|
||||||
<article
|
|
||||||
class={
|
|
||||||
"flex-1",
|
|
||||||
"prose prose-primary prose-sm md:prose lg:prose-lg p-4",
|
|
||||||
"prose-a:link-primary",
|
|
||||||
}
|
|
||||||
>
|
|
||||||
@templ.Raw(contents)
|
|
||||||
</article>
|
|
||||||
@SidebarPrevNextLinks(sidebarGroups, current)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<aside class="drawer-side">
|
|
||||||
<label for="sidebar-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
|
||||||
<ul class="min-h-full p-4 menu w-80 bg-base-300 text-base-content">
|
|
||||||
@SidebarContents(sidebarGroups, current)
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templ SidebarContents(sidebarGroups []*SidebarGroup, current *SidebarLink) {
|
|
||||||
<div class="flex flex-col gap-8 uppercase">
|
|
||||||
for i, grp := range sidebarGroups {
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<h3 class="text-xs font-bold text-primary">{ grp.Label }</h3>
|
|
||||||
for _, link := range grp.Links {
|
|
||||||
if link.IsDisabled {
|
|
||||||
<div class="opacity-25">{ link.Label }</div>
|
|
||||||
} else {
|
|
||||||
<a
|
|
||||||
class="link-secondary link-hover"
|
|
||||||
href={ link.URL }
|
|
||||||
>{ link.Label }</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i != len(sidebarGroups)-1 {
|
|
||||||
<div class="divider"></div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ SidebarPrevNextLinks(essayGroups []*SidebarGroup, current *SidebarLink) {
|
|
||||||
<div class="flex flex-wrap justify-between w-full gap-4">
|
|
||||||
<div>
|
|
||||||
if current.Prev != nil {
|
|
||||||
<a
|
|
||||||
class="btn btn-sm btn-ghost"
|
|
||||||
disabled?={ current.Prev.IsDisabled }
|
|
||||||
href={ current.Prev.URL }
|
|
||||||
>
|
|
||||||
@icon("material-symbols:arrow-back-ios")
|
|
||||||
{ current.Prev.Label }
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
if current.Next != nil {
|
|
||||||
<a
|
|
||||||
class="btn btn-sm btn-ghost"
|
|
||||||
disabled?={ current.Next.IsDisabled }
|
|
||||||
href={ current.Next.URL }
|
|
||||||
>
|
|
||||||
{ current.Next.Label }
|
|
||||||
@icon("material-symbols:arrow-forward-ios")
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package site
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/gomarkdown/markdown"
|
|
||||||
"github.com/gomarkdown/markdown/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
var iifeBuildSize string
|
|
||||||
|
|
||||||
func upsertIIfeBuildSize() string {
|
|
||||||
if iifeBuildSize != "" {
|
|
||||||
return iifeBuildSize
|
|
||||||
}
|
|
||||||
build, err := staticFS.ReadFile("static/library/datastar.iife.js")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
w, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.Write(build); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
log.Printf("iife build size: %s", humanize.IBytes(uint64(buf.Len())))
|
|
||||||
iifeBuildSize = humanize.IBytes(uint64(buf.Len()))
|
|
||||||
return iifeBuildSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func markdownRenders(staticMdPath string) (mdElementRenderers map[string]string, mdAnchors map[string][]string, err error) {
|
|
||||||
mdDir := "static/md/" + staticMdPath
|
|
||||||
docs, err := staticFS.ReadDir(mdDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error reading docs dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// regExpImg := regexp.MustCompile(`(?P<whole>!\[[^\]]+]\((?P<path>[^)]+)\))`)
|
|
||||||
// prefix := []byte("/static/")
|
|
||||||
|
|
||||||
mdElementRenderers = map[string]string{}
|
|
||||||
mdAnchors = map[string][]string{}
|
|
||||||
for _, de := range docs {
|
|
||||||
fullPath := mdDir + "/" + de.Name()
|
|
||||||
|
|
||||||
b, err := staticFS.ReadFile(fullPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error reading doc %s: %w", de.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors := []string{}
|
|
||||||
lines := strings.Split(string(b), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.HasPrefix(line, "#") {
|
|
||||||
parts := strings.Split(line, " ")
|
|
||||||
anchor := strings.Join(parts[1:], " ")
|
|
||||||
anchors = append(anchors, anchor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Footnotes)
|
|
||||||
doc := mdParser.Parse(b)
|
|
||||||
renderedHTML := string(markdown.Render(doc, mdRenderer()))
|
|
||||||
|
|
||||||
name := de.Name()[0 : len(de.Name())-3]
|
|
||||||
mdElementRenderers[name] = renderedHTML
|
|
||||||
mdAnchors[name] = anchors
|
|
||||||
}
|
|
||||||
|
|
||||||
return mdElementRenderers, mdAnchors, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func KVPairsAttrs(kvPairs ...string) templ.Attributes {
|
|
||||||
if len(kvPairs)%2 != 0 {
|
|
||||||
panic("kvPairs must be a multiple of 2")
|
|
||||||
}
|
|
||||||
attrs := templ.Attributes{}
|
|
||||||
for i := 0; i < len(kvPairs); i += 2 {
|
|
||||||
attrs[kvPairs[i]] = kvPairs[i+1]
|
|
||||||
}
|
|
||||||
return attrs
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3dcb2b1820e15ee93b35c4825ef62649cd3ae1978b4db33ffcc61c1dab85d1f9
|
|
||||||
size 129778
|
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:e7d99edcb59f0299ff4ed5ad00baefe0b4e937008c7a9a297bc832654b04a8b7
|
|
||||||
size 22011
|
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:694e57fbc74689f5929069de2d8dcd311c052e30b488547fe34e5fc01ed90b0d
|
|
||||||
size 51314
|
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:ded8c96be2a77738ddbd2f43b9d6c49e2e4c40756c8fb12ee2a60d64d4a1a0ec
|
|
||||||
size 34557
|
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:877715a68505229029a7d08627225f98ab5fd7feede8b6945e17abdb4f10a0fe
|
|
||||||
size 748508
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Why another framework?
|
|
||||||
|
|
||||||
## What's different?
|
|
||||||
|
|
||||||
There are many things I find wonderful about Alpine.js and HTMX. However there are a few things that I think could be improved. The biggest challenge it both is plugins (called `directives`/`magic` in Alpine and `extensions` in HTMX). I tried writing in both of them as was disappointed by the breadth of monkey patching and lack of type safety. In general you don't have access to what other plugins do nor can you build a DAG of dependencies. This means you have to be very careful about the order you load plugins and what they do. This is especially true if you want to use a plugin in a plugin. I think this is a big problem and I wanted to solve it. Because Datastar started with the notion **_EVERYTHING_** is a plugin you could theoretically build both Alpine and HTMX inside of Datastar. I don't think that is a good idea but it is possible.
|
|
||||||
|
|
||||||
Beyond that I want the backend to be the _source of truth_ when it comes to state. [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) is a great way to structure your applications but you still need some client side state. Datstar does not use FormData; it lacks the ability to define nested data structures simply. So in general you send up JSON (automatically) and down HTML. This is more prescriptive than HTMX, but again this is with just the included plugins. You can build your own plugins to do whatever you want.
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
I love what [Caleb Porzio](https://calebporzio.com/) has done with [Alpine.js](https://alpinejs.dev/) and likewise with [Carson Gross](https://bigsky.software/cv/) with [HTMX](https://htmx.org/). However after trying to make extensions in both I got disappointed with the limitations. Both are pure Javascript which is fine but leads to having to either keep the whole state in your head and or be prepared for lots of runtime errors. Personally I think the best way to do this is to use Typescript and have the build step and compiler catch as many errors as possible. Plus it makes it easier to extend and create optimized builds.
|
|
||||||
|
|
||||||
### Why not just use Vue/React/Svelte?
|
|
||||||
|
|
||||||
[HTMX Essays](https://htmx.org/essays/) are a great place to start.
|
|
||||||
|
|
||||||
### Why not just use HTMX+Alpine?
|
|
||||||
|
|
||||||
There was a bit of a rant here before. TL;DR tried to show HTMX v2 could do all these things and it wasn't accepted by the community. I think HTMX is great and I will continue to recommend it. In general I think the some of the choices are throwing the baby out with the bathwater when it comes to things like Vite/TS/etc. Good news is that everything I, and some other lurkers, wanted are available in Datastar or can be added easily via plugins.
|
|
||||||
|
|
||||||
[Let's move on to the docs and see what Datastar can do](/docs)
|
|
|
@ -1,82 +0,0 @@
|
||||||
# Yes, you want a build step
|
|
||||||
|
|
||||||
## In response to the HTMX essay [No Build Step](https://htmx.org/essays/no-build-step/)
|
|
||||||
|
|
||||||
This is making the counter argument that a build step are extremely valuable when building a frontend framework. Lack of builds might be the right choice for HTMX but in my eyes its a self full-filling prophecy. The author concurrently wary of progress yet works on a library about extending the HTML spec to do more than it was designed for. The essay is written by a core contributor and not the author of HTMX. In general, its filled with so many fallacies but the real culprit is the _false dilemma fallacy_.
|
|
||||||
|
|
||||||
> This common fallacy misleads by presenting complex issues in terms of two inherently opposed sides. Instead of acknowledging that most (if not all) issues can be thought of on a spectrum of possibilities and stances, the false dilemma fallacy asserts that there are only two mutually exclusive outcomes.
|
|
||||||
> This fallacy is particularly problematic because it can lend false credence to extreme stances, ignoring opportunities for compromise or chances to re-frame the issue in a new way.
|
|
||||||
|
|
||||||
The whole discussion is framed as a false dilemma. Almost every point he makes is demonstrably false by the counter example of Datastar. Its smaller, faster, supports more features, isn't written in Javascript, targets multiple browser versions, and has a build step. If he is trying to prove his point it needs to be done with more rigor. In general I find the HTMX essays to be a beacon for well articulated arguments; this one is not and does a disservice to the community exploring hypermedia.
|
|
||||||
|
|
||||||
## TED Talk
|
|
||||||
|
|
||||||
In the first paragraph he says both:
|
|
||||||
|
|
||||||
> A recurring question from some htmx contributors is why htmx isn’t written in TypeScript, or, for that matter, why htmx **_lacks any build step at all_**. The full htmx source is a single 3,500-line JavaScript file; if you want to contribute to htmx, you do so by modifying the htmx.js file, the same file that gets sent to browsers in production, **_give or take minification and compression_**.
|
|
||||||
|
|
||||||
So before the essay starts he's given up the ghost. Even in as close to pure Javascript as you can get you still need a build step. Thank you, this has been my TED talk.
|
|
||||||
|
|
||||||
## Write once, run forever
|
|
||||||
|
|
||||||
> The best reason to write a library in plain JavaScript is that it lasts forever.
|
|
||||||
|
|
||||||
This is just not the world we live in. ES5 is dead, IE11 is dead, ECMAScript doesn't look anything like it did a decade ago, let alone 20 years ago. ECMAScript has been around for 25 years and has changed a lot in that time, let alone the APIs you are interfacing with. The web is a moving target and you need to keep up with it. If you don't you will be left behind.
|
|
||||||
|
|
||||||
## Reduction in labor
|
|
||||||
|
|
||||||
> Maintenance is a cost paid for with labor, and open-source code bases are the projects that can least afford to pay it. Opting not to use a build step drastically minimizes the labor required to keep htmx up-to-date.
|
|
||||||
|
|
||||||
No, just no. Much like CI/CD pipelines you set it up and largely forget about it. You can even automate the updates. The build step is a one time cost that pays dividends for the life of the project. If you don't have a build step you are going to be spending a lot of time manually updating your codebase. This is a false dichotomy. Datastar supports IIFE, UMD, & Modules and took all of adding the options to the build step. I've seen the crazy wrapper that HTMX uses to support all the different module types. It's a lot of code and a lot of maintenance. I'm not saying that HTMX is doing it wrong, just that it's a lot of work to support all the different module types. I'm not sure how much time it saves in the long run.
|
|
||||||
|
|
||||||
> But htmx is written in JavaScript, with no dependencies, so it will run unmodified for as long as web browsers remain relevant. Let the browser vendors do the hard work for you.
|
|
||||||
|
|
||||||
Every Datastar build or CDN version is a single Javascript file with no dependencies. The FUD here doesn't match reality at sets up a narrative of purity that is just not true. If fact oddly **because** of our build step we have full source mapping supported so you can debug your the production minified version in the browser looking at the original source and stack. This is a huge win for developers. I'm not sure why you would want to give that up.
|
|
||||||
|
|
||||||
## Source Maps and Hot Module Reloading
|
|
||||||
|
|
||||||
> Build step advocates point out that TypeScript can generate source maps, which tell your browser what TypeScript corresponds to what JavaScript, and that’s true! But now you have another thing to keep track of—the TypeScript you wrote, the JavaScript it generated, and the source map that connects these two. The hot-reloading development server you’re now dependent on will keep these up to date for you on localhost
|
|
||||||
|
|
||||||
Yes you have **_the same build folder_** with more static files that reference each other. This is not a big deal, a weird straw man, non-issue. You have to run a dev server to test your code anyway for things like CORS to work. This is just a weird argument, especially in light of things like hot module reloading for both code and styles. Modern tooling is now in the _sub milliseconds_ for rebuilds. Want to run your tests in parallel, well now you are just missing out on the real DX that modern tooling provides.
|
|
||||||
|
|
||||||
> ...but what about on your staging server? What about in production? Bugs that appear in these environments will be harder to track down, because you’ve lost a lot of information about where they come from.
|
|
||||||
|
|
||||||
Github actions for `bun build` or `vite build` are dead easy, and with weird arguments that are just not an issue nowadays. Like complaining about Makefiles when you have Visual Studio.
|
|
||||||
|
|
||||||
## The DX
|
|
||||||
|
|
||||||
> The htmx DX is very simple—your browser loads a single file, which in every environment is the exact same file you wrote. The tradeoffs required to maintain that experience are real, but they’re tradeoffs that make sense for this project.
|
|
||||||
|
|
||||||
The DX is why I personally moved on. The core monkey patches the DOM and that's just not a good DX. The core is not very extendable and you have to keep all 3000 LOC in your head to understand the ramification of your changes let alone the changes that other people make. You are giving up a lot of the DX that you get with a build step.
|
|
||||||
|
|
||||||
> Requiring that htmx remain in a single file (again, around 3,500 LOC) enforces a degree of intention on the library; there is a real pressure when working on the htmx source to justify the addition of new code, a pressure which maintains an equilibrium of relative simplicity.
|
|
||||||
|
|
||||||
Or... make everything an extension and allow them to rely on each other at there discretion. Making it a single file doesn't do anything but make harder to maintain.
|
|
||||||
|
|
||||||
## Amalgamation is a build step
|
|
||||||
|
|
||||||
> the lack of places for functionality to hide makes working on htmx a lot more approachable. Far, far more complex projects use aspects of this approach as well: SQLite3 compiles from a single-file source amalgamation (though they use separate files for development, they’re not crazy) which makes hacking on it significantly easier. You could never build the linux kernel this way—but htmx is not the linux kernel.
|
|
||||||
|
|
||||||
This is one of the worst takes in the whole essay. SQLite is **developed** as a set of files and **deployed** as an amalgamation. Just like Datastar, Alpine, HTMX, Zod, etc. You are conflating distribution with development and even making examples against you're own position.
|
|
||||||
|
|
||||||
> The htmx code does have to be written in JavaScript, though, because browsers run JavaScript. And as long as JavaScript is dynamically typed, the tradeoffs required to get true static typing in the htmx source are not worth it (htmx users can still take advantage of typed APIs, declared with .d.ts files).
|
|
||||||
|
|
||||||
The declaration (.d.ts) he point do are not accurate and aren't maintained last I checked. Also they are filled with `any` escape hatches which don't say anything. Separating definition into documentation seems to always cause this to happen. Conflating a build target with a language target. Like saying you have write in Assembly because x86 run assembly. Javascript can be a runtime target just like C can compile to assembly. You can write in Typescript and compile to Javascript. Another fallacy.
|
|
||||||
|
|
||||||
## Legacy targets stop progress
|
|
||||||
|
|
||||||
> Because htmx maintains support for Internet Explorer 11, and because it does not have a build step, every line of htmx has to be written in IE11-compatible JavaScript, which means no ES6.
|
|
||||||
|
|
||||||
This is certainly a choice and have seems as a hot button debate in the HTMX discord channel. If you had a build step, let alone used Typescript, you could target both **with the same codebase**. The false dilemma fallacy.
|
|
||||||
|
|
||||||
> This point is obvious, but it’s worth re-stating: the htmx source would be a lot tidier if it could be split it into modules. There are other factors that affect code quality besides tidiness, but to the extent that the htmx source is high-quality, it is not because it is tidy.
|
|
||||||
|
|
||||||
This is just a weird argument. You can split it into modules and still have tidiness. You can still split up into extensions, I know because I did it. To be clear, this isn't a declaration of minimal code sizes, but it _should_ be broken up into at least a per module/extension basis.
|
|
||||||
|
|
||||||
> Once you no longer have an enormous codebase of frontend JavaScript, there is far less pressure to adopt JavaScript on the backend. You can write backends in Python, Go, even NodeJS, and it doesn’t matter to htmx—every mainstream language has mature solutions for formatting HTML. This is the principle of Hypermedia On Whatever you’d Like (HOWL).
|
|
||||||
|
|
||||||
This essay is about HTMX build step, not the backend. It appears to me that not only is it clear you are throwing out the baby with the bathwater but demolishing the whole house. Just because SPA's are not the right tool for every job doesn't mean that you should throw out the whole tool box. We are talking about the best way to build a frontend library, not the backend.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Even HTMX has a build step, and if they embraced it they could support multiple targets while still keeping their single un-tree shakeable amalgamation that they enjoy developing with.
|
|
|
@ -1,26 +0,0 @@
|
||||||
## Active Search
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/active-search/)
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This example actively searches a contacts database as the user enters text.
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div id="active_search" data-on-load="$$get('/examples/active_search/data')"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
The interesting part is the input field:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<input
|
|
||||||
data-model="search"
|
|
||||||
data-on-input.debounce_1000ms="$$get('/examples/active_search/data')"
|
|
||||||
placeholder="Search..."
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
The input issues a `GET` to `/active_search/data` with the input value bound to `$search`. The `debounce_1000ms` modifier ensures that the search is not issued on every keystroke, but only after the user has stopped typing for 1 second. This modifiers will be covered in more detail in the [reference section](/reference).
|
|
|
@ -1,73 +0,0 @@
|
||||||
# Animations
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/animations/)
|
|
||||||
|
|
||||||
# Explanation
|
|
||||||
|
|
||||||
Datastar is designed to allow you to use CSS transitions to add smooth animations and transitions to your web page using only CSS and HTML. Below are a few examples of various animation techniques.
|
|
||||||
|
|
||||||
Datastar also allows you to use the new View Transitions API for creating animations.
|
|
||||||
|
|
||||||
<div id="animations" data-on-load="$$get('/examples/animations/data')"></div>
|
|
||||||
|
|
||||||
## Basic CSS Animations
|
|
||||||
|
|
||||||
### Color Throb
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div id="color_throb">Color Throb Demo</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
The simplest animation technique in Datastar is to keep the id of an element stable across a content swap. If the id of an element is kept stable, htmx will swap it in such a way that CSS transitions can be written between the old version of the element and the new one.
|
|
||||||
|
|
||||||
Consider this div:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div
|
|
||||||
id="color_throb"
|
|
||||||
class="transition-all duration-1000 font-bold text-2xl text-center rounded-box p-4 uppercase"
|
|
||||||
style="background-color:#b57614;color:#83a598"
|
|
||||||
>
|
|
||||||
blue on yellow
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
With SSE we just update the style every second:
|
|
||||||
|
|
||||||
### Smooth Progress Bar
|
|
||||||
|
|
||||||
The [Progress Bar](/examples/progress_bar) demo uses this basic CSS animation technique as well, by updating the `length` property of a progress bar element, allowing for a smooth animation.
|
|
||||||
|
|
||||||
## Swap Transitions
|
|
||||||
|
|
||||||
### Fade Out On Swap
|
|
||||||
|
|
||||||
<div id="fade_out_swap">Fade out swap Demo</div>
|
|
||||||
|
|
||||||
If you want to fade out an element that is going to be removed when the request ends, just send a SSE event with the opacity set to 0 and set a transition duration. This will fade out the element before it is removed.
|
|
||||||
|
|
||||||
## Settling Transitions
|
|
||||||
|
|
||||||
### Fade In On Addition
|
|
||||||
|
|
||||||
<div id="fade_me_in">Fade in Demo</div>
|
|
||||||
|
|
||||||
Building on the last example, we can fade in the new content the same way, starting from an opacity of 0 and transitioning to an opacity of 1.
|
|
||||||
|
|
||||||
## Request In Flight Animation
|
|
||||||
|
|
||||||
<div id="request_in_flight">Request in Flight Demo</div>
|
|
||||||
|
|
||||||
One of the nice features for reactivity is to show a spinner when a request is in flight. On any element that is using backend actions you can add a `data-indicator` attribute to show a spinner when the request is in flight. This can be done like so:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button
|
|
||||||
data-fetch-indicator="'#request_in_flight_indicator'"
|
|
||||||
data-on-click="$$post('/examples/animations/data/request_in_flight')"
|
|
||||||
id="submit_request_in_flight"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
This will show the element with the id `request_in_flight_indicator` when the request is in flight and hide it when the request is complete.
|
|
|
@ -1,56 +0,0 @@
|
||||||
## Bulk Update
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/bulk-update/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<style>
|
|
||||||
tr.deactivate.datastar-settling td {
|
|
||||||
background: lightcoral;
|
|
||||||
}
|
|
||||||
tr.activate.datastar-settling td {
|
|
||||||
background: darkseagreen;
|
|
||||||
}
|
|
||||||
tr td {
|
|
||||||
transition: all 1.2s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="bulk_update"
|
|
||||||
data-on-load="$$get('/examples/bulk_update/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is accomplished by putting a form around a table, with checkboxes in the table, and then including the checked values in PUT’s to two different endpoints: `activate` and `deactivate`:
|
|
||||||
|
|
||||||
Added to the page in this way:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<style>
|
|
||||||
tr.deactivate.datastar-settling td {
|
|
||||||
background: lightcoral;
|
|
||||||
}
|
|
||||||
tr.activate.datastar-settling td {
|
|
||||||
background: darkseagreen;
|
|
||||||
}
|
|
||||||
tr td {
|
|
||||||
transition: all 1.2s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
The server will either activate or deactivate the checked users and then rerender the tbody tag with updated rows. It will apply the class activate or deactivate to rows that have been mutated. This allows us to use a bit of CSS to flash a color helping the user see what happened. The server events look like this:
|
|
||||||
|
|
||||||
```go
|
|
||||||
event: datastar-fragment
|
|
||||||
id: 129798448043016254
|
|
||||||
data: selector
|
|
||||||
data: merge upsert_attributes
|
|
||||||
data: settle 500
|
|
||||||
data: fragment ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Notice the `settle` value is 500. This means the `datastar-settling` class will be added immediately and removed after 500ms. This is a nice way to show the user that something is happening.
|
|
|
@ -1,77 +0,0 @@
|
||||||
## Click to Edit
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/click-to-edit/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="contact_1"
|
|
||||||
data-on-load="$$get('/examples/click_to_edit/contact/1')"
|
|
||||||
>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
The click to edit pattern provides a way to offer inline editing of all or part of a record without a page refresh. This pattern starts with a UI that shows the details of a contact. The div has a button that will get the editing UI for the contact from `/contact/1/edit`
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Removed styling -->
|
|
||||||
<div id="contact_1">
|
|
||||||
<label>First Name: John</label>
|
|
||||||
<label>Last Name: Doe</label>
|
|
||||||
<label>Email: joe@blow.com</label>
|
|
||||||
<div>
|
|
||||||
<button data-on-click="$$get('/examples/click_to_edit/contact/1/edit')">
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button data-on-click="$$get('/examples/click_to_edit/contact/1/reset')">
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
This returns a form that can be used to edit the contact
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Removed styling and escaping for brevity -->
|
|
||||||
<div
|
|
||||||
id="contact_1"
|
|
||||||
data-store="{
|
|
||||||
id: 1,
|
|
||||||
firstName: 'John',
|
|
||||||
lastName: 'Doe',
|
|
||||||
email: 'joe@blow.com'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="form-control">
|
|
||||||
<label>First Name</label>
|
|
||||||
<input type="text" data-model="firstName" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Last Name</label>
|
|
||||||
<input type="text" data-model="lastName" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Email</label>
|
|
||||||
<input type="text" data-model="email" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button data-on-click="$$put('/examples/click_to_edit/contact/1')">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button data-on-click="$$get('/examples/click_to_edit/contact/1')">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### There is no form
|
|
||||||
|
|
||||||
If you compare to HTMX you'll notice there is no form, you can use one but its unnecessary. This is because you are already using signals and when you use a `PUT` to `/contact/1/edit`, the body is the entire contents of the store and its available to handle errors and validation holistically. There is also a profanity filter on the normal rendering of the contact that is not applied to the edit form. Controlling the rendering complete on the server allows you to have a single source of truth for the data and the rendering.
|
|
||||||
|
|
||||||
### There is no client validation
|
|
||||||
|
|
||||||
On the backend we've also added a quick santizer on the input to avoid bad actors (to some degree). You already have to deal with the data on the server so you might as well do the validation there. In this case its just modifying how the text is rendered when not editing. This is a simple example but you can see how you can extend it to more complex forms.
|
|
|
@ -1,28 +0,0 @@
|
||||||
## Dialogs
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/dialogs/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="dialogs"
|
|
||||||
data-on-load="$$get('/examples/dialogs_browser/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
Dialogs can be triggered with the standard browser `prompt` and `confirm` within an expression. These are triggered by the user interaction that would trigger the fetch, but the request is only sent if the dialog is accepted.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button
|
|
||||||
id="dialogs"
|
|
||||||
data-store="{prompt:'foo',confirm:false}"
|
|
||||||
data-fetch-url=""
|
|
||||||
data-on-click="$prompt=prompt('Enter a string',$prompt);$confirm=confirm('Are you sure?');$confirm && $$get('/examples/dialogs___browser/sure')"
|
|
||||||
>
|
|
||||||
Click Me
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
The value provided by the user to the prompt dialog is fed back into the store, and the confirm dialog is used to determine whether the request should be sent.
|
|
|
@ -1,46 +0,0 @@
|
||||||
## Edit Row
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/edit-row/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="edit_row"
|
|
||||||
data-on-load="$$get('/examples/edit_row/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This example shows how to implement editable rows. First let’s look at the row prior to editing:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<tr id="contact_0">
|
|
||||||
<td>Joe Smith<//td>
|
|
||||||
<td>joe@smith.org</td>
|
|
||||||
<td>
|
|
||||||
<button data-on-click="$editRowIndex=0;$$get('/examples/edit_row/edit')" >Edit</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
```
|
|
||||||
|
|
||||||
This will tell trigger a whole table replacement as we are going to remove the `Edit` buttons from other rows as well as change out to inputs to allow editing. The `data-on-click` attribute sets a variable `$editRowIndex` to the index of the row that is being edited. This is used to determine which row to edit on the server side. Again we don't need alpine or hyperscript to do this, signals and actions are built-in.
|
|
||||||
|
|
||||||
Finally, here is what the row looks like when the data is being edited:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<tr id="contact_0">
|
|
||||||
<td>
|
|
||||||
<input data-model="name" type="text" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input data-model="email" type="text" />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button data-on-click="$$get('/examples/edit_row/data')">Cancel</button>
|
|
||||||
<button data-on-click="$$patch('/examples/edit_row/edit')">Save</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we have a few things going on, clicking `Cancel` will bring back the read-only version of the row. Finally, there is a save button that issues a `PATCH` to update the contact.
|
|
|
@ -1,38 +0,0 @@
|
||||||
## Fetch Indicator
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div id="ind">Loading Indicator</div>
|
|
||||||
<button
|
|
||||||
class="bg-success-300 hover:bg-success-500 text-success-800 font-bold py-2 px-4 rounded"
|
|
||||||
data-on-click="$$get('/examples/fetch_indicator/greet')"
|
|
||||||
data-fetch-indicator="'#ind'"
|
|
||||||
data-testid="greeting_button"
|
|
||||||
data-bind-disabled="$$isFetching('#ind')"
|
|
||||||
>
|
|
||||||
Click me for a greeting
|
|
||||||
</button>
|
|
||||||
<div id="greeting"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="ind">Loading Indicator</div>
|
|
||||||
<button
|
|
||||||
data-on-click="$$get('/examples/fetch_indicator/greet')"
|
|
||||||
data-fetch-indicator="'#ind'"
|
|
||||||
data-bind-disabled="$$isFetching('#ind')"
|
|
||||||
>
|
|
||||||
Click me for a greeting
|
|
||||||
</button>
|
|
||||||
<div id="greeting"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
The `data-fetch-indicator` attribute is used to specify the elements that should be made visible when the fetch request is in progress. The value of the attribute is a CSS selector that can represent multiple elements. The same `data-fetch-indicator` selector can be used by different elements at the same time.
|
|
||||||
|
|
||||||
The `$$isFetching("#ind")` action returns a computed value that allows you to easily react to the state of the indicator.
|
|
||||||
|
|
||||||
**Note:** The contents of the `data-fetch-indicator` is an expression. In this case, the expression is a string literal, hence the single quotes around the CSS selector.
|
|
|
@ -1,42 +0,0 @@
|
||||||
## File Upload
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/file-upload/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="file_upload"
|
|
||||||
data-on-load="$$get('/examples/file_upload/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
In this example we show how to create a file upload form that will be submitted via fetch.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Removed styling for brevity -->
|
|
||||||
<div
|
|
||||||
id="file_upload" data-on-load="$$get"
|
|
||||||
data-store="{"files": [],"filesMimes": [],"filesNames": []}"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
<span class="label-text">Pick anything reasonably sized</span>
|
|
||||||
</label>
|
|
||||||
<input type="file" data-model="files" multiple>
|
|
||||||
<button
|
|
||||||
data-fetch-url="'/examples/file_upload/upload'"
|
|
||||||
data-on-click="$$post">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
We don't need a form because everything is encoded as signals and automatically sent to the server.
|
|
||||||
We `POST` the form to /upload, since the `input` is using `data-model` the file will be automatically encoded as base64. If your store includes `${signalName}Mimes` and `${signalName}Names` then those will be sent as well. All three signals are arrays and files / metainfo will be appended in the order of selection.
|
|
||||||
|
|
||||||
## Differences from HTMX
|
|
||||||
|
|
||||||
Since HTMX uses standard forms the [file upload is lost](https://htmx.org/examples/file-upload-input/) when the form is submitted. This is a limitation of the browser and not HTMX. However, since we are using signals we can send the file back down to the client and hook up to the signal if desired.
|
|
|
@ -1,47 +0,0 @@
|
||||||
## Infinite Scroll
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/infinite-scroll/)
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
The infinite scroll pattern provides a way to load content dynamically on user scrolling action.
|
|
||||||
|
|
||||||
Let’s focus on the final row (or the last element of your content):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div
|
|
||||||
data-intersects="$offset=0;$limit=10;$$get('/examples/infinite_scroll/data')"
|
|
||||||
id="more_btn"
|
|
||||||
>
|
|
||||||
<div>Loading...</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
This last element contains a listener which, when scrolled into view, will trigger a request. The result is then appended after it. The last element of the results will itself contain the listener to load the next page of results, and so on. `data-intersects` is a custom attribute that triggers a request when the element is scrolled into view. The `$offset` and `$limit` parameters are used to control the pagination of the results. The server sents back a new `more_btn` element with a new offset and the results to be appended.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
event: datastar-fragment
|
|
||||||
data: selector #more_btn
|
|
||||||
data: merge morph_element
|
|
||||||
data: settle 500
|
|
||||||
data: fragment <div data-intersects="$offset=10;$limit=10;$$get('/examples/infinite_scroll/data')" id="more_btn"><div>Loading...</div></div>
|
|
||||||
|
|
||||||
event: datastar-fragment
|
|
||||||
data: selector #click_to_load_rows
|
|
||||||
data: merge append_element
|
|
||||||
data: settle: 500
|
|
||||||
data: fragment <<tr id=\"agent_60\"><td>Agent Smith 3c</td><td>void61@null.org</td><td class=\"uppercase\">39b02fcf39c047c5</td></tr>"
|
|
||||||
|
|
||||||
....More rows to be appended
|
|
||||||
```
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
id="infinite_scroll"
|
|
||||||
data-on-load="$$get('/examples/infinite_scroll/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div id="more_btn"></div>
|
|
||||||
</div>
|
|
|
@ -1,17 +0,0 @@
|
||||||
## Inline Validation
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/inline-validation/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
The only email that will be accepted is test@test.com.
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="inline_validation"
|
|
||||||
data-on-load="$$get('/examples/inline_validation/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This example shows how to do inline field validation, in this case of an email address. To do this we need to create a form with an input that POSTs back to the server with the value to be validated and updates the DOM with the validation results. Since its easy to replace the whole form the logic for displaying the validation results is kept simple.
|
|
|
@ -1,32 +0,0 @@
|
||||||
## Lazy Load
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/lazy-load/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.datastar-settling {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="lazy_load" data-on-load="$$get('/examples/lazy_load/data')">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This example shows how to lazily load an element on a page. We start with an initial state that looks like this:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-fetch-url="" data-on-load="$$get('/examples/lazy_load/graph')">
|
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Which shows a progress indicator as we are loading the graph. The graph is then loaded and faded gently into view via a settling CSS transition:
|
|
||||||
|
|
||||||
```css
|
|
||||||
.datastar-settling {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
```
|
|
|
@ -1,15 +0,0 @@
|
||||||
## Lazy Tabs
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/tabs-hateoas/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="lazy_tabs"
|
|
||||||
data-on-load="$$get('/examples/lazy_tabs/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This example shows how easy it is to implement tabs using Datastar. Following the principle of [Hypertext As The Engine Of Application State](https://en.wikipedia.org/wiki/HATEOAS), the selected tab is a part of the application state. Therefore, to display and select tabs in your application, simply include the tab markup in the returned HTML fragment.
|
|
|
@ -1,11 +0,0 @@
|
||||||
## Merge Options
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div id="contents" data-on-load="$$get('/examples/merge_options/reset')"></div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
Shows the current merge options available. For best understanding open up your dev tools and look at the contents of `#contents` to see how the merge options effect the DOM.
|
|
||||||
|
|
||||||
** Note:** This uses `#target` and can create multiple elements with the same ID (which is [not valid HTML](https://html.spec.whatwg.org/#the-id-attribute)) but is fine for this example. In actual use be sure to make IDs unique.
|
|
|
@ -1,57 +0,0 @@
|
||||||
## Model Binding
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div id="container" data-on-load="$$get('/examples/model_binding/data')"></div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div
|
|
||||||
id="container"
|
|
||||||
data-store="{bindText:'foo',bindNumber:42,bindBool:true,bindSelection:1}"
|
|
||||||
>
|
|
||||||
<input type="text" data-model="bindText" />
|
|
||||||
<input type="number" data-model="bindNumber" />
|
|
||||||
<textarea data-model="bindText"></textarea>
|
|
||||||
<label>
|
|
||||||
<span>Checkbox</span>
|
|
||||||
<input type="checkbox" data-model="bindBool" />
|
|
||||||
</label>
|
|
||||||
<select data-model="bindSelection">
|
|
||||||
<option value="1">Option 1</option>
|
|
||||||
<option value="2">Option 2</option>
|
|
||||||
<option value="3">Option 3</option>
|
|
||||||
</select>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label>
|
|
||||||
<input type="radio" data-model="bindSelection" value="1" />
|
|
||||||
<span>Option 1</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" data-model="bindSelection" value="2" />
|
|
||||||
<span>Option 2</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" data-model="bindSelection" value="3" />
|
|
||||||
<span>Option 3</span>
|
|
||||||
</label>
|
|
||||||
...
|
|
||||||
</div>
|
|
||||||
<code>
|
|
||||||
<pre data-star-text="JSON.stringify(ctx.store(),null,2)"></pre>
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Current the two-way data binding generated by `data-model` supports
|
|
||||||
|
|
||||||
- `input`
|
|
||||||
- `textarea`
|
|
||||||
- `select`
|
|
||||||
- `checkbox`
|
|
||||||
- `radio`
|
|
||||||
|
|
||||||
**Note**: For radio buttons if you don't include a name for grouping the radio button will get the name of the signal as their name.
|
|
||||||
|
|
||||||
If there are more types that you need support for please [reach out](https://github.com/delaneyj/datastar)!
|
|
|
@ -1,24 +0,0 @@
|
||||||
## Multiline Fragments
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div id="replaceMe" data-on-load="$$get('/examples/multiline_fragments/data')">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
As long as the fragment has a newline at the end of the line it will be treated as a multiline fragment. This is useful when you are writing a lot of text by hand.
|
|
||||||
|
|
||||||
```text
|
|
||||||
event: datastar-fragment
|
|
||||||
data: merge morph_element
|
|
||||||
data: settle 500
|
|
||||||
data: fragment
|
|
||||||
data: <div id="replaceMe">
|
|
||||||
data: <pre>
|
|
||||||
data: This is a multiline fragment.
|
|
||||||
data:
|
|
||||||
data: Used when you are writing a lot of text by hand
|
|
||||||
data: </pre>
|
|
||||||
data: </div>
|
|
||||||
```
|
|
|
@ -1,125 +0,0 @@
|
||||||
## Explanation
|
|
||||||
|
|
||||||
An example backend in Node.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const express = require("express");
|
|
||||||
const { randomBytes } = require("crypto");
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
const backendData = {};
|
|
||||||
|
|
||||||
function indexPage() {
|
|
||||||
const indexPage = `
|
|
||||||
<!doctype html><html>
|
|
||||||
<head>
|
|
||||||
<title>Node/Express + Datastar Example</title>
|
|
||||||
<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"></script></head>
|
|
||||||
<body>
|
|
||||||
<h2>Node/Express + Datastar Example</h2>
|
|
||||||
<main class="container" id="main" data-store='{ input: "", show: false }'>
|
|
||||||
<input type="text" placeholder="Type here!" data-model="input" />
|
|
||||||
<button data-on-click="$$put('/put')">Send State</button>
|
|
||||||
<div id="output"></div>
|
|
||||||
<button data-on-click="$$get('/get')">Get Backend State</button>
|
|
||||||
<div id="output2"></div>
|
|
||||||
<button data-on-click="$show=!$show">Toggle</button>
|
|
||||||
<div data-show="$show">
|
|
||||||
<span>Hello From Datastar!</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Feed from server: </span>
|
|
||||||
<span id="feed" data-on-load="$$get('/feed')"></span>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
return indexPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
|
||||||
res.send(indexPage()).end();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setHeaders(res) {
|
|
||||||
res.set({
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
Connection: "keep-alive",
|
|
||||||
});
|
|
||||||
res.flushHeaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendSSE({ res, frag, selector, merge, mergeType, end }) {
|
|
||||||
res.write("event: datastar-fragment\n");
|
|
||||||
if (selector) res.write(`data: selector ${selector}\n`);
|
|
||||||
if (merge) res.write(`data: merge ${mergeType}\n`);
|
|
||||||
res.write(`data: fragment ${frag}\n\n`);
|
|
||||||
if (end) res.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.put("/put", (req, res) => {
|
|
||||||
setHeaders(res);
|
|
||||||
const { input } = req.body;
|
|
||||||
backendData.input = input;
|
|
||||||
const output = `Your input: ${input}, is ${input.length} long.`;
|
|
||||||
let frag = `<div id="output">${output}</div>`;
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: null,
|
|
||||||
merge: true,
|
|
||||||
mergeType: "morph_element",
|
|
||||||
end: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/get", (req, res) => {
|
|
||||||
setHeaders(res);
|
|
||||||
|
|
||||||
const output = `Backend State: ${JSON.stringify(backendData)}.`;
|
|
||||||
let frag = `<div id="output2">${output}</div>`;
|
|
||||||
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: null,
|
|
||||||
merge: true,
|
|
||||||
mergeType: "morph_element",
|
|
||||||
end: false,
|
|
||||||
});
|
|
||||||
frag = `<div id="output3">Check this out!</div>;`;
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: "#main",
|
|
||||||
merge: true,
|
|
||||||
mergeType: "prepend_element",
|
|
||||||
end: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/feed", async (req, res) => {
|
|
||||||
setHeaders(res);
|
|
||||||
while (res.writable) {
|
|
||||||
const rand = randomBytes(8).toString("hex");
|
|
||||||
const frag = `<span id="feed">${rand}</span>`;
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: null,
|
|
||||||
merge: false,
|
|
||||||
mergeType: null,
|
|
||||||
end: false,
|
|
||||||
});
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Server is running on http://localhost:${PORT}`);
|
|
||||||
});
|
|
||||||
```
|
|
|
@ -1,22 +0,0 @@
|
||||||
## On load
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
id="replaceMe"
|
|
||||||
data-on-load="$$post('/examples/on_load/data')"
|
|
||||||
>No session data</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="replaceMe" data-on-load="$$post('/examples/on_load/data')">
|
|
||||||
No session data
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
The `data-on-load` attribute is used to specify a fetch request that should be made when the element is loaded. The value of the attribute is a JavaScript expression that is evaluated when the element is loaded. This example comes from a [Github issue](https://github.com/delaneyj/datastar/issues/15) where the user wanted to load session data when the page was loaded.
|
|
||||||
|
|
||||||
**Note:** In this case its targeting the `#replaceMe` element but its up to the SSE returned to how the page is updated.
|
|
|
@ -1,15 +0,0 @@
|
||||||
## Progress Bar
|
|
||||||
|
|
||||||
[Original HTMX Version](https://htmx.org/examples/progress-bar/)
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="progress_bar"
|
|
||||||
data-on-load="$$get('/examples/progress_bar/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This example shows how to implement an updating progress graphic. Since Datastar is using SSE this is very easy to implement. The server sends a progress value every 500 milliseconds, and the client updates the progress bar accordingly sending down a new SVG. After the progress is complete, the server a button to restart the job.
|
|
|
@ -1,78 +0,0 @@
|
||||||
## Explanation
|
|
||||||
|
|
||||||
An example backend in Python.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import os, json, time, secrets, uvicorn
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.responses import HTMLResponse, StreamingResponse
|
|
||||||
|
|
||||||
app = Starlette()
|
|
||||||
target = 'target'
|
|
||||||
|
|
||||||
def send_index():
|
|
||||||
store = {'input': '', 'output': '', 'show': True}
|
|
||||||
index_page = f'''
|
|
||||||
<!doctype html><html>
|
|
||||||
<head>
|
|
||||||
<title>Node/Express + Datastar Example</title>
|
|
||||||
<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"></script></head>
|
|
||||||
<body>
|
|
||||||
<h2>Node/Express + Datastar Example</h2>
|
|
||||||
<main class="container" id="main" data-store=\'{json.dumps(store)}\'>
|
|
||||||
<input type="text" placeholder="Send to server..." data-model="input"/>
|
|
||||||
<button data-on-click="$$get('/get')">Send State Roundtrip</button>
|
|
||||||
<button data-on-click="$$get('/target')">Target HTML Element</button>
|
|
||||||
<button data-on-click="$show=!$show">Toggle Feed</button>
|
|
||||||
<div id="output" data-text="$output"></div>
|
|
||||||
<div id="{target}"></div>
|
|
||||||
<div data-show="$show">
|
|
||||||
<span>Feed from server: </span>
|
|
||||||
<span id="feed" data-on-load="$$get('/feed')"></span>
|
|
||||||
</div></main></body></html>
|
|
||||||
'''
|
|
||||||
return HTMLResponse(index_page)
|
|
||||||
|
|
||||||
def send_event(frag, merge=False):
|
|
||||||
yield 'event: datastar-fragment\n'
|
|
||||||
if merge:
|
|
||||||
yield 'data: merge upsert_attributes\n'
|
|
||||||
yield f'data: fragment {frag}\n\n'
|
|
||||||
|
|
||||||
def send_stream():
|
|
||||||
while True:
|
|
||||||
rand = secrets.token_hex(8)
|
|
||||||
frag = f'<span id="feed">{rand}</span>'
|
|
||||||
yield from send_event(frag)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
async def homepage(request):
|
|
||||||
return send_index()
|
|
||||||
|
|
||||||
@app.route('/get')
|
|
||||||
async def get_data(request):
|
|
||||||
store = json.loads(dict(request.query_params)['datastar'])
|
|
||||||
store['output'] = f"Your input: {store['input']}, is {len(store['input'])} long."
|
|
||||||
frag = f'<main id="main" data-store=\'{json.dumps(store)}\'></main>'
|
|
||||||
return StreamingResponse(send_event(frag, True))
|
|
||||||
|
|
||||||
@app.route('/target')
|
|
||||||
async def target_element(request):
|
|
||||||
today = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
frag = f'<div id="{target}"><b>{today}</b></div>'
|
|
||||||
return StreamingResponse(send_event(frag))
|
|
||||||
|
|
||||||
@app.route('/feed')
|
|
||||||
async def feed(request):
|
|
||||||
return StreamingResponse(send_stream())
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
uvicorn.run(app, host="localhost", port=int(os.environ.get('PORT', 3000)))
|
|
||||||
```
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="container">
|
|
||||||
<img src="foo.img" />
|
|
||||||
</div>
|
|
||||||
```
|
|
|
@ -1,31 +0,0 @@
|
||||||
## Request Animation Frame Update
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div data-store="{currentTime:Date.now()}" data-on-raf="$currentTime = Date.now()">
|
|
||||||
<div id="time">Current Time: <span data-text="new Date($currentTime).toLocaleString()">will be replaced by current time</span></div>
|
|
||||||
<pre data-text="JSON.stringify(ctx.store().value, null,2)"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
In the [Title Update Backend](/examples/title_update_backend) example we showed how to update the title of the page using a server sent event fragment. In this example we show how to update the title of the page using a requestAnimationFrame event on the client side.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div
|
|
||||||
data-store="{currentTime:Date.now()}"
|
|
||||||
data-on-raf="$currentTime = Date.now()"
|
|
||||||
>
|
|
||||||
<div id="time">
|
|
||||||
Current Time:
|
|
||||||
<span data-text="new Date($currentTime).toLocaleString()"
|
|
||||||
>will be replaced by current time</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<pre data-text="JSON.stringify(ctx.store().value, null,2)"></pre>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
`data-on-raf` is a special event that is triggered on every requestAnimationFrame event. This is useful for updating the UI at maximum at the rendering refresh rate of the browser. In this example we update the currentTime store with a new Date object. This triggers a re-render of the currentTime span element. You can still use the `throttle` and `debounce` modifiers to control the rate of updates even further.
|
|
||||||
|
|
||||||
In this case we are updating the currentTime store with the current time. This triggers a re-render of the `currentTime` span element, however if you inspect with the browser debugger you will notice that `#time`'s `<span>` element is not updated every frame. This is because the signals are smartly updated only when the value changes. This is a performance optimization that is done by default.
|
|
|
@ -1,10 +0,0 @@
|
||||||
## Redirects
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div id="update" data-on-load="$$get('/examples/redirects/data')">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
As part of SSE updates you may want to redirect the user to a different page. This can be done by returning a `data: redirect` event from the server. The client will then redirect the user to the specified URL.
|
|
|
@ -1,18 +0,0 @@
|
||||||
## Scroll into View
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
We are highlighting the middle paragraph of the page to make it easier to see the scroll into view effect. Depending on the size of your browser window, you may need to scroll to see the effect. In this specific example the options are sent to the server to then send down the scroll command but normally you would just send the scroll command directly from the server.
|
|
||||||
|
|
||||||
**Note:** The defaults are to have smooth centered scroll. If you want to also focus() the element just add the `.focus` modifier.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<p data-scroll-into-view.instant.vstart.hcenter id="p10"></p>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="replaceMe"
|
|
||||||
data-on-load="$$get('/examples/scroll_into_view/data')"
|
|
||||||
>No session data</div>
|
|
|
@ -1,18 +0,0 @@
|
||||||
## Shoelace Kitchensink
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/dark.css" />
|
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace-autoloader.js"></script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="shoelace_kitchensink"
|
|
||||||
data-on-load="$$get('/examples/shoelace_kitchensink/data')"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
[Shoelace](https://shoelace.style/) is a wonderful webcomponent library. It is a great fit for Datastar, because it is designed to be used with plain HTML. All the Datastar built-in plugins just work with Shoelace.
|
|
||||||
|
|
||||||
Note this is using a `nested` data store just to verify that works. The store gets sent will get sent to the server as the whole context of the client should be available to the server.
|
|
|
@ -1,78 +0,0 @@
|
||||||
## Store Changed
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
data-store="{clicks:0, _localState: { bar: 1234}, _anotherLocalVar: 'hello'}"
|
|
||||||
data-on-store-change.remote="$$post('/examples/store_changed/updates')"
|
|
||||||
>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<button
|
|
||||||
id="increment"
|
|
||||||
class="btn btn-success"
|
|
||||||
data-on-click="$clicks++"
|
|
||||||
>Click Me</button>
|
|
||||||
<button
|
|
||||||
id="clear"
|
|
||||||
class="btn btn-warning"
|
|
||||||
data-on-click="$clicks=0; $$delete('/examples/store_changed/updates')"
|
|
||||||
>Clear Local & Server</button>
|
|
||||||
<button
|
|
||||||
id="reload"
|
|
||||||
class="btn btn-error"
|
|
||||||
data-on-click="window.location.reload()"
|
|
||||||
>Reload Page</button>
|
|
||||||
</div>
|
|
||||||
<div id="local_clicks">Local Clicks: <span data-text="$clicks"></span></div>
|
|
||||||
<div id="from_server"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div
|
|
||||||
data-store="{clicks:0, _localState: { bar: 1234}, _anotherLocalVar: 'hello'}"
|
|
||||||
data-on-store-change.remote="$$post('/examples/store_changed/updates')"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<button id="increment" data-on-click="$clicks++">Click Me</button>
|
|
||||||
<button
|
|
||||||
id="clear"
|
|
||||||
data-on-click="$clicks=0; $$delete('/examples/store_changed/updates')"
|
|
||||||
>
|
|
||||||
Clear Local & Server
|
|
||||||
</button>
|
|
||||||
<button id="reload" data-on-click="window.location.reload()">
|
|
||||||
Reload Page
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>Local Clicks: <span data-text="$clicks"></span></div>
|
|
||||||
<div id="from_server"></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
`data-on-store-change` is a special event that is triggered when the store changes. This is useful for updating the UI when the store changes. In this example we update the `clicks` store with a new value. This triggers a re-render of the `clicks` span element. You can still use the `throttle` and `debounce` modifiers to control the rate of updates even further. In this case we are sending the store changes to the server to update the lifetime total clicks the server has seen.
|
|
||||||
|
|
||||||
**Note**: The `.remote` modifier is used to only trigger this event when remotely viewable signals are updated. This is useful for not sending data that is not needed to the server. To look at the details run `console.log(JSON.stringify(ds.store.value,null,2))` in the browser console. You should see something like
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"_dsPlugins": {
|
|
||||||
"on": {}
|
|
||||||
},
|
|
||||||
"_sidebarOpen": false,
|
|
||||||
"clicks": 0,
|
|
||||||
"_localState": {
|
|
||||||
"bar": 1234
|
|
||||||
},
|
|
||||||
"_anotherLocalVar": "hello"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Whereas if you look at the Network tab in the browser you should see the following request payload
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "clicks": 0 }
|
|
||||||
```
|
|
||||||
|
|
||||||
Any signal (or nested set of signals) starting with an underscore `_` is considered local and will not be sent to the server. In this example `_localState` and `_anotherLocalVar` are local only.
|
|
|
@ -1,19 +0,0 @@
|
||||||
## Title Update Backend
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
Look at the title change in the browser tab!
|
|
||||||
|
|
||||||
<div data-on-load="$$get('/examples/title_update_backend/updates')"></div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
A user [in the Discord channel](https://discord.com/channels/725789699527933952/1180902694999838752) was asking about needing a plugin similar to [HTMX's head support](https://v1.htmx.org/extensions/head-support/) to update title or head elements. With Datastar this is unneccessary as you can just update the title directly with a SSE fragment
|
|
||||||
|
|
||||||
```
|
|
||||||
event: datastar-fragment
|
|
||||||
data: selector title
|
|
||||||
data: fragment <title>08:30:36 from server</title>
|
|
||||||
```
|
|
||||||
|
|
||||||
In a similar fashion you can append, prepend directly to the HEAD element using [Merge Options](/examples/merge_options)
|
|
|
@ -1,31 +0,0 @@
|
||||||
## Update Store directly
|
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
<div
|
|
||||||
data-store='{"stuffAlreadyInStore":"this is already in the store"}'
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-success"
|
|
||||||
data-on-click="$$post('/examples/update_store/data/patch')"
|
|
||||||
>
|
|
||||||
Apply random signal patch
|
|
||||||
</button>
|
|
||||||
<pre
|
|
||||||
class="bg-accent border-2 border-accent text-accent text-sm rounded-lg focus:ring-primary focus:border-primary block w-full p-2.5"
|
|
||||||
data-text="JSON.stringify(ctx.store(),null,2)"
|
|
||||||
>
|
|
||||||
Stuff in store
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Explanation
|
|
||||||
|
|
||||||
This example demonstrates how to update the store directly from the frontend. This is using a SSE event
|
|
||||||
|
|
||||||
```text/event-stream
|
|
||||||
event: datastar-signal,
|
|
||||||
data: { HYAAA4BK7IFQE: "2024-05-23T10:33:14.167189214-07:00", stuffAlreadyInStore: "this is already in the store"}
|
|
||||||
```
|
|
||||||
|
|
||||||
It will accept anything that is also accepted by the `data-store` attribute.
|
|
|
@ -1,51 +0,0 @@
|
||||||
# Batteries Included
|
|
||||||
|
|
||||||
## Build modular, think globally
|
|
||||||
|
|
||||||
Datastar was originally a branch of HTMX that was meant to make it more modular and easier to write plugins for. What was not expected is in creating what felt like essential plugins was actually able to recreate all the needed features of a full SPA frameworks, HTMX and Alpine and do it all in smaller package. While you can still build your own plugins, the core set of plugins is enough to build a full application. Let's explore some more features
|
|
||||||
|
|
||||||
## Evaluating data-\* attributes
|
|
||||||
|
|
||||||
Before we had used something like `data-text="$foo"` attribute. What this is actually doing under the hood is turning the string into a function that is evaluated safely into `(ctx)=> ctx.store.foo.value` where `ctx` is the Datastar object that was injected into the page. In this case its connected directly to the contents of a signal but you could do any javascript that is valid. This also means you could eval to a constant string but that would looks something like `data-text="'hello world'"`, notice the quotes, otherwise it would be looking for variable `hello world` which isn't valid and would fail to evaluate. This is the one major gotcha of declarative code, you have to be careful about the context.
|
|
||||||
|
|
||||||
## More attributes
|
|
||||||
|
|
||||||
So far we've seen `data-store`, `data-model` and `data-text`. There are many more attributes that can be used to make your page more reactive. Check out the API for a full list of attributes but here are a few more that get used the most.
|
|
||||||
|
|
||||||
### `data-show`
|
|
||||||
|
|
||||||
This is a simple way to show and hide elements. It's a simple way to do `v-if` in Vue or `x-show` in Alpine.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-show="$foo">Hello World</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
When the `foo` signal is truthy the div will be shown, otherwise it will be hidden.
|
|
||||||
|
|
||||||
### `data-on`
|
|
||||||
|
|
||||||
This is a way to add event listeners to elements. It's a simple way to do `v-on` in Vue or `x-on` in Alpine.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button data-on-click="console.log('hello world')">Click me</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
When the button is clicked it will log `hello world` to the console.
|
|
||||||
|
|
||||||
## Actions
|
|
||||||
|
|
||||||
Actions are helper functions that are made available during a data-\* attribute evaluation. They are a way to do more complex operations without having to write a lot of javascript. Here are a few of the most common actions. They are prefixed with `$$` to avoid any conflicts with other attributes.
|
|
||||||
|
|
||||||
### `$$setAll`
|
|
||||||
|
|
||||||
Sometimes you want to set multiple values at once. It takes a prefix regexp and a set of signals. For example if you have a form with a bunch of inputs and you want to set them all at once you could do something like this.
|
|
||||||
|
|
||||||
```html
|
|
||||||
data-text="$$setAll('contact_',true)`
|
|
||||||
```
|
|
||||||
|
|
||||||
### `$$toggleAll`
|
|
||||||
|
|
||||||
Works the same as `$$setAll` but will flip the value of the signals.
|
|
||||||
|
|
||||||
There can be many more actions and you can even write your own. But the majority of the time you'll be using the actions that allow for backend integration, which we'll cover in the next section.
|
|
|
@ -1,359 +0,0 @@
|
||||||
# Getting Started
|
|
||||||
|
|
||||||
## A Birds Eye View
|
|
||||||
|
|
||||||
If you are familiar with libraries like [HTMX](https://htmx.org/) or [AlpineJs](https://alpinejs.dev/); Datastar brings them together. This breaks down essentially to:
|
|
||||||
|
|
||||||
1. Send the current UI from your backend via HTML fragments like HTMX.
|
|
||||||
2. Manage client side state that wouldn't make sense to be managed by your backend like AlpineJS.
|
|
||||||
|
|
||||||
I've had [thoughts](/essays/why_another_framework) on both of these in the past. TLDR; While both libraries are great, I wanted to go in a different direction.
|
|
||||||
|
|
||||||
Datastar accomplishes both tasks in a unified manner and it's [tiny](https://bundlephobia.com/package/@sudodevnull/datastar).
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
To get started you must first get a copy of Datastar. There are a few ways to do this.
|
|
||||||
|
|
||||||
**Remotely**
|
|
||||||
|
|
||||||
You can include it directly into your html using a script tag:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script
|
|
||||||
type="module"
|
|
||||||
defer
|
|
||||||
src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"
|
|
||||||
></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
**NPM**
|
|
||||||
|
|
||||||
For npm-style build systems, you can install Datastar via npm and then import this in your server file.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i @sudodevnull/datastar
|
|
||||||
```
|
|
||||||
|
|
||||||
## A Quick Primer
|
|
||||||
|
|
||||||
Now let's get our feet wet. We'll walk through some ways to use Datastar with a quick example. For our example we'll just spin up an [Express](https://expressjs.com/en/starter/hello-world.html) server on Node. We'll have the server prepare a template for us when we first navigate to it.
|
|
||||||
|
|
||||||
You can copy the code below to get started. Don't worry, we've already installed Datastar for you using a [CDN](#remotely).
|
|
||||||
|
|
||||||
```js
|
|
||||||
const express = require("express");
|
|
||||||
const { randomBytes } = require("crypto");
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
const backendData = {};
|
|
||||||
|
|
||||||
function indexPage() {
|
|
||||||
const indexPage = `
|
|
||||||
<!doctype html><html>
|
|
||||||
<head>
|
|
||||||
<title>Node/Express + Datastar Example</title>
|
|
||||||
<script type="module" defer src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"></script></head>
|
|
||||||
<body>
|
|
||||||
<h2>Node/Express + Datastar Example</h2>
|
|
||||||
<main class="container" id="main"></main>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
return indexPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get("/", (req, res) => {
|
|
||||||
res.send(indexPage()).end();
|
|
||||||
});
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Server is running on http://localhost:${PORT}`);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
We have basic server set up. Now let's get to the fun part. Let's add some Datastar functionality.
|
|
||||||
|
|
||||||
## Handling State
|
|
||||||
|
|
||||||
Let's start out with how Datastar handles state. Enter the [store](/reference/plugins_core#merge-store) attribute.
|
|
||||||
|
|
||||||
Go ahead and add this as a data attribute to the `<main>` element:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<main class="container" id="main" data-store='{ "input": "" }'></main>
|
|
||||||
```
|
|
||||||
|
|
||||||
This is the global store, if you make multiple stores they will actually merge into one store behind the scenes. If there are two fields with the same name, Datastar will resolve as last in wins.
|
|
||||||
|
|
||||||
The store is great and all but how can we use it? There are many ways. Let's check some of them out.
|
|
||||||
|
|
||||||
## Some Reactivity
|
|
||||||
|
|
||||||
If you had a keen eye, you noticed we put `input` as a field on our store. What's that about? Glad you asked! It's Datastar's way to sets up two-way data binding on an element. In our case this `input` element. Say hi to the [Model](/reference/plugins_attributes#model) attribute.
|
|
||||||
|
|
||||||
Stick this inside of your `<main>` element:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<input type="text" placeholder="Type here!" data-model="input" />
|
|
||||||
```
|
|
||||||
|
|
||||||
This binds to a signal so our store can stay up to date with whatever is typed into this input. You can even nest your state like this `{"nested":{"label":"foo"}}` and use `data-model="nested.label"` or access it from the backend as needed.
|
|
||||||
|
|
||||||
Good stuff so far. How can we see this? We can check the changes locally using the [data-text](/reference/plugins_attributes#text) attribute.
|
|
||||||
|
|
||||||
Create a div in your `<main>` Element:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-text="$input"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Sets the text content of an element to the value of the signal. Now check it out, client-side reactivity! We can have different types of state as well. We can even do fun stuff like `data-text="$value.toUpperCase()"`.
|
|
||||||
|
|
||||||
Speaking of which, let's do some more! Let's play hide 'n seek with the [data-show](/reference/plugins_visibility#show) attribute.
|
|
||||||
|
|
||||||
Add this to your store:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{ input: "", show: false };
|
|
||||||
```
|
|
||||||
|
|
||||||
We can hide elements and show them without using JavaScript! How will we trigger this though?
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
We bring in the [On](/reference/plugins_attributes#on) attribute. This sets up an event listener on an element. In this example, we're using `data-on-click`. You will later see there are other `data-on` actions we can utilize. You can also do silly things like `data-on-click="console.log('hello world')"`.
|
|
||||||
|
|
||||||
Add this inside of your `<main>` element:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button data-on-click="$show=!$show">Toggle</button>
|
|
||||||
<div data-show="$show">
|
|
||||||
<span>Hello From Datastar!</span>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
So what else can we do? You can mess around and do some fun stuff with expressions. For instance, something like: `"$prompt=prompt('Enter something',$prompt);$confirm=confirm('Sure?');$confirm && $$get('/sure')"` is totally feasible.
|
|
||||||
|
|
||||||
Anyhow, we haven't really even scratched the surface. Let's keep going.
|
|
||||||
|
|
||||||
## Backend Plumbing
|
|
||||||
|
|
||||||
Now, let's send some data. To do this there's a few things we must understand but it's all fun and easy and you'll want to know it if you do not already!
|
|
||||||
|
|
||||||
Datastar uses [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) or SSE. To use SSE, we have to set our backend up for it. Luckily it's extremely simple and [provides us with many advantages](/essays/event_streams_all_the_way_down).
|
|
||||||
|
|
||||||
Let's set things up. Copy the below code to your server.
|
|
||||||
|
|
||||||
Copy this to your server code:
|
|
||||||
|
|
||||||
```js
|
|
||||||
function setHeaders(res) {
|
|
||||||
res.set({
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Content-Type": "text/event-stream",
|
|
||||||
Connection: "keep-alive",
|
|
||||||
});
|
|
||||||
res.flushHeaders();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`setHeaders` is simple a utility function we will use on our endpoints to set our headers to use SSE.
|
|
||||||
|
|
||||||
Copy this to your server code:
|
|
||||||
|
|
||||||
```js
|
|
||||||
function sendSSE({ res, frag, selector, merge, mergeType, end }) {
|
|
||||||
res.write("event: datastar-fragment\n");
|
|
||||||
if (selector) res.write(`data: selector ${selector}\n`);
|
|
||||||
if (merge) res.write(`data: merge ${mergeType}\n`);
|
|
||||||
res.write(`data: fragment ${frag}\n\n`);
|
|
||||||
if (end) res.end();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We will use `sendSSE` as another utility function that will help us configure our response to fit SSE and Datastar formats. Let's check that out real quick.
|
|
||||||
|
|
||||||
## Stay In Formation
|
|
||||||
|
|
||||||
SSE messages are text-based and consist of one or more "events". Each event is separated by a pair (`\n\n`) of newline characters. An individual event consists of one or more lines of text, each followed by a newline character (`\n)`), and uses a simple key-value pair [format](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format).
|
|
||||||
|
|
||||||
For our Datastar example:
|
|
||||||
|
|
||||||
```
|
|
||||||
event: datastar-fragment // \n
|
|
||||||
id: 129618219840307262 // \n
|
|
||||||
data: merge morph_element // \n
|
|
||||||
data: fragment <div id="id">...</div> // \n\n
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
**NOTE** in the real message the comments and newlines wouldn't be visible
|
|
||||||
|
|
||||||
Each data message in the event is separated by a (`\n`) newline and each event is separated by a pair of (`\n\n`) newlines. If you notice that is what we are doing in `sendSSE` except with a little flavor added so we can tell Datastar what we want to do using [Datastar's format](/reference/plugins_backend#datastar-sse-event).
|
|
||||||
|
|
||||||
Now let's make our route.
|
|
||||||
|
|
||||||
Copy this to your server code:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.put("/put", (req, res) => {
|
|
||||||
setHeaders(res);
|
|
||||||
const { input } = req.body;
|
|
||||||
backendData.input = input;
|
|
||||||
const output = `Your input: ${input}, is ${input.length} long.`;
|
|
||||||
let frag = `<div id="output">${output}</div>`;
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: null,
|
|
||||||
merge: true,
|
|
||||||
mergeType: "morph_element",
|
|
||||||
end: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
So here you see we're setting our headers with `setHeaders`. We modify state that's stored specifically on the backend. This can be anything you want, like a database. Then we construct the response string much like HTMX and include the store attribute. We send the response with the `morph_element` merge type.
|
|
||||||
|
|
||||||
We need to make some changes now to reflect this.
|
|
||||||
|
|
||||||
Go ahead and modify your html.
|
|
||||||
|
|
||||||
Change this in your `<main>` element:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-text="$input"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
To this:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="output"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Give ourselves a button to perform this action.
|
|
||||||
|
|
||||||
Add this to your `<main>` element:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button data-on-click="$$put('/put')">Send State</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
...and give ourselves a place to show our new state on the client.
|
|
||||||
|
|
||||||
Voila! Now if you check out what you've done, you'll find you're able to send data to your `/put` endpoint and respond with HTML updating the output `div`. Neato!
|
|
||||||
|
|
||||||
Let's retrieve the backend data we're now storing.
|
|
||||||
|
|
||||||
Add this to your server code:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.get("/get", (req, res) => {
|
|
||||||
setHeaders(res);
|
|
||||||
|
|
||||||
const output = `Backend State: ${JSON.stringify(backendData)}.`;
|
|
||||||
let frag = `<div id="output2">${output}</div>`;
|
|
||||||
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: null,
|
|
||||||
merge: true,
|
|
||||||
mergeType: "morph_element",
|
|
||||||
end: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
And this to your HTML:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button data-on-click="$$get('/get')">Get Backend State</button>
|
|
||||||
<div id="output2"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
We're now fetching state that's stored on the backend.
|
|
||||||
|
|
||||||
Let's try something for fun. In your `/get` route, change your call to `sendSSE` so that we do not immediately end the request connection.
|
|
||||||
|
|
||||||
Change your `sendSSE` function call in your `\get` route.
|
|
||||||
|
|
||||||
```js
|
|
||||||
sendSSE({
|
|
||||||
...
|
|
||||||
end: false,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Add this to your `sendSSE` function below the first call:
|
|
||||||
|
|
||||||
```js
|
|
||||||
frag = `<div id="output3">Check this out!</div>;`;
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: "#main",
|
|
||||||
merge: true,
|
|
||||||
mergeType: "prepend_element",
|
|
||||||
end: true,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Now you'll notice you're sending two events in one call. That's because Datastar uses SSE. So using `prepend_element` we're able to prepend what we want to a target element. We do this using a `selector` and in our case this is the `<main>` element. Good stuff! You can check out all of Datastar's event types [here](http://localhost:8080/reference/plugins_backend).
|
|
||||||
|
|
||||||
There's one last thing we're going to do. Let's add a simple data feed upon loading the page.
|
|
||||||
|
|
||||||
Copy this to your server code:
|
|
||||||
|
|
||||||
```js
|
|
||||||
app.get("/feed", async (req, res) => {
|
|
||||||
setHeaders(res);
|
|
||||||
while (res.writable) {
|
|
||||||
const rand = randomBytes(8).toString("hex");
|
|
||||||
const frag = `<span id="feed">${rand}</span>`;
|
|
||||||
sendSSE({
|
|
||||||
res,
|
|
||||||
frag,
|
|
||||||
selector: null,
|
|
||||||
merge: false,
|
|
||||||
mergeType: null,
|
|
||||||
end: false,
|
|
||||||
});
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Add this inside your `<main>` element:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div>
|
|
||||||
<span>Feed from server: </span>
|
|
||||||
<span id="feed" data-on-load="$$get('/feed')"></span>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
I told you we would use another `data-on` action earlier and here it is. `data-on-load` will perform this request when the page loads. If you check things out now you should see a feed that updates using SSE upon loading. Cool!
|
|
||||||
|
|
||||||
Datastar supports all the verbs without requiring a `<form>` element: `GET, POST, PUT, PATCH, DELETE`.
|
|
||||||
|
|
||||||
So that concludes our primer! Check out the full code for our Node example [here](/examples/node).
|
|
||||||
|
|
||||||
If you're still here I imagine you want to know more. Let's define things a little better.
|
|
||||||
|
|
||||||
## A Better View
|
|
||||||
|
|
||||||
To be more precise, think of Datastar as an extension to HTML's [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). Using attributes, you can introduce state to your frontend, then access it anywhere in your DOM, or a backend of your choice. You can also setup events that trigger endpoints, then respond with HTML that targets fragments of your DOM.
|
|
||||||
|
|
||||||
- Declare global state: `data-store = "{foo: ''}"`
|
|
||||||
- Link-up HTML elements to state slots: `data-model = "foo"`
|
|
||||||
- Adjust HTML elements text content: `data-text = "$foo"`
|
|
||||||
- Hookup other effects on your DOM to the state: `data-show= "$foo"`
|
|
||||||
- Setup events using `data-on-(load or click) = "$$get(/endpoint)"`
|
|
||||||
- Respond in HTML wrapped in SSE with a target element ID to update
|
|
||||||
|
|
||||||
It's that simple. To dive deeper check out some of the other links or just click below.
|
|
|
@ -1,134 +0,0 @@
|
||||||
# Go Deeper
|
|
||||||
|
|
||||||
## Javascript Fatigue
|
|
||||||
|
|
||||||
Web development has become a technical occultism activity, in which the focus is on JavaScript and the capabilities of making HTML content dynamic instead of making a better job of delivering HTML. This JavaScript religion has led to the rise of frontend frameworks such as React, Vue, Svelte, Solid, etc. In turn those were not enough for full application and progress led the industry to full-stack JavaScript frameworks like Next.js, Nuxt, Svelte and Solid Start. Once you need a framework for reactivity it makes sense to embrace it in the backend too for consistency.
|
|
||||||
|
|
||||||
In reality almost all frameworks come down to updating the DOM as fast and as simply as possible with some effort around improving developer experience.
|
|
||||||
|
|
||||||
## Philosophy
|
|
||||||
|
|
||||||
- **Be declarative**
|
|
||||||
- **Use signals**
|
|
||||||
- **Supply a set of plugins that handle 99% of problems**
|
|
||||||
|
|
||||||
Datastar started as just a plugin framework but found that by having no overlap in features, it was possible to replace any SPA framework and even hypermedia focused libraries like HTMX while being much smaller and _(we think)_ easier to use.
|
|
||||||
|
|
||||||
With Datastar, even if you have never coded before, with a few examples, you can easily create high interconnected web assets. It doesn't matter if you are a making a user interface for bank or a simple blog. The approach is simplicity through declarative HTML.
|
|
||||||
|
|
||||||
If Datastar doesn't match your needs, you still might be interested in using it as originally intended [and write your own library](https://github.com/delaneyj/datastar/tree/main/library/src/lib/plugins).
|
|
||||||
|
|
||||||
## Time to think declaratively
|
|
||||||
|
|
||||||
Declarative code is amazing.
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT *
|
|
||||||
FROM orders
|
|
||||||
WHERE state='completed'
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT 10
|
|
||||||
```
|
|
||||||
|
|
||||||
This is SQL. As a user you don't have to know how the query will get executed, its up to the engine used. Sure you can read the query plan or introspect indexes but you can start at a high level and drill down only when necessary.
|
|
||||||
|
|
||||||
HTML work in a similar fashion. You don't have to worry about how a `<div>Hello</div>` turned into pixels nor how the page uses resources when you tab away. This is wonderful for the majority of cases but at least in modern HTML is a bit limiting.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```html
|
|
||||||
...PSEUDO CODE..
|
|
||||||
<div>
|
|
||||||
<label>HELLO WORLD</label>
|
|
||||||
<input value="hello world" />
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
if you want to use the result of changing the input to modify the label you have to write code. What if we could extend HTML to make that process declarative? Well funny you ask...
|
|
||||||
|
|
||||||
## Custom data attributes
|
|
||||||
|
|
||||||
Luckily HTML5 has the concept of [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) which allows anyone to add thier own attributes and still be valid HTML. The only real requirement is they be kebab-cased and start with `data-*`. Data star... that'd be a clever name... oh I get it!
|
|
||||||
|
|
||||||
Before we can actually use the `data-*` we need a quick aside about signals.
|
|
||||||
|
|
||||||
## Signals
|
|
||||||
|
|
||||||
Signals are a way to do fine grain reactivity in a very efficient way. They are similar to formulas in Excel. Instead of doing something like:
|
|
||||||
|
|
||||||
```js
|
|
||||||
let a = 2;
|
|
||||||
let b = 2;
|
|
||||||
let c = a * b;
|
|
||||||
console.log(c);
|
|
||||||
```
|
|
||||||
|
|
||||||
You can do:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const a = signal(2);
|
|
||||||
const b = signal(2);
|
|
||||||
const c = computed(() => a() * b());
|
|
||||||
effect(() => {
|
|
||||||
console.log(c());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
The difference is if you change `a` or `b`, `c` will auto schedule for updates. So you spend your time declaring relationships more than procedures. They have been popularized by [Solid,js](https://www.solidjs.com/) but are now used by many frameworks.
|
|
||||||
|
|
||||||
## The Store
|
|
||||||
|
|
||||||
Ok so back to our hypothetical framework let's have a way to declare stuff that can setup signals on the page using `data-*` attributes.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-store="{value:'hello world'}"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
The contents is just a set of data that can evaluated by [JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) and will convert this data into a tree of signals and merge into a store that tracks all of the reactivity on the page. In this case we want there to be a a single `value` signal with the contents of `'hello world'`.
|
|
||||||
|
|
||||||
## Models
|
|
||||||
|
|
||||||
Let's replace the hard coded value in the input with another attribute
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-store="{value:'hello world'}">
|
|
||||||
<label>HELLO WORLD</label>
|
|
||||||
<input data-model="value" />
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we've created a new attribute `data-model` with the contents of `value`. We are just saying when the signal `value` changes **or** input is edited on the page make sure you keep them in sync. We don't care how, just do it.
|
|
||||||
|
|
||||||
## Contents
|
|
||||||
|
|
||||||
Now we want to update the label relationship.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-store="{value:'hello world'}">
|
|
||||||
<label data-text="$value"></label>
|
|
||||||
<input data-model="value" />
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we've added another attribute `data-text` but the content has a `$` prefix. Remember that `data-*` attributes _are just strings_, which means we can give any semantics we want as long as it's consistent. It this case we are designating the use of a signal by adding a `$` prefix. Now went the `value` signal updates, so will the `innerHTML` of the label. Pretty neat.
|
|
||||||
|
|
||||||
However it doesn't yet match the original intent, which was to make it uppercase, so let's make a quick adjustment.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-store="{value:'hello world'}">
|
|
||||||
<label data-text="$value.toUpperCase()"></label>
|
|
||||||
<input data-model="value" />
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
So with this change and in a declarative nature you'd be able extend HTML and focus on relationships.
|
|
||||||
|
|
||||||
Our HTML looks pretty neat but what would it take to make it actually work? Well just add:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@sudodevnull/datastar"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
somewhere on your page and it should **_just work_**. If you look at the dev tools of your browser you'll also see its tiny compared to any other full framework and we haven't scratched the surface of what's included.
|
|
||||||
|
|
||||||
So far looking a lot like [Alpine.js](https://alpinejs.dev/) which is great, but as we add more features will diverge.
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Hypermedia, lost knowledge and H.A.T.E.O.A.S.
|
|
||||||
|
|
||||||
The rise of the frontend framework wars makes sense. When compared with full page reloads, the ability to update only the parts of the page that need to change is a huge win. However, the way this is done is often at odds with the declarative nature of HTML. This has led to a lot of lost knowledge and a lot of confusion. When your application logic lives in both browser and backend your synchronization state is in constant flux.
|
|
||||||
|
|
||||||
One of the biggest loses of knowledge is the concept of hypermedia. Hypermedia is the idea that the web is a set resources that are interconnected. This is the basis of the web and the reason it has been so successful. However, the rise of the frontend framework wars has led to a lot of confusion about how to use hypermedia. This is where H.A.T.E.O.A.S. comes in.
|
|
||||||
|
|
||||||
## Hypermedia as the Engine of Application State (H.A.T.E.O.A.S.)
|
|
||||||
Browsers don't care about your application, they care about the rendering hypermedia. For example if you have a bank website you can go to the home page and then click on a link to go to the login page, only then are valid links to your accounts available. This has huge benefits.
|
|
||||||
1. Each interaction fuels the valid next state.
|
|
||||||
2. When implemented correctly this means the backend is where all your logic lives, leading to no client side routers, validation, etc.
|
|
||||||
3. HTML can be generated in any language.
|
|
||||||
|
|
||||||
## Hypermedia on whatever you Like (HOWL)
|
|
||||||
|
|
||||||
[Carson Gross](https://hypermedia.systems/) coined the term [HOWL stack](https://htmx.org/essays/hypermedia-on-whatever-youd-like/) which is really just getting back to the roots of the web. People make fun of HTML not being a real language but its a wonderful way of exchanging interconnect assets. The HOWL stack is about leveraging the web's capabilities and with just a thin set of shims make it as powerful as any SPA framework. This is the basis of Datastar.
|
|
|
@ -1,65 +0,0 @@
|
||||||
# Streaming Backend
|
|
||||||
|
|
||||||
HTMX is amazing for resurrecting the server rendered web. In general its far easier and faster than SPA development but there are still a few issues.
|
|
||||||
|
|
||||||
- For any larger page you have to add [hyperscript](https://hyperscript.org/) or [Alpine.js](https://alpinejs.dev/) to make the page reactive.
|
|
||||||
- There are multiple ways to target the elements on the page with `hx-target` and [out of band swaps](https://htmx.org/attributes/hx-swap-oob/). This if flexible but leads to a ton of ways to do the same thing.
|
|
||||||
- Responses are still single responses. If you want to have multiple updates as soon as possible you need to use plugins that aren't well supported (at this time).
|
|
||||||
- The list of `hx-*` attributes is growing quickly and think it points to a bigger issue with scope.
|
|
||||||
|
|
||||||
None of these are deal breakers but starting from first principles we can do better. The first thing we need to do is make the page reactive. We can do this by using [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) or [Websockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). We'll use version of SSE because
|
|
||||||
|
|
||||||
- At the end of the day its "just" a HTTP request. All your sessions, cookies, etc are still valid.
|
|
||||||
- Websockets are harder to deal with depending on sticky sessions and load balancers.
|
|
||||||
- Websockets aren't compatible with HTTP2/3. Datastar will probably move to [WebTransport](https://web.dev/webtransport/) when its more available (looking at you Apple).
|
|
||||||
- HTTP2/3 is already multiplexed so you can have multiple SSE streams open at once.
|
|
||||||
|
|
||||||
## SSE
|
|
||||||
|
|
||||||
If you aren't familiar with SSE its a way to a chunked response to the browser. The browser will automatically reconnect if the connection is lost. The server can also send a `retry` header to tell the browser how often to reconnect. The browser will also automatically reconnect if the connection is lost. The nice part of having a chunked response is you can send multiple updates or a single update and the browser will handle it. A modern backend HTTP server framework should allow for easy SSE support (it's just a few headers and a specific text format for content).
|
|
||||||
|
|
||||||
## SSE backend, fetch on the frontend
|
|
||||||
|
|
||||||
For a reason that I still haven't figured out, the SSE spec and [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) only supports GET requests. This seems like a huge oversight, but we can work around it. From the backend's perspective they can send a `Content-Type: text/event-stream` from any request. This means we can use a normal `fetch` request to get the SSE stream. The only downside is we have to manually handle the reconnects. This is handled by Datastar but it's good to be aware of.
|
|
||||||
|
|
||||||
## Let's get some data
|
|
||||||
|
|
||||||
On the [home page](/) there is a global store example. When you first load the page the contents is basically empty.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="global-count-example" data-on-load="$$get('/api/globalCount')">
|
|
||||||
<span>Loading example on delay...</span>
|
|
||||||
<div class="spinner datastar-indicator"></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Normally you don't need to do this but its trying to show that you can load a page and then have the data update. The `$$get` action is a helper function that will make a fetch request and update the store with the results. The `datastar-indicator` class is a helper class that will show a spinner when the request is in flight.
|
|
||||||
|
|
||||||
On the backend (Go in this case) we return a set of render fragments in the form of a set of `text/event-stream` events. In this case it's just a single event.
|
|
||||||
|
|
||||||
```go
|
|
||||||
event: datastar-fragment
|
|
||||||
id: 129618219840307262
|
|
||||||
data: selector
|
|
||||||
data: merge morph_element
|
|
||||||
data: settle 0
|
|
||||||
data: fragment <div ...>...</div>
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The `selector` is the CSS selector that the fragment will be inserted into. The `merge` decides how the element is added to the page. `morph_element` uses [idiomorph](https://github.com/bigskysoftware/idiomorph) to merge into the page intelligently. The `settle` is the time in milliseconds that the fragment will be inserted. The `fragment` is the HTML that will be inserted into the page. If you are familiar with HTMX and the the structure of SSE messages this should look quite familiar. Nice thing is most of these are optional (except for `fragment`) and can be optionally added to the response. In the case of the Go implementation it includes all for the sake of completeness.
|
|
||||||
|
|
||||||
This is still a lot to explore but the main point is that we can have a reactive page without having to write any Javascript. This is the power of declarative code. I'd suggest you check out the [examples](/examples) to see how this all fits together. Then when you have a specific question check out the [reference](/reference) for more details.
|
|
||||||
|
|
||||||
# Conclusion
|
|
||||||
|
|
||||||
Please don't hesitate to reach out if you have any questions. We are always looking for feedback and ways to improve. If you are interested in contributing please check out the [Github](https://github.com/delaneyj/datastar)
|
|
||||||
|
|
||||||
# Choose your own adventure
|
|
||||||
|
|
||||||
<div class="flex gap-4 items-center">
|
|
||||||
<a href="/examples" class="flex-1 no-underline p-4 rounded-md bg-primary-600 text-center hover:bg-primary-500">Show me</a>
|
|
||||||
<a href="/reference" class="flex-1 no-underline p-4 rounded-md bg-primary-600 text-center hover:bg-primary-500">Let me dig in</a>
|
|
||||||
<a href="/essays" class="flex-1 no-underline p-4 rounded-md bg-primary-600 text-center hover:bg-primary-500">Pontificating</a>
|
|
||||||
</div>
|
|
|
@ -1,67 +0,0 @@
|
||||||
# Attribute Plugins
|
|
||||||
|
|
||||||
[Source](https://github.com/delaneyj/datastar/blob/main/library/src/lib/plugins/attributes.ts)
|
|
||||||
|
|
||||||
Primarily used to help hook up the store and the DOM.
|
|
||||||
|
|
||||||
## Attributes Plugins
|
|
||||||
|
|
||||||
### Bind
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-bind-disabled="$iShouldBeDisabled"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Allows any valid attribute to be bound to an expression. This is useful for making elements reactive. Also can be used as a fallback for any attribute that is not supported by a plugin currently.
|
|
||||||
|
|
||||||
### Model
|
|
||||||
|
|
||||||
```html
|
|
||||||
<input data-model="foo" />
|
|
||||||
```
|
|
||||||
|
|
||||||
Sets up two-way data-binding on an element.
|
|
||||||
|
|
||||||
**Note:** Always binds to a signal and therefore should exclude the `$` prefix from the signal name. Only allowed on `input`,`textarea`, `select`, `checkbox` and `radio` elements.
|
|
||||||
|
|
||||||
### Text
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-text="$foo"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Sets the text content of an element to the value of the signal. This is useful for setting the text content of an element to a signal value. Can use any expression that is valid in the system. For example, `data-text="$foo + 'bar'"` would set the text content to the value of `$foo` plus the string `bar`.
|
|
||||||
|
|
||||||
### On
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button data-on-click="$$fn('foo','bar',1234)">Click Me</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
Sets up an event listener on an element. The event listener will trigger the action specified in the expression. The expression can be any valid expression in the system. For example, `data-on-click="$$fn('foo','bar',1234)"` would trigger the action `fn` with the arguments `'foo','bar',1234` when the button is clicked.
|
|
||||||
|
|
||||||
If any signal in the expression changes, the event listener will be updated to reflect the new value of the signal automatically.
|
|
||||||
|
|
||||||
**Note:** the `data-on-*` matches DOM events, however there are currently a few special cases for custom events.
|
|
||||||
|
|
||||||
1. `data-on-load` which is triggered when the element is loaded into the DOM.
|
|
||||||
2. `data-on-store-change` which is triggered when the store changes.
|
|
||||||
3. `data-on-raf` which is triggered on every requestAnimationFrame event.
|
|
||||||
|
|
||||||
#### Modifiers
|
|
||||||
|
|
||||||
- `.once`\* - Only trigger the event once
|
|
||||||
- `.passive`\* - Do not call `preventDefault` on the event
|
|
||||||
- `.capture`\* - Use a capture event listener
|
|
||||||
- `.debounce` - Debounce the event listener
|
|
||||||
- `_1000ms` - Debounce for 1000ms
|
|
||||||
- `_1s` - Debounce for 1s
|
|
||||||
- `_leading` - Debounce with leading edge
|
|
||||||
- `_noTrail` - Debounce without trailing edge
|
|
||||||
- `.throttle` - Throttle the event listener
|
|
||||||
- `_1000ms` - Throttle for 1000ms
|
|
||||||
- `_1s` - Throttle for 1s
|
|
||||||
- `_noLead` - Throttle without leading edge
|
|
||||||
- `_noTrail` - Throttle without trailing edge
|
|
||||||
|
|
||||||
\*only works currently on native events
|
|
|
@ -1,84 +0,0 @@
|
||||||
# Backend Plugins
|
|
||||||
|
|
||||||
[Source](https://github.com/delaneyj/datastar/blob/main/library/src/lib/plugins/backend.ts)
|
|
||||||
|
|
||||||
A set of plugins that allow for the integration of any backend services that supports SSE with Datastar.
|
|
||||||
|
|
||||||
## Attribute Plugins
|
|
||||||
|
|
||||||
Request for data from the server via SSE and merge with the page.
|
|
||||||
|
|
||||||
## Action Plugins
|
|
||||||
|
|
||||||
### `$$get`, `$$post`, `$$put`, `$$patch`, `$$delete`
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-on-click="$$get('/examples/click_to_edit/contact/1')"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Makes an HTML_VERB request to the server and merges the response with the current DOM and store. The URL can be any valid URL but the response must be a Datastar formatted SSE event.
|
|
||||||
|
|
||||||
Every request will be sent with a `{datastar: *}` object containing the current store. When using `$$get` the store will be sent as a query parameter, otherwise it will be sent as a JSON body.
|
|
||||||
|
|
||||||
## Datastar SSE Event
|
|
||||||
|
|
||||||
An example of a minimal valid response would be:
|
|
||||||
|
|
||||||
```go
|
|
||||||
event: datastar-fragment
|
|
||||||
data: fragment <div id="foo">Hello!</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Addtional `data` lines can be added to the response to override the default behavior.
|
|
||||||
|
|
||||||
| Key | Description | Default | |
|
|
||||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --- |
|
|
||||||
| `data: selector #foo` | Select the target element using a CSS selector. Will be come the target of the `merge` process, otherwise it will use the target of the initiating element's id. | |
|
|
||||||
| `data: merge morph_element` | Merge the fragment using [Idiomorph](https://github.com/bigskysoftware/idiomorph). | \* |
|
|
||||||
| `data: merge inner_html` | Replace target's innerHTML with fragment | |
|
|
||||||
| `data: merge outer_html` | Replace target's outerHTML with fragment | |
|
|
||||||
| `data: merge prepend_element` | Prepend fragment to target's children | |
|
|
||||||
| `data: merge append_element` | Append fragment to target's children | |
|
|
||||||
| `data: merge before_element` | Insert fragment before target as sibling | |
|
|
||||||
| `data: merge after_element` | Insert fragment after target as sibling | |
|
|
||||||
| `data: merge delete_element` | Remove target from the DOM | |
|
|
||||||
| `data: merge upsert_attributes` | Merge attributes from fragment into target, useful when wanting to just update a store | |
|
|
||||||
| `data: settle 1000` | Settle the element after 1000ms, useful for transitions. Defaults to `500` if missing | \* |
|
|
||||||
| `data: fragment` | The HTML fragment to merge into the DOM. **_Should only be one per event_** | \* |
|
|
||||||
| `data: redirect /foo` | Redirect the page to `/foo`. Can be used in place of a `data: fragment` **_Should only be one per event_** | |
|
|
||||||
| `data: error oh noes` | Will throw an error with the message `oh noes` and stop the request. Can be used in place of a `data: fragment` **_Should only be one per event_** | |
|
|
||||||
|
|
||||||
## Attribute Plugins
|
|
||||||
|
|
||||||
### Header
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-header-X-My-Header="foo"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Add a header to the request. This can be used to send additional information to the server.
|
|
||||||
|
|
||||||
### Fetch Indicator
|
|
||||||
|
|
||||||
```html
|
|
||||||
<svg id="foo">Spinner</svg>
|
|
||||||
<div
|
|
||||||
data-on-click="$$get('/examples/click_to_edit/contact/1')"
|
|
||||||
data-fetch-indicator="#foo"
|
|
||||||
></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Show a spinner when the request is in flight. The `data-fetch-indicator` attribute should be a CSS selector to the element(s). When the attribute is present, the element will be hidden when requests are not in flight and shown when they are.
|
|
||||||
|
|
||||||
### Is Loading Identifier
|
|
||||||
|
|
||||||
```html
|
|
||||||
<svg data-show="$isLoading.includes('edit-contact')">Spinner</svg>
|
|
||||||
<div
|
|
||||||
data-on-click="$$get('/examples/click_to_edit/contact/1')"
|
|
||||||
data-is-loading-id="edit-contact"
|
|
||||||
></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
The `data-is-loading-id` attribute is used to specify the name of the identifier that will be present in the store's isLoading array when an element is fetching.
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
# Core Plugins
|
|
||||||
|
|
||||||
[Source](https://github.com/delaneyj/datastar/blob/main/library/src/lib/plugins/core.ts)
|
|
||||||
|
|
||||||
These are the only plugins that are required in order to have a working system. In the default build all (including core) plugins are included as they cover the most common use cases.
|
|
||||||
|
|
||||||
## Attributes Plugins
|
|
||||||
|
|
||||||
### MergeStore
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-store="{foo:1234}"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Takes the contents of the attribute and runs a BigInt aware JSON parse on it. It then merges the contents into the store. This can be used anywhere as the store is a global singleton. All keys are converted into signals, works with nested objects.
|
|
||||||
|
|
||||||
### Ref
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-ref="foo"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Makes an element available in the `ctx.refs` object. This is useful for accessing elements in the DOM. Can be accessed in scripts via `~foo`.
|
|
||||||
|
|
||||||
## Preprocessor Plugins
|
|
||||||
|
|
||||||
### SignalProcessor
|
|
||||||
|
|
||||||
Takes a `$var` and converts into a `ctx.store().var.value`. Since all expressions are evaluated within an effect it setups of a reactive system.
|
|
||||||
|
|
||||||
### ActionProcessor
|
|
||||||
|
|
||||||
Takes a `$$fn('foo','bar',1234)` and converts into a `ctx.actions.fn('foo','bar',1234()`. This is used to trigger actions plugins.
|
|
||||||
|
|
||||||
### RefProcessor
|
|
||||||
|
|
||||||
Takes a `~foo` and converts into a `ctx.refs.foo`. This is used to access refs similar to how you would in a Vue or Svlete components.
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Expressions
|
|
||||||
|
|
||||||
Expressions are the building blocks of Datastar. In fact, Datastar started as just a way to take `data-*` attributes and turn them into expressions. For exact `data-*` attribute found
|
|
||||||
|
|
||||||
1. All `Preprocesser` plugins are run. This allows for a custom DSL. The included plugins use `$` for signals, `$$` for actions, and `~` for refs. The plugins check for regular expressions and replace them with the appropriate value. Some plugins will setup extra state on load like adding CSS classes or setting up event listeners.
|
|
||||||
2. All Datastar `AttributePlugin` plugins are run in order. Most of the time these plugins are creating `effect()` signals so that that changes to the expression with automatically update the DOM and other parts of the system.
|
|
||||||
3. Check for any elemen removals and cancel any effects that are no longer needed.
|
|
||||||
|
|
||||||
Each expression is evaluated in a new Function declaration and not in a call to `eval()`. This is done to prevent access to the global scope and to prevent access to the `Function` constructor. This is done to prevent XSS attacks. Its also why all expressions take a `ctx` which has access to the store, actions, and refs, but not the global scope. This was gleamed from how Alpine.js works but with a different reactive model.
|
|
|
@ -1,27 +0,0 @@
|
||||||
# Action Plugins
|
|
||||||
|
|
||||||
[Source](https://github.com/delaneyj/datastar/blob/main/library/src/lib/plugins/helpers.ts)
|
|
||||||
|
|
||||||
## `$$setAll(regexp: string, value: any)`
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-on-change="$$setAll('contact_', $selections.all)"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Sets all the signals that start with the prefix to the value of the second argument. This is useful for setting all the values of a form at once.
|
|
||||||
|
|
||||||
## `$$toggleAll(regexp: string)`
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-on-click="$$toggleAll('contact_')"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Toggles all the signals that start with the prefix. This is useful for toggling all the values of a form at once.
|
|
||||||
|
|
||||||
## `$$clipboard(text: string)`
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-on-click="$$clipboard('Hello, world!')"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Copies the text to the clipboard. This is useful for copying text to the clipboard.
|
|
|
@ -1,73 +0,0 @@
|
||||||
# Visibility Plugins
|
|
||||||
|
|
||||||
[Source](https://github.com/delaneyj/datastar/blob/main/library/src/lib/plugins/visibility.ts)
|
|
||||||
|
|
||||||
Focused on showing and hiding elements based on signals. Most of the time you want to send updates from the server but is useful for things like modals, dropdowns, and other UI elements.
|
|
||||||
|
|
||||||
## Attributes Plugins
|
|
||||||
|
|
||||||
### Show
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-show="$showMe"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Shows the element when the expression is true.
|
|
||||||
|
|
||||||
### Intersects
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-intersects="console.log('I am intersecting!')"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Runs the expression when the element intersects with the viewport.
|
|
||||||
|
|
||||||
#### Modifiers
|
|
||||||
|
|
||||||
- `.once` - Only trigger the event once
|
|
||||||
- `.half` - Trigger when half of the element is visible
|
|
||||||
- `.full` - Trigger when the full element is visible
|
|
||||||
|
|
||||||
### Teleport
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-teleport="#foo"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Moves the element to the target.
|
|
||||||
|
|
||||||
#### Modifiers
|
|
||||||
|
|
||||||
- `.prepend` - Prepend the element to the target
|
|
||||||
- `.append` - Append the element to the target
|
|
||||||
|
|
||||||
### Scroll Into View
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-scroll-into-view></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Scrolls the element into view. Useful when updating DOM from the server and you want to scroll to the new content.
|
|
||||||
|
|
||||||
#### Modifiers
|
|
||||||
|
|
||||||
- `.smooth` - Scrolling should be animate smoothly
|
|
||||||
- `.instant` - Scrolling should be instant
|
|
||||||
- `.auto` - Scrolling is determined by the computed `scroll-behavior` CSS property
|
|
||||||
- `.hstart` - Scroll to the left of the element
|
|
||||||
- `.hcenter` - Scroll to the horizontal center of the element
|
|
||||||
- `.hend` - Scroll to the right of the element
|
|
||||||
- `.hnearest` - Scroll to the nearest horizontal edge of the element
|
|
||||||
- `.vstart` - Scroll to the top of the element
|
|
||||||
- `.vcenter` - Scroll to the vertical center of the element
|
|
||||||
- `.vend` - Scroll to the bottom of the element
|
|
||||||
- `.vnearest` - Scroll to the nearest vertical edge of the element
|
|
||||||
- `.focus` - Focus the element after scrolling
|
|
||||||
|
|
||||||
### View Transition
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div data-view-transition="foo"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
Page level transitions are automatically handled by an injected meta tag. Inter-page elements are automatically transitioned if the [View Transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) is available. To set the `view-transition-name` style attribute explictily you use the `data-view-transition` attribute. The spec is still in draft and not available in all browsers but Datastar will do best effort.
|
|
|
@ -1,6 +0,0 @@
|
||||||
# Revision history for data-star
|
|
||||||
|
|
||||||
## 0.1.0.0 -- 2024-05-13
|
|
||||||
|
|
||||||
* First version. Reproduce datastar server.js example in Haskell
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
You should be able to run this with just a:
|
|
||||||
|
|
||||||
cabal run
|
|
||||||
|
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
module Main where
|
|
||||||
|
|
||||||
import Control.Applicative ( Alternative((<|>)) )
|
|
||||||
import Control.Concurrent ( threadDelay )
|
|
||||||
import Control.Exception ( SomeException, handle, throw )
|
|
||||||
import Control.Monad ( when )
|
|
||||||
import Control.Monad.IO.Class ( MonadIO(..) )
|
|
||||||
import Data.Aeson ( FromJSON, ToJSON, encode, decodeStrict )
|
|
||||||
import Data.ByteString.Builder ( Builder, hPutBuilder, byteString )
|
|
||||||
import Data.ByteString.Builder.Extra ( flush )
|
|
||||||
import Data.Convertible.Utf8 ( Convertible(..) )
|
|
||||||
import Data.Maybe ( fromMaybe )
|
|
||||||
import Data.String ( IsString )
|
|
||||||
import Data.Time.Clock ( getCurrentTime )
|
|
||||||
import GHC.Generics ( Generic )
|
|
||||||
import NeatInterpolation ( trimming )
|
|
||||||
import Snap
|
|
||||||
import Snap.Util.FileServe ( serveDirectory )
|
|
||||||
import System.IO
|
|
||||||
( hSetBuffering, stderr, stdout, BufferMode(NoBuffering) )
|
|
||||||
import System.Random ( RandomGen, mkStdGen, Random(randomR) )
|
|
||||||
import qualified System.IO.Streams as Streams
|
|
||||||
( OutputStream, write )
|
|
||||||
|
|
||||||
data Datastar = Datastar {
|
|
||||||
input :: !String
|
|
||||||
, output :: !String
|
|
||||||
, show :: !Bool
|
|
||||||
} deriving (Show, Generic)
|
|
||||||
|
|
||||||
instance FromJSON Datastar
|
|
||||||
instance ToJSON Datastar
|
|
||||||
|
|
||||||
target :: String -- like const target = "target"; in server.js
|
|
||||||
target = "target"
|
|
||||||
|
|
||||||
makeIndexPage :: String -> Builder
|
|
||||||
makeIndexPage x = byteString . convert $ [trimming|
|
|
||||||
<head>
|
|
||||||
<title>Node/Express + Datastar Example</title>
|
|
||||||
<script type="module" defer src="/datastar.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>Node/Express + Datastar Example</h2>
|
|
||||||
<main class="container" id="main" data-store="{input:'', output:'', show: true}">
|
|
||||||
<input type="text" placeholder="Send to server..." data-model="input"/>
|
|
||||||
<button data-on-click="$$$$get('/get')">Send State Roundtrip</button>
|
|
||||||
<button data-on-click="$$$$get('/target')">Target HTML Element</button>
|
|
||||||
<button data-on-click="$$show=!$$show">Toggle Feed</button>
|
|
||||||
<div id="output" data-text="$$output"></div>
|
|
||||||
<div id="$y"></div>
|
|
||||||
<div data-show="$$show">
|
|
||||||
<span>Feed from server: </span>
|
|
||||||
<span id="feed" data-on-load="$$$$get('/feed')"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5>Datastar Store</h5>
|
|
||||||
<pre data-text="JSON.stringify(ctx.store(),null,2)"></pre>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>|]
|
|
||||||
where y = convert x
|
|
||||||
|
|
||||||
htmlify :: ToJSON p => p -> String
|
|
||||||
htmlify x = go (convert . encode $ x) []
|
|
||||||
where
|
|
||||||
go [] acc = acc -- I could have used regexp, bud it isn't the "Haskell way"
|
|
||||||
go (a:as) acc = if a == '"' then go as (acc <> """) else go as (acc <> [a])
|
|
||||||
|
|
||||||
setHeaders :: MonadSnap m => (Streams.OutputStream Builder -> IO ()) -> m ()
|
|
||||||
setHeaders f = do
|
|
||||||
escapeHttp $ \tickle _ writeEnd -> do
|
|
||||||
tickle (max (60*60))
|
|
||||||
writeStream (WriteBuilder sseHeaders) writeEnd
|
|
||||||
f writeEnd
|
|
||||||
|
|
||||||
sseHeaders :: Builder
|
|
||||||
sseHeaders = byteString . convert $ [trimming|
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
server: Snap/1.1.2.1
|
|
||||||
cache-control: no-cache
|
|
||||||
connection: keep-alive
|
|
||||||
Content-type: text/event-stream; charset=utf-8
|
|
||||||
|] <> "\n\n"
|
|
||||||
|
|
||||||
data SSE = SSE { -- A little data type to decode the store
|
|
||||||
frag :: !String
|
|
||||||
, merge :: !Bool
|
|
||||||
, end :: !Bool
|
|
||||||
} deriving Show
|
|
||||||
|
|
||||||
datastarSetupFragment :: MonadIO m => SSE -> Streams.OutputStream Builder -> m ()
|
|
||||||
datastarSetupFragment sse writeEnd = do
|
|
||||||
liftIO $ handle handleSSEexception $ do
|
|
||||||
writeStream (WriteBuilder "event: datastar-fragment\n") writeEnd
|
|
||||||
when (merge sse)
|
|
||||||
(writeStream (WriteList ["data: merge upsert_attributes\n"]) writeEnd)
|
|
||||||
writeStream (WriteList ["data: fragment ", frag sse, "\n\n"]) writeEnd
|
|
||||||
writeStream Flush writeEnd
|
|
||||||
when (end sse) (writeStream Stop writeEnd)
|
|
||||||
where
|
|
||||||
handleSSEexception :: SomeException -> IO ()
|
|
||||||
handleSSEexception e = print e >> throw e
|
|
||||||
|
|
||||||
-- A little DSL so I can watch the messages
|
|
||||||
data SW = Flush | WriteBuilder !Builder | WriteList ![String] | Stop
|
|
||||||
|
|
||||||
|
|
||||||
writeStream :: SW -> Streams.OutputStream Builder -> IO ()
|
|
||||||
writeStream Flush writeEnd = do
|
|
||||||
putStrLn "Flush"
|
|
||||||
Streams.write (Just flush) writeEnd
|
|
||||||
writeStream Stop writeEnd = do
|
|
||||||
putStrLn "Stop"
|
|
||||||
Streams.write Nothing writeEnd
|
|
||||||
writeStream (WriteBuilder builder) writeEnd = do
|
|
||||||
putStr "WriteBuilder "
|
|
||||||
hPutBuilder stdout builder
|
|
||||||
Streams.write (Just builder) writeEnd
|
|
||||||
writeStream (WriteList strs) writeEnd = do
|
|
||||||
putStr "WriteList "
|
|
||||||
putStr . mconcat $ strs
|
|
||||||
Streams.write (Just (toBuilder strs)) writeEnd
|
|
||||||
|
|
||||||
|
|
||||||
decodeDatastar :: Snap Datastar
|
|
||||||
decodeDatastar = do
|
|
||||||
mbBS <- getParam "datastar"
|
|
||||||
let
|
|
||||||
bs = fromMaybe (error "In decodeDatastar, expected parameter datastar to exits") mbBS
|
|
||||||
ds = fromMaybe (error ("bad decoding in decodeDatastar " <> Prelude.show bs)) (decodeStrict bs)
|
|
||||||
return ds
|
|
||||||
|
|
||||||
handlerGet :: Snap ()
|
|
||||||
handlerGet = do
|
|
||||||
store <- decodeDatastar
|
|
||||||
let
|
|
||||||
storeInput = input store
|
|
||||||
storeOutput = mconcat ["Yout input: ", storeInput, ", is ", Prelude.show . length $ storeInput, " long."]
|
|
||||||
newStore = store { output = storeOutput }
|
|
||||||
frag1 = mconcat ["<main id=\"main\" data-store=\"", htmlify newStore, "\"></main>" ]
|
|
||||||
sse = SSE frag1 True True
|
|
||||||
setHeaders (datastarSetupFragment sse)
|
|
||||||
|
|
||||||
-- This example is a little silly in haskell because of the lack of string interprolation that
|
|
||||||
-- is present in javascript. In the JS example, target is a variable which contains the string
|
|
||||||
-- "target". In haskell, while you can do interpolation with template haskell, it is more of
|
|
||||||
-- a hassle, so I am just using the monoid instance
|
|
||||||
handlerTarget :: Snap ()
|
|
||||||
handlerTarget = do
|
|
||||||
stamp <- liftIO $ Prelude.show <$> getCurrentTime
|
|
||||||
let
|
|
||||||
frag1 = mconcat ["<div id=\"", target , "\"><b>", stamp, "</b></div>"]
|
|
||||||
sse = SSE frag1 False True
|
|
||||||
setHeaders (datastarSetupFragment sse)
|
|
||||||
|
|
||||||
handlerFeed :: Snap ()
|
|
||||||
handlerFeed = do
|
|
||||||
liftIO $ putStrLn "enter handlerFeed"
|
|
||||||
let
|
|
||||||
loop g writeEnd = do
|
|
||||||
let
|
|
||||||
(str, g') = randomHex g
|
|
||||||
msg = mconcat ["<span id=\"feed\">", str, "</span>"]
|
|
||||||
sse = SSE msg False False
|
|
||||||
datastarSetupFragment sse writeEnd
|
|
||||||
liftIO $ threadDelay 1000000 -- Delay for 1 second
|
|
||||||
_ <- loop g' writeEnd
|
|
||||||
return ()
|
|
||||||
setHeaders (loop (mkStdGen 0))
|
|
||||||
where
|
|
||||||
-- randomR :: RandomGen g => (a, a) -> g -> (a, g)
|
|
||||||
-- iterate :: (a -> a) -> a -> [a]
|
|
||||||
randomHex :: RandomGen g => g -> (String,g)
|
|
||||||
randomHex g = (str,newG)
|
|
||||||
where
|
|
||||||
hex = "0123456789ABCDEF"
|
|
||||||
f x = randomR (0,15) (snd x)
|
|
||||||
f8 = take 8 . drop 1 $ iterate f (0,g)
|
|
||||||
asChar (i,_) = hex !! i
|
|
||||||
str = map asChar f8
|
|
||||||
newG = snd . last $ f8
|
|
||||||
|
|
||||||
toBuilder :: (Convertible a Builder, Monoid a, IsString a) => [a] -> Builder
|
|
||||||
toBuilder = convert . mconcat
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
hSetBuffering stdout NoBuffering
|
|
||||||
hSetBuffering stderr NoBuffering
|
|
||||||
let
|
|
||||||
mbPort = getPort (defaultConfig :: Config Snap a)
|
|
||||||
newConfig = setPort (fromMaybe 3000 mbPort) (defaultConfig :: Config Snap a)
|
|
||||||
conf <- commandLineConfig newConfig
|
|
||||||
print conf
|
|
||||||
simpleHttpServe conf site
|
|
||||||
|
|
||||||
site :: Snap ()
|
|
||||||
site =
|
|
||||||
ifTop (writeBuilder $ makeIndexPage target) <|> -- writeBuilder defined in Snap.Core
|
|
||||||
route [
|
|
||||||
("favicon.ico" , return ())
|
|
||||||
, ("get" , handlerGet)
|
|
||||||
, ("feed" , handlerFeed)
|
|
||||||
, ("target" , handlerTarget)
|
|
||||||
] <|> (serveDirectory "www")
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue