mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-28 14:52:35 -05:00
Compare commits
128 Commits
leptos_dom
...
component-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4360a73392 | ||
|
|
50b0fe157a | ||
|
|
64a5d75ec4 | ||
|
|
baf3cc8712 | ||
|
|
23777ad67b | ||
|
|
08be1ba622 | ||
|
|
605398bcea | ||
|
|
aca2c131d4 | ||
|
|
9d950b97ff | ||
|
|
f6a299ae3c | ||
|
|
1ba602ec47 | ||
|
|
1f3dde5b4a | ||
|
|
a65cd67db3 | ||
|
|
bacd99260b | ||
|
|
2b726f1a88 | ||
|
|
5c45538e9f | ||
|
|
7f696a9ac4 | ||
|
|
bcd6e671f7 | ||
|
|
7a72f127de | ||
|
|
2ff5ec21c8 | ||
|
|
a1f94b609f | ||
|
|
da5034da33 | ||
|
|
0c509970b5 | ||
|
|
d894c4dcf9 | ||
|
|
dc15184781 | ||
|
|
3200068ab3 | ||
|
|
0a9da8d55e | ||
|
|
52ad546710 | ||
|
|
f88d2fa56a | ||
|
|
f63cb02277 | ||
|
|
4b363f9b33 | ||
|
|
7b376b6d3a | ||
|
|
8fbb4abc76 | ||
|
|
d0ff64daaa | ||
|
|
bb97234817 | ||
|
|
19698d86b6 | ||
|
|
21ef96806f | ||
|
|
70e18d2aeb | ||
|
|
5152703f0c | ||
|
|
3d54055573 | ||
|
|
a5b99a3e40 | ||
|
|
101e65b724 | ||
|
|
a3f91604b9 | ||
|
|
f457d8f319 | ||
|
|
58abe55d7b | ||
|
|
634ac17095 | ||
|
|
79faad4aac | ||
|
|
cedc68c341 | ||
|
|
8ec772a129 | ||
|
|
8d671866a3 | ||
|
|
2edc5b3b8b | ||
|
|
be96a230ee | ||
|
|
0f8930b6f2 | ||
|
|
2b5c4abac5 | ||
|
|
db8c393f49 | ||
|
|
f18a7b35f2 | ||
|
|
a2c5855362 | ||
|
|
644d097cb6 | ||
|
|
9c0be9e317 | ||
|
|
5faa2efa2d | ||
|
|
c5a1e9a447 | ||
|
|
e88e131ec3 | ||
|
|
80df7a0dac | ||
|
|
493f05fda1 | ||
|
|
4578622b6f | ||
|
|
c7dd6200e8 | ||
|
|
6e20f31df1 | ||
|
|
5f58db40f0 | ||
|
|
321e11e97a | ||
|
|
c472a1c5ef | ||
|
|
1180eeeadb | ||
|
|
2348bbc5cc | ||
|
|
ee41ea8b1d | ||
|
|
a0ea3cfd7c | ||
|
|
edb0f8c848 | ||
|
|
2b71c07fa9 | ||
|
|
a109e3d51c | ||
|
|
40a842ff1d | ||
|
|
17baec46b7 | ||
|
|
fe5c9c6f0d | ||
|
|
6c22c47bbf | ||
|
|
2d88a113c4 | ||
|
|
b0dd759bcf | ||
|
|
507191e1a4 | ||
|
|
36de06f183 | ||
|
|
b54c0f14e8 | ||
|
|
41c03852e1 | ||
|
|
c3fb9396e1 | ||
|
|
3a9d16ad29 | ||
|
|
c0709b210d | ||
|
|
569fa9b1c6 | ||
|
|
ed24e47c1d | ||
|
|
fdd07aafb7 | ||
|
|
1a0168bf28 | ||
|
|
de524e21b1 | ||
|
|
dbe3daf16a | ||
|
|
3f6eeb319a | ||
|
|
db34565959 | ||
|
|
d5cd2b814e | ||
|
|
2b9ac037e3 | ||
|
|
66ecc2ac25 | ||
|
|
4093f4c2d8 | ||
|
|
a46e92bed8 | ||
|
|
611a1aeb28 | ||
|
|
994debea3f | ||
|
|
5399f54255 | ||
|
|
22668f7999 | ||
|
|
f7b1e732c7 | ||
|
|
93f68e022f | ||
|
|
2b4dc76d95 | ||
|
|
55f70367b5 | ||
|
|
a01b0cbbc6 | ||
|
|
6d329f33eb | ||
|
|
5a863ec411 | ||
|
|
4800600e4f | ||
|
|
a051b1e08c | ||
|
|
4a426be6fb | ||
|
|
d9ab70de0d | ||
|
|
aaac1d37ac | ||
|
|
498b5345d5 | ||
|
|
02a7af2c1e | ||
|
|
3ac92dc0fe | ||
|
|
440719071a | ||
|
|
b6d902a584 | ||
|
|
931e60347d | ||
|
|
2a547936d4 | ||
|
|
6b77b51fa0 | ||
|
|
6564b95342 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ blob.rs
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.leptos.kdl
|
||||
|
||||
31
Cargo.toml
31
Cargo.toml
@@ -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",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -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) {
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
@@ -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)(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
1
examples/counter-isomorphic/.gitignore
vendored
1
examples/counter-isomorphic/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.leptos.kdl
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/> }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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/> })
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
10
examples/counters-stable/README.md
Normal file
10
examples/counters-stable/README.md
Normal 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/)
|
||||
10
examples/counters/README.md
Normal file
10
examples/counters/README.md
Normal 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/)
|
||||
@@ -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());
|
||||
|
||||
@@ -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
10
examples/fetch/README.md
Normal 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/)
|
||||
@@ -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
8
examples/gtk/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Leptos in a GTK App
|
||||
|
||||
This example creates a basic GTK app that uses Leptos’s 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.
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 Leptos’s 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!
|
||||
|
||||
@@ -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/> }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
17
examples/parent-child/README.md
Normal file
17
examples/parent-child/README.md
Normal 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/)
|
||||
@@ -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! {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# Leptos Router Example
|
||||
|
||||
This example demonstrates how Leptos' router works
|
||||
This example demonstrates how Leptos’s 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/)
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
10
examples/tailwind/.gitignore
vendored
10
examples/tailwind/.gitignore
vendored
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 |
74
examples/tailwind/end2end/package-lock.json
generated
74
examples/tailwind/end2end/package-lock.json
generated
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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!");
|
||||
});
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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 /> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
@@ -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>"##;
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
/** Imports your Tailwind output */
|
||||
@import './output.scss';
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
files: ["*.html", "./src/**/*.rs"],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
48
examples/todo-app-cbor/Cargo.toml
Normal file
48
examples/todo-app-cbor/Cargo.toml
Normal 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"]]
|
||||
@@ -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
|
||||
22
examples/todo-app-cbor/README.md
Normal file
22
examples/todo-app-cbor/README.md
Normal 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!
|
||||
BIN
examples/todo-app-cbor/Todos.db
Normal file
BIN
examples/todo-app-cbor/Todos.db
Normal file
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
||||
22
examples/todo-app-cbor/src/lib.rs
Normal file
22
examples/todo-app-cbor/src/lib.rs
Normal 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/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/todo-app-cbor/src/main.rs
Normal file
49
examples/todo-app-cbor/src/main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
212
examples/todo-app-cbor/src/todo.rs
Normal file
212
examples/todo-app-cbor/src/todo.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
3
examples/todo-app-cbor/style.css
Normal file
3
examples/todo-app-cbor/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,3 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
|
||||
@@ -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/> }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
1
examples/todo-app-sqlite/.gitignore
vendored
1
examples/todo-app-sqlite/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.leptos.kdl
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
@@ -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/> }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
10
examples/todomvc/README.md
Normal file
10
examples/todomvc/README.md
Normal 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/)
|
||||
@@ -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()}
|
||||
|
||||
10
examples/view-tests/README.md
Normal file
10
examples/view-tests/README.md
Normal 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/)
|
||||
@@ -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>
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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>),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
12
leptos/build.rs
Normal 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
Reference in New Issue
Block a user