Compare commits

..

128 Commits

Author SHA1 Message Date
Greg Johnston
4360a73392 Fix SimpleCounter example 2022-12-09 14:57:58 -05:00
Greg Johnston
50b0fe157a Fix example test 2022-12-09 13:34:35 -05:00
Greg Johnston
64a5d75ec4 .into() calls were interfering with components that have generic props 2022-12-09 13:09:02 -05:00
Greg Johnston
baf3cc8712 Correct imports 2022-12-09 12:36:33 -05:00
Greg Johnston
23777ad67b Use leptos reexport of typed-builder crate 2022-12-09 12:30:21 -05:00
Greg Johnston
08be1ba622 Fix warnings 2022-12-08 19:28:23 -05:00
Greg Johnston
605398bcea Only use default for Option<T> 2022-12-08 19:27:45 -05:00
Greg Johnston
aca2c131d4 Add the ability to document Component and ComponentProps in a single doc comment. 2022-12-08 17:08:54 -05:00
Greg Johnston
9d950b97ff Better error message for RouterIntegrationContext 2022-12-07 07:52:01 -05:00
Greg Johnston
f6a299ae3c Merge pull request #154 from gbj/fix-component-siblings-in-hydration
Fix issue #109
2022-12-07 00:06:48 -05:00
Greg Johnston
1ba602ec47 Fix issue #109 2022-12-06 22:31:54 -05:00
Greg Johnston
1f3dde5b4a Fix Hackernews CSS 2022-12-06 19:22:29 -05:00
Greg Johnston
a65cd67db3 Fix name of Wasm export 2022-12-06 18:18:46 -05:00
Greg Johnston
bacd99260b Fix benchmarks 2022-12-06 18:18:38 -05:00
Greg Johnston
2b726f1a88 Fix docs on props for each component 2022-12-06 11:42:47 -05:00
Greg Johnston
5c45538e9f Make necessary changes for stable support for router and meta 2022-12-05 18:55:03 -05:00
Greg Johnston
7f696a9ac4 support 2022-12-05 17:25:02 -05:00
Greg Johnston
bcd6e671f7 0.0.20 2022-12-05 17:23:22 -05:00
Greg Johnston
7a72f127de Stable compatibility 2022-12-05 17:18:17 -05:00
Greg Johnston
2ff5ec21c8 0.0.20 2022-12-05 16:25:16 -05:00
Greg Johnston
a1f94b609f Improvements to example to show off transitions and streaming 2022-12-05 16:17:47 -05:00
Greg Johnston
da5034da33 Bump versions after WASM-less fix 2022-12-05 16:17:29 -05:00
Greg Johnston
0c509970b5 Fix ability of server functions to work without WASM 2022-12-05 16:17:15 -05:00
Greg Johnston
d894c4dcf9 Merge branch 'main' of https://github.com/gbj/leptos 2022-12-05 16:10:33 -05:00
Greg Johnston
dc15184781 Merge pull request #152 from benwis/cargo-leptos-updates
Add config crate and generate file for cargo-leptos to watch
2022-12-05 12:04:56 -05:00
Ben Wishovich
3200068ab3 Doc tweaks 2022-12-04 18:11:20 -08:00
Ben Wishovich
0a9da8d55e Add some doc comments, and change the behavior of the reload_port 2022-12-04 17:55:51 -08:00
Ben Wishovich
52ad546710 Update rest of the examples and make the tests pass 2022-12-04 17:25:03 -08:00
Ben Wishovich
f88d2fa56a Add socket_address option to configure the ip address and port to serve 2022-12-04 15:50:29 -08:00
Ben Wishovich
f63cb02277 Commit WIP version of common config struct that writes a KDL file for cargo-leptos 2022-12-04 14:50:36 -08:00
Greg Johnston
4b363f9b33 0.0.3 for axum 0.6 compatibility 2022-12-03 22:12:17 -05:00
Ben Wishovich
7b376b6d3a Draft Builder Pattern for Render Options to add Leptos Autorender Code 2022-12-02 16:33:59 -08:00
Ben Wishovich
8fbb4abc76 Switch integrations to pass in a full path and name v the name to enable different pkg structures 2022-12-02 12:01:51 -08:00
Greg Johnston
d0ff64daaa Merge pull request #149 from gbj/a-tag-class-helper
Allow styling `<A/>` tags with `class` property
2022-12-02 14:09:10 -05:00
Greg Johnston
bb97234817 Merge pull request #148 from gbj/explicit-stable-not-required
Automatically enable the `stable` feature if you're on `stable` Rust
2022-12-02 14:08:22 -05:00
Greg Johnston
19698d86b6 Allow styling <A/> component with class 2022-12-02 13:20:07 -05:00
Greg Johnston
21ef96806f Rename ToHref to something a little more generic 2022-12-02 13:04:37 -05:00
Greg Johnston
70e18d2aeb Automatically enable the stable feature if you're on stable Rust 2022-12-02 12:56:05 -05:00
Greg Johnston
5152703f0c Clear warnings 2022-12-02 12:39:32 -05:00
Greg Johnston
3d54055573 Add <Meta/> component to leptos_meta 2022-12-02 12:36:51 -05:00
Greg Johnston
a5b99a3e40 Merge branches 'main' and 'main' of https://github.com/gbj/leptos 2022-12-01 21:42:02 -05:00
Greg Johnston
101e65b724 Does adding skip_feature_sets here help with CI problem? 2022-12-01 21:41:58 -05:00
Greg Johnston
a3f91604b9 Merge pull request #141 from benwis/axum-0.6
Update Axum examples to latest 0.6 release and streamline them a bit
2022-12-01 17:23:20 -05:00
Ben Wishovich
f457d8f319 Fix doc test 2022-12-01 12:56:27 -08:00
Greg Johnston
58abe55d7b Merge branch 'main' into axum-0.6 2022-12-01 13:10:06 -05:00
Greg Johnston
634ac17095 Merge pull request #144 from Indrazar/main
update functions for Windows file directories
2022-12-01 12:43:19 -05:00
Ben Wishovich
79faad4aac Missed another couple imports 2022-11-30 22:41:31 -08:00
IcosaHedron
cedc68c341 remove debug string from axum integration 2022-11-30 23:20:14 -05:00
indrazar
8ec772a129 update functions for Windows file directories
- leptos_macro/src/server.rs server_macro_impl
 - integrations/axum/src/lib.rs handle_server_fns
2022-11-30 23:01:59 -05:00
Greg Johnston
8d671866a3 Merge pull request #142 from FDiskas/patch-1
Update example lib.rs
2022-11-30 20:47:24 -05:00
Ben Wishovich
2edc5b3b8b Remove extra print 2022-11-30 17:31:14 -08:00
Vytenis
be96a230ee Update lib.rs 2022-12-01 01:47:54 +02:00
Ben Wishovich
0f8930b6f2 Update Axum examples to latest 0.6 release and streamline things 2022-11-30 15:02:22 -08:00
Greg Johnston
2b5c4abac5 Merge pull request #140 from gbj/transition-component
Transition component
2022-11-30 16:20:02 -05:00
Greg Johnston
db8c393f49 Update examples 2022-11-30 11:36:54 -05:00
Greg Johnston
f18a7b35f2 Use SignalSetter in <Transition/> API 2022-11-30 11:36:50 -05:00
Greg Johnston
a2c5855362 <Transition/> component 2022-11-30 11:27:07 -05:00
Greg Johnston
644d097cb6 Fix SignalSetter tests 2022-11-30 11:22:05 -05:00
Greg Johnston
9c0be9e317 Finishing implementing SignalSetter wrapper. 2022-11-30 07:46:04 -05:00
Greg Johnston
5faa2efa2d Merge pull request #137 from benwis/example_readmes
Add READMEs to all examples and fix typo in todo-app-axum
2022-11-29 20:00:36 -05:00
Greg Johnston
c5a1e9a447 Copy edited and added Trunk install instructions 2022-11-29 20:00:09 -05:00
Ben Wishovich
e88e131ec3 Add READMEs to all examples and fix typo in todo-app-axum 2022-11-29 13:14:59 -08:00
Greg Johnston
80df7a0dac Merge pull request #135 from ghassanachi/patch-1
Update `counters` example link in docs
2022-11-29 14:48:40 -05:00
Ghassan Gedeon Achi
493f05fda1 Update counters example link in docs 2022-11-29 11:51:27 -07:00
Greg Johnston
4578622b6f Merge pull request #134 from gbj/fix-router-hydration-panic
Fix out-of-order hydration issue
2022-11-29 08:56:03 -05:00
Greg Johnston
c7dd6200e8 Fix GTK example 2022-11-29 07:07:10 -05:00
Greg Johnston
6e20f31df1 Fix out-of-order hydration issue by removing old code that was handling this in an incorrect way 2022-11-29 07:06:25 -05:00
Greg Johnston
5f58db40f0 Merge pull request #131 from gbj/fix-3x-server-resource-fetching
Fix issue in which server-side resource are called 3x
2022-11-29 06:14:22 -05:00
Greg Johnston
321e11e97a Fix issue in which server-side resource are called 3x 2022-11-28 22:28:02 -05:00
Greg Johnston
c472a1c5ef Fix misnamed optional-import feature exclusion that was causing CI to break 2022-11-28 20:50:04 -05:00
Greg Johnston
1180eeeadb Merge pull request #127 from akesson/cargo-path-fix
Fix path deps' going one level too high
2022-11-28 12:17:32 -05:00
Greg Johnston
2348bbc5cc Merge branch 'main' of https://github.com/gbj/leptos 2022-11-28 08:42:23 -05:00
Greg Johnston
ee41ea8b1d Update axum integration 2022-11-28 08:42:19 -05:00
Greg Johnston
a0ea3cfd7c Merge pull request #126 from benwis/axum-server-functions
Mostly working version of axum with server functions
2022-11-28 08:41:37 -05:00
Greg Johnston
edb0f8c848 Fix import for CSR/no-features verson 2022-11-28 07:43:31 -05:00
Greg Johnston
2b71c07fa9 Guard against fragments that don't actually exist 2022-11-28 07:39:30 -05:00
Greg Johnston
a109e3d51c Remove my unnecessary nested closure 2022-11-28 07:39:17 -05:00
Greg Johnston
40a842ff1d Correct name of the root component we're rendering 2022-11-28 07:39:04 -05:00
hakesson
17baec46b7 Fix path deps' going one level too high 2022-11-28 06:03:56 +01:00
Ben Wishovich
fe5c9c6f0d Fix accept heading behavior in Axum to match Actix 2022-11-27 18:25:43 -08:00
Ben Wishovich
6c22c47bbf Cleanup, it now works except for when the server FN response is () or empty 2022-11-27 17:17:34 -08:00
Ben Wishovich
2d88a113c4 Typoed 2022-11-27 17:04:34 -08:00
Ben Wishovich
b0dd759bcf Remove commented code in main 2022-11-27 17:01:42 -08:00
Ben Wishovich
507191e1a4 Mostly working version of axum with server functions 2022-11-27 16:55:38 -08:00
Greg Johnston
36de06f183 0.0.19 2022-11-27 09:13:21 -05:00
Greg Johnston
b54c0f14e8 Remove erroneous Clone bound on calling WriteSignal as a function 2022-11-26 21:24:55 -05:00
Greg Johnston
41c03852e1 Create SignalSetter wrapper for writable signals corresponding to Signal wrapper for readable signals 2022-11-26 21:15:19 -05:00
Greg Johnston
c3fb9396e1 Add RwSignal::split() 2022-11-26 17:35:46 -05:00
Greg Johnston
3a9d16ad29 Add RwSignal::write_only() 2022-11-26 17:33:13 -05:00
Greg Johnston
c0709b210d Enable wasm-bindgen string interning for certain types by default 2022-11-26 17:19:11 -05:00
Greg Johnston
569fa9b1c6 Fix CI w.r.t. server functions 2022-11-26 17:13:35 -05:00
Greg Johnston
ed24e47c1d Add examples of canceling in-flight requests (issue #32) and filter against empty IDs to avoid extra requests (issue #123) 2022-11-26 15:29:46 -05:00
Greg Johnston
fdd07aafb7 #[server] docs 2022-11-26 09:02:36 -05:00
Greg Johnston
1a0168bf28 Clear warnings 2022-11-26 09:02:26 -05:00
Greg Johnston
de524e21b1 Clear warnings 2022-11-26 09:01:05 -05:00
Greg Johnston
dbe3daf16a Update skip lists for CI 2022-11-26 08:43:18 -05:00
Greg Johnston
3f6eeb319a NodeRef should actually track so you can use it in effects 2022-11-26 08:01:19 -05:00
Greg Johnston
db34565959 Merge pull request #107 from benwis/msgpack-encoding
Binary encoding as an option for server functions
2022-11-25 22:44:53 -05:00
Ben Wishovich
d5cd2b814e Make cargo check happy 2022-11-25 17:05:27 -08:00
Greg Johnston
2b9ac037e3 Merge pull request #120 from gbj/node-ref
Change `_ref` attribute to use `NodeRef` type
2022-11-25 17:45:11 -05:00
Greg Johnston
66ecc2ac25 Fix NodeRef doctest 2022-11-25 16:53:48 -05:00
Greg Johnston
4093f4c2d8 Fix todomvc 2022-11-25 16:28:21 -05:00
Greg Johnston
a46e92bed8 Clean up Fn implementation issue 2022-11-25 15:48:40 -05:00
Greg Johnston
611a1aeb28 Use relative paths in book for CI 2022-11-25 15:48:32 -05:00
Greg Johnston
994debea3f Change _ref attribute to use NodeRef type 2022-11-25 15:38:46 -05:00
Greg Johnston
5399f54255 Merge pull request #117 from gbj/router-rerenders
Fix issue #115
2022-11-25 14:53:23 -05:00
Greg Johnston
22668f7999 Merge branch 'msgpack-encoding' of https://github.com/benwis/leptos into pr/107 2022-11-25 14:52:19 -05:00
Greg Johnston
f7b1e732c7 Update integrations 2022-11-25 14:52:14 -05:00
Greg Johnston
93f68e022f Merge branch 'main' into msgpack-encoding 2022-11-25 14:35:52 -05:00
Greg Johnston
2b4dc76d95 Clear warnings in examples 2022-11-25 14:34:14 -05:00
Greg Johnston
55f70367b5 Clear warnings in library 2022-11-25 14:32:25 -05:00
Greg Johnston
a01b0cbbc6 Clear warnings from examples 2022-11-25 14:31:03 -05:00
Greg Johnston
6d329f33eb Remove logging 2022-11-25 14:29:26 -05:00
Greg Johnston
5a863ec411 Actix implementation 2022-11-25 14:28:03 -05:00
Greg Johnston
4800600e4f original_path() for <Outlet/> logic 2022-11-25 13:48:39 -05:00
Greg Johnston
a051b1e08c Proper <Outlet/> logic so we only rerender if it's actually a different parameter 2022-11-25 13:46:55 -05:00
Greg Johnston
4a426be6fb Logging to track rerenders 2022-11-25 13:44:58 -05:00
Greg Johnston
d9ab70de0d Untrack <Outlet/> child to avoid rerenders 2022-11-25 07:57:09 -05:00
Greg Johnston
aaac1d37ac Untrack to avoid double-rendering <Outlet/> 2022-11-24 22:22:27 -05:00
Greg Johnston
498b5345d5 Fix Outlet 2022-11-24 08:51:53 -05:00
Greg Johnston
02a7af2c1e Reduce exponential rerenders to max of 2 2022-11-24 08:40:47 -05:00
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
187 changed files with 6376 additions and 12202 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

@@ -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,20 +2,18 @@ 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 {
pub fn SimpleCounter(cx: Scope, initial_value: i32, step: i32) -> web_sys::Element {
let (value, set_value) = create_signal(cx, initial_value);
view! { cx,
<div>
<button on:click=move |_| set_value(0)>"Clear"</button>
<button on:click=move |_| set_value(initial_value)>"Clear"</button>
<button on:click=move |_| set_value.update(|value| *value -= step)>"-1"</button>
<span>"Value: " {move || value().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>

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=0 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

@@ -1,10 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

View File

@@ -1,92 +0,0 @@
[workspace]
[package]
name = "example"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
gloo-net = { version = "0.2", features = ["http"] }
log = "0.4"
cfg-if = "1.0"
# dependecies for client (enable when csr or hydrate set)
wasm-bindgen = { version = "0.2", optional = true }
console_log = { version = "0.2", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# dependecies for server (enable when ssr set)
actix-files = { version = "0.6", optional = true }
actix-web = { version = "4", features = ["macros"], optional = true }
futures = { version = "0.3", optional = true }
simple_logger = { version = "4.0", optional = true }
serde_json = { version = "1.0", optional = true }
reqwest = { version = "0.11", features = ["json"], optional = true }
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'
[features]
leptos_autoreload = []
default = ["csr"]
hydrate = [
"leptos/hydrate",
"leptos_meta/hydrate",
"leptos_router/hydrate",
"dep:wasm-bindgen",
"dep:console_log",
"dep:console_error_panic_hook",
]
csr = [
"leptos/csr",
"leptos_meta/csr",
"leptos_router/csr",
"dep:wasm-bindgen",
"dep:console_log",
"dep:console_error_panic_hook",
]
ssr = [
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:reqwest",
"dep:actix-web",
"dep:actix-files",
"dep:futures",
"dep:simple_logger",
"dep:serde_json",
]
[package.metadata.leptos]
# Path, relative to root, to generat rust code to
gen_file = "src/server/generated.rs"
# Path to the source index.html file
index_file = "index.html"
# [Optional] Files in the asset_dir will be copied to the target/site directory
assets_dir = "assets"
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
end2end_test_cmd = "npx playwright test"
# On which port to serve the client side rendered site (when using --csr option)
csr_port = 3000
# The port to use for automatic reload monitoring
reload_port = 3001
[package.metadata.leptos.style]
# This points to the TailwindCSS output file
file = "style/output.css"
# A https://browsersl.ist query
browserquery = "defaults"

View File

@@ -1,74 +0,0 @@
# Leptos Starter Template
This is a template demonstrating how to integrate [TailwindCSS](https://tailwindcss.com/) with the [Leptos](https://github.com/gbj/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool.
If you don't have `cargo-leptos` installed you can install it with
`cargo install --locked cargo-leptos`
Then run
`npx tailwindcss -i ./input.css -o ./style/output.css --watch`
and
`cargo leptos watch`
in this directory.
You can begin editing your app at `src/app/mod.rs`.
## Installing Tailwind
You can install Tailwind using `npm`:
```bash
npm install -D tailwindcss
```
If you'd rather not use `npm`, you can install the Tailwind binary [here](https://github.com/tailwindlabs/tailwindcss/releases).
## Setting up with VS Code and Additional Tools
If you're using VS Code, add the following to your `settings.json`
```json
"emmet.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"tailwindCSS.includeLanguages": {
"rust": "html",
"*.rs": "html"
},
"files.associations": {
"*.rs": "rust"
},
"editor.quickSuggestions": {
"other": "on",
"comments": "on",
"strings": true
},
"css.validate": false,
```
Install [Tailwind CSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss).
Install "VS Browser" extension, a browser at the right window.
Allow vscode Ports forward: 3000, 3001.
## Notes about Tooling
By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools.
1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly
2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on
3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly
4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future)
5. `npm install -g sass` - install `dart-sass` (should be optional in future
## 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).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -1,74 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
},
"node_modules/@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.28.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz",
"integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.28.0"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"playwright-core": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz",
"integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==",
"dev": true
}
}
}

View File

@@ -1,13 +0,0 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.28.0"
}
}

View File

@@ -1,107 +0,0 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
export default config;

View File

@@ -1,9 +0,0 @@
import { test, expect } from "@playwright/test";
test("homepage has title and links to intro page", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Cargo Leptos");
await expect(page.locator("h1")).toHaveText("Hi from your Leptos WASM!");
});

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cargo Leptos</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- INJECT HEAD -->
</head>
<body>
<!-- INJECT BODY -->
</body>
</html>

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,26 +0,0 @@
use leptos::*;
use leptos_meta::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
provide_context(cx, MetaContext::default());
let (count, set_count) = create_signal(cx, 0);
view! {
cx,
<main class="my-0 mx-auto max-w-3xl text-center">
<h2 class="p-6 text-4xl">"Welcome to Leptos with Tailwind"</h2>
<p class="px-10 pb-10 text-left">"Tailwind will scan your Rust files for Tailwind class names and compile them into a CSS file."</p>
<button
class="bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg"
on:click=move |_| set_count.update(|count| *count += 1)
>
{move || if count() == 0 {
"Click me!".to_string()
} else {
count().to_string()
}}
</button>
</main>
}
}

View File

@@ -1,22 +0,0 @@
mod app;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(any(feature = "hydrate", feature = "csr"))] {
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!("csr mode - mounting to body");
mount_to_body(|cx| {
view! { cx, <App /> }
});
}
}
}

View File

@@ -1,17 +0,0 @@
mod app;
#[cfg(feature = "ssr")]
mod server;
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
server::run().await
}
}
else {
pub fn main() {}
}
}

View File

@@ -1,49 +0,0 @@
//! THIS FILE IS AUTOGENERATED, DO NOT MODIFY
//! When building, `cargo-leptos` generates this file based on
//! the `index.html` file specified in the Config.toml
//!
//! This file can be commited to version control. It only
//! changes when the configuration changes
#[cfg(feature = "leptos_autoreload")]
/// index.html content up to `<!-- INJECT HEAD -->` plus `cargo leptos` injected css and js content.
pub const HTML_START: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<title>Cargo Leptos</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script type="module">import init from '/pkg/app.js';init('/pkg/app.wasm');</script>
<link rel="preload" href="/pkg/app.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="stylesheet" href="/pkg/app.css">
<link rel="modulepreload" href="/pkg/app.js">
<script crossorigin="">(function () {
var ws = new WebSocket('ws://127.0.0.1:3001/autoreload');
ws.onmessage = (ev) => {
console.log(`Reload message: `);
if (ev.data === 'reload') window.location.reload();
};
ws.onclose = () => console.warn('Autoreload stopped. Manual reload necessary.');
})()
</script>"##;
#[cfg(not(feature = "leptos_autoreload"))]
/// index.html content up to `<!-- INJECT HEAD -->` plus `cargo leptos` injected css and js content.
pub const HTML_START: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<title>Cargo Leptos</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script type="module">import init from '/pkg/app.js';init('/pkg/app.wasm');</script>
<link rel="preload" href="/pkg/app.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="stylesheet" href="/pkg/app.css">
<link rel="modulepreload" href="/pkg/app.js">"##;
/// index.html content from `<!-- INJECT HEAD -->` up to `<!-- INJECT BODY -->`
pub const HTML_MIDDLE: &str = r##" </head>
<body>"##;
/// index.html content from `<!-- INJECT BODY -->` until the end
pub const HTML_END: &str = r##" </body>
</html>"##;

View File

@@ -1,95 +0,0 @@
mod generated;
use crate::app::*;
use actix_files::Files;
use actix_web::*;
use futures::StreamExt;
use generated::{HTML_END, HTML_MIDDLE, HTML_START};
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
#[derive(Copy, Clone, Debug)]
struct ActixIntegration {
path: ReadSignal<String>,
}
impl History for ActixIntegration {
fn location(&self, cx: leptos::Scope) -> ReadSignal<LocationChange> {
create_signal(
cx,
LocationChange {
value: self.path.get(),
replace: false,
scroll: true,
state: State(None),
},
)
.0
}
fn navigate(&self, _loc: &LocationChange) {}
}
// match every path — our router will handle actual dispatch
#[get("{tail:.*}")]
async fn render_app(req: HttpRequest) -> impl Responder {
let path = req.path();
let query = req.query_string();
let path = if query.is_empty() {
"http://leptos".to_string() + path
} else {
"http://leptos".to_string() + path + "?" + query
};
let app = move |cx| {
let integration = ActixIntegration {
path: create_signal(cx, path.clone()).0,
};
provide_context(cx, RouterIntegrationContext(std::rc::Rc::new(integration)));
view! { cx, <App /> }.into_view(cx)
};
HttpResponse::Ok().content_type("text/html").streaming(
futures::stream::once(async { HTML_START.to_string() })
.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(futures::stream::once(async { HTML_END.to_string() }))
.map(|html| Ok(web::Bytes::from(html)) as Result<web::Bytes>),
)
}
pub async fn run() -> std::io::Result<()> {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse::<u16>()
.unwrap();
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
log::info!("serving at {host}:{port}");
HttpServer::new(|| {
App::new()
.service(
web::scope("/pkg")
.service(Files::new("", "target/site/pkg"))
.wrap(middleware::Compress::default()),
)
.service(render_app)
})
.bind((host, port))?
.run()
.await
}

View File

@@ -1,2 +0,0 @@
/** Imports your Tailwind output */
@import './output.scss';

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

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

@@ -1,10 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: {
files: ["*.html", "./src/**/*.rs"],
},
theme: {
extend: {},
},
plugins: [],
}

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

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 henrik
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

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)
}
};
@@ -259,61 +201,26 @@ where IV: IntoView
<head>
<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>
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().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();
@@ -314,9 +228,7 @@ where IV: IntoView
<head>
<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>
<script type="module">import init, {{ hydrate }} from '{pkg_path}.js'; init().then(hydrate);</script>
{leptos_autoreload}
"#
);
@@ -324,7 +236,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 +246,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 +275,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\"")
}
_ => {}
}
}

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