Compare commits

..

142 Commits

Author SHA1 Message Date
Greg Johnston
1c8b640855 Update #[component] docs 2022-12-14 06:44:14 -05:00
Greg Johnston
621976c92c Add correct import for doctest 2022-12-13 14:14:04 -05:00
Greg Johnston
b2d7ad2afd Fix a couple issues with intra-doc links 2022-12-13 13:10:04 -05:00
Greg Johnston
73b21487b9 Add more entry-level docs for #[component] macro 2022-12-13 13:06:37 -05:00
Greg Johnston
0b448daf3a Fix SimpleCounter example in tests 2022-12-09 14:58:53 -05:00
Greg Johnston
c01dba5138 Merge pull request #160 from gbj/component-documentation
Allows documenting `Component` and `ComponentProps` in a single doc comment
2022-12-09 14:27:36 -05:00
Greg Johnston
1929f2d8b2 Merge pull request #161 from gbj/docs-improvements
Docs improvements
2022-12-09 13:56:18 -05:00
Greg Johnston
50b0fe157a Fix example test 2022-12-09 13:34:35 -05:00
Greg Johnston
dc7f44933c Add cargo-leptos to Readme 2022-12-09 13:28:26 -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
b56dde9a6d Add working Tailwind example per issue #147 2022-12-09 13:05:20 -05:00
Greg Johnston
74ec8925dc Additional documentation for issue #156 2022-12-09 12:41:17 -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
3d10bbb0c6 Merge pull request #159 from benwis/dashes
Replace _ with - for KDL files
2022-12-08 13:15:21 -05:00
Ben Wishovich
8d325fce5c Replace _ with - for KDL files 2022-12-08 10:08:04 -08:00
Greg Johnston
7e457ee202 Merge pull request #157 from akesson/integration-html-updates
Integration html updates
2022-12-08 08:05:25 -05:00
hakesson
bb282189c3 Add preload of js and wasm 2022-12-08 08:11:15 +01:00
hakesson
2694d2e93c Add missing init param 2022-12-08 08:10:56 +01: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
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
Ben Wishovich
440719071a Switch MessagePack for CBOR, as it's more standardized 2022-11-23 14:23:49 -08:00
Greg Johnston
b6d902a584 Passes leptos_server tests now 2022-11-22 21:44:02 -05: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
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
174 changed files with 6490 additions and 11018 deletions

1
.gitignore vendored
View File

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

View File

@@ -3,6 +3,8 @@ members = [
# core
"leptos",
"leptos_dom",
"leptos_core",
"leptos_config",
"leptos_macro",
"leptos_reactive",
"leptos_server",
@@ -28,6 +30,8 @@ members = [
"examples/todomvc",
"examples/todo-app-sqlite",
"examples/todo-app-sqlite-axum",
"examples/todo-app-cbor",
"examples/view-tests",
# book
"docs/book/project/ch02_getting_started",
@@ -42,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

@@ -54,6 +54,17 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
- **Fine-grained reactivity**: The entire framework is build from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signals value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
## Getting Started
The best way to get started with a Leptos project right now is to use the [`cargo-leptos`](https://github.com/akesson/cargo-leptos) build tool and our [starter template](https://github.com/leptos-rs/start).
```bash
cargo install cargo-leptos
cargo leptos new --git https://github.com/leptos-rs/start
cd [your project name]
cargo leptos watch
```
## Learn more
Here are some resources for learning more about Leptos:

View File

@@ -2,12 +2,12 @@ use test::Bencher;
#[bench]
fn leptos_ssr_bench(b: &mut Bencher) {
b.iter(|| {
use leptos::*;
use leptos::*;
b.iter(|| {
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
fn Counter(cx: Scope, initial: i32) -> Element {
let (value, set_value) = create_signal(cx, initial);
view! {
cx,
@@ -28,16 +28,16 @@ fn leptos_ssr_bench(b: &mut Bencher) {
<Counter initial=2/>
<Counter initial=3/>
</main>
}.into_view(cx).render_to_string(cx);
};
assert_eq!(
rendered,
"<main><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><div><button>-1</button><span>Value: <!>1<template id=\"_3\"></template>!</span><button>+1</button></div><template id=\"_1\"></template><div><button>-1</button><span>Value: <!>2<template id=\"_2\"></template>!</span><button>+1</button></div><template id=\"_0\"></template><div><button>-1</button><span>Value: <!>3<template id=\"_2\"></template>!</span><button>+1</button></div><template id=\"_0\"></template></main>"
"<main data-hk=\"0-0\"><h1>Welcome to our benchmark page.</h1><p>Here's some introductory text.</p><!--#--><div data-hk=\"0-2-0\"><button>-1</button><span>Value: <!--#-->1<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-3-0\"><button>-1</button><span>Value: <!--#-->2<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-4-0\"><button>-1</button><span>Value: <!--#-->3<!--/-->!</span><button>+1</button></div><!--/--></main>"
);
});
});
}
/*
#[bench]
fn tera_ssr_bench(b: &mut Bencher) {
use tera::*;
@@ -194,4 +194,3 @@ fn yew_ssr_bench(b: &mut Bencher) {
});
});
}
*/

View File

@@ -1,4 +1,4 @@
pub use leptos::*;
use leptos::*;
use miniserde::*;
use web_sys::HtmlInputElement;
@@ -8,320 +8,314 @@ pub struct Todos(pub Vec<Todo>);
const STORAGE_KEY: &str = "todos-leptos";
impl Todos {
pub fn new(cx: Scope) -> Self {
Self(vec![])
}
pub fn new(cx: Scope) -> Self {
Self(vec![])
}
pub fn new_with_1000(cx: Scope) -> Self {
let todos = (0..1000)
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
.collect();
Self(todos)
}
pub fn new_with_1000(cx: Scope) -> Self {
let todos = (0..1000)
.map(|id| Todo::new(cx, id, format!("Todo #{id}")))
.collect();
Self(todos)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn add(&mut self, todo: Todo) {
self.0.push(todo);
}
pub fn add(&mut self, todo: Todo) {
self.0.push(todo);
}
pub fn remove(&mut self, id: usize) {
self.0.retain(|todo| todo.id != id);
}
pub fn remove(&mut self, id: usize) {
self.0.retain(|todo| todo.id != id);
}
pub fn remaining(&self) -> usize {
self.0.iter().filter(|todo| !(todo.completed)()).count()
}
pub fn remaining(&self) -> usize {
self.0.iter().filter(|todo| !(todo.completed)()).count()
}
pub fn completed(&self) -> usize {
self.0.iter().filter(|todo| (todo.completed)()).count()
}
pub fn completed(&self) -> usize {
self.0.iter().filter(|todo| (todo.completed)()).count()
}
pub fn toggle_all(&self) {
// if all are complete, mark them all active instead
if self.remaining() == 0 {
for todo in &self.0 {
if todo.completed.get() {
(todo.set_completed)(false);
pub fn toggle_all(&self) {
// if all are complete, mark them all active instead
if self.remaining() == 0 {
for todo in &self.0 {
if todo.completed.get() {
(todo.set_completed)(false);
}
}
}
// otherwise, mark them all complete
else {
for todo in &self.0 {
(todo.set_completed)(true);
}
}
}
}
// otherwise, mark them all complete
else {
for todo in &self.0 {
(todo.set_completed)(true);
}
}
}
fn clear_completed(&mut self) {
self.0.retain(|todo| !todo.completed.get());
}
fn clear_completed(&mut self) {
self.0.retain(|todo| !todo.completed.get());
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Todo {
pub id: usize,
pub title: ReadSignal<String>,
pub set_title: WriteSignal<String>,
pub completed: ReadSignal<bool>,
pub set_completed: WriteSignal<bool>,
pub id: usize,
pub title: ReadSignal<String>,
pub set_title: WriteSignal<String>,
pub completed: ReadSignal<bool>,
pub set_completed: WriteSignal<bool>,
}
impl Todo {
pub fn new(cx: Scope, id: usize, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
}
pub fn new_with_completed(
cx: Scope,
id: usize,
title: String,
completed: bool,
) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
Self {
id,
title,
set_title,
completed,
set_completed,
pub fn new(cx: Scope, id: usize, title: String) -> Self {
Self::new_with_completed(cx, id, title, false)
}
}
pub fn toggle(&self) {
self
.set_completed
.update(|completed| *completed = !*completed);
}
pub fn new_with_completed(cx: Scope, id: usize, title: String, completed: bool) -> Self {
let (title, set_title) = create_signal(cx, title);
let (completed, set_completed) = create_signal(cx, completed);
Self {
id,
title,
set_title,
completed,
set_completed,
}
}
pub fn toggle(&self) {
self.set_completed
.update(|completed| *completed = !*completed);
}
}
const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope,todos: Todos) -> impl IntoView {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
pub fn TodoMVC(cx: Scope, todos: Todos) -> Element {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
let (mode, set_mode) = create_signal(cx, Mode::All);
window_event_listener("hashchange", move |_| {
let new_mode = location_hash().map(|hash| route(&hash)).unwrap_or_default();
set_mode(new_mode);
});
let 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();
if key_code == ENTER_KEY {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(cx, next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
}
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();
if key_code == ENTER_KEY {
let title = event_target_value(&ev);
let title = title.trim();
if !title.is_empty() {
let new = Todo::new(cx, next_id, title.to_string());
set_todos.update(|t| t.add(new));
next_id += 1;
target.set_value("");
}
}
};
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
.0
.iter()
.filter(|todo| !todo.completed.get())
.cloned()
.collect(),
Mode::Completed => todos
.0
.iter()
.filter(|todo| todo.completed.get())
.cloned()
.collect(),
})
});
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
.get()
.0
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json = json::to_string(&objs);
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
}
});
view! { cx,
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus on:keydown=add_todo />
</header>
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
<input id="toggle-all" class="toggle-all" type="checkbox"
prop:checked={move || todos.with(|t| t.remaining() > 0)}
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For each=filtered_todos key=|todo| todo.id>
{move |cx, todo: &Todo| view! { cx, <Todo todo=todo.clone() /> }}
</For>
</ul>
</section>
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 {
" item"
} else {
" items"
}}
" left"
</span>
<ul class="filters">
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
</ul>
<button
class="clear-completed hidden"
class:hidden={move || todos.with(|t| t.completed() == 0)}
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}
};
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
.0
.iter()
.filter(|todo| !todo.completed.get())
.cloned()
.collect(),
Mode::Completed => todos
.0
.iter()
.filter(|todo| todo.completed.get())
.cloned()
.collect(),
})
});
// effect to serialize to JSON
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos
.get()
.0
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
let json = json::to_string(&objs);
if storage.set_item(STORAGE_KEY, &json).is_err() {
log::error!("error while trying to set item in localStorage");
}
}
});
view! { cx,
<main>
<section class="todoapp">
<header class="header">
<h1>"todos"</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" on:keydown=add_todo />
</header>
<section class="main" class:hidden={move || todos.with(|t| t.is_empty())}>
<input id="toggle-all" class="toggle-all" type="checkbox"
prop:checked={move || todos.with(|t| t.remaining() > 0)}
on:input=move |_| set_todos.update(|t| t.toggle_all())
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| view! { cx, <Todo todo=todo.clone() /> }
/>
</ul>
</section>
<footer class="footer" class:hidden={move || todos.with(|t| t.is_empty())}>
<span class="todo-count">
<strong>{move || todos.with(|t| t.remaining().to_string())}</strong>
{move || if todos.with(|t| t.remaining()) == 1 {
" item"
} else {
" items"
}}
" left"
</span>
<ul class="filters">
<li><a href="#/" class="selected" class:selected={move || mode() == Mode::All}>"All"</a></li>
<li><a href="#/active" class:selected={move || mode() == Mode::Active}>"Active"</a></li>
<li><a href="#/completed" class:selected={move || mode() == Mode::Completed}>"Completed"</a></li>
</ul>
<button
class="clear-completed hidden"
class:hidden={move || todos.with(|t| t.completed() == 0)}
on:click=move |_| set_todos.update(|t| t.clear_completed())
>
"Clear completed"
</button>
</footer>
</section>
<footer class="info">
<p>"Double-click to edit a todo"</p>
<p>"Created by "<a href="http://todomvc.com">"Greg Johnston"</a></p>
<p>"Part of "<a href="http://todomvc.com">"TodoMVC"</a></p>
</footer>
</main>
}.into_view(cx)
}
#[component]
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
//let input = NodeRef::new(cx);
pub fn Todo(cx: Scope, todo: Todo) -> Element {
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
let input = NodeRef::new(cx);
let save = move |value: &str| {
let value = value.trim();
if value.is_empty() {
set_todos.update(|t| t.remove(todo.id));
} else {
(todo.set_title)(value.to_string());
}
set_editing(false);
};
let save = move |value: &str| {
let value = value.trim();
if value.is_empty() {
set_todos.update(|t| t.remove(todo.id));
} else {
(todo.set_title)(value.to_string());
}
set_editing(false);
};
view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || (todo.completed)()}
//_ref=input
>
<div class="view">
<input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
let tpl = view! { cx,
<li
class="todo"
class:editing={editing}
class:completed={move || (todo.completed)()}
_ref=input
>
<div class="view">
<input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
/>
<label on:dblclick=move |_| set_editing(true)>
{move || todo.title.get()}
</label>
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
</div>
{move || editing().then(|| view! { cx,
<input
class="edit"
class:hidden={move || !(editing)()}
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {
set_editing(false);
}
}}
/>
})
}
</li>
}
/>
<label on:dblclick=move |_| set_editing(true)>
{move || todo.title.get()}
</label>
<button class="destroy" on:click=move |_| set_todos.update(|t| t.remove(todo.id))/>
</div>
{move || editing().then(|| view! { cx,
<input
class="edit"
class:hidden={move || !(editing)()}
prop:value={move || todo.title.get()}
on:focusout=move |ev| save(&event_target_value(&ev))
on:keyup={move |ev| {
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
if key_code == ENTER_KEY {
save(&event_target_value(&ev));
} else if key_code == ESCAPE_KEY {
set_editing(false);
}
}}
/>
})
}
</li>
};
tpl
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Active,
Completed,
All,
Active,
Completed,
All,
}
impl Default for Mode {
fn default() -> Self {
Mode::All
}
fn default() -> Self {
Mode::All
}
}
pub fn route(hash: &str) -> Mode {
match hash {
"/active" => Mode::Active,
"/completed" => Mode::Completed,
_ => Mode::All,
}
match hash {
"/active" => Mode::Active,
"/completed" => Mode::Completed,
_ => Mode::All,
}
}
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: usize,
pub title: String,
pub completed: bool,
pub id: usize,
pub title: String,
pub completed: bool,
}
impl TodoSerialized {
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
}
impl From<&Todo> for TodoSerialized {
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
}
}
}
}

View File

@@ -7,14 +7,15 @@ mod yew;
#[bench]
fn leptos_todomvc_ssr(b: &mut Bencher) {
b.iter(|| {
use crate::todomvc::leptos::*;
use self::leptos::*;
use ::leptos::*;
b.iter(|| {
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new(cx)/>
}.into_view(cx).render_to_string(cx);
};
assert!(rendered.len() > 1);
});
@@ -58,15 +59,15 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
#[bench]
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
b.iter(|| {
use self::leptos::*;
use ::leptos::*;
use self::leptos::*;
use ::leptos::*;
b.iter(|| {
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new_with_1000(cx)/>
}.into_view(cx).render_to_string(cx);
};
assert!(rendered.len() > 1);
});

View File

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

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[dependencies]
leptos = "0.0.18"
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.18"
leptos = { path = "../../../../leptos" }

View File

@@ -1 +0,0 @@
.leptos.kdl

View File

@@ -24,10 +24,10 @@ leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4"
simple_logger = "2"
gloo-net = { git = "https://github.com/rustwasm/gloo" }
gloo = { git = "https://github.com/rustwasm/gloo" }
[features]
default = []
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [

View File

@@ -7,10 +7,10 @@ This example demonstrates how to use a function isomorphically, to run a server
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
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

View File

@@ -43,39 +43,41 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
Ok(0)
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
pub fn Counters(cx: Scope) -> Element {
view! {
cx,
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
</header>
<nav>
<ul>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" view=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" view=|cx| view! {
cx,
<MultiuserCounter/>
}/>
</Routes>
</main>
</Router>
<div>
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
</header>
<nav>
<ul>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<main>
<Routes>
<Route path="" element=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" element=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" element=|cx| view! {
cx,
<MultiuserCounter/>
}/>
</Routes>
</main>
</Router>
</div>
}
}
@@ -84,7 +86,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
// it's invalidated by one of the user's own actions
// This is the typical pattern for a CRUD app
#[component]
pub fn Counter(cx: Scope) -> impl IntoView {
pub fn Counter(cx: Scope) -> Element {
let dec = create_action(cx, |_| adjust_server_count(-1, "decing".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "incing".into()));
let clear = create_action(cx, |_| clear_server_count());
@@ -124,7 +126,7 @@ pub fn Counter(cx: Scope) -> impl IntoView {
// It uses the same invalidation pattern as the plain counter,
// but uses HTML forms to submit the actions
#[component]
pub fn FormCounter(cx: Scope) -> impl IntoView {
pub fn FormCounter(cx: Scope) -> Element {
let adjust = create_server_action::<AdjustServerCount>(cx);
let clear = create_server_action::<ClearServerCount>(cx);
@@ -187,7 +189,7 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
// Whenever another user updates the value, it will update here
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
pub fn MultiuserCounter(cx: Scope) -> Element {
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
@@ -196,7 +198,7 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let multiplayer_value = {
use futures::StreamExt;
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
let mut source = gloo::net::eventsource::futures::EventSource::new("/api/events")
.expect_throw("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,

View File

@@ -14,7 +14,7 @@ cfg_if! {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <Counters/> }
});
}

View File

@@ -3,3 +3,5 @@
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

@@ -2,16 +2,14 @@ use leptos::*;
/// A simple counter component.
///
/// You can use doc comments like this to document your 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,
/// The starting value for the counter
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32
) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
pub fn SimpleCounter(cx: Scope, initial_value: i32, step: i32) -> web_sys::Element {
let (value, set_value) = create_signal(cx, 0);
view! { cx,
<div>

View File

@@ -4,10 +4,5 @@ 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,
<SimpleCounter
initial_value=0
step=1
/>
})
mount_to_body(|cx| view! { cx, <SimpleCounter initial_value=3 step=1/> })
}

View File

@@ -3,23 +3,19 @@ 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();
let clear = div
let dec = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let dec = clear
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let text = dec
.next_sibling()
.unwrap()
@@ -34,16 +30,12 @@ fn inc() {
inc.click();
inc.click();
assert_eq!(text.text_content(), Some("Value: 2!".to_string()));
assert_eq!(text.text_content(), Some("2".to_string()));
dec.click();
dec.click();
dec.click();
dec.click();
assert_eq!(text.text_content(), Some("Value: -2!".to_string()));
clear.click();
assert_eq!(text.text_content(), Some("Value: 0!".to_string()));
assert_eq!(text.text_content(), Some("-2".to_string()));
}

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

@@ -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

@@ -11,7 +11,7 @@ struct CounterUpdater {
}
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
pub fn Counters(cx: Scope) -> web_sys::Element {
let (next_counter_id, set_next_counter_id) = create_signal(cx, 0);
let (counters, set_counters) = create_signal::<CounterHolder>(cx, vec![]);
provide_context(cx, CounterUpdater { set_counters });
@@ -39,7 +39,7 @@ pub fn Counters(cx: Scope) -> impl IntoView {
};
view! { cx,
<>
<div>
<button on:click=add_counter>
"Add Counter"
</button>
@@ -63,18 +63,16 @@ pub fn Counters(cx: Scope) -> impl IntoView {
" counters."
</p>
<ul>
<For
each=counters
key=|counter| counter.0
view=move |(id, (value, set_value)): (usize, (ReadSignal<i32>, WriteSignal<i32>))| {
<For each=counters key=|counter| counter.0>{
|cx, (id, (value, set_value)): &(usize, (ReadSignal<i32>, WriteSignal<i32>))| {
view! {
cx,
<Counter id value set_value/>
<Counter id=*id value=*value set_value=*set_value/>
}
}
/>
}</For>
</ul>
</>
</div>
}
}
@@ -84,7 +82,7 @@ fn Counter(
id: usize,
value: ReadSignal<i32>,
set_value: WriteSignal<i32>,
) -> impl IntoView {
) -> web_sys::Element {
let CounterUpdater { set_counters } = use_context(cx).unwrap_throw();
let input = move |ev| set_value(event_target_value(&ev).parse::<i32>().unwrap_or_default());

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={}",
@@ -29,41 +34,50 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
}
}
pub fn fetch_example(cx: Scope) -> impl IntoView {
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?"
<input type="number"
prop:value={move || cat_count.get().to_string()}
value={move || cat_count.get().to_string()}
on:input=move |ev| {
let val = event_target_value(&ev).parse::<u32>().unwrap_or(0);
set_cat_count(val);
}
/>
</label>
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
{move || {
cats.read().map(|data| match data {
Err(_) => view! { cx, <pre>"Error"</pre> }.into_view(cx),
Ok(cats) => view! { cx,
<div>{
cats.iter()
.map(|src| {
view! { cx,
<img src={src}/>
}
})
.collect::<Vec<_>>()
}</div>
}.into_view(cx),
})
{move || pending().then(|| view! { cx, <p>"Loading more cats..."</p> })}
<div>
// <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,
<div>{
cats.iter()
.map(|src| {
view! { cx,
<img src={src}/>
}
})
.collect::<Vec<_>>()
}</div>
},
})
}
}
}
</Transition>
</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

@@ -7,27 +7,27 @@ edition = "2021"
crate-type = ["cdylib", "rlib"]
[dependencies]
anyhow = "1"
console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
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"
simple_logger = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
axum = { version = "0.5.17", optional = true }
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.0", features = ["full"], optional = true }
tokio = { version = "1.22.0", features = ["full"], optional = true }
http = { version = "0.2.8", optional = true }
[features]

View File

@@ -3,18 +3,27 @@
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 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
```
> 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,66 +0,0 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
http::{Request, Response, StatusCode, Uri},
};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/pkg").await?;
println!("FIRST URI{:?}", uri);
if res.status() == StatusCode::NOT_FOUND {
// try with `.html`
// TODO: handle if the Uri has query parameters
match format!("{}.html", uri).parse() {
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
}
} else {
Ok(res)
}
}
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
let res = get_static_file(uri.clone(), "/static").await?;
println!("FIRST URI{:?}", uri);
if res.status() == StatusCode::NOT_FOUND {
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
} else {
Ok(res)
}
}
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// When run normally, the root should be the crate root
println!("Base: {:#?}", base);
if base == "/static" {
match ServeDir::new("./static").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
))
}
} else if base == "/pkg" {
match ServeDir::new("./pkg").oneshot(req).await {
Ok(res) => Ok(res.map(boxed)),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
)),
}
} else{
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
}
}
}
}

View File

@@ -3,7 +3,6 @@ use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod handlers;
mod routes;
use routes::nav::*;
use routes::stories::*;
@@ -22,9 +21,9 @@ pub fn App(cx: Scope) -> Element {
<Nav />
<main>
<Routes>
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
<Route path="*stories" view=|cx| view! { cx, <Stories/> }/>
<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>

View File

@@ -4,32 +4,47 @@ use leptos::*;
// boilerplate to run in different modes
cfg_if! {
if #[cfg(feature = "ssr")] {
// use actix_files::{Files, NamedFile};
// use actix_web::*;
use axum::{
routing::{get},
Router,
handler::Handler,
error_handling::HandleError,
};
use http::StatusCode;
use std::net::SocketAddr;
use leptos_hackernews_axum::handlers::{file_handler, get_static_file_handler};
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], 8082));
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("/pkg", get(file_handler))
.nest("/static", get(get_static_file_handler))
.fallback(leptos_axum::render_app_to_stream("leptos_hackernews_axum", |cx| view! { cx, <App/> }).into_service());
.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`

View File

@@ -1,7 +0,0 @@
// This file is auto-generated. Changing it will have no effect on leptos. Change these by changing RenderOptions and rerunning
RenderOptions {
pkg-path "/pkg/leptos_hackernews"
environment "PROD"
socket-address "127.0.0.1:3000"
reload-port 3001
}

View File

@@ -14,10 +14,10 @@ console_log = "0.2"
console_error_panic_hook = "0.1"
futures = "0.3"
cfg-if = "1"
leptos = { path = "../../leptos", default-features = false, features = ["serde"] }
leptos_meta = { path = "../../meta", default-features = false }
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
leptos_router = { path = "../../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"] }
@@ -25,9 +25,7 @@ serde_json = "1"
gloo-net = { version = "0.2", features = ["http"] }
reqwest = { version = "0.11", features = ["json"] }
# openssl = { version = "0.10", features = ["v110"] }
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
tracing = "0.1"
[features]
default = ["csr"]

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
```
> 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,5 +1,5 @@
use cfg_if::cfg_if;
use leptos::{component, Scope, IntoView, provide_context, view};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
mod api;
@@ -10,23 +10,25 @@ use routes::story::*;
use routes::users::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App(cx: Scope) -> Element {
provide_context(cx, MetaContext::default());
view! {
cx,
<>
<div>
<Stylesheet href="/style.css"/>
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
<Router>
<Nav />
<main>
<Routes>
<Route path="users/:id" view=|cx| view! { cx, <User/> }/>
<Route path="stories/:id" view=|cx| view! { cx, <Story/> }/>
<Route path="*stories" view=|cx| view! { cx, <Stories/> }/>
<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>
}
}
@@ -39,7 +41,7 @@ cfg_if! {
pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(move |cx| {
leptos::hydrate(body().unwrap(), move |cx| {
view! { cx, <App/> }
});
}

View File

@@ -1,12 +1,12 @@
use leptos::{component, Scope, IntoView, view};
use leptos::*;
use leptos_router::*;
#[component]
pub fn Nav(cx: Scope) -> impl IntoView {
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

@@ -14,7 +14,7 @@ fn category(from: &str) -> &'static str {
}
#[component]
pub fn Stories(cx: Scope) -> impl IntoView {
pub fn Stories(cx: Scope) -> Element {
let query = use_query_map(cx);
let params = use_params_map(cx);
let page = move || {
@@ -54,14 +54,14 @@ pub fn Stories(cx: Scope) -> impl IntoView {
>
"< prev"
</a>
}.into_any()
}
} else {
view! {
cx,
<span class="page-link disabled" aria-hidden="true">
"< prev"
</span>
}.into_any()
}
}}
</span>
<span>"page " {page}</span>
@@ -79,26 +79,24 @@ pub fn Stories(cx: Scope) -> impl IntoView {
<main class="news-list">
<div>
<Transition
fallback=move || view! { cx, <p>"Loading..."</p> }
set_pending=set_pending.into()
fallback=view! { cx, <p>"Loading..."</p> }
set_pending
>
{move || match stories.read() {
None => None,
Some(None) => Some(view! { cx, <p>"Error loading stories."</p> }.into_any()),
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
view=move |story: api::Story| {
<For each=move || stories.clone() key=|story| story.id>{
move |cx: Scope, story: &api::Story| {
view! { cx,
<Story story/>
<Story story=story.clone() />
}
}
/>
}</For>
</ul>
}.into_any())
})
}
}}
</Transition>
@@ -109,7 +107,7 @@ pub fn Stories(cx: Scope) -> impl IntoView {
}
#[component]
fn Story(cx: Scope, story: api::Story) -> impl IntoView {
fn Story(cx: Scope, story: api::Story) -> Element {
view! { cx,
<li class="news-item">
<span class="score">{story.points}</span>
@@ -122,10 +120,10 @@ fn Story(cx: Scope, story: api::Story) -> impl IntoView {
</a>
<span class="host">"("{story.domain}")"</span>
</span>
}.into_view(cx)
}
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view(cx)
view! { cx, <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }
}}
</span>
<br />
@@ -144,15 +142,17 @@ fn Story(cx: Scope, story: api::Story) -> impl IntoView {
}}
</A>
</span>
}.into_view(cx)
}
} else {
let title = story.title.clone();
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view(cx)
view! { cx, <A href=format!("/item/{}", story.id)>{title.clone()}</A> }
}}
</span>
{(story.story_type != "link").then(|| view! { cx,
" "
<span class="label">{story.story_type}</span>
<span>
//{" "}
<span class="label">{story.story_type}</span>
</span>
})}
</li>
}

View File

@@ -4,7 +4,7 @@ use leptos_meta::*;
use leptos_router::*;
#[component]
pub fn Story(cx: Scope) -> impl IntoView {
pub fn Story(cx: Scope) -> Element {
let params = use_params_map(cx);
let story = create_resource(
cx,
@@ -20,7 +20,7 @@ pub fn Story(cx: Scope) -> impl IntoView {
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> },
@@ -49,21 +49,19 @@ pub fn Story(cx: Scope) -> impl IntoView {
}}
</p>
<ul class="comment-children">
<For
each=move || story.comments.clone().unwrap_or_default()
key=|comment| comment.id
view=move |comment| view! { cx, <Comment comment /> }
/>
<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) -> impl IntoView {
pub fn Comment(cx: Scope, comment: api::Comment) -> Element {
let (open, set_open) = create_signal(cx, true);
view! { cx,
@@ -92,11 +90,9 @@ pub fn Comment(cx: Scope, comment: api::Comment) -> impl IntoView {
let comments = comment.comments.clone();
move || view! { cx,
<ul class="comment-children">
/* <For
each=move || comments.clone()
key=|comment| comment.id
view=move |comment: api::Comment| view! { cx, <Comment comment /> }
/> */
<For each=move || comments.clone() key=|comment| comment.id>
{|cx, comment: &api::Comment| view! { cx, <Comment comment=comment.clone() /> }}
</For>
</ul>
}
})}

View File

@@ -3,7 +3,7 @@ use leptos::*;
use leptos_router::*;
#[component]
pub fn User(cx: Scope) -> impl IntoView {
pub fn User(cx: Scope) -> Element {
let params = use_params_map(cx);
let user = create_resource(
cx,
@@ -19,7 +19,7 @@ pub fn User(cx: Scope) -> impl IntoView {
view! { cx,
<div class="user-view">
{move || user.read().map(|user| match user {
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
None => view! { cx, <h1>"User not found."</h1> },
Some(user) => view! { cx,
<div>
<h1>"User: " {&user.id}</h1>
@@ -38,7 +38,7 @@ pub fn User(cx: Scope) -> impl IntoView {
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
}.into_any()
}
})}
</div>
}

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

@@ -7,21 +7,23 @@ use web_sys::MouseEvent;
// 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
// 4) <ButtonC/>: providing a context that is used in the component (rather than prop drilling)
// 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)
#[derive(Copy, Clone)]
struct SmallcapsContext(WriteSignal<bool>);
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App(cx: Scope) -> Element {
// just some signals to toggle three classes on our <p>
let (red, set_red) = create_signal(cx, false);
let (right, set_right) = create_signal(cx, false);
let (italics, set_italics) = create_signal(cx, false);
let (smallcaps, set_smallcaps) = create_signal(cx, false);
// the newtype pattern isn't *necessary* here but is a good practice
// it avoids confusion with other possible future `WriteSignal<bool>` contexts
// and makes it easier to refer to it in ButtonC
// and makes it easier to refer to it in ButtonD
provide_context(cx, SmallcapsContext(set_smallcaps));
view! {
@@ -31,6 +33,7 @@ pub fn App(cx: Scope) -> impl IntoView {
// class: attributes take F: Fn() => bool, and these signals all implement Fn()
class:red=red
class:right=right
class:italics=italics
class:smallcaps=smallcaps
>
"Lorem ipsum sit dolor amet."
@@ -42,19 +45,18 @@ pub fn App(cx: Scope) -> impl IntoView {
// Button B: pass a closure
<ButtonB on_click=move |_| set_right.update(|value| *value = !*value)/>
// Button C: components that return an Element, like elements, can take on: event handler attributes
<ButtonC on:click=move |_| set_italics.update(|value| *value = !*value)/>
// Button D gets its setter from context rather than props
<ButtonC/>
<ButtonD/>
</main>
}
}
/// Button A receives a signal setter and updates the signal itself
// Button A receives a signal setter and updates the signal itself
#[component]
pub fn ButtonA(
cx: Scope,
/// Signal that will be toggled when the button is clicked.
setter: WriteSignal<bool>
) -> impl IntoView {
pub fn ButtonA(cx: Scope, setter: WriteSignal<bool>) -> Element {
view! {
cx,
<button
@@ -65,13 +67,9 @@ pub fn ButtonA(
}
}
/// Button B receives a closure
// Button B receives a closure
#[component]
pub fn ButtonB<F>(
cx: Scope,
/// Callback that will be invoked when the button is clicked.
on_click: F
) -> impl IntoView
pub fn ButtonB<F>(cx: Scope, on_click: F) -> Element
where
F: Fn(MouseEvent) + 'static,
{
@@ -97,10 +95,22 @@ where
// if Rust ever had named function arguments we could drop this requirement
}
/// Button D is very similar to Button A, but instead of passing the setter as a prop
/// we get it from the context
// Button C will have its event listener added by the parent
// This is just a way of encapsulating whatever markup you need for the button
#[component]
pub fn ButtonC(cx: Scope) -> impl IntoView {
pub fn ButtonC(cx: Scope) -> Element {
view! {
cx,
<button>
"Toggle Italics"
</button>
}
}
// Button D is very similar to Button A, but instead of passing the setter as a prop
// we get it from the context
#[component]
pub fn ButtonD(cx: Scope) -> Element {
let setter = use_context::<SmallcapsContext>(cx).unwrap().0;
view! {

View File

@@ -7,7 +7,7 @@ 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"

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

@@ -27,9 +27,9 @@ pub struct Contact {
pub phone: String,
}
pub async fn get_contacts(_search: String) -> Vec<ContactSummary> {
pub async fn get_contacts(search: String) -> Vec<ContactSummary> {
// fake an API call with an artificial delay
_ = delay(Duration::from_millis(300)).await;
delay(Duration::from_millis(300)).await;
vec![
ContactSummary {
id: 0,
@@ -51,7 +51,7 @@ pub async fn get_contacts(_search: String) -> Vec<ContactSummary> {
pub async fn get_contact(id: Option<usize>) -> Option<Contact> {
// fake an API call with an artificial delay
_ = delay(Duration::from_millis(500)).await;
delay(Duration::from_millis(500)).await;
match id {
Some(0) => Some(Contact {
id: 0,
@@ -97,7 +97,7 @@ fn delay(duration: Duration) -> impl Future<Output = Result<(), Canceled>> {
let (tx, rx) = oneshot::channel();
set_timeout(
move || {
_ = tx.send(());
tx.send(());
},
duration,
);

View File

@@ -1,54 +1,55 @@
mod api;
use api::{Contact, ContactSummary};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use crate::api::{get_contact, get_contacts};
#[component]
pub fn RouterExample(cx: Scope) -> impl IntoView {
log::debug!("rendering <RouterExample/>");
pub fn router_example(cx: Scope) -> Element {
provide_context(cx, MetaContext::default());
view! { cx,
<Router>
<nav>
<A exact=true href="/">"Contacts"</A>
<A href="about">"About"</A>
<A href="settings">"Settings"</A>
</nav>
<main>
<Routes>
<Route
path=""
view=move |cx| view! { cx, <ContactList/> }
>
<div id="root">
<Router>
<nav>
<A exact=true href="/">"Contacts"</A>
<A href="about">"About"</A>
<A href="settings">"Settings"</A>
</nav>
<main>
<Routes>
<Route
path=":id"
view=move |cx| view! { cx, <Contact/> }
path=""
element=move |cx| view! { cx, <ContactList/> }
>
<Route
path=":id"
element=move |cx| view! { cx, <Contact/> }
/>
<Route
path="/"
element=move |_| view! { cx, <p>"Select a contact."</p> }
/>
</Route>
<Route
path="about"
element=move |cx| view! { cx, <About/> }
/>
<Route
path="/"
view=move |_| view! { cx, <p>"Select a contact."</p> }
path="settings"
element=move |cx| view! { cx, <Settings/> }
/>
</Route>
<Route
path="about"
view=move |cx| view! { cx, <About/> }
/>
<Route
path="settings"
view=move |cx| view! { cx, <Settings/> }
/>
</Routes>
</main>
</Router>
</Routes>
</main>
</Router>
</div>
}
}
#[component]
pub fn ContactList(cx: Scope) -> impl IntoView {
log::debug!("rendering <ContactList/>");
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 || {
@@ -77,9 +78,8 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Contact(cx: Scope) -> impl IntoView {
log::debug!("rendering <Contact/>");
pub fn Contact(cx: Scope) -> Element {
log!("rendering <Contact/> page");
let params = use_params_map(cx);
let contact = create_resource(
cx,
@@ -103,14 +103,14 @@ pub fn Contact(cx: Scope) -> impl IntoView {
// I'm only doing this explicitly for the example
None => None,
// Some(None) => has loaded and found no contact
Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }.into_any()),
Some(None) => Some(view! { cx, <p>"No contact with this ID was found."</p> }),
// Some(Some) => has loaded and found a contact
Some(Some(contact)) => Some(view! { cx,
<section class="card">
<h1>{contact.first_name} " " {contact.last_name}</h1>
<p>{contact.address_1}<br/>{contact.address_2}</p>
</section>
}.into_any()),
}),
};
view! { cx,
@@ -123,31 +123,30 @@ pub fn Contact(cx: Scope) -> impl IntoView {
}
#[component]
pub fn About(cx: Scope) -> impl IntoView {
log::debug!("rendering <About/>");
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) -> impl IntoView {
log::debug!("rendering <Settings/>");
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

@@ -1,9 +1,9 @@
use leptos::*;
use router::*;
use router::router_example;
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, <RouterExample/> })
mount_to_body(router_example)
}

View File

@@ -12,11 +12,11 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { path = "../../leptos", default-features = false, features = [
leptos = { git = "https://github.com/gbj/leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
leptos_meta = { git = "https://github.com/gbj/leptos", default-features = false }
leptos_router = { git = "https://github.com/gbj/leptos", default-features = false }
gloo-net = { version = "0.2", features = ["http"] }
log = "0.4"

View File

@@ -8,9 +8,9 @@ If you don't have `cargo-leptos` installed you can install it with
Then run
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
`npx tailwindcss -i ./input.css -o ./style/output.scss --watch`
and
and
`cargo leptos watch`
@@ -71,4 +71,4 @@ By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If
## Attribution
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/gbj/leptos/discussions/125).
Many thanks to GreatGreg for putting together this guide. You can find the original, with added details, [here](https://github.com/gbj/leptos/discussions/125).

View File

@@ -2,7 +2,7 @@ use leptos::*;
use leptos_meta::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
pub fn App(cx: Scope) -> Element {
provide_context(cx, MetaContext::default());
let (count, set_count) = create_signal(cx, 0);

View File

@@ -2,7 +2,27 @@ mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(any(feature = "hydrate", feature = "csr"))] {
if #[cfg(feature = "hydrate")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen(start)]
pub fn main() {
use app::*;
use leptos::*;
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
log!("hydrate mode - hydrating");
leptos::hydrate(body().unwrap(), move |cx| {
view! { cx, <App/> }
});
}
}
else if #[cfg(feature = "csr")] {
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen(start)]

View File

@@ -49,21 +49,18 @@ async fn render_app(req: HttpRequest) -> impl Responder {
};
provide_context(cx, RouterIntegrationContext(std::rc::Rc::new(integration)));
view! { cx, <App /> }.into_view(cx)
view! { cx, <App /> }
};
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { HTML_START.to_string() })
.chain(render_to_stream(move |cx| {
use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default()
}))
.chain(futures::stream::once(async { HTML_MIDDLE.to_string() }))
.chain(render_to_stream_with_prefix(
app,
|cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
}
))
.chain(render_to_stream(move |cx| app(cx).to_string()))
.chain(futures::stream::once(async { HTML_END.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)

View File

@@ -1,583 +0,0 @@
/*
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font family by default.
2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.my-0 {
margin-top: 0px;
margin-bottom: 0px;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.max-w-3xl {
max-width: 48rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.bg-sky-600 {
--tw-bg-opacity: 1;
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
.px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.pb-10 {
padding-bottom: 2.5rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:bg-sky-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
}

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

@@ -12,12 +12,12 @@ 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 = [
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
"serde",
] }
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
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"] }
@@ -28,7 +28,7 @@ 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" }
http = { version = "0.2.8", optional = true }
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
@@ -43,6 +43,7 @@ ssr = [
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:http",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
@@ -56,6 +57,7 @@ denylist = [
"tower",
"tower-http",
"tokio",
"http",
"sqlx",
"leptos_axum",
]

View File

@@ -7,7 +7,7 @@ This example creates a basic todo app with an Axum backend that uses Leptos' ser
To run it as a server side app with hydration, first you should run
```bash
wasm-pack build --target=web --debug --no-default-features --features=hydrate
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the WebAssembly to hydrate the HTML that is generated on the server.

View File

@@ -1,4 +1,3 @@
CREATE TABLE IF NOT EXISTS todos
(
id INTEGER NOT NULL PRIMARY KEY,

View File

@@ -14,7 +14,7 @@ cfg_if! {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount_to_body(|cx| {
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <TodoApp/> }
});
}

View File

@@ -24,10 +24,10 @@ if #[cfg(feature = "ssr")] {
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!()
sqlx::migrate!()
.run(&mut conn)
.await
.expect("could not run SQLx migrations"); */
.expect("could not run SQLx migrations");
crate::todo::register_server_functions();

View File

@@ -1,22 +1,20 @@
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};
use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
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();
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@@ -36,12 +34,12 @@ cfg_if! {
}
#[server(GetTodos, "/api")]
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
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_parts = use_context::<leptos_axum::RequestParts>(cx).unwrap();
println!("\ncalling server fn");
println!("Uri = {:?}", req_parts.uri);
// 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;
@@ -57,20 +55,6 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
todos.push(row);
}
// Add a random header(because why not)
let mut res_headers = HeaderMap::new();
res_headers.insert(SET_COOKIE, HeaderValue::from_str("fizz=buzz").unwrap());
let res_parts = leptos_axum::ResponseParts {
headers: res_headers,
status: Some(StatusCode::IM_A_TEAPOT),
};
let res_options_outer = use_context::<leptos_axum::ResponseOptions>(cx);
if let Some(res_options) = res_options_outer {
res_options.overwrite(res_parts).await;
}
Ok(todos)
}
@@ -104,28 +88,29 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
pub fn TodoApp(cx: Scope) -> Element {
view! {
cx,
<Stylesheet href="/style.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<Todos/>
}/>
</Routes>
</main>
</Router>
<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) -> impl IntoView {
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();
@@ -151,77 +136,78 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<Suspense fallback=move || view! {cx, <p>"Loading..."</p> }>
{
let delete_todo = delete_todo.clone();
move || {
let existing_todos = {
<div>
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
{
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>}.into_any()]
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
} else {
todos
.into_iter()
.map({
let delete_todo = delete_todo.clone();
move |todo| {
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();
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>
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>
}
}
.into_any()
}
})
.collect::<Vec<_>>()
})
.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>
}
})
.unwrap_or_default()
.collect::<Vec<_>>()
};
view! {
cx,
<ul>
<div>{existing_todos}</div>
<div>{pending_todos}</div>
</ul>
}
};
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>
{existing_todos}
{pending_todos}
</ul>
}
}
}
</Suspense>
</Suspense>
</div>
</div>
}
}

View File

@@ -1 +0,0 @@
.leptos.kdl

View File

@@ -7,25 +7,25 @@ edition = "2021"
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"
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"
simple_logger = "2"
log = "0.4.17"
simple_logger = "4.0.0"
gloo = { git = "https://github.com/rustwasm/gloo" }
sqlx = { version = "0.6", features = [
sqlx = { version = "0.6.2", features = [
"runtime-tokio-rustls",
"sqlite",
], optional = true }

View File

@@ -1,6 +1,6 @@
# Leptos Counter Isomorphic Example
# Leptos Todo App Sqlite
This example demonstrates how to use a server functions and multi-actions to build a simple todo app.
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
@@ -10,11 +10,12 @@ To run it as a server side app with hydration, first you should run
wasm-pack build --target=web --no-default-features --features=hydrate
```
to generate the Webassembly to provide hydration features for the server.
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
cargo run --no-default-features --features=ssr
```
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above

Binary file not shown.

View File

@@ -1,5 +1,4 @@
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.
@@ -7,13 +6,15 @@ 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::mount_to_body(|cx| {
leptos::hydrate(body().unwrap(), |cx| {
view! { cx, <TodoApp/> }
});
}

View File

@@ -1,3 +1,5 @@
use std::net::SocketAddr;
use cfg_if::cfg_if;
use leptos::*;
mod todo;
@@ -9,7 +11,7 @@ cfg_if! {
use actix_files::{Files};
use actix_web::*;
use crate::todo::*;
use std::{net::SocketAddr, env};
use std::env;
#[get("/style.css")]
async fn css() -> impl Responder {
@@ -18,7 +20,6 @@ cfg_if! {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let addr = SocketAddr::from(([127,0,0,1],3000));
let mut conn = db().await.expect("couldn't connect to DB");
sqlx::migrate!()
.run(&mut conn)
@@ -27,15 +28,11 @@ cfg_if! {
crate::todo::register_server_functions();
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();
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)
@@ -43,7 +40,7 @@ cfg_if! {
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
//.wrap(middleware::Compress::default())
})
.bind(addr)?
.bind(&addr)?
.run()
.await
}

View File

@@ -13,9 +13,9 @@ cfg_if! {
}
pub fn register_server_functions() {
GetTodos::register();
AddTodo::register();
DeleteTodo::register();
_ = GetTodos::register();
_ = AddTodo::register();
_ = DeleteTodo::register();
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@@ -39,12 +39,16 @@ 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());
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
@@ -55,6 +59,8 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
todos.push(row);
}
println!(" returning todos\n");
Ok(todos)
}
@@ -63,16 +69,14 @@ 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));
std::thread::sleep(std::time::Duration::from_millis(350));
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
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())),
}
.map(|_| ())
.map_err(|e| ServerFnError::ServerError(e.to_string()))
}
#[server(DeleteTodo, "/api")]
@@ -88,28 +92,30 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
}
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
pub fn TodoApp(cx: Scope) -> Element {
view! {
cx,
<Stylesheet href="/style.css"/>
<Router>
<header>
<h1>"My Tasks"</h1>
</header>
<main>
<Routes>
<Route path="" view=|cx| view! {
cx,
<Todos/>
}/>
</Routes>
</main>
</Router>
<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) -> impl IntoView {
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();
@@ -135,77 +141,78 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</label>
<input type="submit" value="Add"/>
</MultiActionForm>
<Suspense fallback=move || view! {cx, <p>"Loading..."</p> }>
{
let delete_todo = delete_todo.clone();
move || {
let existing_todos = {
<div>
<Transition fallback=view! {cx, <p>"Loading..."</p> }>
{
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>}.into_any()]
}
Ok(todos) => {
if todos.is_empty() {
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
} else {
todos
.into_iter()
.map({
let delete_todo = delete_todo.clone();
move |todo| {
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();
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>
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>
}
}
.into_any()
}
})
.collect::<Vec<_>>()
})
.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>
}
})
.unwrap_or_default()
.collect::<Vec<_>>()
};
view! {
cx,
<ul>
<div>{existing_todos}</div>
<div>{pending_todos}</div>
</ul>
}
};
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>
{existing_todos}
{pending_todos}
</ul>
}
}
}
</Suspense>
</Transition>
</div>
</div>
}
}

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

@@ -12,7 +12,9 @@ const STORAGE_KEY: &str = "todos-leptos";
// Basic operations to manipulate the todo list: nothing really interesting here
impl Todos {
pub fn new(cx: Scope) -> Self {
let starting_todos = if let Ok(Some(storage)) = window().local_storage() {
let starting_todos = if is_server!() {
Vec::new()
} else if let Ok(Some(storage)) = window().local_storage() {
storage
.get_item(STORAGE_KEY)
.ok()
@@ -115,7 +117,7 @@ const ESCAPE_KEY: u32 = 27;
const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope) -> impl IntoView {
pub fn TodoMVC(cx: Scope) -> Element {
// The `todos` are a signal, since we need to reactively update the list
let (todos, set_todos) = create_signal(cx, Todos::new(cx));
@@ -213,11 +215,9 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
/>
<label for="toggle-all">"Mark all as complete"</label>
<ul class="todo-list">
<For
each=filtered_todos
key=|todo| todo.id
view=move |todo: Todo| view! { cx, <Todo todo /> }
/>
<For each=filtered_todos key=|todo| todo.id>
{move |cx, todo: &Todo| view! { cx, <Todo todo=todo.clone() /> }}
</For>
</ul>
</section>
<footer
@@ -257,12 +257,12 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
}
#[component]
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
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();
// this will be filled by _ref=input below
let todo_input = NodeRef::new(cx);
let input = NodeRef::new(cx);
let save = move |value: &str| {
let value = value.trim();
@@ -282,7 +282,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
>
<div class="view">
<input
_ref=todo_input
_ref=input
class="toggle"
type="checkbox"
prop:checked={move || (todo.completed)()}
@@ -293,9 +293,10 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
/>
<label on:dblclick=move |_| {
set_editing(true);
if let Some(input) = todo_input.get() {
_ = input.focus();
// guard against the fact that in SSR mode, that ref is actually to a String
if let Some(input) = input.get().expect("should have loaded input already").dyn_ref::<HtmlInputElement>() {
input.focus();
}
}>
{move || todo.title.get()}

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

@@ -26,7 +26,7 @@ fn Tests(cx: Scope) -> Element {
view! {
cx,
<div>
<div><SelfUpdatingEffect/></div>
//<div><SelfUpdatingEffect/></div>
<div><BlockOrders/></div>
//<div><TemplateConsumer/></div>
</div>

View File

@@ -19,5 +19,3 @@ leptos_meta = { path = "../../meta", default-features = false, version = "0.0",
leptos_router = { path = "../../router", default-features = false, version = "0.0", features = [
"ssr",
] }
tokio = { version = "1.0", features = ["full"] }

View File

@@ -1,34 +1,8 @@
use actix_web::{http::header::HeaderMap, web::Bytes, *};
use futures::{StreamExt, executor};
use http::StatusCode;
use actix_web::{web::Bytes, *};
use futures::StreamExt;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use std::sync::Arc;
use tokio::sync::RwLock;
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
pub headers: HeaderMap,
pub status: Option<StatusCode>,
}
/// Adding this Struct to your Scope inside of a Server Fn or Elements will allow you to override details of the Response
/// like StatusCode and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced
#[derive(Debug, Clone, Default)]
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
*writable = parts
}
}
/// 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,
@@ -78,38 +52,22 @@ pub fn handle_server_fns() -> Route {
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
let res_options = ResponseOptions::default();
// provide HttpRequest as context in server scope
provide_context(cx, req.clone());
provide_context(cx, res_options.clone());
match server_fn(cx, body).await {
Ok(serialized) => {
let res_options = use_context::<ResponseOptions>(cx).unwrap();
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
let mut res: HttpResponseBuilder;
let mut res_parts = res_options.0.write().await;
// let (status, mut res_headers) = match res_parts {
// Some(parts) => (parts.status, parts.headers),
// None => (None, HeaderMap::new()),
// };
if accept_header == Some("application/json")
|| accept_header == Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = HttpResponse::Ok();
// Override Status if Status is set in ResponseParts and
// We're not trying to do a form submit
if let Some(status) = res_parts.status {
res.status(status);
}
res = HttpResponse::Ok()
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
@@ -122,17 +80,6 @@ pub fn handle_server_fns() -> Route {
res.insert_header(("Location", referer))
.content_type("application/json");
};
// Use provided ResponseParts headers if they exist
let _count = res_parts
.headers
.drain()
.map(|(k, v)| {
if let Some(k) = k {
res.insert_header((k, v));
}
})
.count();
match serialized {
Payload::Binary(data) => {
res.content_type("application/cbor");
@@ -175,7 +122,7 @@ pub fn handle_server_fns() -> Route {
/// use std::{env,net::SocketAddr};
///
/// #[component]
/// fn MyApp(cx: Scope) -> impl IntoView {
/// fn MyApp(cx: Scope) -> Element {
/// view! { cx, <main>"Hello, world!"</main> }
/// }
///
@@ -198,17 +145,13 @@ pub fn handle_server_fns() -> Route {
/// }
/// # }
/// ```
pub fn render_app_to_stream<IV>(
pub fn render_app_to_stream(
options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where IV: IntoView
{
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();
let res_options = ResponseOptions::default();
let res_options_default = res_options.clone();
async move {
let path = req.path();
@@ -225,10 +168,9 @@ where IV: IntoView
let integration = ServerIntegration { path: path.clone() };
provide_context(cx, RouterIntegrationContext::new(integration));
provide_context(cx, MetaContext::new());
provide_context(cx, res_options_default.clone());
provide_context(cx, req.clone());
(app_fn)(cx).into_view(cx)
(app_fn)(cx)
}
};
@@ -260,60 +202,27 @@ where IV: IntoView
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="{pkg_path}.js">
<link rel="preload" href="{pkg_path}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}_bg.wasm').then(hydrate);</script>
<link rel="preload" href="{pkg_path}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
let tail = "</body></html>";
let (stream, runtime, _) = render_to_stream_with_prefix_undisposed(
app,
move |cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
});
let mut stream = Box::pin(futures::stream::once(async move { head.clone() })
.chain(stream)
.chain(futures::stream::once(async move {
runtime.dispose();
tail.to_string()
}))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>));
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
let first_chunk = stream.next().await;
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
let res_options = res_options.0.read().await;
println!("Reading Options");
println!("Response Options: {:#?}", res_options);
let (status, mut headers) = (res_options.status.clone(), res_options.headers.clone());
let status = status.unwrap_or_default();
let complete_stream =
futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap(), third_chunk.unwrap()])
.chain(stream);
let mut res = HttpResponse::Ok().content_type("text/html").streaming(
complete_stream
);
// Add headers manipulated in the response
for (key, value) in headers.drain(){
if let Some(key) = key{
res.headers_mut().append(key, value);
}
};
// Set status to what is returned in the function
let res_status = res.status_mut();
*res_status = status;
// Return the response
res
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

@@ -8,11 +8,9 @@ repository = "https://github.com/gbj/leptos"
description = "Axum integrations for the Leptos web framework."
[dependencies]
axum = {version="0.6", features=["macros"]}
axum = "0.6"
derive_builder = "0.12.0"
futures = "0.3"
http = "0.2.8"
hyper = "0.14.23"
kdl = "4.6.0"
leptos = { path = "../../leptos", default-features = false, version = "0.0", features = [
"ssr",

View File

@@ -2,62 +2,13 @@ use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::Path,
http::{HeaderMap, HeaderValue, Request, StatusCode},
response::IntoResponse,
response::{IntoResponse, Response},
};
use futures::{Future, SinkExt, Stream, StreamExt};
use http::{method::Method, uri::Uri, version::Version, Response};
use hyper::body;
use leptos::*;
use leptos_meta::MetaContext;
use leptos_router::*;
use std::{io, pin::Pin, sync::Arc};
use tokio::{sync::RwLock, task::spawn_blocking};
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
/// to construct this for Leptos to use in Axum
#[derive(Debug, Clone)]
pub struct RequestParts {
pub version: Version,
pub method: Method,
pub uri: Uri,
pub headers: HeaderMap<HeaderValue>,
pub body: Bytes,
}
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
#[derive(Debug, Clone, Default)]
pub struct ResponseParts {
pub status: Option<StatusCode>,
pub headers: HeaderMap,
}
/// Adding this Struct to your Scope inside of a Server Fn or Elements will allow you to override details of the Response
/// like StatusCode and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced
#[derive(Debug, Clone, Default)]
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
impl ResponseOptions {
/// A less boilerplatey way to overwrite the default contents of `ResponseOptions` with a new `ResponseParts`
pub async fn overwrite(&self, parts: ResponseParts) {
let mut writable = self.0.write().await;
*writable = parts
}
}
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
// provide request headers as context in server scope
let (parts, body) = req.into_parts();
let body = body::to_bytes(body).await.unwrap_or_default();
RequestParts {
method: parts.method,
uri: parts.uri,
headers: parts.headers,
version: parts.version,
body: body.clone(),
}
}
/// 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].
///
@@ -87,13 +38,11 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
/// .unwrap();
/// }
/// # }
/// ```
/// Leptos provides a generic implementation of `handle_server_fns`. If access to more specific parts of the Request is desired,
/// you can specify your own server fn handler based on this one and give it it's own route in the server macro.
pub async fn handle_server_fns(
Path(fn_name): Path<String>,
headers: HeaderMap,
req: Request<Body>,
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("/") {
@@ -102,7 +51,7 @@ pub async fn handle_server_fns(
};
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking({
std::thread::spawn({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
@@ -112,17 +61,11 @@ pub async fn handle_server_fns(
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
let req_parts = generate_request_parts(req).await;
// Add this so we can get details about the Request
provide_context(cx, req_parts.clone());
// Add this so that we can set headers and status of the response
provide_context(cx, ResponseOptions::default());
// provide request as context in server scope
// provide_context(cx, Arc::new(req));
match server_fn(cx, &req_parts.body).await {
match server_fn(cx, body.as_ref()).await {
Ok(serialized) => {
// If ResponseParts are set, add the headers and extension to the request
let res_options = use_context::<ResponseOptions>(cx);
// clean up the scope, which we only needed to run the server fn
disposer.dispose();
runtime.dispose();
@@ -132,35 +75,12 @@ pub async fn handle_server_fns(
headers.get("Accept").and_then(|value| value.to_str().ok());
let mut res = Response::builder();
// Add headers from ResponseParts if they exist. These should be added as long
// as the server function returns an OK response
let res_options_outer = res_options.unwrap().0;
let res_options_inner = res_options_outer.read().await;
let (status, mut res_headers) = (
res_options_inner.status.clone(),
res_options_inner.headers.clone(),
);
match res.headers_mut() {
Some(header_ref) => {
header_ref.extend(res_headers.drain());
}
None => (),
};
if accept_header == Some("application/json")
|| accept_header
== Some("application/x-www-form-urlencoded")
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
// Override Status if Status is set in ResponseParts and
// We're not trying to do a form submit
res = match status {
Some(status) => res.status(status),
None => res,
}
}
// otherwise, it's probably a <form> submit or something: redirect back to the referrer
else {
@@ -256,25 +176,19 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
/// # }
/// ```
///
pub fn render_app_to_stream<IV>(
pub fn render_app_to_stream(
options: RenderOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
app_fn: impl Fn(leptos::Scope) -> Element + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>> + Send + 'static>>
) -> Pin<Box<dyn Future<Output = StreamBody<PinnedHtmlStream>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where IV: IntoView
{
+ 'static {
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
async move {
// Need to get the path and query string of the Request
let path = req.uri();
@@ -315,8 +229,8 @@ where IV: IntoView
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="modulepreload" href="{pkg_path}.js">
<link rel="preload" href="{pkg_path}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}_bg.wasm').then(hydrate);</script>
<link rel="preload" href="{pkg_path}.wasm" as="fetch" type="application/wasm" crossorigin="">
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init('{pkg_path}.wasm').then(hydrate);</script>
{leptos_autoreload}
"#
);
@@ -324,7 +238,7 @@ where IV: IntoView
let (mut tx, rx) = futures::channel::mpsc::channel(8);
spawn_blocking({
std::thread::spawn({
let app_fn = app_fn.clone();
move || {
tokio::runtime::Runtime::new()
@@ -334,57 +248,27 @@ where IV: IntoView
async move {
tokio::task::LocalSet::new()
.run_until(async {
let app = {
let mut shell = Box::pin(render_to_stream({
let full_path = full_path.clone();
let req_parts =
generate_request_parts(req).await;
move |cx| {
let integration = ServerIntegration {
path: full_path.clone(),
};
provide_context(
cx,
RouterIntegrationContext::new(
integration,
),
RouterIntegrationContext::new(integration),
);
provide_context(cx, MetaContext::new());
provide_context(cx, req_parts);
provide_context(cx, default_res_options);
app_fn(cx).into_view(cx)
let app = app_fn(cx);
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>{app}")
}
};
let (bundle, runtime, scope) =
render_to_stream_with_prefix_undisposed(
app,
|cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
}
);
let mut shell = Box::pin(bundle);
}));
while let Some(fragment) = shell.next().await {
_ = tx.send(fragment).await;
}
// Extract the value of ResponseOptions from here
let cx = Scope {
runtime,
id: scope
};
let res_options =
use_context::<ResponseOptions>(cx).unwrap();
let new_res_parts = res_options.0.read().await.clone();
let mut writable = res_options2.0.write().await;
*writable = new_res_parts;
runtime.dispose();
tx.close_channel();
})
.await;
@@ -393,40 +277,11 @@ where IV: IntoView
}
});
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })
.chain(rx)
.chain(futures::stream::once(async { tail.to_string() }))
.map(|html| Ok(Bytes::from(html))),
);
// Get the first, second, and third chunks in the stream, which renders the app shell, and thus allows Resources to run
let first_chunk = stream.next().await;
let second_chunk = stream.next().await;
let third_chunk = stream.next().await;
// Extract the resources now that they've been rendered
let res_options = res_options3.0.read().await;
let complete_stream = futures::stream::iter([
first_chunk.unwrap(),
second_chunk.unwrap(),
third_chunk.unwrap(),
])
.chain(stream);
let mut res = Response::new(StreamBody::new(
Box::pin(complete_stream) as PinnedHtmlStream
));
match res_options.status {
Some(status) => *res.status_mut() = status,
None => (),
};
let mut res_headers = res_options.headers.clone();
res.headers_mut().extend(res_headers.drain());
res
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.18"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -9,51 +9,76 @@ description = "Leptos is a full-stack, isomorphic Rust web framework leveraging
readme = "../README.md"
[dependencies]
cfg-if = "1"
leptos_config = { path = "../leptos_config", default-features = false, version = "0.0.18" }
leptos_dom = { path = "../leptos_dom", default-features = false, version = "0.0.18" }
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0.18" }
leptos_reactive = { path = "../leptos_reactive", default-features = false, version = "0.0.18" }
leptos_server = { path = "../leptos_server", default-features = false, version = "0.0.18" }
tracing = "0.1"
typed-builder = "0.11"
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" }
[dev-dependencies]
leptos = { path = ".", default-features = false }
[build-dependencies]
rustc_version = "0.4"
[features]
default = ["csr", "serde"]
default = ["csr", "serde", "interning"]
csr = [
"leptos/csr",
"leptos_dom/web",
"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/hydrate",
"leptos_dom/web",
"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/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/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

@@ -1,63 +0,0 @@
use leptos_dom::IntoView;
use leptos_macro::component;
use leptos_reactive::Scope;
use std::hash::Hash;
/// Iterates over children and displays them, keyed by the `key` function given.
///
/// This is much more efficient than naively iterating over nodes with `.iter().map(|n| view! { cx, ... })...`,
/// as it avoids re-creating DOM nodes that are not being changed.
///
/// ```
/// # use leptos::*;
///
/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// struct Counter {
/// id: HydrationKey,
/// count: RwSignal<i32>
/// }
///
/// fn Counters(cx: Scope) -> Element {
/// let (counters, set_counters) = create_signal::<Vec<Counter>>(cx, vec![]);
///
/// view! {
/// cx,
/// <div>
/// <For
/// // a function that returns the items we're iterating over; a signal is fine
/// each=counters
/// // a unique key for each item
/// key=|counter| counter.id
/// // renders each item to a view
/// view=move |counter: Counter| {
/// view! {
/// cx,
/// <button>"Value: " {move || counter.count.get()}</button>
/// }
/// }
/// />
/// </div>
/// }
/// }
/// ```
#[component(transparent)]
pub fn For<IF, I, T, EF, N, KF, K>(
cx: Scope,
/// Items over which the component should iterate.
each: IF,
/// A key function that will be applied to each item.
key: KF,
/// The view that will be displayed for each item.
view: EF,
) -> impl IntoView
where
IF: Fn() -> I + 'static,
I: IntoIterator<Item = T>,
EF: Fn(T) -> N + 'static,
N: IntoView,
KF: Fn(&T) -> K + 'static,
K: Eq + Hash + 'static,
T: 'static,
{
leptos_dom::Each::new(each, key, view).into_view(cx)
}

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/)).)
@@ -77,6 +88,10 @@
//! from the server to the client.
//! - `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.
@@ -87,7 +102,7 @@
//! use leptos::*;
//!
//! #[component]
//! pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
//! pub fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
//! // create a reactive signal with the initial value
//! let (value, set_value) = create_signal(cx, initial_value);
//!
@@ -116,7 +131,7 @@
//! # if false { // can't run in doctests
//!
//! #[component]
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
//! fn SimpleCounter(cx: Scope, initial_value: i32) -> Element {
//! todo!()
//! }
//!
@@ -127,6 +142,7 @@
//! ```
pub use leptos_config::*;
pub use leptos_core::*;
pub use leptos_dom;
pub use leptos_dom::wasm_bindgen::{JsCast, UnwrapThrowExt};
pub use leptos_dom::*;
@@ -135,16 +151,4 @@ pub use leptos_reactive::*;
pub use leptos_server;
pub use leptos_server::*;
pub use tracing;
pub use typed_builder;
mod for_loop;
pub use for_loop::*;
mod suspense;
pub use suspense::*;
mod transition;
pub use transition::*;
pub use leptos_reactive::debug_warn;
extern crate self as leptos;

View File

@@ -1,127 +0,0 @@
use cfg_if::cfg_if;
use leptos_macro::component;
use std::rc::Rc;
use leptos_dom::{DynChild, Fragment, IntoView, Component};
use leptos_reactive::{provide_context, Scope, SuspenseContext};
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
use leptos_dom::{HydrationCtx, HydrationKey};
/// If any [Resources](leptos_reactive::Resource) are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`.
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources have
/// `Some` value in `children`.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_core::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
///
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
///
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
///
/// view! { cx,
/// <div>
/// <Suspense fallback=move || view! { cx, <p>"Loading (Suspense Fallback)..."</p> }>
/// {move || {
/// cats.read().map(|data| match data {
/// Err(_) => view! { cx, <pre>"Error"</pre> },
/// Ok(cats) => view! { cx,
/// <div>{
/// cats.iter()
/// .map(|src| {
/// view! { cx,
/// <img src={src}/>
/// }
/// })
/// .collect::<Vec<_>>()
/// }</div>
/// },
/// })
/// }
/// }
/// </Suspense>
/// </div>
/// };
/// # }
/// # });
/// ```
#[component(transparent)]
pub fn Suspense<F, E>(
cx: Scope,
/// Returns a fallback UI that will be shown while `async` [Resources](leptos_reactive::Resource) are still loading.
fallback: F,
/// Children will be displayed once all `async` [Resources](leptos_reactive::Resource) have resolved.
children: Box<dyn Fn(Scope) -> Fragment>,
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
{
let context = SuspenseContext::new(cx);
// provide this SuspenseContext to any resources below it
provide_context(cx, context);
let orig_child = Rc::new(children);
Component::new("Suspense", move |cx| {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
let current_id = HydrationCtx::peek();
DynChild::new(move || {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if context.ready() {
orig_child(cx).into_view(cx)
} else {
fallback().into_view(cx)
}
} else {
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child.clone()
}
// show the fallback, but also prepare to stream HTML
else {
let orig_child = Rc::clone(&orig_child);
cx.register_suspense(context, &current_id.to_string(), {
let current_id = current_id.clone();
let fragment_id = HydrationKey {
previous: current_id.previous,
offset: current_id.offset + 1
};
move || {
HydrationCtx::continue_from(fragment_id);
orig_child(cx)
.into_view(cx)
.render_to_string(cx)
.to_string()
}
});
// return the fallback for now, wrapped in fragment identifer
fallback().into_view(cx)
}
};
HydrationCtx::continue_from(current_id.clone());
initial
}
}
})
})
}

View File

@@ -1,154 +0,0 @@
use cfg_if::cfg_if;
use leptos_dom::{Component, DynChild, Fragment, IntoView};
use leptos_macro::component;
use leptos_reactive::{provide_context, Scope, SignalSetter, SuspenseContext};
use std::{cell::RefCell, rc::Rc};
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
use leptos_dom::{HydrationCtx, HydrationKey};
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`. Unlike [`Suspense`](crate::Suspense), this will not fall
/// back to the `fallback` state if there are further changes after the initial load.
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources have
/// `Some` value in `children`.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_core::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
///
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
/// let (pending, set_pending) = create_signal(cx, false);
///
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
///
/// view! { cx,
/// <div>
/// <Transition
/// fallback=move || view! { cx, <p>"Loading..."</p>}
/// set_pending=set_pending
/// >
/// {move || {
/// cats.read().map(|data| match data {
/// Err(_) => view! { cx, <pre>"Error"</pre> },
/// Ok(cats) => view! { cx,
/// <div>{
/// cats.iter()
/// .map(|src| {
/// view! { cx,
/// <img src={src}/>
/// }
/// })
/// .collect::<Vec<_>>()
/// }</div>
/// },
/// })
/// }
/// }
/// </Transition>
/// </div>
/// };
/// # }
/// # });
/// ```
#[component(transparent)]
pub fn Transition<F, E>(
cx: Scope,
/// Will be displayed while resources are pending.
fallback: F,
/// A function that will be called when the component transitions into or out of
/// the `pending` state, with its argument indicating whether it is pending (`true`)
/// or not pending (`false`).
#[prop(optional)]
set_pending: Option<SignalSetter<bool>>,
/// Will be displayed once all resources have resolved.
children: Box<dyn Fn(Scope) -> Fragment>
) -> impl IntoView
where
F: Fn() -> E + 'static,
E: IntoView,
{
let context = SuspenseContext::new(cx);
// provide this SuspenseContext to any resources below it
provide_context(cx, context);
let orig_child = Rc::new(children);
#[cfg(any(feature = "csr", feature = "hydrate"))]
let prev_child = RefCell::new(None);
Component::new("Transition", move |cx| {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
let current_id = HydrationCtx::peek();
DynChild::new(move || {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
if context.ready() {
let current_child = orig_child(cx).into_view(cx);
*prev_child.borrow_mut() = Some(current_child.clone());
if let Some(pending) = &set_pending {
pending.set(false);
}
current_child
} else if let Some(prev_child) = &*prev_child.borrow() {
if let Some(pending) = &set_pending {
pending.set(true);
}
prev_child.clone()
} else {
if let Some(pending) = &set_pending {
pending.set(true);
}
let fallback = fallback().into_view(cx);
*prev_child.borrow_mut() = Some(fallback.clone());
fallback
}
} else {
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
let initial = {
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child.clone()
}
// show the fallback, but also prepare to stream HTML
else {
let orig_child = Rc::clone(&orig_child);
cx.register_suspense(context, &current_id.to_string(), {
let current_id = current_id.clone();
let fragment_id = HydrationKey {
previous: current_id.previous,
offset: current_id.offset + 1
};
move || {
HydrationCtx::continue_from(fragment_id);
orig_child(cx)
.into_view(cx)
.render_to_string(cx)
.to_string()
}
});
// return the fallback for now, wrapped in fragment identifer
fallback().into_view(cx)
}
};
HydrationCtx::continue_from(current_id.clone());
initial
}
}
})
})
}

View File

@@ -1,7 +1,9 @@
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn simple_ssr_test() {
use leptos::*;
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_runtime, create_scope, create_signal};
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
@@ -9,13 +11,13 @@ fn simple_ssr_test() {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value.get().to_string()} "!"</span>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
rendered,
r#"<div data-hk="0-0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>"# //r#"<div data-hk="0" id="hydrated" data-hk="0"><button>-1</button><span>Value: <!--#-->0<!--/-->!</span><button>+1</button></div>"#
);
});
@@ -24,16 +26,20 @@ fn simple_ssr_test() {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_test_with_components() {
use leptos::*;
use leptos_core as leptos;
use leptos_core::Prop;
use leptos_dom::*;
use leptos_macro::*;
use leptos_reactive::{create_runtime, create_scope, create_signal, Scope};
#[component]
fn Counter(cx: Scope, initial_value: i32) -> impl IntoView {
fn Counter(cx: Scope, initial_value: i32) -> Element {
let (value, set_value) = create_signal(cx, initial_value);
view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value.get().to_string()} "!"</span>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}
@@ -49,7 +55,7 @@ fn ssr_test_with_components() {
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
rendered,
"<div data-hk=\"0-0\" class=\"counters\"><!--#--><div data-hk=\"0-2-0\"><button>-1</button><span>Value: <!--#-->1<!--/-->!</span><button>+1</button></div><!--/--><!--#--><div data-hk=\"0-3-0\"><button>-1</button><span>Value: <!--#-->2<!--/-->!</span><button>+1</button></div><!--/--></div>"
);
});
@@ -58,17 +64,19 @@ fn ssr_test_with_components() {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn test_classes() {
use leptos::*;
use leptos_dom::*;
use leptos_macro::view;
use leptos_reactive::{create_runtime, create_scope, create_signal};
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 5);
let rendered = view! {
cx,
<div class="my big" class:a={move || value.get() > 10} class:red=true class:car={move || value.get() > 1}></div>
<div class="my big" class:a={move || value() > 10} class:red=true class:car={move || value() > 1}></div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
rendered,
r#"<div data-hk="0-0" class="my big red car"></div>"#
);
});

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_config"
version = "0.0.18"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

50
leptos_core/Cargo.toml Normal file
View File

@@ -0,0 +1,50 @@
[package]
name = "leptos_core"
version = "0.0.20"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
repository = "https://github.com/gbj/leptos"
description = "Core functionality for the Leptos web framework."
[dependencies]
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" }
log = "0.4"
typed-builder = "0.11"
[dev-dependencies]
leptos = { path = "../leptos", default-features = false, version = "0.0" }
[build-dependencies]
rustc_version = "0.4"
[features]
csr = [
"leptos/csr",
"leptos_dom/csr",
"leptos_macro/csr",
"leptos_reactive/csr",
]
hydrate = [
"leptos/hydrate",
"leptos_dom/hydrate",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
]
ssr = [
"leptos/ssr",
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
]
stable = [
"leptos/stable",
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
]
[package.metadata.cargo-all-features]
denylist = ["stable"]

12
leptos_core/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

@@ -0,0 +1,80 @@
use leptos_dom::Element;
use leptos_reactive::{Memo, Scope};
use std::fmt::Debug;
use std::hash::Hash;
use crate::map::map_keyed;
use typed_builder::TypedBuilder;
/// Properties for the [For](crate::For) component, a keyed list.
#[derive(TypedBuilder)]
pub struct ForProps<E, T, G, I, K>
where
E: Fn() -> Vec<T>,
G: Fn(Scope, &T) -> Element,
I: Fn(&T) -> K,
K: Eq + Hash,
T: 'static,
{
/// Items over which the component should iterate.
pub each: E,
/// A key function that will be applied to each item
pub key: I,
/// Should provide a single child function, which takes
pub children: Box<dyn Fn() -> Vec<G>>,
}
/// Iterates over children and displays them, keyed by the `key` function given.
///
/// This is much more efficient than naively iterating over nodes with `.iter().map(|n| view! { cx, ... })...`,
/// as it avoids re-creating DOM nodes that are not being changed.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_core::*;
/// # use leptos_dom::*; use leptos::*;
///
/// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// struct Counter {
/// id: usize,
/// count: RwSignal<i32>
/// }
///
/// fn Counters(cx: Scope) -> Element {
/// let (counters, set_counters) = create_signal::<Vec<Counter>>(cx, vec![]);
///
/// view! {
/// cx,
/// <div>
/// <For
/// // a function that returns the items we're iterating over; a signal is fine
/// each=counters
/// // a unique key for each item
/// key=|counter| counter.id
/// >
/// {|cx: Scope, counter: &Counter| {
/// let count = counter.count;
/// view! {
/// cx,
/// <button>"Value: " {move || count.get()}</button>
/// }
/// }
/// }
/// </For>
/// </div>
/// }
/// }
/// ```
#[allow(non_snake_case)]
pub fn For<E, T, G, I, K>(cx: Scope, props: ForProps<E, T, G, I, K>) -> Memo<Vec<Element>>
where
E: Fn() -> Vec<T> + 'static,
G: Fn(Scope, &T) -> Element + 'static,
I: Fn(&T) -> K + 'static,
K: Eq + Hash,
T: Eq + Debug + 'static,
{
let map_fn = (props.children)().swap_remove(0);
map_keyed(cx, props.each, map_fn, props.key)
}

25
leptos_core/src/lib.rs Normal file
View File

@@ -0,0 +1,25 @@
#![deny(missing_docs)]
//! This crate contains several utility pieces that depend on multiple crates.
//! They are all re-exported in the main `leptos` crate.
mod for_component;
mod map;
mod suspense;
mod transition;
pub use for_component::*;
pub use map::*;
pub use suspense::*;
pub use transition::*;
pub use typed_builder;
/// Describes the properties of a component. This is typically generated by the `Prop` derive macro
/// as part of the `#[component]` macro.
pub trait Prop {
/// Builder type, automatically generated.
type Builder;
/// The builder should be automatically generated using the `Prop` derive macro.
fn builder() -> Self::Builder;
}

193
leptos_core/src/map.rs Normal file
View File

@@ -0,0 +1,193 @@
use leptos_reactive::{create_memo, queue_microtask, Memo, Scope, ScopeDisposer};
use std::{cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, ops::IndexMut};
/// Function that maps a `Vec` to another `Vec` via a map function. The mapped `Vec` is lazy
/// computed; its value will only be updated when requested. Modifications to the
/// input `Vec` are diffed using keys to prevent recomputing values that have not changed.
///
/// This function is the underlying utility behind `Keyed`.
///
/// # Params
/// * `list` - The list to be mapped. It is obtained via an accessor function, so can be a ReadSignal, a Memo
/// or a derived signal.
/// * `map_fn` - A closure that maps from the input type to the output type.
/// * `key_fn` - A closure that returns an _unique_ key to each entry.
///
/// _Credits: Based on implementation for [Sycamore](https://github.com/sycamore-rs/sycamore/blob/53735aab9ef72b98439b4d2eaeb85a97f7f32775/packages/sycamore-reactive/src/iter.rs),
/// which is in turned based on on the TypeScript implementation in <https://github.com/solidjs/solid>_
pub fn map_keyed<T, U, K>(
cx: Scope,
list: impl Fn() -> Vec<T> + 'static,
map_fn: impl Fn(Scope, &T) -> U + 'static,
key_fn: impl Fn(&T) -> K + 'static,
) -> Memo<Vec<U>>
//-> impl FnMut() -> Vec<U>
where
T: PartialEq + Debug + 'static,
K: Eq + Hash,
U: PartialEq + Debug + Clone + 'static,
{
// Previous state used for diffing.
let disposers: RefCell<Vec<Option<ScopeDisposer>>> = RefCell::new(Vec::new());
let prev_items: RefCell<Option<Vec<T>>> = RefCell::new(None);
let mapped: RefCell<Vec<U>> = RefCell::new(Vec::new());
// Diff and update signal each time list is updated.
create_memo(cx, move |_| {
let mut prev_items = prev_items.borrow_mut();
let mut mapped = mapped.borrow_mut();
//let mut mapped = mapped.cloned().unwrap_or_default();
let items = prev_items.take().unwrap_or_default();
let new_items = list();
let new_items_len = new_items.len();
if new_items.is_empty() {
// Fast path for removing all items.
let disposers = disposers.take();
// delay disposal until after the current microtask
queue_microtask(move || {
for disposer in disposers.into_iter().flatten() {
disposer.dispose();
}
});
mapped.clear();
} else if items.is_empty() {
let mut disposers = disposers.borrow_mut();
// Fast path for creating items when the existing list is empty.
for new_item in new_items.iter() {
let mut value: Option<U> = None;
let new_disposer = cx.child_scope(|cx| {
value = Some(map_fn(cx, new_item));
});
mapped.push(value.unwrap());
disposers.push(Some(new_disposer));
}
} else {
let mut disposers = disposers.borrow_mut();
let mut temp = vec![None; new_items.len()];
let mut temp_disposers: Vec<Option<ScopeDisposer>> =
(0..new_items.len()).map(|_| None).collect();
// Skip common prefix.
let min_len = usize::min(items.len(), new_items.len());
let start = items
.iter()
.zip(new_items.iter())
.position(|(a, b)| a != b)
.unwrap_or(min_len);
// Skip common suffix.
let mut end = items.len();
let mut new_end = new_items.len();
#[allow(clippy::suspicious_operation_groupings)]
// FIXME: make code clearer so that clippy won't complain
while end > start && new_end > start && items[end - 1] == new_items[new_end - 1] {
end -= 1;
new_end -= 1;
temp[new_end] = Some(mapped[end].clone());
temp_disposers[new_end] = disposers[end].take();
}
// 0) Prepare a map of indices in newItems. Scan backwards so we encounter them in
// natural order.
let mut new_indices = HashMap::with_capacity(new_end - start);
// Indexes for new_indices_next are shifted by start because values at 0..start are
// always None.
let mut new_indices_next = vec![None; new_end - start];
for j in (start..new_end).rev() {
let item = &new_items[j];
let i = new_indices.get(&key_fn(item));
new_indices_next[j - start] = i.copied();
new_indices.insert(key_fn(item), j);
}
// 1) Step through old items and see if they can be found in new set; if so, mark
// them as moved.
for i in start..end {
let item = &items[i];
if let Some(j) = new_indices.get(&key_fn(item)).copied() {
// Moved. j is index of item in new_items.
temp[j] = Some(mapped[i].clone());
temp_disposers[j] = disposers[i].take();
new_indices_next[j - start].and_then(|j| new_indices.insert(key_fn(item), j));
} else {
// Create new.
disposers[i].take().unwrap().dispose();
}
}
// 2) Set all the new values, pulling from the moved array if copied, otherwise
// entering the new value.
for j in start..new_items.len() {
if matches!(temp.get(j), Some(Some(_))) {
// Pull from moved array.
if j >= mapped.len() {
mapped.push(temp[j].clone().unwrap());
disposers.push(temp_disposers[j].take());
} else {
*mapped.index_mut(j) = temp[j].clone().unwrap();
disposers[j] = temp_disposers[j].take();
}
} else {
// Create new value.
let mut tmp = None;
let new_item = &new_items[j];
let new_disposer = cx.child_scope(|cx| {
tmp = Some(map_fn(cx, new_item));
});
if mapped.len() > j {
mapped[j] = tmp.unwrap();
disposers[j] = Some(new_disposer);
} else {
mapped.push(tmp.unwrap());
disposers.push(Some(new_disposer));
}
}
}
}
// 3) In case the new set is shorter than the old, set the length of the mapped array.
mapped.truncate(new_items_len);
disposers.borrow_mut().truncate(new_items_len);
// 4) Return the mapped and new items, for use in next iteration
*prev_items = Some(new_items);
mapped.to_vec()
})
}
#[cfg(test)]
mod tests {
use crate::map::map_keyed;
use leptos_reactive::*;
#[test]
fn test_map_keyed() {
// we can really only run this in SSR mode, so just ignore if we're in CSR or hydrate
if !cfg!(any(feature = "csr", feature = "hydrate")) {
create_scope(create_runtime(), |cx| {
let (rows, set_rows) =
create_signal::<Vec<(usize, ReadSignal<i32>, WriteSignal<i32>)>>(cx, vec![]);
let keyed = map_keyed(
cx,
move || rows.get(),
|cx, row| {
let read = row.1;
create_effect(cx, move |_| println!("row value = {}", read.get()));
},
|row| row.0,
);
create_effect(cx, move |_| println!("keyed = {:#?}", keyed.get()));
let (r, w) = create_signal(cx, 0);
set_rows.update(|n| n.push((0, r, w)));
})
.dispose();
}
}
}

146
leptos_core/src/suspense.rs Normal file
View File

@@ -0,0 +1,146 @@
use leptos_dom::{Child, IntoChild};
use leptos_reactive::{provide_context, Scope, SuspenseContext};
use typed_builder::TypedBuilder;
/// Props for the [Suspense](crate::Suspense) component, which shows a fallback
/// while [Resource](leptos_reactive::Resource)s are being read.
#[derive(TypedBuilder)]
pub struct SuspenseProps<F, E, G>
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E,
{
/// Will be displayed while resources are pending.
pub fallback: F,
/// Will be displayed once all resources have resolved.
pub children: Box<dyn Fn() -> Vec<G>>,
}
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
/// component, it will show the `fallback` while they are loading. Once all are resolved,
/// it will render the `children`. If data begin loading again, falls back to `fallback` again.
///
/// If youd rather continue displaying the previous `children` while loading new data, see
/// [`Transition`](crate::Transition).
///
/// Note that the `children` will be rendered initially (in order to capture the fact that
/// those resources are read under the suspense), so you cannot assume that resources have
/// `Some` value in `children`.
///
/// ```
/// # use leptos_reactive::*;
/// # use leptos_core::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
///
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
///
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
///
/// view! { cx,
/// <div>
/// <Suspense fallback={"Loading (Suspense Fallback)...".to_string()}>
/// {move || {
/// cats.read().map(|data| match data {
/// Err(_) => view! { cx, <pre>"Error"</pre> },
/// Ok(cats) => view! { cx,
/// <div>{
/// cats.iter()
/// .map(|src| {
/// view! { cx,
/// <img src={src}/>
/// }
/// })
/// .collect::<Vec<_>>()
/// }</div>
/// },
/// })
/// }
/// }
/// </Suspense>
/// </div>
/// };
/// # }
/// # });
/// ```
#[allow(non_snake_case)]
pub fn Suspense<F, E, G>(cx: Scope, props: SuspenseProps<F, E, G>) -> impl Fn() -> Child
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E + 'static,
{
let context = SuspenseContext::new(cx);
// provide this SuspenseContext to any resources below it
provide_context(cx, context);
let child = (props.children)().swap_remove(0);
render_suspense(cx, context, props.fallback, child)
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
fn render_suspense<'a, F, E, G>(
cx: Scope,
context: SuspenseContext,
fallback: F,
child: G,
) -> impl Fn() -> Child
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E,
{
move || {
if context.ready() {
(child)().into_child(cx)
} else {
fallback.clone().into_child(cx)
}
}
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
fn render_suspense<'a, F, E, G>(
cx: Scope,
context: SuspenseContext,
fallback: F,
orig_child: G,
) -> impl Fn() -> Child
where
F: IntoChild + Clone,
E: IntoChild,
G: Fn() -> E + 'static,
{
use leptos_dom::IntoAttribute;
use leptos_macro::view;
let initial = {
// run the child; we'll probably throw this away, but it will register resource reads
let mut child = orig_child().into_child(cx);
while let Child::Fn(f) = child {
child = (f.borrow_mut())();
}
// no resources were read under this, so just return the child
if context.pending_resources.get() == 0 {
child
}
// show the fallback, but also prepare to stream HTML
else {
let key = cx.current_fragment_key();
cx.register_suspense(context, &key, move || {
orig_child().into_child(cx).as_child_string()
});
// return the fallback for now, wrapped in fragment identifer
Child::Node(view! { cx, <div data-fragment-id=key>{fallback.into_child(cx)}</div> })
}
};
move || initial.clone()
}

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