Compare commits

..

66 Commits

Author SHA1 Message Date
Greg Johnston
9f704f91fe Use local observers 2023-01-27 13:52:59 -05:00
Greg Johnston
4b0e212f1c Rc/RefCell => Arc/RwLock 2023-01-27 13:07:34 -05:00
Greg Johnston
8fa89cffef Make tests work on stable 2023-01-27 12:54:44 -05:00
Greg Johnston
18ceb4c4f8 Remove unnecessary log dependency 2023-01-27 12:54:11 -05:00
Greg Johnston
0d3e2baa9b remove unused base64 dependency 2023-01-27 12:52:08 -05:00
Greg Johnston
1563d237d0 Check uniqueness of server function names at registration time (#388)
* Check uniqueness of server function names at registration time, and stop leaking src file path in release mode

* Fix missing dev-dependency
2023-01-27 06:57:32 -05:00
Greg Johnston
b861f84e40 Fix a large number of small issues in docs (#386)
* Fix example links in docs

* Restore missing CSR READMEs

* Document need to enable features on `leptos_router` and `leptos_meta`

* Add "Is it production ready?" to FAQs

* Document which types are provided as contexts in server integrations

* Fix broken links and other issues in docs
2023-01-26 21:44:01 -05:00
Greg Johnston
62812af5b2 Allow unused cx in server fn arguments (#385)
* Suppress warning for unused `cx` in server function arguments
2023-01-26 21:43:39 -05:00
Greg Johnston
f300e7fd41 implements From<Signal<T>> for MaybeSignal<T> (#384) 2023-01-26 21:43:21 -05:00
Greg Johnston
44974fcf69 Replace site-address with site-addr in cargo-leptos example Cargo.toml files 2023-01-26 19:52:47 -05:00
Gentle
815c2e6dc2 leptos_axum::handle_server_fns was also duplicated (#383) 2023-01-26 15:53:31 -05:00
Roland Fredenhagen
2fc20d8312 added hgroup element (#379) 2023-01-26 15:05:58 -05:00
Gentle
679692e202 cloning is not needed here (#381) 2023-01-26 13:05:44 -05:00
Gentle
be1343fa88 refactor to eliminate duplicate code (#380) 2023-01-26 13:04:59 -05:00
Greg Johnston
fc7199f188 Fix context in outlets (#374)
* Add `Scope::parent()` to make access to parent `Scope` possible.

* Handle context properly in nested routes
2023-01-25 22:02:43 -05:00
Markus Kohlhase
154e42f3f4 Add a counter example that does not use macros (#373) 2023-01-25 21:10:16 -05:00
Ben Wishovich
4c24795ffd Make Errors Sync (#372) 2023-01-25 20:15:47 -05:00
IcosaHedron
f2e7b00d5a Fix CSR with Trunk on hackernews example, remove CSR option from isomorphic example (#369)
* Fix CSR with Trunk on hackernews example

* Update isomorphic example to remove CSR from Readme
2023-01-25 20:15:12 -05:00
Markus Kohlhase
0b36b68846 Replace urlencoding with percent-encoding (#365)
Motivation: `percent-encoding` is from the Servo team and part of the `url` crate.
2023-01-25 20:15:00 -05:00
Ben Wishovich
f24bad4bf2 Add <Show/> component to avoid rerendering of closures and tweak ErrorBoundary (#363)
Add once_cell to leptos, and add Show component! Modify ErrorBoundary to
take a closure that implements IntoView, not View
2023-01-24 10:58:25 -05:00
Greg Johnston
a2ea1d8483 Reorganize snake-case #[component] docs and please clippy (#362) 2023-01-23 11:14:04 -05:00
Ben Wishovich
9b0fb63632 Add methods to take Actix/Axum Extractors/Route Info/Stuff and pass it to Leptos (#359) 2023-01-23 07:28:05 -05:00
Greg Johnston
2febaf6b99 Merge pull request #358 from martinfrances107/bump_base64
leptos_reactive base64 bump version to 0.21.
2023-01-23 07:27:01 -05:00
Greg Johnston
6c8e8e9ce7 Merge pull request #353 from martinfrances107/redundant_clone
Clippy fixes: redundant clone and .to_string() issues.
2023-01-23 07:26:50 -05:00
Martin
7aa0181192 Removed unused variables. 2023-01-23 09:46:28 +00:00
Martin
8496bd59ce leptos_reactive base64 bump version to 0.21. 2023-01-22 22:15:54 +00:00
Greg Johnston
fd6e63796e Merge pull request #354 from jclmnop/feat/allow-snake-case-components
Allow snake case components
2023-01-22 16:46:47 -05:00
jclmnop
39cddfc82d update docs for component macro 2023-01-22 17:13:24 +00:00
jclmnop
d1333a3402 modify component attribute macro to allow snake_case fn names 2023-01-22 14:04:36 +00:00
Martin
7f9919e2d5 Clippy fixes: redundant clone, .to_string() issues. 2023-01-22 14:03:15 +00:00
Greg Johnston
fc2d6ef19d Merge pull request #343 from killertux/fix/fix-query-params-parser 2023-01-21 17:23:39 -05:00
Greg Johnston
a5531b1a7c Merge pull request #338 from benwis/error-handling
ErrorBoundary Component
2023-01-21 16:03:48 -05:00
benwis
81ab77e8ea One more time! 2023-01-21 11:54:55 -08:00
benwis
23bd399239 I did, I did break it 2023-01-21 11:25:36 -08:00
benwis
3e04318082 Remove extra 2023-01-21 10:58:47 -08:00
benwis
2d88524354 Wrap cfg_if to prevent on_cleanup from panicing on the server 2023-01-21 10:56:38 -08:00
Greg Johnston
ecb784e422 Merge pull request #352 from leptos-rs/gbj-patch-1
Update html.rs
2023-01-21 13:07:07 -05:00
Greg Johnston
69e02bfce2 Update html.rs
Yikes! Fix broken format string.
2023-01-21 13:07:00 -05:00
Greg Johnston
a75abb9e04 Merge pull request #351 from leptos-rs/view-styling
Add support for `class = ...`, in `view` macro to support scoped styling
2023-01-21 12:56:21 -05:00
Greg Johnston
bf1ef1b7c2 Fix missing {} after cleaning up unnecessary formats 2023-01-21 11:42:52 -05:00
Greg Johnston
7fb7bb90f8 Merge pull request #350 from leptos-rs/fix-script
Add SVG `<script>`, `<style>`, and `<title>` to set of ambiguous elements
2023-01-21 09:52:53 -05:00
Greg Johnston
a22a693de7 Add support for class = ..., in view macro to support scoped styling solutions 2023-01-21 09:52:05 -05:00
Clemente
cbb1e4c9d2 Update docs 2023-01-21 11:19:28 -03:00
Clemente
dbccf525ac Added some tests 2023-01-21 11:17:25 -03:00
Greg Johnston
ed6d6ae4b0 Add node_ref to docs 2023-01-21 07:26:06 -05:00
Greg Johnston
89ee88d75e Add SVG <script>, <style>, and <title> to set of ambiguous elements — closes #349 2023-01-21 07:23:32 -05:00
benwis
9ea604f516 Merge branch 'main' into error-handling 2023-01-20 15:53:18 -08:00
benwis
b5ab7b107a Test of SSR/Hydration of ErrorBoundary 2023-01-20 15:52:43 -08:00
Clemente
18eecd9606 Use URLSearchParams to handle client side query param logic 2023-01-20 18:11:49 -03:00
Greg Johnston
a49dfd3f8e Merge pull request #344 from leptos-rs/view-ssr
Reenable optimizations for SSR using the `view!` macro
2023-01-20 15:14:06 -05:00
Greg Johnston
7075f58451 Merge pull request #347 from imalexlab/doc/add-port-leptos-watch
doc: add link for leptos watch
2023-01-20 14:29:19 -05:00
Kompreni
bcabdddce5 doc: add link for leptos watch 2023-01-20 20:14:09 +01:00
Greg Johnston
726393c446 Fix SSR tests 2023-01-20 13:45:23 -05:00
Greg Johnston
3d769c9f21 Clean up some rendering issues and the panic when cleaning up 2023-01-20 13:14:09 -05:00
Greg Johnston
9ac0f0a579 Fix todomvc 2023-01-20 12:18:31 -05:00
Greg Johnston
a964e89d1a cargo fmt 2023-01-20 10:08:13 -05:00
Greg Johnston
285092a467 Reenable SSR benchmarks 2023-01-20 10:00:16 -05:00
Greg Johnston
c1c74ead0f Get view-macro SSR optimization working 2023-01-20 09:47:16 -05:00
Clemente
e4c9109278 Fix query params behaviour difference between SSR and Hydrate 2023-01-20 09:13:18 -03:00
benwis
64add54de6 Add example error template and give the ability to access error info
inside it
2023-01-19 14:57:34 -08:00
benwis
ac343427e7 Remember all the files 2023-01-18 23:21:08 -08:00
benwis
452e397048 Made todo_app_sqlite_axum throw an error 2023-01-18 16:47:22 -08:00
benwis
ad3ac5ad3c Remove silly thing 2023-01-18 16:03:53 -08:00
benwis
db63eda2f5 Push gbj's updates 2023-01-18 16:02:06 -08:00
benwis
6cbdc57f7a Add working impl of ErrorBoundary 2023-01-18 15:49:42 -08:00
benwis
3215e44c9a Create example of file_and_error handler for Axum, and create <ErrorBoundary/>
for leptos
2023-01-18 12:59:15 -08:00
89 changed files with 1746 additions and 706 deletions

1
.gitignore vendored
View File

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

View File

@@ -80,7 +80,7 @@ If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
youll just call `.get()`, `.set()`, or `.update()` manually. Check out the
[`counters_stable` example](https://github.com/leptos-rs/leptos/blob/main/examples/counters_stable/src/main.rs)
for examples of the correct API.
@@ -95,8 +95,28 @@ cd [your project name]
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
## FAQs
### Is it production ready?
People usually mean one of three things by this question.
1. **Are the APIs stable?** i.e., will I have to rewrite my whole app from Leptos 0.1 to 0.2 to 0.3 to 0.4, or can I write it now and benefit from new features and updates as new versions come?
With 0.1 the APIs are basically settled. Were adding new features, but were very happy with where the type system and patterns have landed. I would not expect major breaking changes to your code to adapt to, for example, a 0.2.0 release.
2. **Are there bugs?**
Yes, Im sure there are. You can see from the state of our issue tracker over time that there arent that _many_ bugs and theyre usually resolved pretty quickly. But for sure, there may be moments where you encounter something that requires a fix at the framework level, which may not be immediately resolved.
3. **Am I a consumer or a contributor?**
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) dont have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
There are several people in this community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if youre willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
### Can I use this for native GUI?
Sure! Obviously the `view` macro is for generating DOM nodes but you can use the reactive system to drive native any GUI toolkit that uses the same kind of object-oriented, event-callback-based framework as the DOM pretty easily. The principles are the same:

View File

@@ -2,9 +2,9 @@ use test::Bencher;
#[bench]
fn leptos_ssr_bench(b: &mut Bencher) {
b.iter(|| {
b.iter(|| {
use leptos::*;
HydrationCtx::reset_id();
_ = create_scope(create_runtime(), |cx| {
#[component]
fn Counter(cx: Scope, initial: i32) -> impl IntoView {
@@ -32,18 +32,17 @@ fn leptos_ssr_bench(b: &mut Bencher) {
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 id=\"_0-1\"><h1 id=\"_0-2\">Welcome to our benchmark page.</h1><p id=\"_0-3\">Here's some introductory text.</p><div id=\"_0-3-1\"><button id=\"_0-3-2\">-1</button><span id=\"_0-3-3\">Value: <!>1<!--hk=_0-3-4-->!</span><button id=\"_0-3-5\">+1</button></div><!--hk=_0-3-0--><div id=\"_0-3-5-1\"><button id=\"_0-3-5-2\">-1</button><span id=\"_0-3-5-3\">Value: <!>2<!--hk=_0-3-5-4-->!</span><button id=\"_0-3-5-5\">+1</button></div><!--hk=_0-3-5-0--><div id=\"_0-3-5-5-1\"><button id=\"_0-3-5-5-2\">-1</button><span id=\"_0-3-5-5-3\">Value: <!>3<!--hk=_0-3-5-5-4-->!</span><button id=\"_0-3-5-5-5\">+1</button></div><!--hk=_0-3-5-5-0--></main>" );
});
});
}
/*
#[bench]
fn tera_ssr_bench(b: &mut Bencher) {
use tera::*;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use tera::*;
static TEMPLATE: &str = r#"<main>
static TEMPLATE: &str = r#"<main>
<h1>Welcome to our benchmark page.</h1>
<p>Here's some introductory text.</p>
{% for counter in counters %}
@@ -55,37 +54,40 @@ fn tera_ssr_bench(b: &mut Bencher) {
{% endfor %}
</main>"#;
lazy_static::lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
};
}
lazy_static::lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![("template.html", TEMPLATE)]).unwrap();
tera
};
}
#[derive(Serialize, Deserialize)]
struct Counter {
value: i32
}
#[derive(Serialize, Deserialize)]
struct Counter {
value: i32,
}
b.iter(|| {
let mut ctx = Context::new();
ctx.insert("counters", &vec![
Counter { value: 0 },
Counter { value: 1},
Counter { value: 2 }
]);
b.iter(|| {
let mut ctx = Context::new();
ctx.insert(
"counters",
&vec![
Counter { value: 0 },
Counter { value: 1 },
Counter { value: 2 },
],
);
let _ = TERA.render("template.html", &ctx).unwrap();
});
let _ = TERA.render("template.html", &ctx).unwrap();
});
}
#[bench]
fn sycamore_ssr_bench(b: &mut Bencher) {
use sycamore::*;
use sycamore::prelude::*;
use sycamore::prelude::*;
use sycamore::*;
b.iter(|| {
b.iter(|| {
_ = create_scope(|cx| {
#[derive(Prop)]
struct CounterProps {
@@ -139,10 +141,10 @@ fn sycamore_ssr_bench(b: &mut Bencher) {
#[bench]
fn yew_ssr_bench(b: &mut Bencher) {
use yew::prelude::*;
use yew::ServerRenderer;
use yew::prelude::*;
use yew::ServerRenderer;
b.iter(|| {
b.iter(|| {
#[derive(Properties, PartialEq, Eq, Debug)]
struct CounterProps {
initial: i32
@@ -194,4 +196,3 @@ fn yew_ssr_bench(b: &mut Bencher) {
});
});
}
*/

View File

@@ -8,171 +8,165 @@ 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);
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 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
pub fn TodoMVC(cx: Scope, todos: Todos) -> impl IntoView {
let mut next_id = todos
.0
.iter()
.filter(|todo| !todo.completed.get())
.cloned()
.collect(),
Mode::Completed => todos
.0
.iter()
.filter(|todo| todo.completed.get())
.cloned()
.collect(),
})
});
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
// 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");
}
}
});
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
view! { cx,
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 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">
@@ -228,100 +222,100 @@ pub fn TodoMVC(cx: Scope,todos: Todos) -> impl IntoView {
#[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);
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());
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)()}
/>
<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>
}
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)()}
/>
<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>
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Active,
Completed,
All,
Active,
Completed,
All,
}
impl Default for Mode {
fn default() -> Self {
Mode::All
}
fn default() -> Self {
Mode::All
}
}
pub fn route(hash: &str) -> Mode {
match hash {
"/active" => Mode::Active,
"/completed" => Mode::Completed,
_ => Mode::All,
}
match hash {
"/active" => Mode::Active,
"/completed" => Mode::Completed,
_ => Mode::All,
}
}
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
pub id: usize,
pub title: String,
pub completed: bool,
pub id: usize,
pub title: String,
pub completed: bool,
}
impl TodoSerialized {
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
pub fn into_todo(self, cx: Scope) -> Todo {
Todo::new_with_completed(cx, self.id, self.title, self.completed)
}
}
impl From<&Todo> for TodoSerialized {
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
fn from(todo: &Todo) -> Self {
Self {
id: todo.id,
title: todo.title.get(),
completed: (todo.completed)(),
}
}
}
}

View File

@@ -14,7 +14,9 @@ fn leptos_todomvc_ssr(b: &mut Bencher) {
let rendered = view! {
cx,
<TodoMVC todos=Todos::new(cx)/>
}.into_view(cx).render_to_string(cx);
}
.into_view(cx)
.render_to_string(cx);
assert!(rendered.len() > 1);
});
@@ -55,7 +57,7 @@ fn yew_todomvc_ssr(b: &mut Bencher) {
});
});
}
/*
#[bench]
fn leptos_todomvc_ssr_with_1000(b: &mut Bencher) {
b.iter(|| {
@@ -107,3 +109,4 @@ fn yew_todomvc_ssr_with_1000(b: &mut Bencher) {
});
});
}
*/

View File

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

View File

@@ -47,6 +47,7 @@ skip_feature_sets = [["ssr", "hydrate"]]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "counter_isomorphic"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
# When NOT using cargo-leptos this must be updated to "." or the counters will not work. The above warning still applies if you do switch to cargo-leptos later.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
@@ -56,7 +57,7 @@ site-pkg-dir = "pkg"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.

View File

@@ -3,8 +3,8 @@
This example demonstrates how to use a function isomorphically, to run a server side function from the browser and receive a result.
## 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. Make sure you have trunk installed with `cargo install trunk`.
For this example the server must store the counter state since it can be modified by many users.
This means it is not possible to produce a working CSR-only version as a non-static server is required.
## Server Side Rendering with cargo-leptos
cargo-leptos is now the easiest and most featureful way to build server side rendered apps with hydration. It provides automatic recompilation of client and server code, wasm optimisation, CSS minification, and more! Check out more about it [here](https://github.com/akesson/cargo-leptos)
@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
@@ -25,7 +28,7 @@ cargo leptos build --release
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. For examples with CSS you also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack

View File

@@ -34,6 +34,7 @@ cfg_if! {
crate::counters::register_server_functions();
// Setting this to None means we'll be using cargo-leptos and its env vars.
// when not using cargo-leptos None must be replaced with Some("Cargo.toml")
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_address.clone();
@@ -56,14 +57,11 @@ cfg_if! {
}
}
// client-only stuff for Trunk
// client-only main for Trunk
else {
use counter_isomorphic::counters::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <Counter/> });
// isomorphic counters cannot work in a Client-Side-Rendered only
// app as a server is required to maintain state
}
}
}

View File

@@ -0,0 +1,13 @@
[package]
name = "counter_without_macros"
version = "0.1.0"
edition = "2021"
[dependencies]
leptos = { path = "../../leptos", features = ["stable"] }
console_log = "0.2"
log = "0.4"
console_error_panic_hook = "0.1.7"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -0,0 +1,5 @@
# Leptos Counter Example
This example is the same like the `counter` but it's written without using macros and can be build with stable Rust.
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.

View File

@@ -2,6 +2,7 @@
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,44 @@
use leptos::{ev, *};
pub struct Props {
/// The starting value for the counter
pub initial_value: i32,
/// The change that should be applied each time the button is clicked.
pub step: i32,
}
/// A simple counter view.
pub fn view(cx: Scope, props: Props) -> impl IntoView {
let Props {
initial_value,
step,
} = props;
let (value, set_value) = create_signal(cx, initial_value);
div(cx)
.child((
cx,
button(cx)
.on(ev::click, move |_| set_value.update(|value| *value = 0))
.child((cx, "Clear")),
))
.child((
cx,
button(cx)
.on(ev::click, move |_| set_value.update(|value| *value -= step))
.child((cx, "-1")),
))
.child((
cx,
span(cx)
.child((cx, "Value: "))
.child((cx, move || value.get()))
.child((cx, "!")),
))
.child((
cx,
button(cx)
.on(ev::click, move |_| set_value.update(|value| *value += step))
.child((cx, "+1")),
))
}

View File

@@ -0,0 +1,16 @@
use counter_without_macros as counter;
use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
counter::view(
cx,
counter::Props {
initial_value: 0,
step: 1,
},
)
})
}

View File

@@ -0,0 +1,58 @@
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counter_without_macros as counter;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| {
counter::view(
cx,
counter::Props {
initial_value: 0,
step: 1,
},
)
});
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let dec = clear
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let text = dec
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
let inc = text
.next_sibling()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
inc.click();
inc.click();
assert_eq!(text.text_content(), Some("Value: 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()));
}

View File

@@ -0,0 +1,9 @@
# Leptos Counters Example
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.
## Client Side Rendering
To run it as a client-side app, you can issue `trunk serve --open` in the root. This will build the entire app into one CSR bundle.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -0,0 +1,9 @@
# 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/)

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

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

View File

@@ -58,7 +58,7 @@ style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.

View File

@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="rust" data-wasm-opt="z" data-cargo-features="csr"/>
<link data-trunk rel="css" href="./style.css"/>
</head>
<body></body>

View File

@@ -46,7 +46,11 @@ cfg_if! {
}
} else {
fn main() {
// no client-side main function
use hackernews::{App, AppProps};
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| view! { cx, <App/> })
}
}
}

View File

@@ -66,7 +66,7 @@ style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.

View File

@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release

View File

@@ -0,0 +1,28 @@
use leptos::Errors;
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {cx,
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.get().0.into_iter()}
// a unique key for each item as a reference
key=|error| error.0.clone()
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view(cx)
}

View File

@@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::LeptosOptions;
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await?;
let res = get_static_file(uri.clone(), &root).await.unwrap();
match res.status() {
StatusCode::OK => Ok(res),
_ => Err((res.status(), "File Not Found".to_string()))
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
handler(req).await.into_response()
}
}
@@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
)),
}
}
}
}

View File

@@ -11,7 +11,6 @@ if #[cfg(feature = "ssr")] {
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`
@@ -27,7 +26,6 @@ if #[cfg(feature = "ssr")] {
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()))
@@ -41,7 +39,6 @@ if #[cfg(feature = "ssr")] {
// `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)),

View File

@@ -3,7 +3,8 @@ use leptos::{component, view, IntoView, Scope};
use leptos_meta::*;
use leptos_router::*;
mod api;
pub mod file;
pub mod error_template;
pub mod fallback;
pub mod handlers;
mod routes;
use routes::nav::*;

View File

@@ -11,7 +11,7 @@ if #[cfg(feature = "ssr")] {
};
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use hackernews_axum::file::file_handler;
use hackernews_axum::fallback::file_and_error_handler;
#[tokio::main]
async fn main() {
@@ -26,9 +26,9 @@ if #[cfg(feature = "ssr")] {
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(file_handler))
.route("/favicon.ico", get(file_and_error_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> } )
.fallback(file_handler)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ style-file = "style/output.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.

View File

@@ -16,6 +16,8 @@ and
in this directory.
Open browser on [http://localhost:3000/](http://localhost:3000/)
You can begin editing your app at `src/app.rs`.
## Installing Tailwind

View File

@@ -60,7 +60,7 @@ style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.

View File

@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release

View File

@@ -18,6 +18,7 @@ leptos = { path = "../../../leptos/leptos", default-features = false, features =
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 }
leptos_reactive = { path = "../../../leptos/leptos_reactive", default-features = false }
log = "0.4.17"
simple_logger = "4.0.0"
serde = { version = "1.0.148", features = ["derive"] }
@@ -38,17 +39,7 @@ sqlx = { version = "0.6.2", features = [
default = ["csr"]
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:sqlx",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"leptos_axum",
]
ssr = ["dep:axum", "dep:tower", "dep:tower-http", "dep:tokio", "dep:sqlx", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "leptos_axum"]
[package.metadata.cargo-all-features]
denylist = [
@@ -74,7 +65,7 @@ style-file = "./style.css"
# [Optional] Files in the asset-dir will be copied to the site-root directory
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-address = "127.0.0.1:3000"
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
@@ -103,4 +94,4 @@ lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
lib-default-features = false

View File

@@ -17,6 +17,9 @@ cargo install --locked cargo-leptos
```bash
cargo leptos watch
```
Open browser on [http://localhost:3000/](http://localhost:3000/)
3. When ready to deploy, run
```bash
cargo leptos build --release
@@ -25,7 +28,7 @@ cargo leptos build --release
## Server Side Rendering without cargo-leptos
To run it as a server side app with hydration, you'll need to have wasm-pack installed.
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"pkg"`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time
0. Edit the `[package.metadata.leptos]` section and set `site-root` to `"."`. You'll also want to change the path of the `<StyleSheet / >` component in the root component to point towards the CSS file in the root. This tells leptos that the WASM/JS files generated by wasm-pack are available at `./pkg` and that the CSS files are no longer processed by cargo-leptos. Building to alternative folders is not supported at this time. You'll also want to edit the call to `get_configuration()` to pass in `Some(Cargo.toml)`, so that Leptos will read the settings instead of cargo-leptos. If you do so, your file/folder names cannot include dashes.
1. Install wasm-pack
```bash
cargo install wasm-pack

View File

@@ -0,0 +1,28 @@
use leptos::Errors;
use leptos::{view, For, ForProps, IntoView, RwSignal, Scope, View};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
pub fn error_template(cx: Scope, errors: Option<RwSignal<Errors>>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
view! {cx,
<h1>"Errors"</h1>
<For
// a function that returns the items we're iterating over; a signal is fine
each= move || {errors.get().0.into_iter()}
// a unique key for each item as a reference
key=|error| error.0.clone()
// renders each item to a view
view= move |error| {
let error_string = error.1.to_string();
view! {
cx,
<p>"Error: " {error_string}</p>
}
}
/>
}
.into_view(cx)
}

View File

@@ -5,22 +5,26 @@ if #[cfg(feature = "ssr")] {
use axum::{
body::{boxed, Body, BoxBody},
extract::Extension,
response::IntoResponse,
http::{Request, Response, StatusCode, Uri},
};
use axum::response::Response as AxumResponse;
use tower::ServiceExt;
use tower_http::services::ServeDir;
use std::sync::Arc;
use leptos::LeptosOptions;
pub async fn file_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>) -> Result<Response<BoxBody>, (StatusCode, String)> {
use leptos::{LeptosOptions};
use crate::error_template::error_template;
pub async fn file_and_error_handler(uri: Uri, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> AxumResponse {
let options = &*options;
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await?;
let res = get_static_file(uri.clone(), &root).await.unwrap();
match res.status() {
StatusCode::OK => Ok(res),
_ => Err((res.status(), "File Not Found".to_string()))
if res.status() == StatusCode::OK {
res.into_response()
} else{
let handler = leptos_axum::render_app_to_stream(options.to_owned(), |cx| error_template(cx, None));
handler(req).await.into_response()
}
}
@@ -37,5 +41,7 @@ if #[cfg(feature = "ssr")] {
)),
}
}
}
}

View File

@@ -1,6 +1,7 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod file;
pub mod error_template;
pub mod fallback;
pub mod todo;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.

View File

@@ -4,15 +4,31 @@ use leptos::*;
cfg_if! {
if #[cfg(feature = "ssr")] {
use axum::{
routing::post,
extract::Extension,
routing::{post, get},
extract::{Extension, Path},
http::Request,
body::StreamBody,
response::{IntoResponse, Response},
Router,
};
use axum::body::Body as AxumBody;
use crate::todo::*;
use todo_app_sqlite_axum::*;
use crate::file::file_handler;
use crate::fallback::file_and_error_handler;
use leptos_axum::{generate_route_list, LeptosRoutes};
use std::sync::Arc;
use leptos_reactive::run_scope;
//Define a handler to test extractor with state
async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<AxumBody>) -> Response{
let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
move |cx| {
provide_context(cx, id.clone());
},
|cx| view! { cx, <TodoApp/> }
);
handler(req).await.into_response()
}
#[tokio::main]
async fn main() {
@@ -35,8 +51,9 @@ if #[cfg(feature = "ssr")] {
// build our application with a route
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.route("/special/:id", get(custom_handler))
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <TodoApp/> } )
.fallback(file_handler)
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));
// run our app with hyper

View File

@@ -107,6 +107,7 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[component]
pub fn TodoApp(cx: Scope) -> impl IntoView {
let id = use_context::<String>(cx);
provide_meta_context(cx);
view! {
cx,
@@ -120,7 +121,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
<Routes>
<Route path="" view=|cx| view! {
cx,
<Todos/>
<Todos/>
}/>
</Routes>
</main>

View File

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

View File

@@ -262,7 +262,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
// this will be filled by _ref=input below
let todo_input = NodeRef::new(cx);
let todo_input = NodeRef::<HtmlElement<Input>>::new(cx);
let save = move |value: &str| {
let value = value.trim();
@@ -293,7 +293,7 @@ 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();
}

View File

@@ -1,5 +1,11 @@
#![forbid(unsafe_code)]
//! Provides functions to easily integrate Leptos with Actix.
//!
//! For more details on how to use the integrations, see the
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
use actix_web::{
body::BoxBody,
dev::{ServiceFactory, ServiceRequest},
@@ -68,8 +74,8 @@ impl ResponseOptions {
}
/// Provides an easy way to redirect the user from within a server function. Mimicing the Remix `redirect()`,
/// it sets a StatusCode of 302 and a LOCATION header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead
/// it sets a [StatusCode] of 302 and a [LOCATION](header::LOCATION) header with the provided value.
/// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead.
pub async fn redirect(cx: leptos::Scope, path: &str) {
let response_options = use_context::<ResponseOptions>(cx).unwrap();
response_options.set_status(StatusCode::FOUND).await;
@@ -114,10 +120,39 @@ pub async fn redirect(cx: leptos::Scope, path: &str) {
/// }
/// # }
/// ```
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
pub fn handle_server_fns() -> Route {
handle_server_fns_with_context(|_cx| {})
}
/// An Actix [Route](actix_web::Route) that listens for a `POST` request with
/// Leptos server function arguments in the body, runs the server function if found,
/// and returns the resulting [HttpResponse].
///
/// This provides the [HttpRequest] to the server [Scope](leptos::Scope).
///
/// This can then be set up at an appropriate route in your application:
///
/// This version allows you to pass in a closure that adds additional route data to the
/// context, allowing you to pass in info about the route or user from Actix, or other info.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
pub fn handle_server_fns_with_context(
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> Route {
web::post().to(
|req: HttpRequest, params: web::Path<String>, body: web::Bytes| async move {
{
move |req: HttpRequest, params: web::Path<String>, body: web::Bytes| {
let additional_context = additional_context.clone();
async move {
let additional_context = additional_context.clone();
let path = params.into_inner();
let accept_header = req
.headers()
@@ -129,6 +164,9 @@ pub fn handle_server_fns() -> Route {
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
// Add additional info to the context of the server function
additional_context(cx);
let res_options = ResponseOptions::default();
// provide HttpRequest as context in server scope
@@ -248,16 +286,47 @@ pub fn handle_server_fns() -> Route {
/// }
/// # }
/// ```
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
render_app_to_stream_with_context(options, |_cx| {}, app_fn)
}
/// Returns an Actix [Route](actix_web::Route) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This function allows you to provide additional information to Leptos for your route.
/// It could be used to pass in Path Info, Connection Info, or anything your heart desires.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_app_to_stream_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + 'static,
) -> Route
where
IV: IntoView,
{
web::get().to(move |req: HttpRequest| {
let options = options.clone();
let app_fn = app_fn.clone();
let additional_context = additional_context.clone();
let res_options = ResponseOptions::default();
async move {
@@ -272,7 +341,7 @@ where
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options).await
stream_app(app, head, tail, res_options, additional_context).await
}
})
}
@@ -321,6 +390,13 @@ where
/// }
/// # }
/// ```
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [ResponseOptions]
/// - [HttpRequest](actix_web::HttpRequest)
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_preloaded_data_app<Data, Fut, IV>(
options: LeptosOptions,
data_fn: impl Fn(HttpRequest) -> Fut + Clone + 'static,
@@ -355,7 +431,7 @@ where
let (head, tail) = html_parts(&options);
stream_app(app, head, tail, res_options).await
stream_app(app, head, tail, res_options, |_cx| {}).await
}
})
}
@@ -385,13 +461,18 @@ async fn stream_app(
head: String,
tail: String,
res_options: ResponseOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
) -> HttpResponse<BoxBody> {
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 (stream, runtime, _) = render_to_stream_with_prefix_undisposed_with_context(
app,
move |cx| {
let head = use_context::<MetaContext>(cx)
.map(|meta| meta.dehydrate())
.unwrap_or_default();
format!("{head}</head><body>").into()
},
additional_context,
);
let mut stream = Box::pin(
futures::stream::once(async move { head.clone() })

View File

@@ -1,5 +1,11 @@
#![forbid(unsafe_code)]
//! Provides functions to easily integrate Leptos with Axum.
//!
//! For more details on how to use the integrations, see the
//! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples)
//! directory in the Leptos repository.
use axum::{
body::{Body, Bytes, Full, StreamBody},
extract::Path,
@@ -91,6 +97,8 @@ pub async fn redirect(cx: leptos::Scope, path: &str) {
.await;
}
/// Decomposes an HTTP request into its parts, allowing you to read its headers
/// and other data without consuming the body.
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
// provide request headers as context in server scope
let (parts, body) = req.into_parts();
@@ -107,8 +115,6 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
/// run the server function if found, and return the resulting [Response].
///
/// This provides an `Arc<[Request<Body>](axum::http::Request)>` [Scope](leptos::Scope).
///
/// This can then be set up at an appropriate route in your application:
///
/// ```
@@ -136,16 +142,53 @@ pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
/// ```
/// 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.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
pub async fn handle_server_fns(
Path(fn_name): Path<String>,
headers: HeaderMap,
req: Request<Body>,
) -> impl IntoResponse {
handle_server_fns_inner(fn_name, headers, |_| {}, req).await
}
/// An Axum handlers to listens for a request with Leptos server function arguments in the body,
/// run the server function if found, and return the resulting [Response].
///
/// This can then be set up at an appropriate route in your application:
///
/// This version allows you to pass in a closure to capture additional data from the layers above leptos
/// and store it in context. To use it, you'll need to define your own route, and a handler function
/// that takes in the data you'd like. See the [render_app_to_stream_with_context] docs for an example
/// of one that should work much like this one.
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
pub async fn handle_server_fns_with_context(
Path(fn_name): Path<String>,
headers: HeaderMap,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
handle_server_fns_inner(fn_name, headers, additional_context, req).await
}
async fn handle_server_fns_inner(
fn_name: String,
headers: HeaderMap,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
req: Request<Body>,
) -> impl IntoResponse {
// Axum Path extractor doesn't remove the first slash from the path, while Actix does
let fn_name: String = match fn_name.strip_prefix('/') {
Some(path) => path.to_string(),
None => fn_name,
};
let fn_name = fn_name
.strip_prefix('/')
.map(|fn_name| fn_name.to_string())
.unwrap_or(fn_name);
let (tx, rx) = futures::channel::oneshot::channel();
spawn_blocking({
@@ -158,6 +201,8 @@ pub async fn handle_server_fns(
let runtime = create_runtime();
let (cx, disposer) = raw_scope_and_disposer(runtime);
additional_context(cx);
let req_parts = generate_request_parts(req).await;
// Add this so we can get details about the Request
provide_context(cx, req_parts.clone());
@@ -166,7 +211,7 @@ pub async fn handle_server_fns(
match server_fn(cx, &req_parts.body).await {
Ok(serialized) => {
// If ResponseParts are set, add the headers and extension to the request
// If ResponseOptions are set, add the headers and status to the request
let res_options = use_context::<ResponseOptions>(cx);
// clean up the scope, which we only needed to run the server fn
@@ -299,6 +344,12 @@ pub type PinnedHtmlStream = Pin<Box<dyn Stream<Item = io::Result<Bytes>> + Send>
/// # }
/// ```
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_app_to_stream<IV>(
options: LeptosOptions,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
@@ -310,11 +361,53 @@ pub fn render_app_to_stream<IV>(
+ 'static
where
IV: IntoView,
{
render_app_to_stream_with_context(options, |_| {}, app_fn)
}
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
/// to route it using [leptos_router], serving an HTML stream of your application.
///
/// This version allows us to pass Axum State/Extension/Extractor or other infro from Axum or network
/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides
/// the data to leptos in a closure. An example is below
/// ```ignore
/// async fn custom_handler(Path(id): Path<String>, Extension(options): Extension<Arc<LeptosOptions>>, req: Request<Body>) -> Response{
/// let handler = leptos_axum::render_app_to_stream_with_context((*options).clone(),
/// move |cx| {
/// provide_context(cx, id.clone());
/// },
/// |cx| view! { cx, <TodoApp/> }
/// );
/// handler(req).await.into_response()
/// }
/// ```
/// Otherwise, this function is identical to [render_app_to_stream].
///
/// ## Provided Context Types
/// This function always provides context values including the following types:
/// - [RequestParts]
/// - [ResponseOptions]
/// - [MetaContext](leptos_meta::MetaContext)
/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext)
pub fn render_app_to_stream_with_context<IV>(
options: LeptosOptions,
additional_context: impl Fn(leptos::Scope) + 'static + Clone + Send,
app_fn: impl Fn(leptos::Scope) -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
) -> Pin<Box<dyn Future<Output = Response<StreamBody<PinnedHtmlStream>>> + Send + 'static>>
+ Clone
+ Send
+ 'static
where
IV: IntoView,
{
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
let default_res_options = ResponseOptions::default();
let res_options2 = default_res_options.clone();
let res_options3 = default_res_options.clone();
@@ -386,11 +479,13 @@ where
spawn_blocking({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on({
let app_fn = app_fn.clone();
let add_context = add_context.clone();
async move {
tokio::task::LocalSet::new()
.run_until(async {
@@ -413,7 +508,7 @@ where
};
let (bundle, runtime, scope) =
render_to_stream_with_prefix_undisposed(
render_to_stream_with_prefix_undisposed_with_context(
app,
|cx| {
let head = use_context::<MetaContext>(cx)
@@ -421,6 +516,7 @@ where
.unwrap_or_default();
format!("{head}</head><body>").into()
},
add_context,
);
let mut shell = Box::pin(bundle);
while let Some(fragment) = shell.next().await {
@@ -516,13 +612,8 @@ where
let routes = routes.0.read().await.to_owned();
// Axum's Router defines Root routes as "/" not ""
let routes: Vec<String> = routes
.iter()
.map(|s| {
if s.is_empty() {
return "/".to_string();
}
s.to_string()
})
.into_iter()
.map(|s| if s.is_empty() { "/".to_string() } else { s })
.collect();
if routes.is_empty() {

View File

@@ -17,36 +17,17 @@ leptos_server = { workspace = true }
leptos_config = { workspace = true }
tracing = "0.1"
typed-builder = "0.11"
once_cell = "1.17.0"
[dev-dependencies]
leptos = { path = ".", default-features = false }
[features]
default = ["csr", "serde"]
csr = [
"leptos_dom/web",
"leptos_macro/csr",
"leptos_reactive/csr",
"leptos_server/csr",
]
hydrate = [
"leptos_dom/web",
"leptos_macro/hydrate",
"leptos_reactive/hydrate",
"leptos_server/hydrate",
]
ssr = [
"leptos_dom/ssr",
"leptos_macro/ssr",
"leptos_reactive/ssr",
"leptos_server/ssr",
]
stable = [
"leptos_dom/stable",
"leptos_macro/stable",
"leptos_reactive/stable",
"leptos_server/stable",
]
csr = ["leptos_dom/web", "leptos_macro/csr", "leptos_reactive/csr", "leptos_server/csr"]
hydrate = ["leptos_dom/web", "leptos_macro/hydrate", "leptos_reactive/hydrate", "leptos_server/hydrate"]
ssr = ["leptos_dom/ssr", "leptos_macro/ssr", "leptos_reactive/ssr", "leptos_server/ssr"]
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"]

View File

@@ -0,0 +1,28 @@
use leptos_dom::{Errors, Fragment, IntoView};
use leptos_macro::component;
use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
#[component(transparent)]
pub fn ErrorBoundary<F, IV>(
cx: Scope,
/// The components inside the tag which will get rendered
children: Box<dyn FnOnce(Scope) -> Fragment>,
/// A fallback that will be shown if an error occurs.
fallback: F,
) -> impl IntoView
where
F: Fn(Scope, Option<RwSignal<Errors>>) -> IV + 'static,
IV: IntoView,
{
let errors: RwSignal<Errors> = create_rw_signal(cx, Errors::default());
provide_context(cx, errors);
// Run children so that they render and execute resources
let children = children(cx);
move || match errors.get().0.is_empty() {
true => children.clone().into_view(cx),
false => fallback(cx, Some(errors)).into_view(cx),
}
}

View File

@@ -8,9 +8,8 @@
//! or mutating data via async requests to the server
//! - multi-page apps (MPAs) rendered on the server, managing navigation, data, and mutations via
//! web-standard `<a>` and `<form>` tags
//! - progressively-enhanced multi-page apps ([PEMPAs](https://www.epicweb.dev/the-webs-next-transition)?)
//! that are rendered on the server and then hydrated on the client, enhancing your `<a>` and `<form>`
//! navigations and mutations seamlessly when WASM is available.
//! - progressively-enhanced single-page apps that are rendered on the server and then hydrated on the client,
//! enhancing your `<a>` and `<form>` navigations and mutations seamlessly when WASM is available.
//!
//! And you can do all three of these **using the same Leptos code.**
//!
@@ -24,12 +23,17 @@
//!
//! # Learning by Example
//!
//! These docs are a work in progress. If you want to see what Leptos is capable of, check out
//! If you want to see what Leptos is capable of, check out
//! the [examples](https://github.com/leptos-rs/leptos/tree/main/examples):
//! - [`counter`](https://github.com/leptos-rs/leptos/tree/main/examples/counter) is the classic
//! counter example, showing the basics of client-side rendering and reactive DOM updates
//! - [`counter_without_macros`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_without_macros)
//! adapts the counter example to use the builder pattern for the UI and avoids other macros, instead showing
//! the code that Leptos generates.
//! - [`counters`](https://github.com/leptos-rs/leptos/tree/main/examples/counters) introduces parent-child
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
//! - [`counters_stable`](https://github.com/leptos-rs/leptos/tree/main/examples/counters_stable) adapts the `counters` example
//! to show how to use Leptos with `stable` Rust.
//! - [`parent_child`](https://github.com/leptos-rs/leptos/tree/main/examples/parent_child) shows four different
//! ways a parent component can communicate with a child, including passing a closure, context, and more
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) implements the classic to-do
@@ -42,15 +46,23 @@
//! HTTP request within your reactive code.
//! - [`router`](https://github.com/leptos-rs/leptos/tree/main/examples/router) shows how to use Leptoss nested router
//! to enable client-side navigation and route-specific, reactive data loading.
//! - [`counter_isomorphic`](https://github.com/leptos-rs/leptos/tree/main/examples/counter_isomorphic) shows
//! different methods of interaction with a stateful server, including server functions, server actions, forms,
//! and server-sent events (SSE).
//! - [`todomvc`](https://github.com/leptos-rs/leptos/tree/main/examples/todomvc) shows the basics of building an
//! isomorphic web app. Both the server and the client import the same app code from the `todomvc` example.
//! The server renders the app directly to an HTML string, and the client hydrates that HTML to make it interactive.
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews) pulls everything together.
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run.
//! - [`hackernews`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews)
//! and [`hackernews_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/hackernews_axum)
//! integrate calls to a real external REST API, routing, server-side rendering and hydration to create
//! a fully-functional that works as intended even before WASM has loaded and begun to run.
//! - [`todo_app_sqlite`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite) and
//! [`todo_app_sqlite_axum`](https://github.com/leptos-rs/leptos/tree/main/examples/todo_app_sqlite_axum)
//! show how to build a full-stack app using server functions and database connections.
//! - [`tailwind`](https://github.com/leptos-rs/leptos/tree/main/examples/tailwind) shows how to integrate
//! TailwindCSS with `cargo-leptos`.
//!
//! (The SPA examples can be run using `trunk serve`. For information about Trunk,
//! [see here]((https://trunkrs.dev/)).)
//! Details on how to run each example can be found in its README.
//!
//! # Quick Links
//!
@@ -138,9 +150,12 @@ pub use leptos_server::*;
pub use tracing;
pub use typed_builder;
mod error_boundary;
pub use error_boundary::*;
mod for_loop;
mod show;
pub use for_loop::*;
pub use show::*;
mod suspense;
pub use suspense::*;
mod transition;

32
leptos/src/show.rs Normal file
View File

@@ -0,0 +1,32 @@
use leptos::component;
use leptos_dom::{Fragment, IntoView};
use leptos_reactive::Scope;
use once_cell::sync::Lazy;
/// A component that will show it's children when the passed in closure is True, and show the fallback
/// when the closure is false
#[component]
pub fn Show<F, W, IV>(
/// The scope the component is running in
cx: Scope,
/// The components Show wraps
children: Box<dyn FnOnce(Scope) -> Fragment>,
/// A closure that returns a bool that determines whether this thing runs
when: W,
/// A closure that returns what gets rendered if the when statement is false
fallback: F,
) -> impl IntoView
where
W: Fn() -> bool + 'static,
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
// now you don't render until `when` is actually true
let children = Lazy::new(move || children(cx).into_view(cx));
let fallback = Lazy::new(move || fallback(cx).into_view(cx));
move || match when() {
true => children.clone(),
false => fallback.clone(),
}
}

View File

@@ -132,5 +132,5 @@ where
_ => unreachable!(),
};
leptos_dom::View::Suspense(current_id.clone(), core_component)
leptos_dom::View::Suspense(current_id, core_component)
}

View File

@@ -50,7 +50,41 @@ fn ssr_test_with_components() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div class=\"counters\" id=\"_0-1\"><!--hk=_0-1-0o|leptos-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-counter-end--><!--hk=_0-1-5-0o|leptos-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-counter-end--></div>"
"<div id=\"_0-1\" class=\"counters\"><!--hk=_0-1-0o|leptos-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-counter-end--><!--hk=_0-1-5-0o|leptos-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-counter-end--></div>"
);
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_test_with_snake_case_components() {
use leptos::*;
#[component]
fn snake_case_counter(cx: Scope, initial_value: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
view! {
cx,
<div>
<button on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
<span>"Value: " {move || value.get().to_string()} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += 1)>"+1"</button>
</div>
}
}
_ = create_scope(create_runtime(), |cx| {
let rendered = view! {
cx,
<div class="counters">
<SnakeCaseCounter initial_value=1/>
<SnakeCaseCounter initial_value=2/>
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" class=\"counters\"><!--hk=_0-1-0o|leptos-snake-case-counter-start--><div id=\"_0-1-1\"><button id=\"_0-1-2\">-1</button><span id=\"_0-1-3\">Value: <!--hk=_0-1-4o|leptos-dyn-child-start-->1<!--hk=_0-1-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5\">+1</button></div><!--hk=_0-1-0c|leptos-snake-case-counter-end--><!--hk=_0-1-5-0o|leptos-snake-case-counter-start--><div id=\"_0-1-5-1\"><button id=\"_0-1-5-2\">-1</button><span id=\"_0-1-5-3\">Value: <!--hk=_0-1-5-4o|leptos-dyn-child-start-->2<!--hk=_0-1-5-4c|leptos-dyn-child-end-->!</span><button id=\"_0-1-5-5\">+1</button></div><!--hk=_0-1-5-0c|leptos-snake-case-counter-end--></div>"
);
});
}
@@ -69,7 +103,29 @@ fn test_classes() {
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div class=\"my big red car\" id=\"_0-1\"></div>"
"<div id=\"_0-1\" class=\"my big red car\"></div>"
);
});
}
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
#[test]
fn ssr_with_styles() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
let (value, set_value) = create_signal(cx, 0);
let styles = "myclass";
let rendered = view! {
cx, class = styles,
<div>
<button class="btn" on:click=move |_| set_value.update(|value| *value -= 1)>"-1"</button>
</div>
};
assert_eq!(
rendered.into_view(cx).render_to_string(cx),
"<div id=\"_0-1\" class=\" myclass\"><button id=\"_0-2\" class=\"btn myclass\">-1</button></div>"
);
});
}

View File

@@ -72,8 +72,8 @@ fn env_w_default(key: &str, default: &str) -> Result<String, LeptosConfigError>
}
}
/// An enum that can be used to define the environment Leptos is running in. Can be passed to [RenderOptions].
/// Setting this to the `PROD` variant will not include the websockets code for `cargo-leptos` watch mode.
/// An enum that can be used to define the environment Leptos is running in.
/// Setting this to the `PROD` variant will not include the WebSocket code for `cargo-leptos` watch mode.
/// Defaults to `DEV`.
#[derive(Debug, Clone, serde::Deserialize)]
pub enum Env {

View File

@@ -1,5 +1,6 @@
mod dyn_child;
mod each;
mod errors;
mod fragment;
mod unit;
@@ -11,6 +12,7 @@ use crate::{
use crate::{mount_child, prepare_to_move, MountKind, Mountable};
pub use dyn_child::*;
pub use each::*;
pub use errors::*;
pub use fragment::*;
use leptos_reactive::Scope;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
@@ -26,12 +28,12 @@ use wasm_bindgen::JsCast;
#[derive(educe::Educe)]
#[educe(Default, Clone, PartialEq, Eq)]
pub enum CoreComponent {
/// The [`Unit`] component.
/// The [Unit] component.
#[educe(Default)]
Unit(UnitRepr),
/// The [`DynChild`] component.
/// The [DynChild] component.
DynChild(DynChildRepr),
/// The [`EachKey`] component.
/// The [Each] component.
Each(EachRepr),
}

View File

@@ -0,0 +1,68 @@
use crate::{HydrationCtx, HydrationKey, IntoView};
use cfg_if::cfg_if;
use leptos_reactive::{use_context, RwSignal};
use std::{collections::HashMap, error::Error, sync::Arc};
/// A struct to hold all the possible errors that could be provided by child Views
#[derive(Debug, Clone, Default)]
pub struct Errors(pub HashMap<HydrationKey, Arc<dyn Error>>);
impl<T, E> IntoView for Result<T, E>
where
T: IntoView + 'static,
E: std::error::Error + Send + Sync + 'static,
{
fn into_view(self, cx: leptos_reactive::Scope) -> crate::View {
match self {
Ok(stuff) => stuff.into_view(cx),
Err(error) => {
match use_context::<RwSignal<Errors>>(cx) {
Some(errors) => {
let id = HydrationCtx::id();
errors.update(move |errors: &mut Errors| errors.insert(id, error));
// remove the error from the list if this drops,
// i.e., if it's in a DynChild that switches from Err to Ok
// Only can run on the client, will panic on the server
cfg_if! {
if #[cfg(any(feature = "hydrate", feature="csr"))] {
use leptos_reactive::{on_cleanup, queue_microtask};
on_cleanup(cx, move || {
queue_microtask(move || {
errors.update(|errors: &mut Errors| {
errors.remove::<E>(&id);
});
});
});
}
}
}
None => {
#[cfg(debug_assertions)]
warn!(
"No ErrorBoundary components found! Returning errors will not \
be handled and will silently disappear"
);
}
}
().into_view(cx)
}
}
}
}
impl Errors {
/// Add an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn insert<E>(&mut self, key: HydrationKey, error: E)
where
E: Error + 'static,
{
self.0.insert(key, Arc::new(error));
}
/// Remove an error to Errors that will be processed by `<ErrorBoundary/>`
pub fn remove<E>(&mut self, key: &HydrationKey)
where
E: Error + 'static,
{
self.0.remove(key);
}
}

View File

@@ -982,14 +982,19 @@ macro_rules! generate_html_tags {
#[cfg(all(debug_assertions, target_arch = "wasm32", feature = "web"))]
fn warn_on_ambiguous_a(parent: &web_sys::Element, child: &View) {
if let View::Element(el) = &child {
if el.name == "a" {
if el.name == "a"
|| el.name == "script"
|| el.name == "style"
|| el.name == "title"
{
if parent.namespace_uri() != el.element.namespace_uri() {
crate::warn!(
"Warning: you are appending an SVG <a/> to an HTML element, or an \
HTML <a/> to an SVG. Typically, this occurs when you create an \
<a/> with the `view` macro and append it to an SVG, but the \
framework assumed it was HTML when you created it. To specify that \
it is an SVG <a/>, use <svg::a/> in the view macro."
"Warning: you are appending an SVG element to an HTML element, or \
an HTML element to an SVG. Typically, this occurs when you create \
an <a/> or <script/> with the `view` macro and append it to an \
SVG, but the framework assumed it was HTML when you created it. To \
specify that it is an SVG element, use <svg::{{tag name}}/> in the \
view macro."
)
}
}
@@ -1038,6 +1043,8 @@ generate_html_tags![
footer HtmlElement,
/// The `<header>` HTML element represents introductory content, typically a group of introductory or navigational aids. It may contain some heading elements but also a logo, a search form, an author name, and other elements.
header HtmlElement,
/// The `<hgroup>` HTML element represents a heading and related content. It groups a single `<h1><h6>` element with one or more `<p>`.
hgroup HtmlElement,
/// The `<h1>` to `<h6>` HTML elements represent six levels of section headings. `<h1>` is the highest section level and `<h6>` is the lowest.
h1 HtmlHeadingElement,
/// The `<h1>` to `<h6>` HTML elements represent six levels of section headings. `<h1>` is the highest section level and `<h6>` is the lowest.

View File

@@ -49,7 +49,7 @@ cfg_if! {
}
/// A stable identifer within the server-rendering or hydration process.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct HydrationKey {
/// The key of the previous component.
pub previous: String,
@@ -104,8 +104,9 @@ impl HydrationCtx {
})
}
#[doc(hidden)]
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) fn reset_id() {
pub fn reset_id() {
ID.with(|id| *id.borrow_mut() = Default::default());
}

View File

@@ -31,7 +31,7 @@ pub use hydration::{HydrationCtx, HydrationKey};
pub use js_sys;
use leptos_reactive::Scope;
pub use logging::*;
pub use macro_helpers::{IntoAttribute, IntoClass, IntoProperty};
pub use macro_helpers::*;
pub use node_ref::*;
#[cfg(all(target_arch = "wasm32", feature = "web"))]
use once_cell::unsync::Lazy as LazyCell;

View File

@@ -78,7 +78,7 @@ pub fn render_to_stream_with_prefix(
stream
}
/// Renders a function to a stream of HTML strings and returns the [Scope] and [Runtime] that were created, so
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
///
@@ -96,6 +96,29 @@ pub fn render_to_stream_with_prefix(
pub fn render_to_stream_with_prefix_undisposed(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
render_to_stream_with_prefix_undisposed_with_context(view, prefix, |_cx| {})
}
/// Renders a function to a stream of HTML strings and returns the [Scope] and [RuntimeId] that were created, so
/// they can be disposed when appropriate. After the `view` runs, the `prefix` will run with
/// the same scope. This can be used to generate additional HTML that has access to the same `Scope`.
///
/// This renders:
/// 1) the prefix
/// 2) the application shell
/// a) HTML for everything that is not under a `<Suspense/>`,
/// b) the `fallback` for any `<Suspense/>` component that is not already resolved, and
/// c) JavaScript necessary to receive streaming [Resource](leptos_reactive::Resource) data.
/// 3) streaming [Resource](leptos_reactive::Resource) data. Resources begin loading on the
/// server and are sent down to the browser to resolve. On the browser, if the app sees that
/// it is waiting for a resource to resolve from the server, it doesn't run it initially.
/// 4) HTML fragments to replace each `<Suspense/>` fallback with its actual data as the resources
/// read under that `<Suspense/>` resolve.
pub fn render_to_stream_with_prefix_undisposed_with_context(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
additional_context: impl FnOnce(Scope) + 'static,
) -> (impl Stream<Item = String>, RuntimeId, ScopeId) {
HydrationCtx::reset_id();
@@ -108,6 +131,8 @@ pub fn render_to_stream_with_prefix_undisposed(
_,
) = run_scope_undisposed(runtime, {
move |cx| {
// Add additional context items
additional_context(cx);
// the actual app body/template code
// this does NOT contain any of the data being loaded asynchronously in resources
let shell = view(cx).render_to_string(cx);
@@ -433,3 +458,11 @@ fn to_kebab_case(name: &str) -> String {
new_name
}
#[doc(hidden)]
pub fn escape_attr<T>(value: &T) -> Cow<'_, str>
where
T: AsRef<str>,
{
html_escape::encode_double_quoted_attribute(value)
}

View File

@@ -27,6 +27,7 @@ leptos_dom = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
lazy_static = "1.4"
convert_case = "0.6.0"
[dev-dependencies]
log = "0.4"

View File

@@ -1,3 +1,7 @@
use convert_case::{
Case::{Pascal, Snake},
Casing,
};
use itertools::Itertools;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, ToTokens, TokenStreamExt};
@@ -75,7 +79,7 @@ impl Parse for Model {
is_transparent: false,
docs,
vis: item.vis.clone(),
name: item.sig.ident.clone(),
name: convert_from_snake_case(&item.sig.ident),
scope_name,
props,
ret: item.sig.output.clone(),
@@ -97,6 +101,15 @@ fn drain_filter<T>(vec: &mut Vec<T>, mut some_predicate: impl FnMut(&mut T) -> b
}
}
fn convert_from_snake_case(name: &Ident) -> Ident {
let name_str = name.to_string();
if !name_str.is_case(Snake) {
name.clone()
} else {
Ident::new(&name_str.to_case(Pascal), name.span())
}
}
impl ToTokens for Model {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {

View File

@@ -4,7 +4,8 @@
#[macro_use]
extern crate proc_macro_error;
use proc_macro::{TokenStream, TokenTree};
use proc_macro::TokenStream;
use proc_macro2::TokenTree;
use quote::ToTokens;
use server::server_macro_impl;
use syn::{parse_macro_input, DeriveInput};
@@ -196,8 +197,8 @@ mod server;
/// # });
/// ```
///
/// 8. You can use the `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](leptos_reactive::NodeRef) to use later.
/// 8. You can use the `node_ref` or `_ref` attribute to store a reference to its DOM element in a
/// [NodeRef](leptos_dom::NodeRef) to use later.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
@@ -211,6 +212,24 @@ mod server;
/// # });
/// ```
///
/// 9. You can add the same class to every element in the view by passing in a special
/// `class = {/* ... */}` argument after `cx, `. This is useful for injecting a class
/// providing by a scoped styling library.
/// ```rust
/// # use leptos::*;
/// # run_scope(create_runtime(), |cx| {
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
/// let class = "mycustomclass";
/// view! { cx, class = class,
/// <div> // will have class="mycustomclass"
/// <p>"Some text"</p> // will also have class "mycustomclass"
/// </div>
/// }
/// # ;
/// # }
/// # });
/// ```
///
/// Heres a simple example that shows off several of these features, put together
/// ```rust
/// # use leptos::*;
@@ -243,16 +262,45 @@ mod server;
#[proc_macro_error::proc_macro_error]
#[proc_macro]
pub fn view(tokens: TokenStream) -> TokenStream {
let tokens: proc_macro2::TokenStream = tokens.into();
let mut tokens = tokens.into_iter();
let (cx, comma) = (tokens.next(), tokens.next());
match (cx, comma) {
(Some(TokenTree::Ident(cx)), Some(TokenTree::Punct(punct))) if punct.as_char() == ',' => {
match parse(tokens.collect()) {
let first = tokens.next();
let second = tokens.next();
let third = tokens.next();
let fourth = tokens.next();
let global_class = match (&first, &second, &third, &fourth) {
(
Some(TokenTree::Ident(first)),
Some(TokenTree::Punct(eq)),
Some(val),
Some(TokenTree::Punct(comma)),
) if *first == "class"
&& eq.to_string() == '='.to_string()
&& comma.to_string() == ','.to_string() =>
{
Some(val.clone())
}
_ => None,
};
let tokens = if global_class.is_some() {
tokens.collect::<proc_macro2::TokenStream>()
} else {
[first, second, third, fourth]
.into_iter()
.flatten()
.chain(tokens)
.collect()
};
match parse(tokens.into()) {
Ok(nodes) => render_view(
&proc_macro2::Ident::new(&cx.to_string(), cx.span().into()),
&proc_macro2::Ident::new(&cx.to_string(), cx.span()),
&nodes,
// swap to Mode::default() to use faster SSR templating
Mode::Client, //Mode::default(),
Mode::default(),
global_class.as_ref(),
),
Err(error) => error.to_compile_error(),
}
@@ -267,10 +315,10 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// Annotates a function so that it can be used with your template as a Leptos `<Component/>`.
///
/// The `#[component]` macro allows you to annotate plain Rust functions as components
/// and use them within your Leptos [view](mod@view) as if they were custom HTML elements. The
/// and use them within your Leptos [view](crate::view!) as if they were custom HTML elements. The
/// component function takes a [Scope](leptos_reactive::Scope) and any number of other arguments.
/// When you use the component somewhere else, the names of its arguments are the names
/// of the properties you use in the [view](mod@view) macro.
/// of the properties you use in the [view](crate::view!) macro.
///
/// Every component function should have the return type `-> impl IntoView`.
///
@@ -325,18 +373,21 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// to do relatively expensive work within the component function, as it will only happen once,
/// not on every state change.
///
/// 2. The component name should be `CamelCase` instead of `snake_case`. This is how the renderer
/// recognizes that a particular tag is a component, not an HTML element.
/// 2. Component names are usually in `PascalCase`. If you use a `snake_case` name,
/// then the generated component's name will still be in `PascalCase`. This is how the framework
/// recognizes that a particular tag is a component, not an HTML element. It's important to be aware
/// of this when using or importing the component.
///
/// ```
/// # use leptos::*;
/// // ❌ not snake_case
/// #[component]
/// fn my_component(cx: Scope) -> impl IntoView { todo!() }
///
/// // ✅ CamelCase
/// // PascalCase: Generated component will be called MyComponent
/// #[component]
/// fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
///
/// // snake_case: Generated component will be called MySnakeCaseComponent
/// #[component]
/// fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
/// ```
///
/// 3. The macro generates a type `ComponentProps` for every `Component` (so, `HomePage` generates `HomePageProps`,
@@ -355,6 +406,18 @@ pub fn view(tokens: TokenStream) -> TokenStream {
/// pub fn MyComponent(cx: Scope) -> impl IntoView { todo!() }
/// }
/// ```
/// ```
/// # use leptos::*;
///
/// use snake_case_component::{MySnakeCaseComponent, MySnakeCaseComponentProps};
///
/// mod snake_case_component {
/// use leptos::*;
///
/// #[component]
/// pub fn my_snake_case_component(cx: Scope) -> impl IntoView { todo!() }
/// }
/// ```
///
/// 4. You can pass generic arguments, but they should be defined in a `where` clause and not inline.
///
@@ -491,11 +554,11 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization) or `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function to power an
/// [ActionForm](leptos_router::ActionForm) the encoding must be `"Url"`.
/// Defaults to `"Url"`. If you want to use this server function to power a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos::Scope),
/// and deserializable with `serde`. Optionally, its first argument can be a Leptos [Scope](leptos_reactive::Scope),
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
/// server-side context into the server function.
///

View File

@@ -43,7 +43,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
let block = body.block;
cfg_if! {
if #[cfg(not(feature = "stable"))] {
if #[cfg(all(not(feature = "stable"), debug_assertions))] {
use proc_macro::Span;
let span = Span::call_site();
#[cfg(not(target_os = "windows"))]
@@ -71,6 +71,7 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
let cx_assign_statement = if let Some(FnArg::Typed(arg)) = cx_arg {
if let Pat::Ident(id) = &*arg.pat {
quote! {
#[allow(unused)]
let #id = cx;
}
} else {
@@ -90,7 +91,15 @@ pub fn server_macro_impl(args: proc_macro::TokenStream, s: TokenStream2) -> Resu
FnArg::Receiver(_) => panic!("cannot use receiver types in server function macro"),
FnArg::Typed(t) => t,
};
quote! { #typed_arg }
let is_cx = fn_arg_is_cx(f);
if is_cx {
quote! {
#[allow(unused)]
#typed_arg
}
} else {
quote! { #typed_arg }
}
});
let fn_args_2 = fn_args.clone();

View File

@@ -1,4 +1,4 @@
use proc_macro2::{Ident, Span, TokenStream};
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use syn::{spanned::Spanned, Expr, ExprLit, ExprPath, Lit};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName};
@@ -142,7 +142,12 @@ const TYPED_EVENTS: [&str; 126] = [
"visibilitychange",
];
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
pub(crate) fn render_view(
cx: &Ident,
nodes: &[Node],
mode: Mode,
global_class: Option<&TokenTree>,
) -> TokenStream {
if mode == Mode::Ssr {
if nodes.is_empty() {
let span = Span::call_site();
@@ -150,9 +155,9 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
span => leptos::Unit
}
} else if nodes.len() == 1 {
root_node_to_tokens_ssr(cx, &nodes[0])
root_node_to_tokens_ssr(cx, &nodes[0], global_class)
} else {
fragment_to_tokens_ssr(cx, Span::call_site(), nodes)
fragment_to_tokens_ssr(cx, Span::call_site(), nodes, global_class)
}
} else if nodes.is_empty() {
let span = Span::call_site();
@@ -160,16 +165,27 @@ pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream
span => leptos::Unit
}
} else if nodes.len() == 1 {
node_to_tokens(cx, &nodes[0], TagType::Unknown)
node_to_tokens(cx, &nodes[0], TagType::Unknown, global_class)
} else {
fragment_to_tokens(cx, Span::call_site(), nodes, false, TagType::Unknown)
fragment_to_tokens(
cx,
Span::call_site(),
nodes,
false,
TagType::Unknown,
global_class,
)
}
}
fn root_node_to_tokens_ssr(cx: &Ident, node: &Node) -> TokenStream {
fn root_node_to_tokens_ssr(
cx: &Ident,
node: &Node,
global_class: Option<&TokenTree>,
) -> TokenStream {
match node {
Node::Fragment(fragment) => {
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children)
fragment_to_tokens_ssr(cx, Span::call_site(), &fragment.children, global_class)
}
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
Node::Text(node) => {
@@ -181,16 +197,22 @@ fn root_node_to_tokens_ssr(cx: &Ident, node: &Node) -> TokenStream {
Node::Block(node) => {
let value = node.value.as_ref();
quote! {
#[allow(unused_braces)]
#value
}
}
Node::Element(node) => root_element_to_tokens_ssr(cx, node),
Node::Element(node) => root_element_to_tokens_ssr(cx, node, global_class),
}
}
fn fragment_to_tokens_ssr(cx: &Ident, _span: Span, nodes: &[Node]) -> TokenStream {
fn fragment_to_tokens_ssr(
cx: &Ident,
_span: Span,
nodes: &[Node],
global_class: Option<&TokenTree>,
) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = root_node_to_tokens_ssr(cx, node);
let node = root_node_to_tokens_ssr(cx, node, global_class);
quote! {
#node.into_view(#cx)
}
@@ -204,33 +226,49 @@ fn fragment_to_tokens_ssr(cx: &Ident, _span: Span, nodes: &[Node]) -> TokenStrea
}
}
fn root_element_to_tokens_ssr(cx: &Ident, node: &NodeElement) -> TokenStream {
let mut template = String::new();
let mut holes = Vec::<TokenStream>::new();
let mut exprs_for_compiler = Vec::<TokenStream>::new();
element_to_tokens_ssr(cx, node, &mut template, &mut holes, &mut exprs_for_compiler);
let template = if holes.is_empty() {
quote! {
#template
}
fn root_element_to_tokens_ssr(
cx: &Ident,
node: &NodeElement,
global_class: Option<&TokenTree>,
) -> TokenStream {
if is_component_node(node) {
component_to_tokens(cx, node, global_class)
} else {
quote! {
format!(
#template,
#(#holes)*
)
}
};
let mut template = String::new();
let mut holes = Vec::<TokenStream>::new();
let mut exprs_for_compiler = Vec::<TokenStream>::new();
let tag_name = node.name.to_string();
let typed_element_name = Ident::new(&camel_case_tag_name(&tag_name), node.name.span());
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, leptos::#typed_element_name::default(), #template)
}
element_to_tokens_ssr(
cx,
node,
&mut template,
&mut holes,
&mut exprs_for_compiler,
true,
global_class,
);
let template = if holes.is_empty() {
quote! {
#template
}
} else {
quote! {
format!(
#template,
#(#holes)*
)
}
};
let tag_name = node.name.to_string();
let typed_element_name = Ident::new(&camel_case_tag_name(&tag_name), node.name.span());
quote! {
{
#(#exprs_for_compiler)*
::leptos::HtmlElement::from_html(cx, leptos::#typed_element_name::default(), #template)
}
}
}
}
@@ -240,10 +278,12 @@ fn element_to_tokens_ssr(
template: &mut String,
holes: &mut Vec<TokenStream>,
exprs_for_compiler: &mut Vec<TokenStream>,
is_root: bool,
global_class: Option<&TokenTree>,
) {
if is_component_node(node) {
template.push_str("{}");
let component = component_to_tokens(cx, node);
let component = component_to_tokens(cx, node, global_class);
holes.push(quote! {
{#component}.into_view(cx).render_to_string(cx),
})
@@ -257,7 +297,27 @@ fn element_to_tokens_ssr(
}
}
set_class_attribute_ssr(cx, node, template, holes);
// insert hydration ID
let hydration_id = if is_root {
quote! { leptos::HydrationCtx::peek(), }
} else {
quote! { leptos::HydrationCtx::id(), }
};
match node
.attributes
.iter()
.find(|node| matches!(node, Node::Attribute(attr) if attr.key.to_string() == "id"))
{
Some(_) => {
template.push_str(" leptos-hk=\"_{}\"");
}
None => {
template.push_str(" id=\"_{}\"");
}
}
holes.push(hydration_id);
set_class_attribute_ssr(cx, node, template, holes, global_class);
if is_self_closing(node) {
template.push_str("/>");
@@ -265,9 +325,15 @@ fn element_to_tokens_ssr(
template.push('>');
for child in &node.children {
match child {
Node::Element(child) => {
element_to_tokens_ssr(cx, child, template, holes, exprs_for_compiler)
}
Node::Element(child) => element_to_tokens_ssr(
cx,
child,
template,
holes,
exprs_for_compiler,
false,
global_class,
),
Node::Text(text) => {
if let Some(value) = value_to_string(&text.value) {
template.push_str(&value);
@@ -345,13 +411,12 @@ fn attribute_to_tokens_ssr(
.parse::<TokenStream>()
.expect("couldn't parse event name");
let event_type = if is_force_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
exprs_for_compiler.push(quote! {
let event_type = if is_force_undelegated {
quote! { ::leptos::ev::undelegated(::leptos::ev::#event_type) }
} else {
quote! { ::leptos::ev::#event_type }
};
leptos::ssr_event_listener(#event_type, #handler);
})
} else if name.strip_prefix("prop:").is_some() || name.strip_prefix("class:").is_some() {
@@ -366,15 +431,16 @@ fn attribute_to_tokens_ssr(
if let Some(value) = node.value.as_ref() {
if let Some(value) = value_to_string(value) {
template.push_str("=\"");
template.push_str(&value);
template.push('"');
} else {
template.push_str("{}");
template.push_str("=\"{}\"");
let value = value.as_ref();
holes.push(quote! {
leptos::escape_attr(&{#value}.into_attribute(#cx).as_value_string(#name)),
leptos::escape_attr(&{#value}.into_attribute(#cx).as_nameless_value_string()),
})
}
template.push('"');
}
}
}
@@ -385,7 +451,17 @@ fn set_class_attribute_ssr(
node: &NodeElement,
template: &mut String,
holes: &mut Vec<TokenStream>,
global_class: Option<&TokenTree>,
) {
let static_global_class = match global_class {
Some(TokenTree::Literal(lit)) => lit.to_string(),
_ => String::new(),
};
let dyn_global_class = match global_class {
None => None,
Some(TokenTree::Literal(_)) => None,
Some(val) => Some(val),
};
let static_class_attr = node
.attributes
.iter()
@@ -400,6 +476,8 @@ fn set_class_attribute_ssr(
None
}
})
.chain(std::iter::once(static_global_class))
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
@@ -463,7 +541,11 @@ fn set_class_attribute_ssr(
})
.collect::<Vec<_>>();
if !static_class_attr.is_empty() || !dyn_class_attr.is_empty() || !class_attrs.is_empty() {
if !static_class_attr.is_empty()
|| !dyn_class_attr.is_empty()
|| !class_attrs.is_empty()
|| dyn_global_class.is_some()
{
template.push_str(" class=\"");
template.push_str(&static_class_attr);
@@ -485,6 +567,11 @@ fn set_class_attribute_ssr(
});
}
if let Some(dyn_global_class) = dyn_global_class {
template.push_str(" {}");
holes.push(quote! { #dyn_global_class, });
}
template.push('"');
}
}
@@ -495,9 +582,10 @@ fn fragment_to_tokens(
nodes: &[Node],
lazy: bool,
parent_type: TagType,
global_class: Option<&TokenTree>,
) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = node_to_tokens(cx, node, parent_type);
let node = node_to_tokens(cx, node, parent_type, global_class);
quote! {
#node.into_view(#cx)
@@ -522,7 +610,12 @@ fn fragment_to_tokens(
}
}
fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream {
fn node_to_tokens(
cx: &Ident,
node: &Node,
parent_type: TagType,
global_class: Option<&TokenTree>,
) -> TokenStream {
match node {
Node::Fragment(fragment) => fragment_to_tokens(
cx,
@@ -530,6 +623,7 @@ fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream
&fragment.children,
false,
parent_type,
global_class,
),
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Text(node) => {
@@ -543,13 +637,18 @@ fn node_to_tokens(cx: &Ident, node: &Node, parent_type: TagType) -> TokenStream
quote! { #value }
}
Node::Attribute(node) => attribute_to_tokens(cx, node),
Node::Element(node) => element_to_tokens(cx, node, parent_type),
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
}
}
fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -> TokenStream {
fn element_to_tokens(
cx: &Ident,
node: &NodeElement,
mut parent_type: TagType,
global_class: Option<&TokenTree>,
) -> TokenStream {
if is_component_node(node) {
component_to_tokens(cx, node)
component_to_tokens(cx, node, global_class)
} else {
let tag = node.name.to_string();
let name = if is_custom_element(&tag) {
@@ -590,6 +689,14 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
None
}
});
let global_class_expr = match global_class {
None => quote! {},
Some(class) => {
quote! {
.class(#class, true)
}
}
};
let children = node.children.iter().map(|node| {
let child = match node {
Node::Fragment(fragment) => fragment_to_tokens(
@@ -598,6 +705,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
&fragment.children,
false,
parent_type,
global_class,
),
Node::Text(node) => {
let value = node.value.as_ref();
@@ -611,7 +719,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
#[allow(unused_braces)] #value
}
}
Node::Element(node) => element_to_tokens(cx, node, parent_type),
Node::Element(node) => element_to_tokens(cx, node, parent_type, global_class),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
};
quote! {
@@ -621,6 +729,7 @@ fn element_to_tokens(cx: &Ident, node: &NodeElement, mut parent_type: TagType) -
quote! {
#name
#(#attrs)*
#global_class_expr
#(#children)*
}
}
@@ -797,7 +906,11 @@ fn attribute_to_tokens(cx: &Ident, node: &NodeAttribute) -> TokenStream {
}
}
fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
fn component_to_tokens(
cx: &Ident,
node: &NodeElement,
global_class: Option<&TokenTree>,
) -> TokenStream {
let name = &node.name;
let component_name = ident_from_tag_name(&node.name);
let span = node.name.span();
@@ -848,7 +961,14 @@ fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
let children = if node.children.is_empty() {
quote! {}
} else {
let children = fragment_to_tokens(cx, span, &node.children, true, TagType::Unknown);
let children = fragment_to_tokens(
cx,
span,
&node.children,
true,
TagType::Unknown,
global_class,
);
let clonables = items_to_clone
.iter()
@@ -1000,16 +1120,13 @@ fn is_svg_element(tag: &str) -> bool {
| "polyline"
| "radialGradient"
| "rect"
| "script"
| "set"
| "stop"
| "style"
| "svg"
| "switch"
| "symbol"
| "text"
| "textPath"
| "title"
| "tspan"
| "use"
| "use_"
@@ -1055,7 +1172,7 @@ fn is_math_ml_element(tag: &str) -> bool {
}
fn is_ambiguous_element(tag: &str) -> bool {
matches!(tag, "a")
tag == "a" || tag == "script"
}
fn parse_event(event_name: &str) -> (&str, bool) {

View File

@@ -8,16 +8,16 @@ repository = "https://github.com/leptos-rs/leptos"
description = "Reactive system for the Leptos web framework."
[dependencies]
log = "0.4"
slotmap = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde-lite = { version = "0.3", optional = true }
futures = { version = "0.3" }
js-sys = "0.3"
lazy_static = "1"
miniserde = { version = "0.1", optional = true }
parking_lot = "0.12"
serde-wasm-bindgen = "0.4"
serde_json = "1"
base64 = "0.13"
thiserror = "1"
tokio = { version = "1", features = ["rt"], optional = true }
tracing = "0.1"

View File

@@ -57,7 +57,7 @@ where
let id = value.type_id();
_ = with_runtime(cx.runtime, |runtime| {
let mut contexts = runtime.scope_contexts.borrow_mut();
let mut contexts = runtime.scope_contexts.write();
let context = contexts.entry(cx.id).unwrap().or_insert_with(HashMap::new);
context.insert(id, Box::new(value) as Box<dyn Any>);
});
@@ -116,7 +116,7 @@ where
let id = TypeId::of::<T>();
with_runtime(cx.runtime, |runtime| {
let local_value = {
let contexts = runtime.scope_contexts.borrow();
let contexts = runtime.scope_contexts.read();
let context = contexts.get(cx.id);
context
.and_then(|context| context.get(&id).and_then(|val| val.downcast_ref::<T>()))
@@ -124,16 +124,12 @@ where
};
match local_value {
Some(val) => Some(val),
None => runtime
.scope_parents
.borrow()
.get(cx.id)
.and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
None => runtime.scope_parents.read().get(cx.id).and_then(|parent| {
use_context::<T>(Scope {
runtime: cx.runtime,
id: *parent,
})
}),
}
})
.ok()

View File

@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
use crate::runtime::{with_runtime, RuntimeId};
use crate::runtime::{with_runtime, LocalObserver, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::cell::RefCell;
use parking_lot::RwLock;
use std::fmt::Debug;
/// Effects run a certain chunk of code whenever the signals they depend on change.
@@ -23,7 +23,6 @@ use std::fmt::Debug;
/// the server, use [create_isomorphic_effect].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
@@ -31,7 +30,7 @@ use std::fmt::Debug;
/// // ✅ use effects to interact between reactive state and the outside world
/// create_effect(cx, move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a());
/// println!("Value: {}", a());
/// });
///
/// set_a(1);
@@ -80,7 +79,6 @@ where
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
@@ -88,7 +86,7 @@ where
/// // ✅ use effects to interact between reactive state and the outside world
/// create_isomorphic_effect(cx, move |_| {
/// // immediately prints "Value: 0" and subscribes to `a`
/// log::debug!("Value: {}", a());
/// println!("Value: {}", a());
/// });
///
/// set_a(1);
@@ -152,7 +150,7 @@ where
F: Fn(Option<T>) -> T,
{
pub(crate) f: F,
pub(crate) value: RefCell<Option<T>>,
pub(crate) value: RwLock<Option<T>>,
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
@@ -179,22 +177,22 @@ where
)
)
)]
fn run(&self, id: EffectId, runtime: RuntimeId) {
_ = with_runtime(runtime, |runtime| {
fn run(&self, id: EffectId, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
// clear previous dependencies
id.cleanup(runtime);
// set this as the current observer
let prev_observer = runtime.observer.take();
runtime.observer.set(Some(id));
let prev_observer = LocalObserver::take(runtime_id);
LocalObserver::set(runtime_id, Some(id));
// run the effect
let value = self.value.take();
let value = std::mem::take(&mut *self.value.write());
let new_value = (self.f)(value);
*self.value.borrow_mut() = Some(new_value);
*self.value.write() = Some(new_value);
// restore the previous observer
runtime.observer.set(prev_observer);
LocalObserver::set(runtime_id, prev_observer);
})
}
}
@@ -203,7 +201,7 @@ impl EffectId {
pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
let effect = {
let effects = runtime.effects.borrow();
let effects = runtime.effects.read();
effects.get(*self).cloned()
};
if let Some(effect) = effect {
@@ -226,12 +224,12 @@ impl EffectId {
)
)]
pub(crate) fn cleanup(&self, runtime: &Runtime) {
let sources = runtime.effect_sources.borrow();
let sources = runtime.effect_sources.read();
if let Some(sources) = sources.get(*self) {
let subs = runtime.signal_subscribers.borrow();
for source in sources.borrow().iter() {
let subs = runtime.signal_subscribers.read();
for source in sources.read().iter() {
if let Some(source) = subs.get(*source) {
source.borrow_mut().remove(self);
source.write().remove(self);
}
}
}

View File

@@ -146,7 +146,7 @@ macro_rules! debug_warn {
{
#[cfg(debug_assertions)]
{
log::warn!($($x)*)
tracing::warn!($($x)*)
}
#[cfg(not(debug_assertions))]
{ }

View File

@@ -33,7 +33,7 @@ use std::fmt::Debug;
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
/// create_effect(cx, move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// println!("expensive = {}", expensive());
/// });
/// create_effect(cx, move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
@@ -46,7 +46,7 @@ use std::fmt::Debug;
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized());
/// println!("memoized = {}", memoized());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**
@@ -103,7 +103,7 @@ where
/// let expensive = move || really_expensive_computation(value()); // lazy: doesn't run until called
/// create_effect(cx, move |_| {
/// // 🆗 run #1: calls `really_expensive_computation` the first time
/// log::debug!("expensive = {}", expensive());
/// println!("expensive = {}", expensive());
/// });
/// create_effect(cx, move |_| {
/// // ❌ run #2: this calls `really_expensive_computation` a second time!
@@ -116,7 +116,7 @@ where
/// let memoized = create_memo(cx, move |_| really_expensive_computation(value()));
/// create_effect(cx, move |_| {
/// // 🆗 reads the current value of the memo
/// log::debug!("memoized = {}", memoized());
/// println!("memoized = {}", memoized());
/// });
/// create_effect(cx, move |_| {
/// // ✅ reads the current value **without re-running the calculation**

View File

@@ -6,15 +6,10 @@ use crate::{
spawn::spawn_local,
use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext, WriteSignal,
};
use parking_lot::RwLock;
use std::{
any::Any,
cell::{Cell, RefCell},
collections::HashSet,
fmt::Debug,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
any::Any, collections::HashSet, fmt::Debug, future::Future, marker::PhantomData, pin::Pin,
sync::Arc,
};
/// Creates [Resource](crate::Resource), which is a signal that reflects the
@@ -113,10 +108,10 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
let r = Arc::new(ResourceState {
scope: cx,
value,
set_value,
@@ -124,18 +119,18 @@ where
set_loading,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
resolved: Arc::new(RwLock::new(resolved)),
scheduled: Arc::new(RwLock::new(false)),
suspense_contexts: Default::default(),
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_serializable_resource(Rc::clone(&r))
runtime.create_serializable_resource(Arc::clone(&r))
})
.expect("tried to create a Resource in a Runtime that has been disposed.");
create_isomorphic_effect(cx, {
let r = Rc::clone(&r);
let r = Arc::clone(&r);
move |_| {
load_resource(cx, id, r.clone());
}
@@ -233,10 +228,10 @@ where
let (loading, set_loading) = create_signal(cx, false);
let fetcher = Rc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let fetcher = Arc::new(move |s| Box::pin(fetcher(s)) as Pin<Box<dyn Future<Output = T>>>);
let source = create_memo(cx, move |_| source());
let r = Rc::new(ResourceState {
let r = Arc::new(ResourceState {
scope: cx,
value,
set_value,
@@ -244,18 +239,18 @@ where
set_loading,
source,
fetcher,
resolved: Rc::new(Cell::new(resolved)),
scheduled: Rc::new(Cell::new(false)),
resolved: Arc::new(RwLock::new(resolved)),
scheduled: Arc::new(RwLock::new(false)),
suspense_contexts: Default::default(),
});
let id = with_runtime(cx.runtime, |runtime| {
runtime.create_unserializable_resource(Rc::clone(&r))
runtime.create_unserializable_resource(Arc::clone(&r))
})
.expect("tried to create a Resource in a runtime that has been disposed.");
create_effect(cx, {
let r = Rc::clone(&r);
let r = Arc::clone(&r);
// This is a local resource, so we're always going to handle it on the
// client
move |_| r.load(false)
@@ -274,7 +269,7 @@ where
}
#[cfg(not(feature = "hydrate"))]
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Arc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
T: 'static,
@@ -283,7 +278,7 @@ where
}
#[cfg(feature = "hydrate")]
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Arc<ResourceState<S, T>>)
where
S: PartialEq + Debug + Clone + 'static,
T: Serializable + 'static,
@@ -291,12 +286,12 @@ where
use wasm_bindgen::{JsCast, UnwrapThrowExt};
_ = with_runtime(cx.runtime, |runtime| {
let mut context = runtime.shared_context.borrow_mut();
let mut context = runtime.shared_context.write();
if let Some(data) = context.resolved_resources.remove(&id) {
// The server already sent us the serialized resource value, so
// deserialize & set it now
context.pending_resources.remove(&id); // no longer pending
r.resolved.set(true);
*r.resolved.write() = true;
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
@@ -318,7 +313,7 @@ where
move |res: String| {
let res =
T::from_json(&res).expect_throw("could not deserialize Resource JSON");
resolved.set(true);
*resolved.write() = true;
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
}
@@ -551,10 +546,10 @@ where
set_loading: WriteSignal<bool>,
source: Memo<S>,
#[allow(clippy::type_complexity)]
fetcher: Rc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
resolved: Rc<Cell<bool>>,
scheduled: Rc<Cell<bool>>,
suspense_contexts: Rc<RefCell<HashSet<SuspenseContext>>>,
fetcher: Arc<dyn Fn(S) -> Pin<Box<dyn Future<Output = T>>>>,
resolved: Arc<RwLock<bool>>,
scheduled: Arc<RwLock<bool>>,
suspense_contexts: Arc<RwLock<HashSet<SuspenseContext>>>,
}
impl<S, T> ResourceState<S, T>
@@ -583,7 +578,7 @@ where
let increment = move |_: Option<()>| {
if let Some(s) = &suspense_cx {
let mut contexts = suspense_contexts.borrow_mut();
let mut contexts = suspense_contexts.write();
if !contexts.contains(s) {
contexts.insert(*s);
@@ -607,21 +602,22 @@ where
fn load(&self, refetching: bool) {
// doesn't refetch if already refetching
if refetching && self.scheduled.get() {
let mut scheduled = self.scheduled.write();
if refetching && *scheduled {
return;
}
self.scheduled.set(false);
*scheduled = false;
_ = self.source.try_with(|source| {
let fut = (self.fetcher)(source.clone());
// `scheduled` is true for the rest of this code only
self.scheduled.set(true);
*self.scheduled.write() = true;
queue_microtask({
let scheduled = Rc::clone(&self.scheduled);
let scheduled = Arc::clone(&self.scheduled);
move || {
scheduled.set(false);
*scheduled.write() = false;
}
});
@@ -630,7 +626,7 @@ where
// increment counter everywhere it's read
let suspense_contexts = self.suspense_contexts.clone();
for suspense_context in suspense_contexts.borrow().iter() {
for suspense_context in suspense_contexts.read().iter() {
suspense_context.increment();
}
@@ -642,12 +638,12 @@ where
async move {
let res = fut.await;
resolved.set(true);
*resolved.write() = true;
set_value.update(|n| *n = Some(res));
set_loading.update(|n| *n = false);
for suspense_context in suspense_contexts.borrow().iter() {
for suspense_context in suspense_contexts.read().iter() {
suspense_context.decrement();
}
}
@@ -686,8 +682,8 @@ where
}
pub(crate) enum AnyResource {
Unserializable(Rc<dyn UnserializableResource>),
Serializable(Rc<dyn SerializableResource>),
Unserializable(Arc<dyn UnserializableResource + Send + Sync>),
Serializable(Arc<dyn SerializableResource + Send + Sync>),
}
pub(crate) trait SerializableResource {

View File

@@ -6,28 +6,30 @@ use crate::{
};
use cfg_if::cfg_if;
use futures::stream::FuturesUnordered;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use slotmap::{SecondaryMap, SlotMap, SparseSecondaryMap};
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell},
cell::RefCell,
collections::{HashMap, HashSet},
fmt::Debug,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
sync::Arc,
};
pub(crate) type PinnedFuture<T> = Pin<Box<dyn Future<Output = T>>>;
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
thread_local! {
pub(crate) static RUNTIME: Runtime = Runtime::new();
lazy_static! {
pub(crate) static ref RUNTIME: Runtime = Runtime::new();
}
} else {
thread_local! {
pub(crate) static RUNTIMES: RefCell<SlotMap<RuntimeId, Runtime>> = Default::default();
lazy_static! {
pub(crate) static ref RUNTIMES: RwLock<SlotMap<RuntimeId, Runtime>> = Default::default();
}
}
}
@@ -39,15 +41,13 @@ pub(crate) fn with_runtime<T>(id: RuntimeId, f: impl FnOnce(&Runtime) -> T) -> R
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
_ = id;
Ok(RUNTIME.with(|runtime| f(runtime)))
Ok(f(&RUNTIME))
} else {
RUNTIMES.with(|runtimes| {
let runtimes = runtimes.borrow();
match runtimes.get(id) {
None => Err(()),
Some(runtime) => Ok(f(runtime))
}
})
let runtimes = RUNTIMES.read();
match runtimes.get(id) {
None => Err(()),
Some(runtime) => Ok(f(runtime))
}
}
}
}
@@ -60,7 +60,7 @@ pub fn create_runtime() -> RuntimeId {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
Default::default()
} else {
RUNTIMES.with(|runtimes| runtimes.borrow_mut().insert(Runtime::new()))
RUNTIMES.with(|runtimes| runtimes.write().insert(Runtime::new()))
}
}
}
@@ -75,7 +75,7 @@ impl RuntimeId {
pub fn dispose(self) {
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
let runtime = RUNTIMES.with(move |runtimes| runtimes.borrow_mut().remove(self));
let runtime = RUNTIMES.with(move |runtimes| runtimes.write().remove(self));
drop(runtime);
}
}
@@ -83,7 +83,7 @@ impl RuntimeId {
pub(crate) fn raw_scope_and_disposer(self) -> (Scope, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let id = { runtime.scopes.write().insert(Default::default()) };
let scope = Scope { runtime: self, id };
let disposer = ScopeDisposer(Box::new(move || scope.dispose()));
(scope, disposer)
@@ -97,9 +97,9 @@ impl RuntimeId {
parent: Option<Scope>,
) -> (T, ScopeId, ScopeDisposer) {
with_runtime(self, |runtime| {
let id = { runtime.scopes.borrow_mut().insert(Default::default()) };
let id = { runtime.scopes.write().insert(Default::default()) };
if let Some(parent) = parent {
runtime.scope_parents.borrow_mut().insert(id, parent.id);
runtime.scope_parents.write().insert(id, parent.id);
}
let scope = Scope { runtime: self, id };
let val = f(scope);
@@ -121,10 +121,7 @@ impl RuntimeId {
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
(
@@ -150,10 +147,7 @@ impl RuntimeId {
T: Any + 'static,
{
let id = with_runtime(self, |runtime| {
runtime
.signals
.borrow_mut()
.insert(Rc::new(RefCell::new(value)))
runtime.signals.write().insert(Arc::new(RwLock::new(value)))
})
.expect("tried to create a signal in a runtime that has been disposed");
RwSignal {
@@ -176,11 +170,11 @@ impl RuntimeId {
with_runtime(self, |runtime| {
let effect = Effect {
f,
value: RefCell::new(None),
value: RwLock::new(None),
#[cfg(debug_assertions)]
defined_at,
};
let id = { runtime.effects.borrow_mut().insert(Rc::new(effect)) };
let id = { runtime.effects.write().insert(Arc::new(effect)) };
id.run::<T>(self);
id
})
@@ -219,33 +213,63 @@ impl RuntimeId {
#[derive(Default)]
pub(crate) struct Runtime {
pub shared_context: RefCell<SharedContext>,
pub observer: Cell<Option<EffectId>>,
pub scopes: RefCell<SlotMap<ScopeId, RefCell<Vec<ScopeProperty>>>>,
pub scope_parents: RefCell<SparseSecondaryMap<ScopeId, ScopeId>>,
pub scope_children: RefCell<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
pub shared_context: RwLock<SharedContext>,
pub scopes: RwLock<SlotMap<ScopeId, RwLock<Vec<ScopeProperty>>>>,
pub scope_parents: RwLock<SparseSecondaryMap<ScopeId, ScopeId>>,
pub scope_children: RwLock<SparseSecondaryMap<ScopeId, Vec<ScopeId>>>,
#[allow(clippy::type_complexity)]
pub scope_contexts: RefCell<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
pub scope_contexts: RwLock<SparseSecondaryMap<ScopeId, HashMap<TypeId, Box<dyn Any>>>>,
#[allow(clippy::type_complexity)]
pub scope_cleanups: RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
pub scope_cleanups: RwLock<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RwLock<SlotMap<SignalId, Arc<RwLock<dyn Any>>>>,
pub signal_subscribers: RwLock<SecondaryMap<SignalId, RwLock<HashSet<EffectId>>>>,
pub effects: RwLock<SlotMap<EffectId, Arc<dyn AnyEffect>>>,
pub effect_sources: RwLock<SecondaryMap<EffectId, RwLock<HashSet<SignalId>>>>,
pub resources: RwLock<SlotMap<ResourceId, AnyResource>>,
}
// track current observer thread-locally
// because effects run synchronously, the current observer
// *in this thread* will not change during the execution of an effect.
// but if we track this across threads, it's possible for overlapping
// executions to cause the stack to be out of order
// so we store at most one current observer per runtime, per thread
thread_local! {
static OBSERVER: RefCell<HashMap<RuntimeId, EffectId>> = Default::default();
}
pub(crate) struct LocalObserver {}
impl LocalObserver {
pub fn take(runtime: RuntimeId) -> Option<EffectId> {
OBSERVER.with(|observer| observer.borrow_mut().remove(&runtime))
}
pub fn get(runtime: RuntimeId) -> Option<EffectId> {
OBSERVER.with(|observer| observer.borrow().get(&runtime).copied())
}
pub fn set(runtime: RuntimeId, effect: Option<EffectId>) {
OBSERVER.with(|observer| {
if let Some(value) = effect {
observer.borrow_mut().insert(runtime, value)
} else {
observer.borrow_mut().remove(&runtime)
}
});
}
}
impl Debug for Runtime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runtime")
.field("shared_context", &self.shared_context)
.field("observer", &self.observer)
.field("scopes", &self.scopes)
.field("scope_parents", &self.scope_parents)
.field("scope_children", &self.scope_children)
.field("signals", &self.signals)
.field("signal_subscribers", &self.signal_subscribers)
.field("effects", &self.effects.borrow().len())
.field("effects", &self.effects.read().len())
.field("effect_sources", &self.effect_sources)
.finish()
}
@@ -258,27 +282,27 @@ impl Runtime {
pub(crate) fn create_unserializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
state: Arc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: 'static,
{
self.resources
.borrow_mut()
.write()
.insert(AnyResource::Unserializable(state))
}
pub(crate) fn create_serializable_resource<S, T>(
&self,
state: Rc<ResourceState<S, T>>,
state: Arc<ResourceState<S, T>>,
) -> ResourceId
where
S: Clone + 'static,
T: Serializable + 'static,
{
self.resources
.borrow_mut()
.write()
.insert(AnyResource::Serializable(state))
}
@@ -291,7 +315,7 @@ impl Runtime {
S: 'static,
T: 'static,
{
let resources = self.resources.borrow();
let resources = self.resources.read();
let res = resources.get(id);
if let Some(res) = res {
let res_state = match res {
@@ -317,7 +341,7 @@ impl Runtime {
/// Returns IDs for all [Resource]s found on any scope.
pub(crate) fn all_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()
.read()
.iter()
.map(|(resource_id, _)| resource_id)
.collect()
@@ -326,7 +350,7 @@ impl Runtime {
/// Returns IDs for all [Resource]s found on any scope, pending from the server.
pub(crate) fn pending_resources(&self) -> Vec<ResourceId> {
self.resources
.borrow()
.read()
.iter()
.filter_map(|(resource_id, res)| {
if matches!(res, AnyResource::Serializable(_)) {
@@ -342,7 +366,7 @@ impl Runtime {
&self,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
let f = FuturesUnordered::new();
for (id, resource) in self.resources.borrow().iter() {
for (id, resource) in self.resources.read().iter() {
if let AnyResource::Serializable(resource) = resource {
f.push(resource.to_serialization_resolver(id));
}

View File

@@ -1,6 +1,6 @@
#![forbid(unsafe_code)]
use crate::{
runtime::{with_runtime, RuntimeId},
runtime::{with_runtime, LocalObserver, RuntimeId},
EffectId, PinnedFuture, ResourceId, SignalId, SuspenseContext,
};
use futures::stream::FuturesUnordered;
@@ -108,7 +108,7 @@ impl Scope {
pub fn run_child_scope<T>(self, f: impl FnOnce(Scope) -> T) -> (T, ScopeDisposer) {
let (res, child_id, disposer) = self.runtime.run_scope_undisposed(f, Some(self));
_ = with_runtime(self.runtime, |runtime| {
let mut children = runtime.scope_children.borrow_mut();
let mut children = runtime.scope_children.write();
children
.entry(self.id)
.expect("trying to add a child to a Scope that has already been disposed")
@@ -145,9 +145,9 @@ impl Scope {
/// ```
pub fn untrack<T>(&self, f: impl FnOnce() -> T) -> T {
with_runtime(self.runtime, |runtime| {
let prev_observer = runtime.observer.take();
let prev_observer = LocalObserver::take(self.runtime);
let untracked_result = f();
runtime.observer.set(prev_observer);
LocalObserver::set(self.runtime, prev_observer);
untracked_result
})
.expect("tried to run untracked function in a runtime that has been disposed")
@@ -167,7 +167,7 @@ impl Scope {
_ = with_runtime(self.runtime, |runtime| {
// dispose of all child scopes
let children = {
let mut children = runtime.scope_children.borrow_mut();
let mut children = runtime.scope_children.write();
children.remove(self.id)
};
@@ -182,7 +182,7 @@ impl Scope {
}
// run cleanups
if let Some(cleanups) = runtime.scope_cleanups.borrow_mut().remove(self.id) {
if let Some(cleanups) = runtime.scope_cleanups.write().remove(self.id) {
for cleanup in cleanups {
cleanup();
}
@@ -190,34 +190,34 @@ impl Scope {
// remove everything we own and run cleanups
let owned = {
let owned = runtime.scopes.borrow_mut().remove(self.id);
owned.map(|owned| owned.take())
let owned = runtime.scopes.write().remove(self.id);
owned.map(|owned| std::mem::take(&mut *owned.write()))
};
if let Some(owned) = owned {
for property in owned {
match property {
ScopeProperty::Signal(id) => {
// remove the signal
runtime.signals.borrow_mut().remove(id);
let subs = runtime.signal_subscribers.borrow_mut().remove(id);
runtime.signals.write().remove(id);
let subs = runtime.signal_subscribers.write().remove(id);
// each of the subs needs to remove the signal from its dependencies
// so that it doesn't try to read the (now disposed) signal
if let Some(subs) = subs {
let source_map = runtime.effect_sources.borrow();
for effect in subs.borrow().iter() {
let source_map = runtime.effect_sources.read();
for effect in subs.read().iter() {
if let Some(effect_sources) = source_map.get(*effect) {
effect_sources.borrow_mut().remove(&id);
effect_sources.write().remove(&id);
}
}
}
}
ScopeProperty::Effect(id) => {
runtime.effects.borrow_mut().remove(id);
runtime.effect_sources.borrow_mut().remove(id);
runtime.effects.write().remove(id);
runtime.effect_sources.write().remove(id);
}
ScopeProperty::Resource(id) => {
runtime.resources.borrow_mut().remove(id);
runtime.resources.write().remove(id);
}
}
}
@@ -227,11 +227,24 @@ impl Scope {
pub(crate) fn with_scope_property(&self, f: impl FnOnce(&mut Vec<ScopeProperty>)) {
_ = with_runtime(self.runtime, |runtime| {
let scopes = runtime.scopes.borrow();
let scopes = runtime.scopes.read();
let scope = scopes
.get(self.id)
.expect("tried to add property to a scope that has been disposed");
f(&mut scope.borrow_mut());
f(&mut scope.write());
})
}
/// Returns the the parent Scope, if any.
pub fn parent(&self) -> Option<Scope> {
with_runtime(self.runtime, |runtime| {
runtime.scope_parents.read().get(self.id).copied()
})
.ok()
.flatten()
.map(|id| Scope {
runtime: self.runtime,
id,
})
}
}
@@ -242,7 +255,7 @@ impl Scope {
/// are invalidated.
pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
_ = with_runtime(cx.runtime, |runtime| {
let mut cleanups = runtime.scope_cleanups.borrow_mut();
let mut cleanups = runtime.scope_cleanups.write();
let cleanups = cleanups
.entry(cx.id)
.expect("trying to clean up a Scope that has already been disposed")
@@ -314,7 +327,7 @@ impl Scope {
use futures::StreamExt;
_ = with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let mut shared_context = runtime.shared_context.write();
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
@@ -337,12 +350,12 @@ impl Scope {
})
}
/// The set of all HTML fragments current pending, by their keys (see [Self::current_fragment_key]).
/// The set of all HTML fragments currently pending.
/// Returns a tuple of the hydration ID of the previous element, and a pinned `Future` that will yield the
/// `<Suspense/>` HTML when all resources are resolved.
pub fn pending_fragments(&self) -> HashMap<String, (String, PinnedFuture<String>)> {
with_runtime(self.runtime, |runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let mut shared_context = runtime.shared_context.write();
std::mem::take(&mut shared_context.pending_fragments)
})
.unwrap_or_default()

View File

@@ -1,7 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
debug_warn,
runtime::{with_runtime, RuntimeId},
runtime::{with_runtime, LocalObserver, RuntimeId},
Runtime, Scope, ScopeProperty, UntrackedGettableSignal, UntrackedSettableSignal,
};
use cfg_if::cfg_if;
@@ -246,7 +246,9 @@ where
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
_ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
_ = with_runtime(self.runtime, |runtime| {
self.id.subscribe(self.runtime, runtime)
})
}
/// Clones and returns the current value of the signal, and subscribes
@@ -286,14 +288,17 @@ where
/// Applies the function to the current Signal, if it exists, and subscribes
/// the running effect.
pub(crate) fn try_with<U>(&self, f: impl FnOnce(&T) -> U) -> Result<U, SignalError> {
match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f)) {
match with_runtime(self.runtime, |runtime| {
self.id.try_with(self.runtime, runtime, f)
}) {
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e),
Err(_) => Err(SignalError::RuntimeDisposed),
}
}
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
/// Generates a [Stream](futures::stream::Stream) that emits the new value of the signal
/// whenever it changes.
pub fn to_stream(&self) -> impl Stream<Item = T>
where
T: Clone,
@@ -1096,7 +1101,8 @@ where
)
}
/// Generates a [Stream] that emits the new value of the signal whenever it changes.
/// Generates a [Stream](futures::stream::Stream) that emits the new value of the signal
/// whenever it changes.
#[cfg_attr(
debug_assertions,
instrument(
@@ -1167,20 +1173,20 @@ pub(crate) enum SignalError {
}
impl SignalId {
pub(crate) fn subscribe(&self, runtime: &Runtime) {
pub(crate) fn subscribe(&self, runtime_id: RuntimeId, runtime: &Runtime) {
// add subscriber
if let Some(observer) = runtime.observer.get() {
if let Some(observer) = LocalObserver::get(runtime_id) {
// add this observer to the signal's dependencies (to allow notification)
let mut subs = runtime.signal_subscribers.borrow_mut();
let mut subs = runtime.signal_subscribers.write();
if let Some(subs) = subs.entry(*self) {
subs.or_default().borrow_mut().insert(observer);
subs.or_default().write().insert(observer);
}
// add this signal to the effect's sources (to allow cleanup)
let mut effect_sources = runtime.effect_sources.borrow_mut();
let mut effect_sources = runtime.effect_sources.write();
if let Some(effect_sources) = effect_sources.entry(observer) {
let sources = effect_sources.or_default();
sources.borrow_mut().insert(*self);
sources.write().insert(*self);
}
}
}
@@ -1195,7 +1201,7 @@ impl SignalId {
{
// get the value
let value = {
let signals = runtime.signals.borrow();
let signals = runtime.signals.read();
match signals.get(*self).cloned().ok_or(SignalError::Disposed) {
Ok(s) => Ok(s),
Err(e) => {
@@ -1204,12 +1210,13 @@ impl SignalId {
}
}
}?;
let value = value.try_borrow().unwrap_or_else(|e| {
debug_warn!(
"Signal::try_with_no_subscription failed on Signal<{}>. It seems you're trying to read the value of a signal within an effect caused by updating the signal.",
let value = value.try_read().unwrap_or_else(|| {
panic!(
"Signal::try_with_no_subscription failed on Signal<{}>. \
It seems you're trying to read the value of a signal within an effect \
caused by updating the signal.",
std::any::type_name::<T>()
);
panic!("{e}");
});
let value = value
.downcast_ref::<T>()
@@ -1219,13 +1226,14 @@ impl SignalId {
pub(crate) fn try_with<T, U>(
&self,
runtime_id: RuntimeId,
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError>
where
T: 'static,
{
self.subscribe(runtime);
self.subscribe(runtime_id, runtime);
self.try_with_no_subscription(runtime, f)
}
@@ -1244,12 +1252,14 @@ impl SignalId {
.expect("tried to access a signal in a runtime that has been disposed")
}
pub(crate) fn with<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&T) -> U) -> U
pub(crate) fn with<T, U>(&self, runtime_id: RuntimeId, f: impl FnOnce(&T) -> U) -> U
where
T: 'static,
{
with_runtime(runtime, |runtime| self.try_with(runtime, f).unwrap())
.expect("tried to access a signal in a runtime that has been disposed")
with_runtime(runtime_id, |runtime| {
self.try_with(runtime_id, runtime, f).unwrap()
})
.expect("tried to access a signal in a runtime that has been disposed")
}
fn update_value<T, U>(&self, runtime: RuntimeId, f: impl FnOnce(&mut T) -> U) -> Option<U>
@@ -1258,11 +1268,11 @@ impl SignalId {
{
with_runtime(runtime, |runtime| {
let value = {
let signals = runtime.signals.borrow();
let signals = runtime.signals.read();
signals.get(*self).cloned()
};
if let Some(value) = value {
let mut value = value.borrow_mut();
let mut value = value.write();
if let Some(value) = value.downcast_mut::<T>() {
Some(f(value))
} else {
@@ -1298,14 +1308,14 @@ impl SignalId {
// notify subscribers
if updated.is_some() {
let subs = {
let subs = runtime.signal_subscribers.borrow();
let subs = runtime.signal_subscribers.read();
let subs = subs.get(*self);
subs.map(|subs| subs.borrow().clone())
subs.map(|subs| subs.read().clone())
};
if let Some(subs) = subs {
for sub in subs {
let effect = {
let effects = runtime.effects.borrow();
let effects = runtime.effects.read();
effects.get(sub).cloned()
};
if let Some(effect) = effect {

View File

@@ -574,6 +574,12 @@ impl<T> From<Memo<T>> for MaybeSignal<T> {
}
}
impl<T> From<Signal<T>> for MaybeSignal<T> {
fn from(value: Signal<T>) -> Self {
Self::Dynamic(value)
}
}
#[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for MaybeSignal<T>
where

View File

@@ -19,7 +19,7 @@ fn untracked_set_doesnt_trigger_effect() {
create_isomorphic_effect(cx, {
let b = b.clone();
move |_| {
let formatted = format!("Value is {}", a());
let formatted = format!("Value is {}", a.get());
*b.borrow_mut() = formatted;
}
});
@@ -52,7 +52,7 @@ fn untracked_get_doesnt_trigger_effect() {
create_isomorphic_effect(cx, {
let b = b.clone();
move |_| {
let formatted = format!("Values are {} and {}", a(), a2.get_untracked());
let formatted = format!("Values are {} and {}", a.get(), a2.get_untracked());
*b.borrow_mut() = formatted;
}
});

View File

@@ -28,7 +28,7 @@
//!
//! ### `#[server]`
//!
//! The [`#[server]` macro](leptos::leptos_macro::server) allows you to annotate a function to
//! The [`#[server]`](https://docs.rs/leptos/latest/leptos/attr.server.html) macro allows you to annotate a function to
//! indicate that it should only run on the server (i.e., when you have an `ssr` feature in your
//! crate that is enabled).
//!
@@ -326,9 +326,19 @@ where
let mut write = REGISTERED_SERVER_FUNCTIONS
.write()
.map_err(|e| ServerFnError::Registration(e.to_string()))?;
write.insert(Self::url(), run_server_fn);
let prev = write.insert(Self::url(), run_server_fn);
Ok(())
// if there was already a server function with this key,
// return Err
match prev {
Some(_) => Err(ServerFnError::Registration(format!(
"There was already a server function registered at {:?}. \
This can happen if you use the same server function name in two different modules
on `stable` or in `release` mode.",
Self::url()
))),
None => Ok(()),
}
}
}

View File

@@ -34,8 +34,16 @@
//! }
//!
//! }
//!
//! ```
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
//! of calling signals as functions. Enable this feature to support `stable` Rust.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
use cfg_if::cfg_if;
use std::{

View File

@@ -20,7 +20,7 @@ log = "0.4"
regex = { version = "1", optional = true }
bincode = "1"
url = { version = "2", optional = true }
urlencoding = "2"
percent-encoding = "2"
thiserror = "1"
serde_urlencoded = "0.7"
serde = "1"

View File

@@ -11,7 +11,7 @@ pub fn Outlet(cx: Scope) -> impl IntoView {
let is_showing = Rc::new(Cell::new(None::<(usize, Scope)>));
let (outlet, set_outlet) = create_signal(cx, None::<View>);
create_isomorphic_effect(cx, move |_| {
match (route.child(), &is_showing.get()) {
match (route.child(cx), &is_showing.get()) {
(None, prev) => {
if let Some(prev_scope) = prev.map(|(_, scope)| scope) {
prev_scope.dispose();

View File

@@ -79,7 +79,7 @@ impl RouteContext {
pub(crate) fn new(
cx: Scope,
router: &RouterContext,
child: impl Fn() -> Option<RouteContext> + 'static,
child: impl Fn(Scope) -> Option<RouteContext> + 'static,
matcher: impl Fn() -> Option<RouteMatch> + 'static,
) -> Option<Self> {
let base = router.base();
@@ -99,7 +99,7 @@ impl RouteContext {
inner: Rc::new(RouteContextInner {
cx,
id,
base_path: base.to_string(),
base_path: base,
child: Box::new(child),
path: RefCell::new(path),
original_path: route.original_path.to_string(),
@@ -151,7 +151,7 @@ impl RouteContext {
cx,
id: 0,
base_path: path.to_string(),
child: Box::new(|| None),
child: Box::new(|_| None),
path: RefCell::new(path.to_string()),
original_path: path.to_string(),
params: create_memo(cx, |_| ParamsMap::new()),
@@ -166,8 +166,8 @@ impl RouteContext {
}
/// The nested child route, if any.
pub fn child(&self) -> Option<RouteContext> {
(self.inner.child)()
pub fn child(&self, cx: Scope) -> Option<RouteContext> {
(self.inner.child)(cx)
}
/// The view associated with the current route.
@@ -180,7 +180,7 @@ pub(crate) struct RouteContextInner {
cx: Scope,
base_path: String,
pub(crate) id: usize,
pub(crate) child: Box<dyn Fn() -> Option<RouteContext>>,
pub(crate) child: Box<dyn Fn(Scope) -> Option<RouteContext>>,
pub(crate) path: RefCell<String>,
pub(crate) original_path: String,
pub(crate) params: Memo<ParamsMap>,
@@ -202,7 +202,6 @@ impl std::fmt::Debug for RouteContextInner {
f.debug_struct("RouteContextInner")
.field("path", &self.path)
.field("ParamsMap", &self.params)
.field("child", &(self.child)())
.finish()
}
}

View File

@@ -216,7 +216,6 @@ impl RouterContextInner {
match resolved_to {
None => Err(NavigationError::NotRoutable(to.to_string())),
Some(resolved_to) => {
let resolved_to = resolved_to.to_string();
if self.referrers.borrow().len() > 32 {
return Err(NavigationError::MaxRedirects);
}

View File

@@ -24,10 +24,8 @@ pub fn Routes(
#[prop(optional)] base: Option<String>,
children: Box<dyn FnOnce(Scope) -> Fragment>,
) -> impl IntoView {
let router = use_context::<RouterContext>(cx).unwrap_or_else(|| {
log::warn!("<Routes/> component should be nested within a <Router/>.");
panic!()
});
let router = use_context::<RouterContext>(cx)
.expect("<Routes/> component should be nested within a <Router/>.");
let base_route = router.base();
let mut branches = Vec::new();
@@ -86,6 +84,8 @@ pub fn Routes(
.map(|prev_matches| next_matches.len() == prev_matches.len())
.unwrap_or(false);
let prev_cx = Rc::new(Cell::new(cx));
for i in 0..next_matches.len() {
let next = next.clone();
let prev_match = prev_matches.and_then(|p| p.get(i));
@@ -112,17 +112,19 @@ pub fn Routes(
root_equal.set(false);
}
let disposer = cx.child_scope({
let disposer = prev_cx.get().child_scope({
let next = next.clone();
let router = Rc::clone(&router.inner);
let prev_cx = Rc::clone(&prev_cx);
move |cx| {
prev_cx.set(cx);
let next = next.clone();
let next_ctx = RouteContext::new(
cx,
&RouterContext { inner: router },
{
let next = next.clone();
move || {
move |cx| {
if let Some(route_states) =
use_context::<Memo<RouterState>>(cx)
{

View File

@@ -19,7 +19,7 @@ pub fn create_location(cx: Scope, path: ReadSignal<String>, state: ReadSignal<St
let pathname = create_memo(cx, move |_| url.with(|url| url.pathname.clone()));
let search = create_memo(cx, move |_| url.with(|url| url.search.clone()));
let hash = create_memo(cx, move |_| url.with(|url| url.hash.clone()));
let query = create_memo(cx, move |_| url.with(|url| url.search_params()));
let query = create_memo(cx, move |_| url.with(|url| url.search_params.clone()));
Location {
pathname,

View File

@@ -61,7 +61,7 @@ impl Default for ParamsMap {
/// ```
/// # use leptos_router::params_map;
/// let map = params_map! {
/// "id".to_string() => "1".to_string()
/// "id" => "1"
/// };
/// assert_eq!(map.get("id"), Some(&"1".to_string()));
/// assert_eq!(map.get("missing"), None)
@@ -78,7 +78,7 @@ macro_rules! params_map {
let start_capacity = common_macros::const_expr_count!($($key);*);
#[allow(unused_mut)]
let mut map = linear_map::LinearMap::with_capacity(start_capacity);
$( map.insert($key, $val); )*
$( map.insert($key.to_string(), $val.to_string()); )*
$crate::ParamsMap(map)
});
}

View File

@@ -1,41 +1,20 @@
use crate::ParamsMap;
#[cfg(not(feature = "ssr"))]
use js_sys::{try_iter, Array, JsString};
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsCast;
#[cfg(not(feature = "ssr"))]
use wasm_bindgen::JsValue;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Url {
pub origin: String,
pub pathname: String,
pub search: String,
pub search_params: ParamsMap,
pub hash: String,
}
impl Url {
pub fn search_params(&self) -> ParamsMap {
let map = self
.search
.trim_start_matches('?')
.split('&')
.filter_map(|piece| {
let mut parts = piece.split('=');
let (k, v) = (parts.next(), parts.next());
match k {
Some(k) if !k.is_empty() => {
Some((unescape(k), unescape(v.unwrap_or_default())))
}
_ => None,
}
})
.collect::<linear_map::LinearMap<String, String>>();
ParamsMap(map)
}
}
#[cfg(feature = "ssr")]
pub fn unescape(s: &str) -> String {
urlencoding::decode(s)
.unwrap_or_else(|_| std::borrow::Cow::from(s))
.replace('+', " ")
}
#[cfg(not(feature = "ssr"))]
pub fn unescape(s: &str) -> String {
js_sys::decode_uri(s).unwrap().into()
@@ -43,7 +22,7 @@ pub fn unescape(s: &str) -> String {
#[cfg(feature = "ssr")]
pub fn escape(s: &str) -> String {
urlencoding::encode(s).into()
percent_encoding::utf8_percent_encode(s, percent_encoding::NON_ALPHANUMERIC).to_string()
}
#[cfg(not(feature = "ssr"))]
@@ -57,12 +36,24 @@ impl TryFrom<&str> for Url {
fn try_from(url: &str) -> Result<Self, Self::Error> {
let fake_host = String::from("http://leptos");
let url = web_sys::Url::new_with_base(url, &fake_host)
.map_err(|e| e.as_string().unwrap_or_default())?;
let url = web_sys::Url::new_with_base(url, &fake_host).map_js_error()?;
Ok(Self {
origin: url.origin(),
pathname: url.pathname(),
search: url.search(),
search_params: ParamsMap(
try_iter(&url.search_params())
.map_js_error()?
.ok_or("Failed to use URLSearchParams as an iterator".to_string())?
.map(|value| {
let array: Array = value.map_js_error()?.dyn_into().map_js_error()?;
Ok((
array.get(0).dyn_into::<JsString>().map_js_error()?.into(),
array.get(1).dyn_into::<JsString>().map_js_error()?.into(),
))
})
.collect::<Result<linear_map::LinearMap<String, String>, Self::Error>>()?,
),
hash: url.hash(),
})
}
@@ -78,7 +69,24 @@ impl TryFrom<&str> for Url {
origin: url.origin().unicode_serialization(),
pathname: url.path().to_string(),
search: url.query().unwrap_or_default().to_string(),
search_params: ParamsMap(
url.query_pairs()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect::<linear_map::LinearMap<String, String>>(),
),
hash: Default::default(),
})
}
}
#[cfg(not(feature = "ssr"))]
trait MapJsError<T> {
fn map_js_error(self) -> Result<T, String>;
}
#[cfg(not(feature = "ssr"))]
impl<T> MapJsError<T> for Result<T, JsValue> {
fn map_js_error(self) -> Result<T, String> {
self.map_err(|e| e.as_string().unwrap_or_default())
}
}

View File

@@ -180,6 +180,16 @@
//! }
//! }
//! ```
//!
//! # Feature Flags
//! - `csr` Client-side rendering: Generate DOM nodes in the browser
//! - `ssr` Server-side rendering: Generate an HTML string (typically on the server)
//! - `hydrate` Hydration: use this to add interactivity to an SSRed Leptos app
//! - `stable` By default, Leptos requires `nightly` Rust, which is what allows the ergonomics
//! of calling signals as functions. Enable this feature to support `stable` Rust.
//!
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]

View File

@@ -36,7 +36,7 @@ cfg_if! {
Some(PathMatch {
path: "/foo/abc-123".into(),
params: params_map!(
"id".into() => "abc-123".into()
"id" => "abc-123"
)
})
);
@@ -72,7 +72,7 @@ cfg_if! {
Some(PathMatch {
path: "/foo/bar".into(),
params: params_map!(
"something".into() => "baz/qux".into()
"something" => "baz/qux"
)
})
);
@@ -87,7 +87,7 @@ cfg_if! {
Some(PathMatch {
path: "/foo/bar".into(),
params: params_map!(
"something".into() => "".into()
"something" => ""
)
})
);

View File

@@ -0,0 +1,52 @@
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use leptos_router::{Url, params_map};
macro_rules! assert_params_map {
([$($key:expr => $val:expr),*] , $actual:expr) => (
assert_eq!(params_map!($($key => $val),*), $actual)
);
}
#[test]
fn test_param_with_plus_sign() {
let url = Url::try_from("http://leptos.com?data=1%2B2%3D3").unwrap();
assert_params_map!{
["data" => "1+2=3"],
url.search_params
};
}
#[test]
fn test_param_with_ampersand() {
let url = Url::try_from("http://leptos.com?data=true+%26+false+%3D+false").unwrap();
assert_params_map!{
["data" => "true & false = false"],
url.search_params
};
}
#[test]
fn test_complext_query_string() {
let url = Url::try_from("http://leptos.com?data=Data%3A+%24+%26+%2B%2B+7").unwrap();
assert_params_map!{
["data" => "Data: $ & ++ 7"],
url.search_params
};
}
#[test]
fn test_multiple_query_params() {
let url = Url::try_from("http://leptos.com?param1=value1&param2=value2").unwrap();
assert_params_map!{
[
"param1" => "value1",
"param2" => "value2"
],
url.search_params
};
}
}
}