Compare commits

..

258 Commits

Author SHA1 Message Date
Greg Johnston
4360a73392 Fix SimpleCounter example 2022-12-09 14:57:58 -05:00
Greg Johnston
50b0fe157a Fix example test 2022-12-09 13:34:35 -05:00
Greg Johnston
64a5d75ec4 .into() calls were interfering with components that have generic props 2022-12-09 13:09:02 -05:00
Greg Johnston
baf3cc8712 Correct imports 2022-12-09 12:36:33 -05:00
Greg Johnston
23777ad67b Use leptos reexport of typed-builder crate 2022-12-09 12:30:21 -05:00
Greg Johnston
08be1ba622 Fix warnings 2022-12-08 19:28:23 -05:00
Greg Johnston
605398bcea Only use default for Option<T> 2022-12-08 19:27:45 -05:00
Greg Johnston
aca2c131d4 Add the ability to document Component and ComponentProps in a single doc comment. 2022-12-08 17:08:54 -05:00
Greg Johnston
9d950b97ff Better error message for RouterIntegrationContext 2022-12-07 07:52:01 -05:00
Greg Johnston
f6a299ae3c Merge pull request #154 from gbj/fix-component-siblings-in-hydration
Fix issue #109
2022-12-07 00:06:48 -05:00
Greg Johnston
1ba602ec47 Fix issue #109 2022-12-06 22:31:54 -05:00
Greg Johnston
1f3dde5b4a Fix Hackernews CSS 2022-12-06 19:22:29 -05:00
Greg Johnston
a65cd67db3 Fix name of Wasm export 2022-12-06 18:18:46 -05:00
Greg Johnston
bacd99260b Fix benchmarks 2022-12-06 18:18:38 -05:00
Greg Johnston
2b726f1a88 Fix docs on props for each component 2022-12-06 11:42:47 -05:00
Greg Johnston
5c45538e9f Make necessary changes for stable support for router and meta 2022-12-05 18:55:03 -05:00
Greg Johnston
7f696a9ac4 support 2022-12-05 17:25:02 -05:00
Greg Johnston
bcd6e671f7 0.0.20 2022-12-05 17:23:22 -05:00
Greg Johnston
7a72f127de Stable compatibility 2022-12-05 17:18:17 -05:00
Greg Johnston
2ff5ec21c8 0.0.20 2022-12-05 16:25:16 -05:00
Greg Johnston
a1f94b609f Improvements to example to show off transitions and streaming 2022-12-05 16:17:47 -05:00
Greg Johnston
da5034da33 Bump versions after WASM-less fix 2022-12-05 16:17:29 -05:00
Greg Johnston
0c509970b5 Fix ability of server functions to work without WASM 2022-12-05 16:17:15 -05:00
Greg Johnston
d894c4dcf9 Merge branch 'main' of https://github.com/gbj/leptos 2022-12-05 16:10:33 -05:00
Greg Johnston
dc15184781 Merge pull request #152 from benwis/cargo-leptos-updates
Add config crate and generate file for cargo-leptos to watch
2022-12-05 12:04:56 -05:00
Ben Wishovich
3200068ab3 Doc tweaks 2022-12-04 18:11:20 -08:00
Ben Wishovich
0a9da8d55e Add some doc comments, and change the behavior of the reload_port 2022-12-04 17:55:51 -08:00
Ben Wishovich
52ad546710 Update rest of the examples and make the tests pass 2022-12-04 17:25:03 -08:00
Ben Wishovich
f88d2fa56a Add socket_address option to configure the ip address and port to serve 2022-12-04 15:50:29 -08:00
Ben Wishovich
f63cb02277 Commit WIP version of common config struct that writes a KDL file for cargo-leptos 2022-12-04 14:50:36 -08:00
Greg Johnston
4b363f9b33 0.0.3 for axum 0.6 compatibility 2022-12-03 22:12:17 -05:00
Ben Wishovich
7b376b6d3a Draft Builder Pattern for Render Options to add Leptos Autorender Code 2022-12-02 16:33:59 -08:00
Ben Wishovich
8fbb4abc76 Switch integrations to pass in a full path and name v the name to enable different pkg structures 2022-12-02 12:01:51 -08:00
Greg Johnston
d0ff64daaa Merge pull request #149 from gbj/a-tag-class-helper
Allow styling `<A/>` tags with `class` property
2022-12-02 14:09:10 -05:00
Greg Johnston
bb97234817 Merge pull request #148 from gbj/explicit-stable-not-required
Automatically enable the `stable` feature if you're on `stable` Rust
2022-12-02 14:08:22 -05:00
Greg Johnston
19698d86b6 Allow styling <A/> component with class 2022-12-02 13:20:07 -05:00
Greg Johnston
21ef96806f Rename ToHref to something a little more generic 2022-12-02 13:04:37 -05:00
Greg Johnston
70e18d2aeb Automatically enable the stable feature if you're on stable Rust 2022-12-02 12:56:05 -05:00
Greg Johnston
5152703f0c Clear warnings 2022-12-02 12:39:32 -05:00
Greg Johnston
3d54055573 Add <Meta/> component to leptos_meta 2022-12-02 12:36:51 -05:00
Greg Johnston
a5b99a3e40 Merge branches 'main' and 'main' of https://github.com/gbj/leptos 2022-12-01 21:42:02 -05:00
Greg Johnston
101e65b724 Does adding skip_feature_sets here help with CI problem? 2022-12-01 21:41:58 -05:00
Greg Johnston
a3f91604b9 Merge pull request #141 from benwis/axum-0.6
Update Axum examples to latest 0.6 release and streamline them a bit
2022-12-01 17:23:20 -05:00
Ben Wishovich
f457d8f319 Fix doc test 2022-12-01 12:56:27 -08:00
Greg Johnston
58abe55d7b Merge branch 'main' into axum-0.6 2022-12-01 13:10:06 -05:00
Greg Johnston
634ac17095 Merge pull request #144 from Indrazar/main
update functions for Windows file directories
2022-12-01 12:43:19 -05:00
Ben Wishovich
79faad4aac Missed another couple imports 2022-11-30 22:41:31 -08:00
IcosaHedron
cedc68c341 remove debug string from axum integration 2022-11-30 23:20:14 -05:00
indrazar
8ec772a129 update functions for Windows file directories
- leptos_macro/src/server.rs server_macro_impl
 - integrations/axum/src/lib.rs handle_server_fns
2022-11-30 23:01:59 -05:00
Greg Johnston
8d671866a3 Merge pull request #142 from FDiskas/patch-1
Update example lib.rs
2022-11-30 20:47:24 -05:00
Ben Wishovich
2edc5b3b8b Remove extra print 2022-11-30 17:31:14 -08:00
Vytenis
be96a230ee Update lib.rs 2022-12-01 01:47:54 +02:00
Ben Wishovich
0f8930b6f2 Update Axum examples to latest 0.6 release and streamline things 2022-11-30 15:02:22 -08:00
Greg Johnston
2b5c4abac5 Merge pull request #140 from gbj/transition-component
Transition component
2022-11-30 16:20:02 -05:00
Greg Johnston
db8c393f49 Update examples 2022-11-30 11:36:54 -05:00
Greg Johnston
f18a7b35f2 Use SignalSetter in <Transition/> API 2022-11-30 11:36:50 -05:00
Greg Johnston
a2c5855362 <Transition/> component 2022-11-30 11:27:07 -05:00
Greg Johnston
644d097cb6 Fix SignalSetter tests 2022-11-30 11:22:05 -05:00
Greg Johnston
9c0be9e317 Finishing implementing SignalSetter wrapper. 2022-11-30 07:46:04 -05:00
Greg Johnston
5faa2efa2d Merge pull request #137 from benwis/example_readmes
Add READMEs to all examples and fix typo in todo-app-axum
2022-11-29 20:00:36 -05:00
Greg Johnston
c5a1e9a447 Copy edited and added Trunk install instructions 2022-11-29 20:00:09 -05:00
Ben Wishovich
e88e131ec3 Add READMEs to all examples and fix typo in todo-app-axum 2022-11-29 13:14:59 -08:00
Greg Johnston
80df7a0dac Merge pull request #135 from ghassanachi/patch-1
Update `counters` example link in docs
2022-11-29 14:48:40 -05:00
Ghassan Gedeon Achi
493f05fda1 Update counters example link in docs 2022-11-29 11:51:27 -07:00
Greg Johnston
4578622b6f Merge pull request #134 from gbj/fix-router-hydration-panic
Fix out-of-order hydration issue
2022-11-29 08:56:03 -05:00
Greg Johnston
c7dd6200e8 Fix GTK example 2022-11-29 07:07:10 -05:00
Greg Johnston
6e20f31df1 Fix out-of-order hydration issue by removing old code that was handling this in an incorrect way 2022-11-29 07:06:25 -05:00
Greg Johnston
5f58db40f0 Merge pull request #131 from gbj/fix-3x-server-resource-fetching
Fix issue in which server-side resource are called 3x
2022-11-29 06:14:22 -05:00
Greg Johnston
321e11e97a Fix issue in which server-side resource are called 3x 2022-11-28 22:28:02 -05:00
Greg Johnston
c472a1c5ef Fix misnamed optional-import feature exclusion that was causing CI to break 2022-11-28 20:50:04 -05:00
Greg Johnston
1180eeeadb Merge pull request #127 from akesson/cargo-path-fix
Fix path deps' going one level too high
2022-11-28 12:17:32 -05:00
Greg Johnston
2348bbc5cc Merge branch 'main' of https://github.com/gbj/leptos 2022-11-28 08:42:23 -05:00
Greg Johnston
ee41ea8b1d Update axum integration 2022-11-28 08:42:19 -05:00
Greg Johnston
a0ea3cfd7c Merge pull request #126 from benwis/axum-server-functions
Mostly working version of axum with server functions
2022-11-28 08:41:37 -05:00
Greg Johnston
edb0f8c848 Fix import for CSR/no-features verson 2022-11-28 07:43:31 -05:00
Greg Johnston
2b71c07fa9 Guard against fragments that don't actually exist 2022-11-28 07:39:30 -05:00
Greg Johnston
a109e3d51c Remove my unnecessary nested closure 2022-11-28 07:39:17 -05:00
Greg Johnston
40a842ff1d Correct name of the root component we're rendering 2022-11-28 07:39:04 -05:00
hakesson
17baec46b7 Fix path deps' going one level too high 2022-11-28 06:03:56 +01:00
Ben Wishovich
fe5c9c6f0d Fix accept heading behavior in Axum to match Actix 2022-11-27 18:25:43 -08:00
Ben Wishovich
6c22c47bbf Cleanup, it now works except for when the server FN response is () or empty 2022-11-27 17:17:34 -08:00
Ben Wishovich
2d88a113c4 Typoed 2022-11-27 17:04:34 -08:00
Ben Wishovich
b0dd759bcf Remove commented code in main 2022-11-27 17:01:42 -08:00
Ben Wishovich
507191e1a4 Mostly working version of axum with server functions 2022-11-27 16:55:38 -08:00
Greg Johnston
36de06f183 0.0.19 2022-11-27 09:13:21 -05:00
Greg Johnston
b54c0f14e8 Remove erroneous Clone bound on calling WriteSignal as a function 2022-11-26 21:24:55 -05:00
Greg Johnston
41c03852e1 Create SignalSetter wrapper for writable signals corresponding to Signal wrapper for readable signals 2022-11-26 21:15:19 -05:00
Greg Johnston
c3fb9396e1 Add RwSignal::split() 2022-11-26 17:35:46 -05:00
Greg Johnston
3a9d16ad29 Add RwSignal::write_only() 2022-11-26 17:33:13 -05:00
Greg Johnston
c0709b210d Enable wasm-bindgen string interning for certain types by default 2022-11-26 17:19:11 -05:00
Greg Johnston
569fa9b1c6 Fix CI w.r.t. server functions 2022-11-26 17:13:35 -05:00
Greg Johnston
ed24e47c1d Add examples of canceling in-flight requests (issue #32) and filter against empty IDs to avoid extra requests (issue #123) 2022-11-26 15:29:46 -05:00
Greg Johnston
fdd07aafb7 #[server] docs 2022-11-26 09:02:36 -05:00
Greg Johnston
1a0168bf28 Clear warnings 2022-11-26 09:02:26 -05:00
Greg Johnston
de524e21b1 Clear warnings 2022-11-26 09:01:05 -05:00
Greg Johnston
dbe3daf16a Update skip lists for CI 2022-11-26 08:43:18 -05:00
Greg Johnston
3f6eeb319a NodeRef should actually track so you can use it in effects 2022-11-26 08:01:19 -05:00
Greg Johnston
db34565959 Merge pull request #107 from benwis/msgpack-encoding
Binary encoding as an option for server functions
2022-11-25 22:44:53 -05:00
Ben Wishovich
d5cd2b814e Make cargo check happy 2022-11-25 17:05:27 -08:00
Greg Johnston
2b9ac037e3 Merge pull request #120 from gbj/node-ref
Change `_ref` attribute to use `NodeRef` type
2022-11-25 17:45:11 -05:00
Greg Johnston
66ecc2ac25 Fix NodeRef doctest 2022-11-25 16:53:48 -05:00
Greg Johnston
4093f4c2d8 Fix todomvc 2022-11-25 16:28:21 -05:00
Greg Johnston
a46e92bed8 Clean up Fn implementation issue 2022-11-25 15:48:40 -05:00
Greg Johnston
611a1aeb28 Use relative paths in book for CI 2022-11-25 15:48:32 -05:00
Greg Johnston
994debea3f Change _ref attribute to use NodeRef type 2022-11-25 15:38:46 -05:00
Greg Johnston
5399f54255 Merge pull request #117 from gbj/router-rerenders
Fix issue #115
2022-11-25 14:53:23 -05:00
Greg Johnston
22668f7999 Merge branch 'msgpack-encoding' of https://github.com/benwis/leptos into pr/107 2022-11-25 14:52:19 -05:00
Greg Johnston
f7b1e732c7 Update integrations 2022-11-25 14:52:14 -05:00
Greg Johnston
93f68e022f Merge branch 'main' into msgpack-encoding 2022-11-25 14:35:52 -05:00
Greg Johnston
2b4dc76d95 Clear warnings in examples 2022-11-25 14:34:14 -05:00
Greg Johnston
55f70367b5 Clear warnings in library 2022-11-25 14:32:25 -05:00
Greg Johnston
a01b0cbbc6 Clear warnings from examples 2022-11-25 14:31:03 -05:00
Greg Johnston
6d329f33eb Remove logging 2022-11-25 14:29:26 -05:00
Greg Johnston
5a863ec411 Actix implementation 2022-11-25 14:28:03 -05:00
Greg Johnston
4800600e4f original_path() for <Outlet/> logic 2022-11-25 13:48:39 -05:00
Greg Johnston
a051b1e08c Proper <Outlet/> logic so we only rerender if it's actually a different parameter 2022-11-25 13:46:55 -05:00
Greg Johnston
4a426be6fb Logging to track rerenders 2022-11-25 13:44:58 -05:00
Greg Johnston
d9ab70de0d Untrack <Outlet/> child to avoid rerenders 2022-11-25 07:57:09 -05:00
Greg Johnston
aaac1d37ac Untrack to avoid double-rendering <Outlet/> 2022-11-24 22:22:27 -05:00
Greg Johnston
498b5345d5 Fix Outlet 2022-11-24 08:51:53 -05:00
Greg Johnston
02a7af2c1e Reduce exponential rerenders to max of 2 2022-11-24 08:40:47 -05:00
Greg Johnston
e465867b30 Fixes issue #110 and improves #[component] docs overall 2022-11-24 07:48:57 -05:00
Greg Johnston
835c465c34 T in For component does *not* need to be Eq 2022-11-24 06:41:15 -05:00
Greg Johnston
45e2c09e53 Merge pull request #114 from safx/typed-event-handlers
Add typed event handlers
2022-11-23 19:04:32 -05:00
Greg Johnston
19d7b8434b Merge branch 'main' into typed-event-handlers 2022-11-23 19:03:52 -05:00
Ben Wishovich
3ac92dc0fe Switched out string for Payload enum in register() function and REGISTERED_SERVER_FUNCTIONS. Not sure if this is the way to go 2022-11-23 15:58:15 -08:00
Greg Johnston
6949750668 Fixing tests and examples 2022-11-23 18:29:20 -05:00
Ben Wishovich
440719071a Switch MessagePack for CBOR, as it's more standardized 2022-11-23 14:23:49 -08:00
Greg Johnston
588ebf51a5 Fix event type in router 2022-11-23 16:54:45 -05:00
Greg Johnston
3a65ad9a51 Fix type inference on server 2022-11-23 16:54:41 -05:00
Greg Johnston
7a10ffd150 A couple small DX improvements re: we, and making sure it builds /tests properly 2022-11-23 15:12:21 -05:00
Greg Johnston
a23d80fe27 Merge pull request #113 from gbj/dx-improvements
Doc and error message improvements
2022-11-23 11:10:20 -05:00
Greg Johnston
6966ef4b39 Fix renderer panic issue on release builds 2022-11-23 11:03:16 -05:00
Greg Johnston
e0c8b827c4 Fix leptos_dom tests 2022-11-23 10:24:50 -05:00
Greg Johnston
ebef5156a5 Final fix to render_to_string tests 2022-11-23 09:59:24 -05:00
safx
f3947abdc2 Merge branch 'main' into typed-event-handlers 2022-11-23 22:51:31 +09:00
Safx
701a12ab46 Add typed event handlers 2022-11-23 22:50:26 +09:00
Greg Johnston
fe830e524c Add docs for component macro (fixes issues #106 and #111) 2022-11-23 07:58:01 -05:00
Greg Johnston
6ddef3018f Better errors on renderer bugs (fixes issue #112) 2022-11-23 07:37:47 -05:00
Greg Johnston
0cbab3ef87 Don't run render_to_string test if csr or hydrate is enabled 2022-11-23 07:21:19 -05:00
Greg Johnston
4fda94144b Add run_child_scope helper 2022-11-23 07:08:18 -05:00
Greg Johnston
b6d902a584 Passes leptos_server tests now 2022-11-22 21:44:02 -05:00
Greg Johnston
102fb9d819 Fix render_to_string test 2022-11-22 21:18:28 -05:00
Greg Johnston
545fcce97c Set book chapters to depend on latest version explicitly 2022-11-22 20:52:46 -05:00
Greg Johnston
33424683d0 Merge pull request #108 from jasonrhansen/fix-counters-example
Assign correct ids in `add_many_counters`
2022-11-22 20:48:50 -05:00
Greg Johnston
19c3186d3f Add a simple render_to_string() helper for synchronous HTML rendering 2022-11-22 19:51:05 -05:00
Greg Johnston
3482f456f8 Add metadata field for integrations 2022-11-22 19:45:38 -05:00
Jason Rodney Hansen
d8a97a81ff Assign correct ids in add_many_counters
The counters and counters-stable examples didn't assign the correct ids when
clicking the "Add 1000 Counters" button if there were already counters added,
which meant clicking the "x" to remove them would remove the wrong counter.
2022-11-22 17:00:04 -07:00
Ben Wishovich
931e60347d It mostly works now. Remove lifetime, edit macro to take encoding option, and flail around a bit 2022-11-22 15:12:45 -08:00
Ben Wishovich
2a547936d4 Almost there maybe? 2022-11-22 10:41:15 -08:00
Greg Johnston
2ce7e71748 0.0.18 2022-11-22 07:37:34 -05:00
Greg Johnston
4f205b5368 0.0.18 2022-11-22 07:32:12 -05:00
Greg Johnston
8e624d4942 Update book 2022-11-22 07:32:05 -05:00
Greg Johnston
cee32a3f8f Merge pull request #105 from gbj/thread-local-runtimes
Thread local runtimes
2022-11-22 07:29:21 -05:00
Greg Johnston
e827ee93e2 leptos_server doctests 2022-11-22 06:42:00 -05:00
Ben Wishovich
6b77b51fa0 Get a bit closer with the macro 2022-11-21 22:38:53 -08:00
Ben Wishovich
6564b95342 WIP commit for MessagePack Encoding 2022-11-21 22:07:56 -08:00
Greg Johnston
2651bf5fef Fix meta and router tests 2022-11-21 22:45:56 -05:00
Greg Johnston
10d19f7fb3 leptos_macro tests 2022-11-21 22:27:25 -05:00
Greg Johnston
00de5d0d88 Fix Suspense doctest 2022-11-21 22:14:17 -05:00
Greg Johnston
0f0e3da407 Fix map_keyed test 2022-11-21 22:03:13 -05:00
Greg Johnston
4a741d772b Fix import in test 2022-11-21 21:50:29 -05:00
Greg Johnston
6c521226e3 Update other packages to handle new thread-local reactives 2022-11-21 21:46:07 -05:00
Greg Johnston
739e7db49d Support for multiple, independent Runtimes on a single thread without leaking 2022-11-21 21:11:03 -05:00
Greg Johnston
7c79cb1b1f Merge pull request #102 from gbj/meta-docs
Fixes issue #98
2022-11-21 16:50:39 -05:00
Greg Johnston
4f522d135b Fx doctests 2022-11-21 16:50:04 -05:00
Greg Johnston
60ecd740f5 Fix root doctest 2022-11-21 16:08:53 -05:00
Greg Johnston
b707eada86 Fix root-level doctest 2022-11-21 10:50:43 -05:00
Greg Johnston
71594daa93 Fix root doctest 2022-11-21 10:50:12 -05:00
Greg Johnston
89f837d3b6 Fixes #98, cleans up leptos_meta, and improves interface by removing manual .into() calls 2022-11-21 10:47:54 -05:00
Greg Johnston
014b5f9453 Update note in router docs 2022-11-21 09:54:10 -05:00
Greg Johnston
55896d97b8 Clear warning 2022-11-21 09:40:48 -05:00
Greg Johnston
5e532b60b0 prevent_default until after navigation so a failed navigation will fall back to browser navigation 2022-11-21 09:40:42 -05:00
Greg Johnston
a3181dea64 Clear some form-related warnings 2022-11-21 09:35:09 -05:00
Greg Johnston
1f1218bbb7 Fix broken links and other issues in docs 2022-11-21 09:25:09 -05:00
Greg Johnston
9322cc991b Provide whole Request<Body> to server functions in Axum 2022-11-21 07:37:58 -05:00
Greg Johnston
d0c6319a72 Resolves issue #97 2022-11-21 07:30:13 -05:00
Greg Johnston
9f1b27ad26 Merge pull request #101 from gbj/server-integrations
Server integrations for Axum and Actix
2022-11-21 07:07:03 -05:00
Greg Johnston
986cd2979a Remove unused Request/Response stuff in router 2022-11-20 22:13:31 -05:00
Greg Johnston
8b3a8489b6 handle_server_fns for Axum 2022-11-20 22:13:17 -05:00
Greg Johnston
525a31bf3d Working render_app_to_stream for Axum 2022-11-20 18:28:47 -05:00
Greg Johnston
f773f52abc Initial work on Axum integration 2022-11-20 17:41:04 -05:00
Greg Johnston
257c07325e Version number to 0.0.1 2022-11-20 16:08:02 -05:00
Greg Johnston
01a1226c53 TODO handling the runtime leak in general 2022-11-20 16:06:16 -05:00
Greg Johnston
5208616178 Consolidate functions 2022-11-20 16:05:07 -05:00
Greg Johnston
4e8c1758c3 render_app_to_stream helper in leptos_actix 2022-11-20 16:03:08 -05:00
Greg Johnston
cbcd7e506f Merge pull request #95 from gbj/server-context-in-server-fns
Allow accessing `Scope` from server functions
2022-11-20 15:46:52 -05:00
Greg Johnston
eff42a196f actix-web integration with builtin server function handler route 2022-11-20 15:25:45 -05:00
Greg Johnston
20634e38a1 Refer to full type, in case it hasn't been imported 2022-11-20 15:04:05 -05:00
Greg Johnston
4f3d7dc492 Add server context to counter-isomorphic example 2022-11-20 14:18:27 -05:00
Greg Johnston
6ddc720227 Allow accessing Scope from server functions, which can be used to inject server-only dependencies like HttpRequest 2022-11-19 14:44:35 -05:00
Greg Johnston
8077ae9ead Doctests: opt out of running futures on csr and hydrate outside the browser 2022-11-19 07:39:09 -05:00
Greg Johnston
75de8a95b6 Make todo-app-sqlite work in fully-WASMless mode with <Suspense/> and streaming 2022-11-19 07:36:16 -05:00
Greg Johnston
63d06211b9 Fix which Span this is using 2022-11-18 16:46:54 -05:00
Greg Johnston
ba199e1acb Fix doctests out of date with new API 2022-11-18 16:46:41 -05:00
Greg Johnston
9f4b3c9f26 Clear warnings 2022-11-18 16:46:25 -05:00
Greg Johnston
d654a13541 Clear some macro warnings 2022-11-18 16:39:17 -05:00
Greg Johnston
63ae4e7dda Fix dependency version numbers 2022-11-18 15:47:45 -05:00
Greg Johnston
ad880efc0d leptos 0.0.17 and leptos_router 0.0.3 2022-11-18 15:45:04 -05:00
Greg Johnston
5ff806d35a Merge pull request #92 from gbj/action-api
`Action` and `MultiAction` API changes
2022-11-18 15:21:07 -05:00
Greg Johnston
165ec069ba Deletion feature 2022-11-18 15:20:33 -05:00
Greg Johnston
be7bce03dc Optimistic UI 2022-11-18 14:58:10 -05:00
Greg Johnston
1b1182114d Fix up example since there's no CSR option 2022-11-18 13:53:16 -05:00
Greg Johnston
412693c2c3 MultiAction, create_multi_action, create_server_multi_action, and MultiActionForm 2022-11-18 13:25:46 -05:00
Greg Johnston
5c36f0963c Initial version of todo app with sqlite 2022-11-18 13:25:12 -05:00
Greg Johnston
491f124669 Merge pull request #89 from jquesada2016/main
Add ability to get/set signals untracked
2022-11-18 12:13:07 -05:00
Greg Johnston
43524c0135 Clean up docs on counter-isomorphic 2022-11-18 11:48:08 -05:00
Greg Johnston
5562e2d6ee Tests 2022-11-18 11:30:26 -05:00
Greg Johnston
bbf2d69b55 Merge pull request #90 from gbj/self-triggering-effect
Allow triggering an effect to re-run from within the effect
2022-11-18 11:28:08 -05:00
Jose Quesada
00b6b39ee0 impl UntrackedGettableSignal for MaybeSignal 2022-11-18 10:08:28 -06:00
Jose Quesada
3d88227bac impl UntrackedGettableSignal for Signal 2022-11-18 10:01:35 -06:00
Greg Johnston
d530b28348 Give direct access to input and value fields on actions 2022-11-18 10:56:00 -05:00
Greg Johnston
97a7240e26 Correct docs on create_scope and child_scope 2022-11-18 10:41:19 -05:00
Greg Johnston
2ad49a0a7e Restore view-tests 2022-11-18 10:28:23 -05:00
Greg Johnston
58e0bead02 Fix JS path in hackernews example 2022-11-18 10:24:36 -05:00
Jose Quesada
fe41b6c840 renamed UntrackedSettableSignal::set to set_untracked 2022-11-18 08:27:14 -06:00
Greg Johnston
8e2930141a ... oops. This is why we have tests. 2022-11-17 21:30:35 -05:00
Jose Quesada
36777c2055 Merge branch 'main' of https://github.com/jquesada2016/leptos 2022-11-17 19:30:33 -06:00
Greg Johnston
7ad8a6bef2 Clear up some r-a issues (and allow for stable in future) 2022-11-17 20:30:32 -05:00
Greg Johnston
e26393a42c Fix router issues 2022-11-17 20:30:18 -05:00
Jose Quesada
6e78e85590 impl UntrackedSettableSignal for WriteSignal, RwSignal 2022-11-17 19:30:05 -06:00
Greg Johnston
46b1a96cc7 Allow triggering an effect to re-run from within the effect (so that e.g., you can get() and set() the same signal within the effect — see issue #83) 2022-11-17 20:16:57 -05:00
jquesada2016
d35fdf71ed Merge branch 'gbj:main' into main 2022-11-17 19:06:33 -06:00
Greg Johnston
0473093d0a Merge pull request #88 from gbj/signal-wrappers
Provide `Signal<T>` and `MaybeSignal<T>` wrapper types.
2022-11-17 19:50:12 -05:00
Jose Quesada
4a187e83f7 impl UntrackedGettableSignal for ReadSignal, RwSignal, and Memo 2022-11-17 18:46:59 -06:00
Greg Johnston
d6c6ab7939 Create list of common bugs (includes #82 and #83) 2022-11-17 18:20:54 -05:00
Greg Johnston
5b64af1fed Fix doctests 2022-11-17 17:55:57 -05:00
Greg Johnston
72f20f7413 Remove to clarify docs 2022-11-17 17:45:58 -05:00
Greg Johnston
f4e5ef41b2 Relax constraint on with function 2022-11-17 17:45:52 -05:00
Greg Johnston
f709b46d29 Add generic wrappers for signal types 2022-11-17 17:45:43 -05:00
Greg Johnston
f87ac34656 Merge pull request #86 from benwis/axum-example
Working Axum Example!
2022-11-17 09:08:26 -05:00
Ben Wishovich
13a1d2efaa Merge branch 'gbj:main' into axum-example 2022-11-16 18:34:01 -08:00
Ben Wishovich
cae3bb8bbd Fix CSS imports, is a bit clunky though 2022-11-16 17:26:45 -08:00
Greg Johnston
77504de8f1 Correctly set value and input when using <ActionForm/> so we can do real optimistic UI (see issue #51) 2022-11-16 20:16:21 -05:00
Greg Johnston
c17c6549cf Resolve ambiguous main import error 2022-11-16 20:15:29 -05:00
Ben Wishovich
971f75b6c5 It mostly works, except for the CSS 2022-11-16 16:09:51 -08:00
Ben Wishovich
fc6a3c0eb2 Getting closer 2022-11-16 13:36:35 -08:00
Ben Wishovich
cca63e6724 Closer to the goal! 2022-11-16 13:05:06 -08:00
Ben Wishovich
becd107290 Commited almost working example 2022-11-16 08:13:49 -08:00
Ben Wishovich
36f86afa02 Merge remote-tracking branch 'origin/main' into axum-example 2022-11-16 07:41:52 -08:00
Greg Johnston
96238c553e Fix router example rendering 2022-11-16 07:24:21 -05:00
Greg Johnston
3c3e87f97c Merge pull request #81 from gbj/send-streaming
Make `render_to_stream()` return a `Stream` that is `Send`
2022-11-16 07:08:54 -05:00
Greg Johnston
fd6c2d3059 Combine resources and suspenses so it's possible for suspenses to resolve first 2022-11-16 07:07:54 -05:00
Greg Johnston
2173bb8a29 Implements render_to_stream() in a way that is Send 2022-11-15 22:20:25 -05:00
Ben Wishovich
19f89633ff Some more WIP improvements for the Axum example 2022-11-15 14:55:38 -08:00
Ben Wishovich
3885816699 Add hackernews-axum example 2022-11-15 14:08:09 -08:00
Greg Johnston
21471f809f Merge pull request #78 from gbj/fix-router-example
Fix rendering issues
2022-11-15 13:27:41 -05:00
Greg Johnston
ccb5aeac6d Resolving lots of sibling order issues 2022-11-15 12:52:50 -05:00
Greg Johnston
04b20ebad4 Merge branch 'main' of https://github.com/gbj/leptos 2022-11-15 12:12:13 -05:00
Greg Johnston
e74e9a3fc9 Restore logging functions 2022-11-15 12:11:44 -05:00
Greg Johnston
4ba9844852 Rendering work 2022-11-15 12:11:35 -05:00
Greg Johnston
7498282936 Merge pull request #76 from benwis/example-improvements
Updated hacker_news and counter-isomorphic to SFA format, fixed Router example, and added some READMEs
2022-11-14 22:20:34 -05:00
Greg Johnston
780c6d2e64 Improvements to the view macro to handle a wider variety of positions/relationships between child nodes 2022-11-14 21:33:49 -05:00
Greg Johnston
796764493b Fix <Suspense/> part of example 2022-11-14 17:31:57 -05:00
Greg Johnston
b0f64aacba leptos_router should default to csr like leptos and leptos_macro 2022-11-14 17:31:45 -05:00
Greg Johnston
7cfd6fa42b Clear warnings in Suspense 2022-11-14 17:12:26 -05:00
Greg Johnston
745317a79b Additions to reactivity chapter 2022-11-14 16:58:35 -05:00
Greg Johnston
777f25e311 Add ability to use mermaid diagrams for reactive graphs 2022-11-14 16:58:22 -05:00
162 changed files with 8166 additions and 3132 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ blob.rs
Cargo.lock
**/*.rs.bk
.DS_Store
.leptos.kdl

View File

@@ -4,10 +4,15 @@ members = [
"leptos",
"leptos_dom",
"leptos_core",
"leptos_config",
"leptos_macro",
"leptos_reactive",
"leptos_server",
# integrations
"integrations/actix",
"integrations/axum",
# libraries
"meta",
"router",
@@ -19,9 +24,13 @@ members = [
"examples/counters-stable",
"examples/fetch",
"examples/hackernews",
"examples/hackernews-axum",
"examples/parent-child",
"examples/router",
"examples/todomvc",
"examples/todo-app-sqlite",
"examples/todo-app-sqlite-axum",
"examples/todo-app-cbor",
"examples/view-tests",
# book
@@ -37,4 +46,29 @@ lto = true
opt-level = 'z'
[workspace.metadata.cargo-all-features]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
skip_feature_sets = [
[
"csr",
"ssr",
],
[
"csr",
"hydrate",
],
[
"ssr",
"hydrate",
],
[
"serde",
"serde-lite",
],
[
"serde-lite",
"miniserde",
],
[
"serde",
"miniserde",
],
]

View File

@@ -59,7 +59,8 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
Here are some resources for learning more about Leptos:
- [Examples](https://github.com/gbj/leptos/tree/main/examples)
- [API Documentation](https://docs.rs/leptos/latest/leptos/) (in progress)
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
- [Common Bugs](https://github.com/gbj/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
- Leptos Guide (in progress)
## `nightly` Note
@@ -107,6 +108,7 @@ The gold standard for testing raw rendering performance for front-end web framew
### Can I use this for native GUI?
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:
- Use signals, derived signals, and memos to create your reactive system
- Create GUI widgets
- Use event listeners to update signals
@@ -131,7 +133,7 @@ There are some practical differences that make a significant difference:
- **Maturity:** Sycamore is obviously a much more mature and stable library with a larger ecosystem.
- **Templating:** Leptos uses a JSX-like template format (built on [syn-rsx](https://github.com/stoically/syn-rsx)) for its `view` macro. Sycamore offers the choice of its own templating DSL or a builder syntax.
- **Template node cloning:** Leptos's `view` macro compiles to a static HTML string and a set of instructions of how to assign its reactive values. This means that at runtime, Leptos can clone a `<template>` node rather than calling `document.createElement()` to create DOM nodes. This is a _significantly_ faster way of rendering components.
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` *(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)*
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
```rust

View File

@@ -5,7 +5,7 @@ fn leptos_ssr_bench(b: &mut Bencher) {
use leptos::*;
b.iter(|| {
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(cx: Scope, initial: i32) -> Element {
let (value, set_value) = create_signal(cx, initial);

View File

@@ -115,7 +115,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
set_mode(new_mode);
});
let add_todo = move |ev: web_sys::Event| {
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
@@ -222,7 +222,7 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
pub fn Todo(cx: Scope, todo: Todo) -> Element {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
let input: Element;
let input = NodeRef::new(cx);
let save = move |value: &str| {
let value = value.trim();

View File

@@ -11,7 +11,7 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
use ::leptos::*;
b.iter(|| {
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new(cx)/>
@@ -63,7 +63,7 @@ fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
use ::leptos::*;
b.iter(|| {
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>

63
docs/COMMON_BUGS.md Normal file
View File

@@ -0,0 +1,63 @@
# Leptos Gotchas: Common Bugs
This document is intended as a running list of common issues, with example code and solutions.
## Reactivity
### Avoid writing to a signal from an effect
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.
```rust
let (a, set_a) = create_signal(cx, 0);
let (b, set_a) = create_signal(cx, false);
create_effect(cx, move |_| {
if a() > 5 {
set_b(true);
}
});
```
This creates an inefficient chain of updates, and can easily lead to infinite loops in more complex applications.
**Solution**: Follow the rule, _What can be derived, should be derived._ In this case, this has the benefit of massively reducing the code size, too!
```rust
let (a, set_a) = create_signal(cx, 0);
let b = move || a () > 5;
```
## Templates and the DOM
### `<input value=...>` doesn't update or stops updating
Many DOM attributes can be updated either by setting an attribute on the DOM node, or by setting an object property directly on it. In general, `setAttribute()` stops working once the property has been set.
This means that in practice, attributes like `value` or `checked` on an `<input/>` element only update the _default_ value for the `<input/>`. If you want to reactively update the value, you should use `prop:value` instead to set the `value` property.
```rust
let (a, set_a) = create_signal(cx, "Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {
cx,
// ❌ reactivity doesn't work as expected: typing only updates the default
// of each input, so if you start typing in the second input, it won't
// update the first one
<input value=a on:input=on_input />
<input value=a on:input=on_input />
}
```
```rust
let (a, set_a) = create_signal(cx, "Starting value".to_string());
let on_input = move |ev| set_a(event_target_value(&ev));
view! {
cx,
// ✅ works as intended by setting the value *property*
<input prop:value=a on:input=on_input />
<input prop:value=a on:input=on_input />
}
```

View File

@@ -4,3 +4,13 @@ language = "en"
multilingual = false
src = "src"
title = "The Leptos Guide"
[preprocessor]
[preprocessor.mermaid]
command = "mdbook-mermaid"
[output]
[output.html]
additional-js = ["mermaid.min.js", "mermaid-init.js"]

View File

@@ -0,0 +1 @@
mermaid.initialize({startOnLoad:true});

4
docs/book/mermaid.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"
leptos = { path = "../../../../leptos" }

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"
leptos = { path = "../../../../leptos" }

View File

@@ -4,7 +4,7 @@ fn main() {
mount_to_body(|cx| {
let name = "gbj";
let userid = 0;
let _input_element: Element;
let _input_element = NodeRef::new(cx);
view! {
cx,

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0"
leptos = { path = "../../../../leptos" }

View File

@@ -1,15 +1,23 @@
use leptos::*;
fn main() {
run_scope(|cx| {
run_scope(create_runtime(), |cx| {
// signal
let (count, set_count) = create_signal(cx, 1);
// derived signal
let double_count = move || count() * 2;
// memo
let memoized_square = create_memo(cx, move |_| count() * count());
// effect
create_effect(cx, move |_| {
println!(
"count =\t\t{}\ndouble_count = \t{}",
"count =\t\t{} \ndouble_count = \t{}, \nsquare = \t{}",
count(),
double_count(),
memoized_square()
);
});

View File

@@ -13,6 +13,8 @@ let mut a = 0;
let mut b = 0;
let c = a + b;
assert_eq!(c, 0); // sanity check
a = 2;
b = 2;
@@ -25,11 +27,13 @@ But thats _exactly_ how reactive programming works.
```rust
use leptos::*;
run_scope(|cx| {
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
assert_eq!(c(), 0); // yep, still true
set_a(2);
set_b(2);
@@ -37,8 +41,73 @@ run_scope(|cx| {
});
```
Hopefully, this makes some intuitive sense. After all, `c` is a closure. Calling it again causes it to access its values a second time. This isnt _that_ cool.
```rust
use leptos::*;
run_scope(create_runtime(), |cx| {
let (a, set_a) = create_signal(cx, 0);
let (b, set_b) = create_signal(cx, 0);
let c = move || a() + b();
create_effect(cx, move |_| {
println!("c = {}", c()); // prints "c = 0"
});
set_a(2); // prints "c = 2"
set_b(2); // prints "c = 4"
});
```
This examples a little different. [`create_effect`](https://docs.rs/leptos/latest/leptos/fn.create_effect.html) defines a “side effect,” a bridge between the reactive system of signals and the outside world. Effects synchronize the reactive system with everything else: the console, the filesystem, an HTTP request, whatever.
Because the closure `c` is called within the effect and in turns calls the signals `a` and `b`, the effect automatically subscribes to the signals `a` and `b`. This means that whenever `a` or `b` is updated, the effect will re-run, logging the value again.
You can picture the reactive graph for this system like this:
```mermaid
graph TD;
A-->C;
B-->C;
C-->Effect;
```
This is the foundation on which _everything_ else is built.
## Reactive Primitives
### Overview
The reactive system is built on the interaction between these two halves: **signals** and **effects**. When a signal is called inside an effect, the effect automatically subscribes to the signal. When a signals value is updated, it automatically notifies all its subscribers, and they re-run.
The following simple example contains most of the core reactive concepts:
```rust
{{#include ../project/ch04_reactivity/src/main.rs}}
```
This creates a reactive graph like this:
```mermaid
graph TD;
count-->double_count;
count-->memoized_square;
count-->effect;
double_count-->effect;
memoized_square-->effect;
```
**Signals** are reactive values created using [`create_signal`](https://docs.rs/leptos/latest/leptos/fn.create_signal.html) or [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html).
**Derived Signals** computations in ordinary closures that rely on other signals. The computation re-runs whenever you access its value.
**Memos** are computations that are memoized with [create_memo](https://docs.rs/leptos/latest/leptos/fn.create_memo.html). Memos only re-run when one of their signal dependencies has changed.
And **effects** (created with [create_effect](<(https://docs.rs/leptos/latest/leptos/fn.create_effect.html)>) synchronize the reactive system with something outside it.
The rest of this chapter will walk through each of these concepts in more depth.
### Signals
A **signal** is a piece of data that may change over time, and notifies other code when it has changed. This is the core primitive of Leptoss reactive system.
@@ -53,7 +122,7 @@ let (value, set_value) = create_signal(cx, 0);
#### `ReadSignal<T>`
The `ReadSignal` half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
The [`ReadSignal`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html) half of this tuple allows you to get the current value of the signal. Reading that value in a reactive context automatically subscribes to any further changes. You can access the value by simply calling the `ReadSignal` as a function.
```rust
let (value, set_value) = create_signal(cx, 0);
@@ -65,7 +134,7 @@ assert_eq!(value(), 0);
> Here, a **reactive context** means anywhere within an `Effect`. Leptoss templating system is built on top of its reactive system, so if youre reading the signals value within the template, the template will automatically subscribe to the signal and update exactly the value that needs to change in the DOM.
Calling a `ReadSignal` clones the value it contains. If thats too expensive, use `ReadSignal::with()` to borrow the value and do whatever you need.
Calling a `ReadSignal` clones the value it contains. If thats too expensive, use [`ReadSignal::with()`](https://docs.rs/leptos/latest/leptos/struct.ReadSignal.html#method.with) to borrow the value and do whatever you need.
```rust
struct MySuperExpensiveStruct {
@@ -77,14 +146,12 @@ let (value, set_value) = create_signal(cx, MySuperExpensiveStruct::default());
// ❌ this is going to clone the `StructThatsSuperExpensiveToClone` unnecessarily!
let lowercased = move || value().a.to_lowercase();
// ✅ only use what we need
let lowercased = move || value.with(|value| value.to_lowercase());
// 🔥 aaaand there's no need to type "value" three times in a row
let lowercased = move || value.with(String::to_lowercase);
let lowercased = move || value.with(|value: &MySuperExpensiveStruct| value.a.to_lowercase());
```
#### `WriteSignal<T>`
The `WriteSignal` half of this tuple allows you to update the value of the signal, which will automatically notify anything thats listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call `WriteSignal::update` instead.
The [`WriteSignal`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html) half of this tuple allows you to update the value of the signal, which will automatically notify anything thats listening to the value that something has changed. If you simply call the `WriteSignal` as a function, its value will be set to the argument you pass. If you want to mutate the value in place instead of replacing it, you can call [`WriteSignal::update`](https://docs.rs/leptos/latest/leptos/struct.WriteSignal.html#method.update) instead.
```rust
// often you just want to replace the value
@@ -99,3 +166,75 @@ assert_eq!(items(), vec![1]);
```
> Under the hood, `set_value(1)` is just syntactic sugar for `set_value.update(|n| *n = 1)`.
#### `RwSignal<T>`
This kind of “read-write segregation,” in which the getter and the setter are stored in separate variables, may be familiar from the tuple-based ”hooks” pattern in libraries like React, Solid, Yew, or Dioxus. It encourages clear contracts between components. For example, if a child component only needs to be able to read a signal, but shouldnt be able to update it (and therefore trigger changes in other parts of the application), you can pass it only the `ReadSignal`.
Sometimes, however, you may prefer to keep the getter and setter combined in one variable. For example, its awkward and repetitive to store both halves of a signal in another data structure:
```rust
# use leptos::*;
// pretty repetitive
struct AppState {
count: ReadSignal<i32>,
set_count: WriteSignal<i32>,
name: ReadSignal<String>,
set_name: WriteSignal<String>
}
#[component]
fn App(cx: Scope) {
let (count, set_count) = create_signal(cx, 0);
let (name, set_name) = create_signal(cx, "Alice".to_string());
provide_context(cx, AppState {
count,
set_count,
name,
set_name
})
todo!()
}
```
Or maybe you just like to keep your getters and setters in one place.
In this case, you can use [`create_rw_signal`](https://docs.rs/leptos/latest/leptos/fn.create_rw_signal.html) and the [`RwSignal`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html) type. This returns a **R**ead-**w**rite Signal, which has the same [`get`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get), [`with`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.with), [`set`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.set), and [`update`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.update) functions as the `ReadSignal` and `WriteSignal` halves.
```rust
# use leptos::*;
// better
struct AppState {
count: RwSignal<i32>,
name: RwSignal<String>,
}
#[component]
fn App(cx: Scope) {
let count = create_rw_signal(cx, 0);
let name = create_rw_signal(cx, "Alice".to_string());
provide_context(cx, AppState {
count,
name,
})
todo!()
}
```
If you still want to hand off read-only access to another part of the app, you can get a `ReadSignal` with [`RwSignal::read_only()`](https://docs.rs/leptos/latest/leptos/struct.RwSignal.html#method.get).
### Derived Signals
(todo)
### Memos
(todo)
### Effects
(todo)

View File

@@ -16,15 +16,15 @@ serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
lazy_static = "1"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
gloo = { git = "https://github.com/rustwasm/gloo" }
#counter = { path = "../counter", default-features = false}
[features]
default = ["csr"]
@@ -34,10 +34,11 @@ ssr = [
"dep:actix-files",
"dep:actix-web",
"leptos/ssr",
"leptos_actix",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web"]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -3,16 +3,19 @@
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
to generate the WebAssembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
If for some reason you want to run it as a fully client side app, that can be done with the instructions below.

View File

@@ -9,7 +9,7 @@ cfg_if! {
use crate::counters::*;
#[wasm_bindgen]
pub fn main() {
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();

View File

@@ -1,6 +1,5 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_router::*;
mod counters;
// boilerplate to run in different modes
@@ -10,74 +9,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;
#[get("{tail:.*}")]
async fn render(req: HttpRequest) -> impl Responder {
let path = req.path();
let path = "http://leptos".to_string() + path;
println!("path = {path}");
HttpResponse::Ok().content_type("text/html").body(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Isomorphic Counter</title>
</head>
<body>
{}
</body>
<script type="module">import init, {{ main }} from './pkg/leptos_counter_isomorphic.js'; init().then(main);</script>
</html>"#,
run_scope({
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <Counters/>}
}
})
))
}
#[post("/api/{tail:.*}")]
async fn handle_server_fns(
req: HttpRequest,
params: web::Path<String>,
body: web::Bytes,
) -> impl Responder {
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
match server_fn(&body).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
if let Some("application/json") = accept_header {
HttpResponse::Ok().body(serialized)
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
HttpResponse::SeeOther()
.insert_header(("Location", "/"))
.content_type("application/json")
.body(serialized)
}
}
Err(e) => {
eprintln!("server function error: {e:#?}");
HttpResponse::InternalServerError().body(e.to_string())
}
}
} else {
HttpResponse::BadRequest().body(format!("Could not find a server function at that route."))
}
}
use std::{net::SocketAddr, env};
#[get("/api/events")]
async fn counter_events() -> impl Responder {
@@ -98,17 +30,20 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let addr = SocketAddr::from(([127,0,0,1],3000));
crate::counters::register_server_functions();
HttpServer::new(|| {
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_counter_isomorphic").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(counter_events)
.service(handle_server_fns)
.service(render)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <Counters/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8081))?
.bind(&addr)?
.run()
.await
}

View File

@@ -2,5 +2,6 @@
This example creates a simple counter in a client side rendered app with Rust and WASM!
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,14 +1,22 @@
use leptos::*;
pub fn simple_counter(cx: Scope) -> web_sys::Element {
let (value, set_value) = create_signal(cx, 0);
/// A simple counter component.
///
/// You can document each of the properties passed to a component using the format below.
///
/// # Props
/// - **initial_value** [`i32`] - The value the counter should start at.
/// - **step** [`i32`] - The change that should be applied on each step.
#[component]
pub fn SimpleCounter(cx: Scope, initial_value: i32, step: i32) -> web_sys::Element {
let (value, set_value) = create_signal(cx, initial_value);
view! { cx,
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<button on:click=move |_| set_value(initial_value)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>
}
}

View File

@@ -1,8 +1,8 @@
use counter::simple_counter;
use counter::*;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(simple_counter)
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> })
}

View File

@@ -3,10 +3,11 @@ use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::*;
use web_sys::HtmlElement;
use counter::*;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(counter::simple_counter);
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=0 step=1/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();

View File

@@ -0,0 +1,10 @@
# Leptos Counters Example on Rust Stable
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -7,6 +7,8 @@ fn main() {
mount_to_body(|cx| view! { cx, <Counters/> })
}
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
@@ -28,12 +30,14 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
};
let add_many_counters = move |_| {
let mut new_counters = vec![];
for next_id in 0..1000 {
let next_id = next_counter_id.get();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
new_counters.push((next_id, signal));
}
set_counters.update(|counters| counters.extend(new_counters.iter()));
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
@@ -46,7 +50,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
"Add Counter"
</button>
<button on:click=add_many_counters>
"Add 1000 Counters"
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"

View File

@@ -0,0 +1,10 @@
# Leptos Counters Example
This example showcases a basic Leptos app with many counters. It is a good example of how to set up a basic reactive app with signals and effects, and how to interact with browser events.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,6 +1,8 @@
use leptos::*;
use leptos::{For, ForProps};
const MANY_COUNTERS: usize = 1000;
type CounterHolder = Vec<(usize, (ReadSignal<i32>, WriteSignal<i32>))>;
#[derive(Copy, Clone)]
@@ -22,12 +24,14 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
};
let add_many_counters = move |_| {
let mut new_counters = vec![];
for next_id in 0..1000 {
let next_id = next_counter_id();
let new_counters = (next_id..next_id + MANY_COUNTERS).map(|id| {
let signal = create_signal(cx, 0);
new_counters.push((next_id, signal));
}
set_counters.update(move |counters| counters.extend(new_counters.iter()));
(id, signal)
});
set_counters.update(move |counters| counters.extend(new_counters));
set_next_counter_id.update(|id| *id += MANY_COUNTERS);
};
let clear_counters = move |_| {
@@ -40,7 +44,7 @@ pub fn Counters(cx: Scope) -> web_sys::Element {
"Add Counter"
</button>
<button on:click=add_many_counters>
"Add 1000 Counters"
{format!("Add {MANY_COUNTERS} Counters")}
</button>
<button on:click=clear_counters>
"Clear Counters"

View File

@@ -11,7 +11,7 @@ serde = { version = "1", features = ["derive"] }
log = "0.4"
console_log = "0.2"
console_error_panic_hook = "0.1.7"
gloo-timers = { version = "0.2", features = ["futures"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

10
examples/fetch/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Client Side Fetch
This example shows how to fetch data from the client in WebAssembly.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,5 +1,6 @@
use std::time::Duration;
use gloo_timers::future::TimeoutFuture;
use leptos::*;
use serde::{Deserialize, Serialize};
@@ -9,6 +10,10 @@ pub struct Cat {
}
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
// artificial delay
// the cat API is too fast to show the transition
TimeoutFuture::new(500).await;
if count > 0 {
let res = reqwasm::http::Request::get(&format!(
"https://api.thecatapi.com/v1/images/search?limit={}",
@@ -32,8 +37,9 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
pub fn fetch_example(cx: Scope) -> web_sys::Element {
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
let (pending, set_pending) = create_signal(cx, false);
view! { cx,
view! { cx,
<div>
<label>
"How many cats would you like?"
@@ -45,16 +51,22 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
}
/>
</label>
{move || pending().then(|| view! { cx, <p>"Loading more cats..."</p> })}
<div>
<Suspense fallback={"Loading (Suspense Fallback)...".to_string()}>
// <Transition/> holds the previous value while new async data is being loaded
// Switch the <Transition/> to <Suspense/> to fall back to "Loading..." every time
<Transition
fallback={"Loading (Suspense Fallback)...".to_string()}
set_pending
>
{move || {
cats.read().map(|data| match data {
Err(_) => view! { cx, <pre>"Error"</pre> },
Ok(cats) => view! { cx,
Ok(cats) => view! { cx,
<div>{
cats.iter()
.map(|src| {
view! { cx,
view! { cx,
<img src={src}/>
}
})
@@ -64,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> web_sys::Element {
})
}
}
</Suspense>
</Transition>
</div>
</div>
}

8
examples/gtk/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Leptos in a GTK App
This example creates a basic GTK app that uses Leptoss reactive primitives.
## Build and Run
Unlike the other examples, this has a variety of build prerequisites that are out of scope of this crate. More detail on that can be found [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation.html). The example comes from [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html) and should be
runnable with `cargo run` if you have the GTK prerequisites installed.

View File

@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
fn main() {
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();

View File

@@ -0,0 +1,51 @@
[package]
name = "leptos-hackernews-axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"leptos/ssr",
"leptos_axum",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "http", "leptos_axum"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
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.

View File

@@ -0,0 +1,29 @@
# Leptos Hacker News Example with Axum
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="css" href="./static/style.css"/>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,78 @@
use leptos::Serializable;
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
format!("https://node-hnapi.herokuapp.com/{path}")
}
pub fn user(path: &str) -> String {
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let json = gloo_net::http::Request::get(path)
.send()
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::from_json(&json).ok()
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
where
T: Serializable,
{
let json = reqwest::get(path)
.await
.map_err(|e| log::error!("{e}"))
.ok()?
.text()
.await
.ok()?;
T::from_json(&json).map_err(|e| log::error!("{e}")).ok()
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Story {
pub id: usize,
pub title: String,
pub points: Option<i32>,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
#[serde(alias = "type")]
pub story_type: String,
pub url: String,
#[serde(default)]
pub domain: String,
#[serde(default)]
pub comments: Option<Vec<Comment>>,
pub comments_count: Option<usize>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct Comment {
pub id: usize,
pub level: usize,
pub user: Option<String>,
pub time: usize,
pub time_ago: String,
pub content: Option<String>,
pub comments: Vec<Comment>,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub struct User {
pub created: usize,
pub id: String,
pub karma: i32,
pub about: Option<String>,
}

View File

@@ -0,0 +1,48 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
mod routes;
use routes::nav::*;
use routes::stories::*;
use routes::story::*;
use routes::users::*;
#[component]
pub fn App(cx: Scope) -> Element {
provide_context(cx, MetaContext::default());
view! {
cx,
<div>
<Stylesheet href="/static/style.css"/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" element=|cx| view! { cx, <User/> }/>
<Route path="stories/:id" element=|cx| view! { cx, <Story/> }/>
<Route path="*stories" element=|cx| view! { cx, <Stories/> }/>
</Routes>
</main>
</Router>
</div>
}
}
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), move |cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -0,0 +1,72 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
routing::{get},
Router,
error_handling::HandleError,
};
use http::StatusCode;
use std::net::SocketAddr;
use tower_http::services::ServeDir;
use std::env;
#[tokio::main]
async fn main() {
use leptos_hackernews_axum::*;
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
log::debug!("serving at {addr}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// These are Tower Services that will serve files from the static and pkg repos.
// HandleError is needed as Axum requires services to implement Infallible Errors
// because all Errors are converted into Responses
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
let pkg_service =HandleError::new( ServeDir::new("./pkg"), handle_file_error);
/// Convert the Errors from ServeDir to a type that implements IntoResponse
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!("File Not Found: {}", err),
)
}
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
// build our application with a route
let app = Router::new()
// `GET /` goes to `root`
.nest_service("/pkg", pkg_service)
.nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <App/> }));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
use leptos_hackernews_axum::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
});
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod nav;
pub mod stories;
pub mod story;
pub mod users;

View File

@@ -0,0 +1,30 @@
use leptos::*;
use leptos_router::*;
#[component]
pub fn Nav(cx: Scope) -> Element {
view! { cx,
<header class="header">
<nav class="inner">
<A href="/">
<strong>"HN"</strong>
</A>
<A href="/new">
<strong>"New"</strong>
</A>
<A href="/show">
<strong>"Show"</strong>
</A>
<A href="/ask">
<strong>"Ask"</strong>
</A>
<A href="/job">
<strong>"Jobs"</strong>
</A>
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
"Built with Leptos"
</a>
</nav>
</header>
}
}

View File

@@ -0,0 +1,154 @@
use leptos::*;
use leptos_router::*;
use crate::api;
fn category(from: &str) -> &'static str {
match from {
"new" => "newest",
"show" => "show",
"ask" => "ask",
"job" => "jobs",
_ => "news",
}
}
#[component]
pub fn Stories(cx: Scope) -> Element {
let query = use_query_map(cx);
let params = use_params_map(cx);
let page = move || {
query
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
.unwrap_or(1)
};
let story_type = move || {
params
.with(|p| p.get("stories").cloned())
.unwrap_or_else(|| "top".to_string())
};
let stories = create_resource(
cx,
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
},
);
let hide_more_link = move || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
<div class="news-view">
<div class="news-list-nav">
<span>
{move || if page() > 1 {
view! {
cx,
<a class="page-link"
href=move || format!("/{}?page={}", story_type(), page() - 1)
attr:aria_label="Previous Page"
>
"< prev"
</a>
}
} else {
view! {
cx,
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}
}}
</span>
<span>"page " {page}</span>
<span class="page-link"
class:disabled=hide_more_link
aria-hidden=hide_more_link
>
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
aria-label="Next Page"
>
"more >"
</a>
</span>
</div>
<main class="news-list">
<div>
<Suspense fallback=view! { cx, <p>"Loading..."</p> }>
{move || match stories.read() {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }),
Some(Some(stories)) => {
Some(view! { cx,
<ul>
<For each=move || stories.clone() key=|story| story.id>{
move |cx: Scope, story: &api::Story| {
view! { cx,
<Story story=story.clone() />
}
}
}</For>
</ul>
})
}
}}
</Suspense>
</div>
</main>
</div>
}
}
#[component]
fn Story(cx: Scope, story: api::Story) -> Element {
view! { cx,
<li class="news-item">
<span class="score">{story.points}</span>
<span class="title">
{if !story.url.starts_with("item?id=") {
view! { cx,
<span>
<a href=story.url target="_blank" rel="noreferrer">
{story.title.clone()}
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }
}}
</span>
<br />
<span class="meta">
{if story.story_type != "job" {
view! { cx,
<span>
{"by "}
{story.user.map(|user| view ! { cx, <A href=format!("/users/{}", user)>{user.clone()}</A>})}
{format!(" {} | ", story.time_ago)}
<A href=format!("/stories/{}", story.id)>
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"discuss".into()
}}
</A>
</span>
}
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }
}}
</span>
{(story.story_type != "link").then(|| view! { cx,
<span>
//{" "}
<span class="label">{story.story_type}</span>
</span>
})}
</li>
}
}

View File

@@ -0,0 +1,103 @@
use crate::api;
use leptos::*;
use leptos_router::*;
#[component]
pub fn Story(cx: Scope) -> Element {
let params = use_params_map(cx);
let story = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move { api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await },
);
view! { cx,
<div>
{move || story.read().map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,
<div class="item-view">
<div class="item-view-header">
<a href=story.url target="_blank">
<h1>{story.title}</h1>
</a>
<span class="host">
"("{story.domain}")"
</span>
{story.user.map(|user| view! { cx, <p class="meta">
{story.points}
" points | by "
<A href=format!("/users/{}", user)>{user.clone()}</A>
{format!(" {}", story.time_ago)}
</p>})}
</div>
<div class="item-view-comments">
<p class="item-view-comments-header">
{if story.comments_count.unwrap_or_default() > 0 {
format!("{} comments", story.comments_count.unwrap_or_default())
} else {
"No comments yet.".into()
}}
</p>
<ul class="comment-children">
<For each=move || story.comments.clone().unwrap_or_default() key=|comment| comment.id>
{move |cx, comment: &api::Comment| view! { cx, <Comment comment=comment.clone() /> }}
</For>
</ul>
</div>
</div>
}})}
</div>
}
}
#[component]
pub fn Comment(cx: Scope, comment: api::Comment) -> Element {
let (open, set_open) = create_signal(cx, true);
view! { cx,
<li class="comment">
<div class="by">
<A href=format!("/users/{}", comment.user.clone().unwrap_or_default())>{comment.user.clone()}</A>
{format!(" {}", comment.time_ago)}
</div>
<div class="text" inner_html=comment.content></div>
{(!comment.comments.is_empty()).then(|| {
view! { cx,
<div>
<div class="toggle" class:open=open>
<a on:click=move |_| set_open.update(|n| *n = !*n)>
{
let comments_len = comment.comments.len();
move || if open() {
"[-]".into()
} else {
format!("[+] {}{} collapsed", comments_len, pluralize(comments_len))
}
}
</a>
</div>
{move || open().then({
let comments = comment.comments.clone();
move || view! { cx,
<ul class="comment-children">
<For each=move || comments.clone() key=|comment| comment.id>
{|cx, comment: &api::Comment| view! { cx, <Comment comment=comment.clone() /> }}
</For>
</ul>
}
})}
</div>
}
})}
</li>
}
}
fn pluralize(n: usize) -> &'static str {
if n == 1 {
" reply"
} else {
" replies"
}
}

View File

@@ -0,0 +1,39 @@
use crate::api::{self, User};
use leptos::*;
use leptos_router::*;
#[component]
pub fn User(cx: Scope) -> Element {
let params = use_params_map(cx);
let user = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move { api::fetch_api::<User>(&api::user(&id)).await },
);
view! { cx,
<div class="user-view">
{move || user.read().map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> },
Some(user) => view! { cx,
<div>
<h1>"User: " {&user.id}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}
})}
</div>
}
}

View File

@@ -0,0 +1,326 @@
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 15px;
background-color: #f2f3f5;
margin: 0;
padding-top: 55px;
color: #34495e;
overflow-y: scroll
}
a {
color: #34495e;
text-decoration: none
}
.header {
background-color: #335d92;
position: fixed;
z-index: 999;
height: 55px;
top: 0;
left: 0;
right: 0
}
.header .inner {
max-width: 800px;
box-sizing: border-box;
margin: 0 auto;
padding: 15px 5px
}
.header a {
color: rgba(255, 255, 255, .8);
line-height: 24px;
transition: color .15s ease;
display: inline-block;
vertical-align: middle;
font-weight: 300;
letter-spacing: .075em;
margin-right: 1.8em
}
.header a:hover {
color: #fff
}
.header a.active {
color: #fff;
font-weight: 400
}
.header a:nth-child(6) {
margin-right: 0
}
.header .github {
color: #fff;
font-size: .9em;
margin: 0;
float: right
}
.logo {
width: 24px;
margin-right: 10px;
display: inline-block;
vertical-align: middle
}
.view {
max-width: 800px;
margin: 0 auto;
position: relative
}
.fade-enter-active,
.fade-exit-active {
transition: all .2s ease
}
.fade-enter,
.fade-exit-active {
opacity: 0
}
@media (max-width:860px) {
.header .inner {
padding: 15px 30px
}
}
@media (max-width:600px) {
.header .inner {
padding: 15px
}
.header a {
margin-right: 1em
}
.header .github {
display: none
}
}
.news-view {
padding-top: 45px
}
.news-list,
.news-list-nav {
background-color: #fff;
border-radius: 2px
}
.news-list-nav {
padding: 15px 30px;
position: fixed;
text-align: center;
top: 55px;
left: 0;
right: 0;
z-index: 998;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.news-list-nav .page-link {
margin: 0 1em
}
.news-list-nav .disabled {
color: #aaa
}
.news-list {
position: absolute;
margin: 30px 0;
width: 100%;
transition: all .5s cubic-bezier(.55, 0, .1, 1)
}
.news-list ul {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.news-list {
margin: 10px 0
}
}
.news-item {
background-color: #fff;
padding: 20px 30px 20px 80px;
border-bottom: 1px solid #eee;
position: relative;
line-height: 20px
}
.news-item .score {
color: #335d92;
font-size: 1.1em;
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
width: 80px;
text-align: center;
margin-top: -10px
}
.news-item .host,
.news-item .meta {
font-size: .85em;
color: #626262
}
.news-item .host a,
.news-item .meta a {
color: #626262;
text-decoration: underline
}
.news-item .host a:hover,
.news-item .meta a:hover {
color: #335d92
}
.item-view-header {
background-color: #fff;
padding: 1.8em 2em 1em;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1)
}
.item-view-header h1 {
display: inline;
font-size: 1.5em;
margin: 0;
margin-right: .5em
}
.item-view-header .host,
.item-view-header .meta,
.item-view-header .meta a {
color: #626262
}
.item-view-header .meta a {
text-decoration: underline
}
.item-view-comments {
background-color: #fff;
margin-top: 10px;
padding: 0 2em .5em
}
.item-view-comments-header {
margin: 0;
font-size: 1.1em;
padding: 1em 0;
position: relative
}
.item-view-comments-header .spinner {
display: inline-block;
margin: -15px 0
}
.comment-children {
list-style-type: none;
padding: 0;
margin: 0
}
@media (max-width:600px) {
.item-view-header h1 {
font-size: 1.25em
}
}
.comment-children .comment-children {
margin-left: 1.5em
}
.comment {
border-top: 1px solid #eee;
position: relative
}
.comment .by,
.comment .text,
.comment .toggle {
font-size: .9em;
margin: 1em 0
}
.comment .by {
color: #626262
}
.comment .by a {
color: #626262;
text-decoration: underline
}
.comment .text {
overflow-wrap: break-word
}
.comment .text a:hover {
color: #335d92
}
.comment .text pre {
white-space: pre-wrap
}
.comment .toggle {
background-color: #fffbf2;
padding: .3em .5em;
border-radius: 4px
}
.comment .toggle a {
color: #626262;
cursor: pointer
}
.comment .toggle.open {
padding: 0;
background-color: transparent;
margin-bottom: -.5em
}
.user-view {
background-color: #fff;
box-sizing: border-box;
padding: 2em 3em
}
.user-view h1 {
margin: 0;
font-size: 1.5em
}
.user-view .meta {
list-style-type: none;
padding: 0
}
.user-view .label {
display: inline-block;
min-width: 4em
}
.user-view .about {
margin: 1em 0
}
.user-view .links a {
text-decoration: underline
}

View File

@@ -14,11 +14,10 @@ console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
leptos = { version = "0.0.20", default-features = false, features = ["serde"] }
leptos_meta = { version = "0.0", default-features = false }
leptos_actix = { version = "0.0.2", default-features = false, optional = true }
leptos_router = { version = "0.0", default-features = false }
log = "0.4"
simple_logger = "2"
serde = { version = "1", features = ["derive"] }
@@ -26,6 +25,7 @@ serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
# openssl = { version = "0.10", features = ["v110"] }
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
[features]
default = ["csr"]
@@ -34,11 +34,12 @@ hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:leptos_actix",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web"]
denylist = ["actix-files", "actix-web", "leptos_actix"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -1,20 +1,29 @@
# Leptos Hacker News Example
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
This example creates a basic clone of the Hacker News site. It showcases Leptoss ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. It uses Actix as its backend.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CRS bundle
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr`
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

View File

@@ -1,4 +1,4 @@
use leptos::Serializable;
use leptos::{on_cleanup, Scope, Serializable};
use serde::{Deserialize, Serialize};
pub fn story(path: &str) -> String {
@@ -10,11 +10,15 @@ pub fn user(path: &str) -> String {
}
#[cfg(not(feature = "ssr"))]
pub async fn fetch_api<T>(path: &str) -> Option<T>
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
where
T: Serializable,
{
let abort_controller = web_sys::AbortController::new().ok();
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
let json = gloo_net::http::Request::get(path)
.abort_signal(abort_signal.as_ref())
.send()
.await
.map_err(|e| log::error!("{e}"))
@@ -22,11 +26,19 @@ where
.text()
.await
.ok()?;
// abort in-flight requests if the Scope is disposed
// i.e., if we've navigated away from this page
on_cleanup(cx, move || {
if let Some(abort_controller) = abort_controller {
abort_controller.abort()
}
});
T::from_json(&json).ok()
}
#[cfg(feature = "ssr")]
pub async fn fetch_api<T>(path: &str) -> Option<T>
pub async fn fetch_api<T>(cx: Scope, path: &str) -> Option<T>
where
T: Serializable,
{

View File

@@ -16,7 +16,8 @@ pub fn App(cx: Scope) -> Element {
view! {
cx,
<div>
<Stylesheet href="/static/style.css".into()/>
<Stylesheet href="/style.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
@@ -37,7 +38,7 @@ cfg_if! {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn main() {
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), move |cx| {

View File

@@ -3,109 +3,39 @@ use leptos::*;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files, NamedFile};
use actix_files::{Files};
use actix_web::*;
use futures::StreamExt;
use leptos_meta::*;
use leptos_router::*;
use leptos_hackernews::*;
use std::{net::SocketAddr, env};
#[get("/static/style.css")]
#[get("/style.css")]
async fn css() -> impl Responder {
NamedFile::open_async("./style.css").await
}
// match every path — our router will handle actual dispatch
#[get("{tail:.*}")]
async fn render_app(req: HttpRequest) -> impl Responder {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
view! { cx, <App/> }
};
let head = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, { main } from '/pkg/leptos_hackernews.js'; init().then(main);</script>"#;
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { head.to_string() })
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
actix_files::NamedFile::open_async("./style.css").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
log::debug!("serving at {host}:{port}");
let addr = SocketAddr::from(([127,0,0,1],3000));
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
// uncomment these lines (and .bind_openssl() below) to enable HTTPS, which is sometimes
// necessary for proper HTTP/2 streaming
// load TLS keys
// to create a self-signed temporary cert for testing:
// `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
// let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
// builder
// .set_private_key_file("key.pem", SslFiletype::PEM)
// .unwrap();
// builder.set_certificate_chain_file("cert.pem").unwrap();
HttpServer::new(|| {
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_hackernews").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.service(
web::scope("/pkg")
.service(Files::new("", "./dist"))
.wrap(middleware::Compress::default()),
)
.service(render_app)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <App/> }))
//.wrap(middleware::Compress::default())
})
.bind(("127.0.0.1", 8080))?
// replace .bind with .bind_openssl to use HTTPS
//.bind_openssl(&format!("{}:{}", host, port), builder)?
.bind(&addr)?
.run()
.await
}
}
// client-only stuff for Trunk
else {
use leptos_hackernews::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <App/> }
});
} else {
fn main() {
// no client-side main function
}
}
}

View File

@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> Element {
view! { cx,
<header class="header">
<nav class="inner">
<A href="/">
<A href="/" class="home".to_string()>
<strong>"HN"</strong>
</A>
<A href="/new">

View File

@@ -32,11 +32,13 @@ pub fn Stories(cx: Scope) -> Element {
move || (page(), story_type()),
move |(page, story_type)| async move {
let path = format!("{}?page={}", category(&story_type), page);
api::fetch_api::<Vec<api::Story>>(&api::story(&path)).await
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
},
);
let (pending, set_pending) = create_signal(cx, false);
let hide_more_link = move || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
let hide_more_link =
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
view! {
cx,
@@ -76,7 +78,10 @@ pub fn Stories(cx: Scope) -> Element {
</div>
<main class="news-list">
<div>
<Suspense fallback=view! { cx, <p>"Loading..."</p> }>
<Transition
fallback=view! { cx, <p>"Loading..."</p> }
set_pending
>
{move || match stories.read() {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }),
@@ -94,7 +99,7 @@ pub fn Stories(cx: Scope) -> Element {
})
}
}}
</Suspense>
</Transition>
</div>
</main>
</div>

View File

@@ -1,5 +1,6 @@
use crate::api;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[component]
@@ -8,11 +9,19 @@ pub fn Story(cx: Scope) -> Element {
let story = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move { api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await },
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(cx, &api::story(&format!("item/{id}"))).await
}
},
);
let meta_description = move || story.read().and_then(|story| story.map(|story| story.title.clone())).unwrap_or_else(|| "Loading story...".to_string());
view! { cx,
<div>
<Meta name="description" content=meta_description/>
{move || story.read().map(|story| match story {
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
Some(story) => view! { cx,

View File

@@ -8,7 +8,13 @@ pub fn User(cx: Scope) -> Element {
let user = create_resource(
cx,
move || params().get("id").cloned().unwrap_or_default(),
move |id| async move { api::fetch_api::<User>(&api::user(&id)).await },
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(cx, &api::user(&id)).await
}
},
);
view! { cx,
<div class="user-view">

View File

@@ -0,0 +1,17 @@
# Parent Child Example
This example highlights four different ways that child components can communicate with their parent:
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
for the child component to write into and the parent to read
2. <ButtonB/>: passing a closure as one of the child component props, for
the child component to call
3. <ButtonC/>: adding a simple event listener on the child component itself
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,5 +1,5 @@
use leptos::*;
use web_sys::Event;
use web_sys::MouseEvent;
// This highlights four different ways that child components can communicate
// with their parent:
@@ -71,7 +71,7 @@ pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> Element {
#[component]
pub fn ButtonB<F>(cx: Scope, on_click: F) -> Element
where
F: Fn(Event) + 'static,
F: Fn(MouseEvent) + 'static,
{
view! {
cx,
@@ -82,11 +82,11 @@ where
</button>
}
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(Event) + 'static
// just a note: in an ordinary function ButtonB could take on_click: impl Fn(MouseEvent) + 'static
// and save you from typing out the generic
// the component macro actually expands to define a
//
// struct ButtonBProps<F> where F: Fn(Event) + 'static {
// struct ButtonBProps<F> where F: Fn(MouseEvent) + 'static {
// on_click: F
// }
//

View File

@@ -7,12 +7,11 @@ edition = "2021"
console_log = "0.2"
log = "0.4"
leptos = { path = "../../leptos" }
leptos_router = { path = "../../router", features=["csr"] }
leptos_router = { path = "../../router", features = ["csr"] }
serde = { version = "1", features = ["derive"] }
futures = "0.3"
console_error_panic_hook = "0.1.7"
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_meta = { path = "../../meta", default-features = false }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -1,8 +1,11 @@
# Leptos Router Example
This example demonstrates how Leptos' router works
This example demonstrates how Leptoss router for client side routing.
## Build and Run it
## Run it
```bash
trunk serve --open
```
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -49,29 +49,29 @@ pub fn router_example(cx: Scope) -> Element {
#[component]
pub fn ContactList(cx: Scope) -> Element {
log!("rendering ContactList");
let location = use_location(cx);
let contacts = create_resource(cx, move || location.search.get(), get_contacts);
let contacts = move || {
contacts.read().map(|contacts| {
// this data doesn't change frequently so we can use .map().collect() instead of a keyed <For/>
contacts
.into_iter()
.map(|contact| {
view! { cx,
<li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
}
})
.collect::<Vec<_>>()
})
};
view! { cx,
<div class="contact-list">
<h1>"Contacts"</h1>
<ul>
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>{
move || {
contacts.read().map(|contacts| view! { cx,
<For each=move || contacts.clone() key=|contact| contact.id>
{move |cx, contact: &ContactSummary| {
let id = contact.id;
let name = format!("{} {}", contact.first_name, contact.last_name);
view! { cx,
<li><A href=id.to_string()><span>{name.clone()}</span></A></li>
}
}}
</For>
})
}
}</Suspense>
</ul>
<Suspense fallback=move || view! { cx, <p>"Loading contacts..."</p> }>
{move || view! { cx, <ul>{contacts}</ul>}}
</Suspense>
<Outlet/>
</div>
}
@@ -79,6 +79,7 @@ pub fn ContactList(cx: Scope) -> Element {
#[component]
pub fn Contact(cx: Scope) -> Element {
log!("rendering <Contact/> page");
let params = use_params_map(cx);
let contact = create_resource(
cx,
@@ -122,28 +123,30 @@ pub fn Contact(cx: Scope) -> Element {
}
#[component]
pub fn About(_cx: Scope) -> Vec<Element> {
pub fn About(_cx: Scope) -> Element {
log!("rendering About page");
view! { cx,
<>
<div>
<h1>"About"</h1>
<p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."</p>
</>
</div>
}
}
#[component]
pub fn Settings(_cx: Scope) -> Vec<Element> {
pub fn Settings(_cx: Scope) -> Element {
log!("rendering Settings page");
view! { cx,
<>
<div>
<h1>"Settings"</h1>
<form>
<fieldset>
<legend>"Name"</legend>
<input type="text" name="first_name" placeholder="First"/>
<input type="text" name="first_name" placeholder="Last"/>
<input type="text" name="last_name" placeholder="Last"/>
</fieldset>
<pre>"This page is just a placeholder."</pre>
</form>
</>
</div>
}
}

View File

@@ -0,0 +1,48 @@
[package]
name = "todo-app-cbor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
anyhow = "1"
broadcaster = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
serde = { version = "1", features = ["derive"] }
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
gloo = { git = "https://github.com/rustwasm/gloo" }
sqlx = { version = "0.6", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:sqlx",
"leptos/ssr",
"leptos_actix",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
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.

View File

@@ -0,0 +1,22 @@
# Leptos Todo App Sqlite with CBOR
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. It is identical to the todo-app-sqlite example, but utilizes CBOR encoding for one of the server functions
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

Binary file not shown.

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS todos
(
id INTEGER NOT NULL PRIMARY KEY,
title VARCHAR,
completed BOOLEAN
);

View File

@@ -0,0 +1,22 @@
use cfg_if::cfg_if;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <TodoApp/> }
});
}
}
}

View File

@@ -0,0 +1,49 @@
use cfg_if::cfg_if;
use leptos::*;
mod todo;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use crate::todo::*;
use std::{ net::SocketAddr,env };
#[get("/style.css")]
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let mut conn = db().await.expect("couldn't connect to DB");
sqlx::migrate!()
.run(&mut conn)
.await
.expect("could not run SQLx migrations");
crate::todo::register_server_functions();
let addr = SocketAddr::from(([127,0,0,1],3000));
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
} else {
fn main() {
// no client-side main function
}
}
}

View File

@@ -0,0 +1,212 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn register_server_functions() {
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
} else {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
}
}
#[server(GetTodos, "/api", "Url")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
let req =
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
println!("req.path = {:?}", req.path());
use futures::TryStreamExt;
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
todos.push(row);
}
Ok(todos)
}
#[server(AddTodo, "/api", "Cbor")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[component]
pub fn TodoApp(cx: Scope) -> Element {
view! {
cx,
<div>
<Stylesheet href="/style.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Todos/>
}/>
</Routes>
</main>
</Router>
</div>
}
}
#[component]
pub fn Todos(cx: Scope) -> Element {
let add_todo = create_server_multi_action::<AddTodo>(cx);
let delete_todo = create_server_action::<DeleteTodo>(cx);
let submissions = add_todo.submissions();
// track mutations that should lead us to refresh the list
let add_changed = add_todo.version;
let todo_deleted = delete_todo.version;
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
cx,
move || (add_changed(), todo_deleted()),
move |_| get_todos(cx),
);
view! {
cx,
<div>
<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<div>
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
{
let delete_todo = delete_todo.clone();
move || {
let existing_todos = {
let delete_todo = delete_todo.clone();
move || {
todos
.read()
.map({
let delete_todo = delete_todo.clone();
move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }]
} else {
todos
.into_iter()
.map({
let delete_todo = delete_todo.clone();
move |todo| {
let delete_todo = delete_todo.clone();
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo.clone()>
<input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
}
})
.collect::<Vec<_>>()
}
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
};
view! {
cx,
<ul>
<div>{existing_todos}</div>
<div>{pending_todos}</div>
</ul>
}
}
}
</Suspense>
</div>
</div>
}
}

View File

@@ -0,0 +1,3 @@
.pending {
color: purple;
}

View File

@@ -0,0 +1,64 @@
[package]
name = "todo-app-sqlite-axum"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1.0.66"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../../leptos/meta", default-features = false }
leptos_router = { path = "../../../leptos/router", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
serde_json = "1.0.89"
gloo-net = { version = "0.2.5", features = ["http"] }
reqwest = { version = "0.11.13", features = ["json"] }
axum = { version = "0.6.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
[features]
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"leptos_axum",
]
[package.metadata.cargo-all-features]
denylist = [
"axum",
"tower",
"tower-http",
"tokio",
"http",
"sqlx",
"leptos_axum",
]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
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.

View File

@@ -0,0 +1,22 @@
# Leptos Todo App Sqlite with Axum
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

Binary file not shown.

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS todos
(
id INTEGER NOT NULL PRIMARY KEY,
title VARCHAR,
completed BOOLEAN
);

View File

@@ -0,0 +1,22 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <TodoApp/> }
});
}
}
}

View File

@@ -0,0 +1,81 @@
use cfg_if::cfg_if;
use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
routing::{post},
error_handling::HandleError,
Router,
};
use std::net::SocketAddr;
use crate::todo::*;
use todo_app_sqlite_axum::*;
use http::StatusCode;
use tower_http::services::ServeDir;
use std::env;
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
log::debug!("serving at {addr}");
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
let mut conn = db().await.expect("couldn't connect to DB");
sqlx::migrate!()
.run(&mut conn)
.await
.expect("could not run SQLx migrations");
crate::todo::register_server_functions();
// These are Tower Services that will serve files from the static and pkg repos.
// HandleError is needed as Axum requires services to implement Infallible Errors
// because all Errors are converted into Responses
let static_service = HandleError::new( ServeDir::new("./static"), handle_file_error);
let pkg_service = HandleError::new( ServeDir::new("./pkg"), handle_file_error);
/// Convert the Errors from ServeDir to a type that implements IntoResponse
async fn handle_file_error(err: std::io::Error) -> (StatusCode, String) {
(
StatusCode::NOT_FOUND,
format!("File Not Found: {}", err),
)
}
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite_axum").socket_address(addr).reload_port(3001).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.nest_service("/pkg", pkg_service)
.nest_service("/static", static_service)
.fallback(leptos_axum::render_app_to_stream(render_options.clone(), |cx| view! { cx, <TodoApp/> }));
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
log!("listening on {}", &render_options.socket_address);
axum::Server::bind(&render_options.socket_address)
.serve(app.into_make_service())
.await
.unwrap();
}
}
// client-only stuff for Trunk
else {
use todo_app_sqlite_axum::todo::*;
pub fn main() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx, <TodoApp/> }
});
}
}
}

View File

@@ -0,0 +1,213 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn register_server_functions() {
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
} else {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
}
}
#[server(GetTodos, "/api")]
pub async fn get_todos(_cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
// http::Request doesn't implement Clone, so more work will be needed to do use_context() on this
// let req = use_context::<http::Request<axum::body::BoxBody>>(cx)
// .expect("couldn't get HttpRequest from context");
// println!("req.path = {:?}", req.uri());
use futures::TryStreamExt;
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
todos.push(row);
}
Ok(todos)
}
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(1250));
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
{
Ok(_row) => Ok(()),
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
}
}
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[component]
pub fn TodoApp(cx: Scope) -> Element {
view! {
cx,
<div>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Todos/>
}/>
</Routes>
</main>
</Router>
</div>
}
}
#[component]
pub fn Todos(cx: Scope) -> Element {
let add_todo = create_server_multi_action::<AddTodo>(cx);
let delete_todo = create_server_action::<DeleteTodo>(cx);
let submissions = add_todo.submissions();
// track mutations that should lead us to refresh the list
let add_changed = add_todo.version;
let todo_deleted = delete_todo.version;
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
cx,
move || (add_changed(), todo_deleted()),
move |_| get_todos(cx),
);
view! {
cx,
<div>
<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<div>
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
{
let delete_todo = delete_todo.clone();
move || {
let existing_todos = {
let delete_todo = delete_todo.clone();
move || {
todos
.read()
.map({
let delete_todo = delete_todo.clone();
move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }]
} else {
todos
.into_iter()
.map({
let delete_todo = delete_todo.clone();
move |todo| {
let delete_todo = delete_todo.clone();
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo.clone()>
<input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
}
})
.collect::<Vec<_>>()
}
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
};
view! {
cx,
<ul>
<div>{existing_todos}</div>
<div>{pending_todos}</div>
</ul>
}
}
}
</Suspense>
</div>
</div>
}
}

View File

@@ -0,0 +1,48 @@
[package]
name = "todo-app-sqlite"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", optional = true, features = ["openssl", "macros"] }
anyhow = "1.0.66"
broadcaster = "1.0.0"
console_log = "0.2.0"
console_error_panic_hook = "0.1.7"
serde = { version = "1.0.148", features = ["derive"] }
futures = "0.3.25"
cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
gloo = { git = "https://github.com/rustwasm/gloo" }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:actix-files",
"dep:actix-web",
"dep:sqlx",
"leptos/ssr",
"leptos_actix",
"leptos_meta/ssr",
"leptos_router/ssr",
]
[package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Greg Johnston
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.

View File

@@ -0,0 +1,22 @@
# Leptos Todo App Sqlite
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server
## Server Side Rendering With Hydration
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML that is generated on the server.
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
```bash
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!

Binary file not shown.

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS todos
(
id INTEGER NOT NULL PRIMARY KEY,
title VARCHAR,
completed BOOLEAN
);

View File

@@ -0,0 +1,22 @@
use cfg_if::cfg_if;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
use crate::todo::*;
use leptos::*;
#[wasm_bindgen]
pub fn hydrate() {
console_error_panic_hook::set_once();
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <TodoApp/> }
});
}
}
}

View File

@@ -0,0 +1,52 @@
use std::net::SocketAddr;
use cfg_if::cfg_if;
use leptos::*;
mod todo;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use actix_files::{Files};
use actix_web::*;
use crate::todo::*;
use std::env;
#[get("/style.css")]
async fn css() -> impl Responder {
actix_files::NamedFile::open_async("./style.css").await
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let mut conn = db().await.expect("couldn't connect to DB");
sqlx::migrate!()
.run(&mut conn)
.await
.expect("could not run SQLx migrations");
crate::todo::register_server_functions();
let addr = SocketAddr::from(([127,0,0,1],3000));
HttpServer::new(move || {
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_sqlite").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
render_options.write_to_file();
App::new()
.service(Files::new("/pkg", "./pkg"))
.service(css)
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
})
.bind(&addr)?
.run()
.await
}
} else {
fn main() {
// no client-side main function
}
}
}

View File

@@ -0,0 +1,218 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use serde::{Deserialize, Serialize};
cfg_if! {
if #[cfg(feature = "ssr")] {
use sqlx::{Connection, SqliteConnection};
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
}
pub fn register_server_functions() {
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
} else {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Todo {
id: u16,
title: String,
completed: bool,
}
}
}
#[server(GetTodos, "/api")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
// this is just an example of how to access server context injected in the handlers
let req =
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
println!("\ncalling server fn");
println!(" req.path = {:?}", req.path());
use futures::TryStreamExt;
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(350));
let mut todos = Vec::new();
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
{
todos.push(row);
}
println!(" returning todos\n");
Ok(todos)
}
#[server(AddTodo, "/api")]
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
let mut conn = db().await?;
// fake API delay
std::thread::sleep(std::time::Duration::from_millis(350));
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
.bind(title)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[server(DeleteTodo, "/api")]
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
let mut conn = db().await?;
sqlx::query("DELETE FROM todos WHERE id = $1")
.bind(id)
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[component]
pub fn TodoApp(cx: Scope) -> Element {
view! {
cx,
<div>
<Stylesheet href="/style.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Todos/>
}/>
</Routes>
</main>
</Router>
</div>
}
}
#[component]
pub fn Todos(cx: Scope) -> Element {
let add_todo = create_server_multi_action::<AddTodo>(cx);
let delete_todo = create_server_action::<DeleteTodo>(cx);
let submissions = add_todo.submissions();
// track mutations that should lead us to refresh the list
let add_changed = add_todo.version;
let todo_deleted = delete_todo.version;
// list of todos is loaded from the server in reaction to changes
let todos = create_resource(
cx,
move || (add_changed(), todo_deleted()),
move |_| get_todos(cx),
);
view! {
cx,
<div>
<MultiActionForm action=add_todo>
<label>
"Add a Todo"
<input type="text" name="title"/>
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<div>
<Transition fallback=view! {cx, <p>"Loading..."</p> }>
{
let delete_todo = delete_todo.clone();
move || {
let existing_todos = {
let delete_todo = delete_todo.clone();
move || {
todos
.read()
.map({
let delete_todo = delete_todo.clone();
move |todos| match todos {
Err(e) => {
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }]
} else {
todos
.into_iter()
.map({
let delete_todo = delete_todo.clone();
move |todo| {
let delete_todo = delete_todo.clone();
view! {
cx,
<li>
{todo.title}
<ActionForm action=delete_todo.clone()>
<input type="hidden" name="id" value=todo.id/>
<input type="submit" value="X"/>
</ActionForm>
</li>
}
}
})
.collect::<Vec<_>>()
}
}
}
})
.unwrap_or_default()
}
};
let pending_todos = move || {
submissions
.get()
.into_iter()
.filter(|submission| submission.pending().get())
.map(|submission| {
view! {
cx,
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect::<Vec<_>>()
};
view! {
cx,
<ul>
<div>{existing_todos}</div>
<div>{pending_todos}</div>
</ul>
}
}
}
</Transition>
</div>
</div>
}
}

View File

@@ -0,0 +1,3 @@
.pending {
color: purple;
}

View File

@@ -0,0 +1,10 @@
# Leptos TodoMVC
This is a Leptos implementation of the TodoMVC example common to many frameworks. This is a relatively-simple application but shows off features like interaction between components and state management.
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -136,10 +136,10 @@ pub fn TodoMVC(cx: Scope) -> Element {
});
// Callback to add a todo on pressing the `Enter` key, if the field isn't empty
let add_todo = move |ev: web_sys::Event| {
let add_todo = move |ev: web_sys::KeyboardEvent| {
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
let key_code = ev.key_code();
if key_code == ENTER_KEY {
let title = event_target_value(&ev);
let title = title.trim();
@@ -262,7 +262,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
// this will be filled by _ref=input below
let input: Element;
let input = NodeRef::new(cx);
let save = move |value: &str| {
let value = value.trim();
@@ -295,8 +295,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
set_editing(true);
// guard against the fact that in SSR mode, that ref is actually to a String
#[cfg(any(feature = "csr", feature = "hydrate"))]
if let Some(input) = input.dyn_ref::<HtmlInputElement>() {
if let Some(input) = input.get().expect("should have loaded input already").dyn_ref::<HtmlInputElement>() {
input.focus();
}
}>
@@ -311,7 +310,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
let key_code = ev.key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {

View File

@@ -0,0 +1,10 @@
# Leptos View Tests
This is a collection of mostly internal view tests for Leptos. Feel free to look if curious to see a variety of ways you can build identical views!
## Client Side Rendering
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -3,7 +3,103 @@ use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <TemplateConsumer/> })
mount_to_body(|cx| view! { cx, <Tests/> })
}
#[component]
fn SelfUpdatingEffect(cx: Scope) -> Element {
let (a, set_a) = create_signal(cx, false);
create_effect(cx, move |_| {
if !a() {
set_a(true);
}
});
view! { cx,
<h1>"Hello " {move || a().to_string()}</h1>
}
}
#[component]
fn Tests(cx: Scope) -> Element {
view! {
cx,
<div>
//<div><SelfUpdatingEffect/></div>
<div><BlockOrders/></div>
//<div><TemplateConsumer/></div>
</div>
}
}
#[component]
fn BlockOrders(cx: Scope) -> Element {
let a = "A";
let b = "B";
let c = "C";
view! {
cx,
<div>
<div>"A"</div>
<div>{a}</div>
<div><span>"A"</span></div>
<div><span>{a}</span></div>
<hr/>
<div>"A" {b}</div>
<div>{a} "B"</div>
<div>{a} {b}</div>
<div>{"A"} {"B"}</div>
<div><span style="color: red">{a}</span> {b}</div>
<hr/>
<div>{a} "B" {c}</div>
<div>"A" {b} "C"</div>
<div>{a} {b} "C"</div>
<div>{a} {b} {c}</div>
<div>"A" {b} {c}</div>
<hr/>
<div>"A" {b} <span style="color: red">"C"</span></div>
<div>"A" {b} <span style="color: red">{c}</span></div>
<div>"A" <span style="color: red">"B"</span> "C"</div>
<div>"A" <span style="color: red">"B"</span> {c}</div>
<div>{a} <span style="color: red">{b}</span> {c}</div>
<div>"A" {b} <span style="color: red">{c}</span></div>
<div><span style="color: red">"A"</span> {b} {c}</div>
<div><span style="color: red">{a}</span> "B" {c}</div>
<div><span style="color: red">"A"</span> {b} "C"</div>
<hr/>
<div><span style="color: red">"A"</span> <span style="color: blue">{b}</span> {c}</div>
<div><span style="color: red">{a}</span> "B" <span style="color: blue">{c}</span></div>
<div><span style="color: red">"A"</span> {b} <span style="color: blue">"C"</span></div>
<hr/>
<div><A/></div>
<div>"A" <B/></div>
<div>{a} <B/></div>
<div><A/> "B"</div>
<div><A/> {b}</div>
<div><A/><B/></div>
<hr/>
<div><A/> "B" <C/></div>
<div><A/> {b} <C/></div>
<div><A/> {b} "C"</div>
</div>
}
}
#[component]
fn A(cx: Scope) -> Element {
view! { cx, <span style="color: red">"A"</span> }
}
#[component]
fn B(cx: Scope) -> Element {
view! { cx, <span style="color: red">"B"</span> }
}
#[component]
fn C(cx: Scope) -> Element {
view! { cx, <span style="color: red">"C"</span> }
}
#[component]
@@ -18,8 +114,8 @@ fn TemplateConsumer(cx: Scope) -> Element {
view! {
cx,
<div id="template">
<h1>"Template Consumer"</h1>
{cloned_tpl}
/* <h1>"Template Consumer"</h1>
{cloned_tpl} */
</div>
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "leptos_actix"
version = "0.0.2"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Actix integrations for the Leptos web framework."
[dependencies]
actix-web = "4"
futures = "0.3"
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
"ssr",
] }
leptos_meta = { path = "../../meta", default-features = false, version = "0.0", features = [
"ssr",
] }
leptos_router = { path = "../../router", default-features = false, version = "0.0", features = [
"ssr",
] }

View File

@@ -0,0 +1,226 @@
use actix_web::{web::Bytes, *};
use futures::StreamExt;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
/// Leptos server function arguments in the body, runs the server function if found,
/// and returns the resulting [HttpResponse].
///
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
///
/// This can then be set up at an appropriate route in your application:
///
/// ```
/// use actix_web::*;
///
/// fn register_server_functions() {
/// // call ServerFn::register() for each of the server functions you've defined
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
/// // make sure you actually register your server functions
/// register_server_functions();
///
/// HttpServer::new(|| {
/// App::new()
/// // "/api" should match the prefix, if any, declared when defining server functions
/// // {tail:.*} passes the remainder of the URL as the server function name
/// .route("/api/{tail:.*}", leptos_actix::handle_server_fns())
/// })
/// .bind(("127.0.0.1", 8080))?
/// .run()
/// .await
/// }
/// # }
/// ```
pub fn handle_server_fns() -> Route {
web::post().to(
|req: HttpRequest, params: web::Path<String>, body: web::Bytes| async move {
{
let path = params.into_inner();
let accept_header = req
.headers()
.get("Accept")
.and_then(|value| value.to_str().ok());
if let Some(server_fn) = server_fn_by_path(path.as_str()) {
let body: &[u8] = &body;
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
// provide HttpRequest as context in server scope
provide_context(cx, req.clone());
match server_fn(cx, body).await {
Ok(serialized) => {
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
let mut res: HttpResponseBuilder;
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok()
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
let referer = req
.headers()
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = HttpResponse::SeeOther();
res.insert_header(("Location", referer))
.content_type("application/json");
};
match serialized {
Payload::Binary(data) => {
res.content_type("application/cbor");
res.body(Bytes::from(data))
}
Payload::Url(data) => {
res.content_type("application/x-www-form-urlencoded");
res.body(data)
}
Payload::Json(data) => {
res.content_type("application/json");
res.body(data)
}
}
}
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
} else {
HttpResponse::BadRequest()
.body(format!("Could not find a server function at that route."))
}
}
},
)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use actix_web::{HttpServer, App};
/// use leptos::*;
/// use std::{env,net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[actix_web::main]
/// async fn main() -> std::io::Result<()> {
///
/// let addr = SocketAddr::from(([127,0,0,1],3000));
/// HttpServer::new(move || {
/// let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/leptos_example").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
/// render_options.write_to_file();
/// App::new()
/// // {tail:.*} passes the remainder of the URL as the route
/// // the actual routing will be handled by `leptos_router`
/// .route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }))
/// })
/// .bind(&addr)?
/// .run()
/// .await
/// }
/// # }
/// ```
pub fn render_app_to_stream(
options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + 'static,
) -> Route {
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
async move {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = {
let app_fn = app_fn.clone();
move |cx| {
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, req.clone());
(app_fn)(cx)
}
};
let pkg_path = &options.pkg_path;
let socket_ip = &options.socket_address.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match options.environment {
RustEnv::DEV => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
ws.onmessage = (ev) => {{
console.log(`Reload message: `);
if (ev.data === 'reload') window.location.reload();
}};
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
}})()
</script>
"#
),
RustEnv::PROD => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async move { head.clone() })
// TODO this leaks a runtime once per invocation
.chain(render_to_stream(move |cx| {
let app = app(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}))
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
})
}

View File

@@ -0,0 +1,24 @@
[package]
name = "leptos_axum"
version = "0.0.4"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Axum integrations for the Leptos web framework."
[dependencies]
axum = "0.6"
derive_builder = "0.12.0"
futures = "0.3"
kdl = "4.6.0"
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
"ssr",
] }
leptos_meta = { path = "../../meta", default-features = false, version = "0.0", features = [
"ssr",
] }
leptos_router = { path = "../../router", default-features = false, version = "0.0", features = [
"ssr",
] }
tokio = { version = "1.0", features = ["full"] }

View File

@@ -0,0 +1,286 @@
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::Path,
http::{HeaderMap, HeaderValue, Request, StatusCode},
response::{IntoResponse, Response},
};
use futures::{Future, SinkExt, Stream, StreamExt};
use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use std::{io, pin::Pin, sync::Arc};
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
/// run the server function if found, and return the resulting [Response].
///
/// This provides an `Arc<[Request<Body>](axum::http::Request)>` [Scope](leptos::Scope).
///
/// This can then be set up at an appropriate route in your application:
///
/// ```
/// use axum::{handler::Handler, routing::post, Router};
/// use std::net::SocketAddr;
/// use leptos::*;
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[tokio::main]
/// async fn main() {
/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
///
/// // build our application with a route
/// let app = Router::new()
/// .route("/api/*fn_name", post(leptos_axum::handle_server_fns));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
/// axum::Server::bind(&addr)
/// .serve(app.into_make_service())
/// .await
/// .unwrap();
/// }
/// # }
pub async fn handle_server_fns(
Path(fn_name): Path<String>,
headers: HeaderMap<HeaderValue>,
body: Bytes,
// req: Request<Body>,
) -> impl IntoResponse {
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
let fn_name: String = match fn_name.strip_prefix("/") {
Some(path) => path.to_string(),
None => fn_name,
};
let (tx, rx) = futures::channel::oneshot::channel();
std::thread::spawn({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
async move {
let res = if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) {
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
// provide request as context in server scope
// provide_context(cx, Arc::new(req));
match server_fn(cx, body.as_ref()).await {
Ok(serialized) => {
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
// if this is Accept: application/json then send a serialized JSON response
let accept_header =
headers.get("Accept").and_then(|value| value.to_str().ok());
let mut res = Response::builder();
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
let referer = headers
.get("Referer")
.and_then(|value| value.to_str().ok())
.unwrap_or("/");
res = res
.status(StatusCode::SEE_OTHER)
.header("Location", referer);
}
match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(Full::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/x-www-form-urlencoded",
)
.body(Full::from(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(Full::from(data)),
}
}
Err(e) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::from(e.to_string())),
}
} else {
Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::from(
"Could not find a server function at that route.".to_string(),
))
}
.expect("could not build Response");
_ = tx.send(res);
}
})
}
});
rx.await.unwrap()
}
pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>>;
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// The provides a [MetaContext] and a [RouterIntegrationContext] to apps context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
/// The HTML stream is rendered using [render_to_stream], and includes everything described in
/// the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
/// use axum::handler::Handler;
/// use axum::Router;
/// use std::{net::SocketAddr, env};
/// use leptos::*;
///
/// #[component]
/// fn MyApp(cx: Scope) -> Element {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
/// # if false { // don't actually try to run a server in a doctest...
/// #[tokio::main]
/// async fn main() {
/// let addr = SocketAddr::from(([127, 0, 0, 1], 8082));
/// let render_options: RenderOptions = RenderOptions::builder()
/// .pkg_path("/pkg/leptos_example")
/// .socket_address(addr)
/// .reload_port(3001)
/// .environment(&env::var("RUST_ENV")).build();
///
/// // build our application with a route
/// let app = Router::new()
/// .fallback(leptos_axum::render_app_to_stream(render_options, |cx| view! { cx, <MyApp/> }));
///
/// // run our app with hyper
/// // `axum::Server` is a re-export of `hyper::Server`
/// axum::Server::bind(&addr)
/// .serve(app.into_make_service())
/// .await
/// .unwrap();
/// }
/// # }
/// ```
///
pub fn render_app_to_stream(
options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = StreamBody<PinnedHtmlStream>> + Send + 'static>>
+ Clone
+ Send
+ 'static {
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
async move {
// Need to get the path and query string of the Request
let path = req.uri();
let query = path.query();
let full_path;
if let Some(query) = query {
full_path = "http://leptos".to_string() + &path.to_string() + "?" + query
} else {
full_path = "http://leptos".to_string() + &path.to_string()
}
let pkg_path = &options.pkg_path;
let socket_ip = &options.socket_address.ip().to_string();
let reload_port = options.reload_port;
let leptos_autoreload = match options.environment {
RustEnv::DEV => format!(
r#"
<script crossorigin="">(function () {{
var ws = new WebSocket('ws://{socket_ip}:{reload_port}/autoreload');
ws.onmessage = (ev) => {{
console.log(`Reload message: `);
if (ev.data === 'reload') window.location.reload();
}};
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
}})()
</script>
"#
),
RustEnv::PROD => "".to_string(),
};
let head = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
let (mut tx, rx) = futures::channel::mpsc::channel(8);
std::thread::spawn({
let app_fn = app_fn.clone();
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let app_fn = app_fn.clone();
async move {
tokio::task::LocalSet::new()
.run_until(async {
let mut shell = Box::pin(render_to_stream({
let full_path = full_path.clone();
move |cx| {
let integration = ServerIntegration {
path: full_path.clone(),
};
provide_context(
cx,
RouterIntegrationContext::new(integration),
);
provide_context(cx, MetaContext::new());
let app = app_fn(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}
}));
while let Some(fragment) = shell.next().await {
_ = tx.send(fragment).await;
}
tx.close_channel();
})
.await;
}
});
}
});
let stream = futures::stream::once(async move { head.clone() })
.chain(rx)
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(Bytes::from(html)));
StreamBody::new(Box::pin(stream) as PinnedHtmlStream)
}
})
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos"
version = "0.0.16"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -9,45 +9,76 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
readme = "../README.md"
[dependencies]
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.16" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.16" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.16" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.16" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.16" }
leptos_core = { path = "../leptos_core", default-features = false, version = "0.0.20" }
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.20" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.20" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.20" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.20" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.20" }
[build-dependencies]
rustc_version = "0.4"
[features]
default = ["csr", "serde"]
default = ["csr", "serde", "interning"]
csr = [
"leptos_core/csr",
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
"leptos_core/csr",
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_core/hydrate",
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
"leptos_core/hydrate",
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
ssr = [
"leptos_core/ssr",
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
"leptos_core/ssr",
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
]
stable = [
"leptos_core/stable",
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
"leptos_core/stable",
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
serde = ["leptos_reactive/serde"]
serde-lite = ["leptos_reactive/serde-lite"]
miniserde = ["leptos_reactive/miniserde"]
interning = ["leptos_dom/interning"]
[package.metadata.cargo-all-features]
denylist = ["stable"]
denylist = ["stable", "interning"]
skip_feature_sets = [
[
"csr",
"ssr",
],
[
"csr",
"hydrate",
],
[
"ssr",
"hydrate",
],
[
"serde",
"serde-lite",
],
[
"serde-lite",
"miniserde",
],
[
"serde",
"miniserde",
],
]

12
leptos/build.rs Normal file
View File

@@ -0,0 +1,12 @@
use rustc_version::{version, version_meta, Channel};
fn main() {
assert!(version().unwrap().major >= 1);
match version_meta().unwrap().channel {
Channel::Stable => {
println!("cargo:rustc-cfg=feature=\"stable\"")
}
_ => {}
}
}

View File

@@ -27,8 +27,12 @@
//! the [examples](https://github.com/gbj/leptos/tree/main/examples):
//! - [`counter`](https://github.com/gbj/leptos/tree/main/examples/counter) is the classic
//! counter example, showing the basics of client-side rendering and reactive DOM updates
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counter) introduces parent-child
//! - [`counter-isomorphic`](https://github.com/gbj/leptos/tree/main/examples/counter-isomorphic) is the classic
//! counter example run on the server using an isomorphic function, showing the basics of client-side rendering and reactive DOM updates
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`counters-stable`](https://github.com/gbj/leptos/tree/main/examples/counters-stable) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates. Unlike counters, this compiles in Rust stable.
//! - [`parent-child`](https://github.com/gbj/leptos/tree/main/examples/parent-child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`todomvc`](https://github.com/gbj/leptos/tree/main/examples/todomvc) implements the classic to-do
@@ -47,6 +51,13 @@
//! - [`hackernews`](https://github.com/gbj/leptos/tree/main/examples/hackernews) pulls everything together.
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run.
//! - [`hackernews-axum`](https://github.com/gbj/leptos/tree/main/examples/hackernews-axum) pulls everything together.
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run. This one uses Axum as it's backend.
//! - [`todo-app-sqlite`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite) is a simple todo app, showcasing the use of
//! functions that run only on the server, but are called from client side function calls
//! - [`todo-app-sqlite-axum`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite-axum) is a simple todo app, showcasing the use of
//! functions that run only on the server, but are called from client side function calls. Now with Axum backend
//!
//! (The SPA examples can be run using `trunk serve`. For information about Trunk,
//! [see here]((https://trunkrs.dev/)).)
@@ -71,12 +82,16 @@
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
//! of calling signals as functions. If you need to use `stable`, you will need to call `.get()`
//! and `.set()` manually.
//! - `serde` (*Default*) In SSR/hydrate mode, uses [serde] to serialize resources and send them
//! - `serde` (*Default*) In SSR/hydrate mode, uses [serde](https://docs.rs/serde/latest/serde/) to serialize resources and send them
//! from the server to the client.
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite] to serialize resources and send them
//! - `serde-lite` In SSR/hydrate mode, uses [serde-lite](https://docs.rs/serde-lite/latest/serde_lite/) to serialize resources and send them
//! from the server to the client.
//! - `miniserde` In SSR/hydrate mode, uses [miniserde] to serialize resources and send them
//! - `miniserde` In SSR/hydrate mode, uses [miniserde](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
//! from the server to the client.
//! - `interning` (*Default*) When client-side rendering, Leptos uses [`wasm_bindgen::intern`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/fn.intern.html)
//! to reduce the cost of copying class names, attribute names, attribute values, and properties through JavaScript to the DOM. This feature
//! (included by default) makes DOM updates marginally faster and WASM binary size marginally larger. Disabling the feature makes binary sizes
//! marginally smaller at the cost of a small decrease in speed.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
@@ -111,12 +126,22 @@
//! ```
//!
//! Leptos is easy to use with [Trunk](https://trunkrs.dev/) (or with a simple wasm-bindgen setup):
//! ```ignore
//! ```
//! # use leptos::*;
//! # if false { // can't run in doctests
//!
//! #[component]
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
//! todo!()
//! }
//!
//! pub fn main() {
//! mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 /> })
//! }
//! # }
//! ```
pub use leptos_config::*;
pub use leptos_core::*;
pub use leptos_dom;
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};

View File

@@ -3,9 +3,9 @@
fn simple_ssr_test() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_scope, create_signal};
use leptos_reactive::{create_runtime, create_scope, create_signal};
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let rendered = view! {
cx,
@@ -30,7 +30,7 @@ fn ssr_test_with_components() {
use leptos_core::Prop;
use leptos_dom::*;
use leptos_macro::*;
use leptos_reactive::{create_scope, create_signal, Scope};
use leptos_reactive::{create_runtime, create_scope, create_signal, Scope};
#[component]
fn Counter(cx: Scope, initial_value: i32) -> Element {
@@ -45,7 +45,7 @@ fn ssr_test_with_components() {
}
}
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<div class="counters">
@@ -66,9 +66,9 @@ fn ssr_test_with_components() {
fn test_classes() {
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_scope, create_signal};
use leptos_reactive::{create_runtime, create_scope, create_signal};
_ = create_scope(|cx| {
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 5);
let rendered = view! {
cx,

11
leptos_config/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "leptos_config"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Configuraiton for the Leptos web framework."
[dependencies]
typed-builder = "0.11.0"

110
leptos_config/src/lib.rs Normal file
View File

@@ -0,0 +1,110 @@
use std::{env::VarError, net::SocketAddr, str::FromStr};
use typed_builder::TypedBuilder;
/// This struct serves as a convenient place to store details used for rendering.
/// It's serialized into a file in the root called `.leptos.kdl` for cargo-leptos
/// to watch. It's also used in our actix and axum integrations to generate the
/// correct path for WASM, JS, and Websockets. Its goal is to be the single source
/// of truth for render options
#[derive(TypedBuilder, Clone)]
pub struct RenderOptions {
/// The path and name of the WASM and JS files generated by wasm-bindgen
/// For example, `/pkg/app` might be a valid input if your crate name was `app`.
#[builder(setter(into))]
pub pkg_path: String,
/// Used to control whether the Websocket code for code watching is included.
/// I recommend passing in the result of `env::var("RUST_ENV")`
#[builder(setter(into), default)]
pub environment: RustEnv,
/// Provides a way to control the address leptos is served from.
/// Using an env variable here would allow you to run the same code in dev and prod
/// Defaults to `127.0.0.1:3000`
#[builder(setter(into), default=SocketAddr::from(([127,0,0,1], 3000)))]
pub socket_address: SocketAddr,
/// The port the Websocket watcher listens on. Should match the `reload_port` in cargo-leptos(if using).
/// Defaults to `3001`
#[builder(default = 3001)]
pub reload_port: u32,
}
impl RenderOptions {
/// Creates a hidden file at ./.leptos_toml so cargo-leptos can monitor settings. We do not read from this file
/// only write to it, you'll want to change the settings in your main function when you create RenderOptions
pub fn write_to_file(&self) {
use std::fs;
let options = format!(
r#"// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
RenderOptions {{
pkg_path "{}"
environment "{:?}"
socket_address "{:?}"
reload_port {:?}
}}
"#,
self.pkg_path, self.environment, self.socket_address, self.reload_port
);
fs::write("./.leptos.kdl", options).expect("Unable to write file");
}
}
/// An enum that can be used to define the environment Leptos is running in. Can be passed to RenderOptions.
/// Setting this to the PROD variant will not include the websockets code for cargo-leptos' watch.
/// Defaults to PROD
#[derive(Debug, Clone)]
pub enum RustEnv {
PROD,
DEV,
}
impl Default for RustEnv {
fn default() -> Self {
Self::PROD
}
}
impl FromStr for RustEnv {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
let sanitized = input.to_lowercase();
match sanitized.as_ref() {
"dev" => Ok(Self::DEV),
"development" => Ok(Self::DEV),
"prod" => Ok(Self::PROD),
"production" => Ok(Self::PROD),
_ => Ok(Self::PROD),
}
}
}
impl From<&str> for RustEnv {
fn from(str: &str) -> Self {
let sanitized = str.to_lowercase();
match sanitized.as_str() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
}
impl From<&Result<String, VarError>> for RustEnv {
fn from(input: &Result<String, VarError>) -> Self {
match input {
Ok(str) => {
let sanitized = str.to_lowercase();
match sanitized.as_ref() {
"dev" => Self::DEV,
"development" => Self::DEV,
"prod" => Self::PROD,
"production" => Self::PROD,
_ => {
panic!("Environment var is not recognized. Maybe try `dev` or `prod`")
}
}
}
Err(_) => Self::PROD,
}
}
}

Some files were not shown because too many files have changed in this diff Show More