mirror of
https://github.com/chenasraf/leptos.git
synced 2026-05-18 01:49:06 +00:00
Merge branch 'main' into pr/119
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ blob.rs
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.leptos.kdl
|
||||
|
||||
29
Cargo.toml
29
Cargo.toml
@@ -3,6 +3,8 @@ members = [
|
||||
# core
|
||||
"leptos",
|
||||
"leptos_dom",
|
||||
"leptos_core",
|
||||
"leptos_config",
|
||||
"leptos_macro",
|
||||
"leptos_reactive",
|
||||
"leptos_server",
|
||||
@@ -42,4 +44,29 @@ lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.metadata.cargo-all-features]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
46
README.md
46
README.md
@@ -54,6 +54,17 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
||||
- **Fine-grained reactivity**: The entire framework is build from reactive primitives. This allows for extremely performant code with minimal overhead: when a reactive signal’s value changes, it can update a single text node, toggle a single class, or remove an element from the DOM without any other code running. (_So, no virtual DOM!_)
|
||||
- **Declarative**: Tell Leptos how you want the page to look, and let the framework tell the browser how to do it.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The best way to get started with a Leptos project right now is to use the [`cargo-leptos`](https://github.com/akesson/cargo-leptos) build tool and our [starter template](https://github.com/leptos-rs/start).
|
||||
|
||||
```bash
|
||||
cargo install cargo-leptos
|
||||
cargo leptos new --git https://github.com/leptos-rs/start
|
||||
cd [your project name]
|
||||
cargo leptos watch
|
||||
```
|
||||
|
||||
## Learn more
|
||||
|
||||
Here are some resources for learning more about Leptos:
|
||||
@@ -65,7 +76,18 @@ Here are some resources for learning more about Leptos:
|
||||
|
||||
## `nightly` Note
|
||||
|
||||
Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
Most of the examples assume you’re using `nightly` Rust.
|
||||
To set up your rustup toolchain using nightly and
|
||||
add the ability to compile Rust to WebAssembly:
|
||||
|
||||
```
|
||||
rustup toolchain install nightly
|
||||
rustup default nightly
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
|
||||
If you’re on stable, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.0", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
@@ -136,17 +158,17 @@ There are some practical differences that make a significant difference:
|
||||
- **Read-write segregation:** Leptos, like Solid, encourages read-write segregation between signal getters and setters, so you end up accessing signals with tuples like `let (count, set_count) = create_signal(cx, 0);` _(If you prefer or if it's more convenient for your API, you can use `create_rw_signal` to give a unified read/write signal.)_
|
||||
- **Signals are functions:** In Leptos, you can call a signal to access it rather than calling a specific method (so, `count()` instead of `count.get()`) This creates a more consistent mental model: accessing a reactive value is always a matter of calling a function. For example:
|
||||
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0); // a signal
|
||||
let double_count = move || count() * 2; // a derived signal
|
||||
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
|
||||
// all are accessed by calling them
|
||||
assert_eq!(count(), 0);
|
||||
assert_eq!(double_count(), 0);
|
||||
assert_eq!(memoized_count(), 0);
|
||||
```rust
|
||||
let (count, set_count) = create_signal(cx, 0); // a signal
|
||||
let double_count = move || count() * 2; // a derived signal
|
||||
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
|
||||
// all are accessed by calling them
|
||||
assert_eq!(count(), 0);
|
||||
assert_eq!(double_count(), 0);
|
||||
assert_eq!(memoized_count(), 0);
|
||||
|
||||
// this function can accept any of those signals
|
||||
fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
|
||||
```
|
||||
// this function can accept any of those signals
|
||||
fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
|
||||
```
|
||||
|
||||
- **Signals and scopes are `'static`:** Both Leptos and Sycamore ease the pain of moving signals in closures (in particular, event listeners) by making them `Copy`, to avoid the `{ let count = count.clone(); move |_| ... }` that's very familiar in Rust UI code. Sycamore does this by using bump allocation to tie the lifetimes of its signals to its scopes: since references are `Copy`, `&'a Signal<T>` can be moved into a closure. Leptos does this by using arena allocation and passing around indices: types like `ReadSignal<T>`, `WriteSignal<T>`, and `Memo<T>` are actually wrapper for indices into an arena. This means that both scopes and signals are both `Copy` and `'static` in Leptos, which means that they can be moved easily into closures without adding lifetime complexity.
|
||||
|
||||
@@ -121,21 +121,21 @@ pub fn TodoMVC(cx: Scope,todos: Todos) -> impl IntoView {
|
||||
set_mode(new_mode);
|
||||
});
|
||||
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
}
|
||||
}
|
||||
};
|
||||
let add_todo = move |ev: web_sys::KeyboardEvent| {
|
||||
let target = event_target::<HtmlInputElement>(&ev);
|
||||
ev.stop_propagation();
|
||||
let key_code = ev.unchecked_ref::<web_sys::KeyboardEvent>().key_code();
|
||||
if key_code == ENTER_KEY {
|
||||
let title = event_target_value(&ev);
|
||||
let title = title.trim();
|
||||
if !title.is_empty() {
|
||||
let new = Todo::new(cx, next_id, title.to_string());
|
||||
set_todos.update(|t| t.add(new));
|
||||
next_id += 1;
|
||||
target.set_value("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
|
||||
todos.with(|todos| match mode.get() {
|
||||
@@ -227,10 +227,10 @@ 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);
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> Element {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
let input: Element;
|
||||
|
||||
let save = move |value: &str| {
|
||||
let value = value.trim();
|
||||
|
||||
@@ -10,7 +10,7 @@ This document is intended as a running list of common issues, with example code
|
||||
|
||||
```rust
|
||||
let (a, set_a) = create_signal(cx, 0);
|
||||
let (b, set_a) = create_signal(cx, false);
|
||||
let (b, set_b) = create_signal(cx, false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if a() > 5 {
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -4,7 +4,7 @@ fn main() {
|
||||
mount_to_body(|cx| {
|
||||
let name = "gbj";
|
||||
let userid = 0;
|
||||
let _input_element: Element;
|
||||
let _input_element = NodeRef::new(cx);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = "0.0.18"
|
||||
leptos = { path = "../../../../leptos" }
|
||||
|
||||
@@ -10,7 +10,7 @@ To run it as a server side app with hydration, first you should run
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the Webassembly to provide hydration features for the server.
|
||||
to generate the WebAssembly to provide hydration features for the server.
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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/)
|
||||
|
||||
10
examples/counters-stable/README.md
Normal file
10
examples/counters-stable/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos Counters Example on Rust Stable
|
||||
|
||||
This example showcases a basic Leptos app with many counters. It is a good example of how to setup a basic reactive app with signals and effects, and how to interact with browser events. Unlike the other counters example, it will compile on Rust stable, because it has the `stable` feature enabled.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
10
examples/counters/README.md
Normal file
10
examples/counters/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos Counters Example
|
||||
|
||||
This example showcases a basic Leptos app with many counters. It is a good example of how to set up a basic reactive app with signals and effects, and how to interact with browser events.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -11,7 +11,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
log = "0.4"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
gloo-timers = { version = "0.2", features = ["futures"] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.0"
|
||||
|
||||
|
||||
10
examples/fetch/README.md
Normal file
10
examples/fetch/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Client Side Fetch
|
||||
|
||||
This example shows how to fetch data from the client in WebAssembly.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -7,6 +10,10 @@ pub struct Cat {
|
||||
}
|
||||
|
||||
async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
// artificial delay
|
||||
// the cat API is too fast to show the transition
|
||||
TimeoutFuture::new(500).await;
|
||||
|
||||
if count > 0 {
|
||||
let res = reqwasm::http::Request::get(&format!(
|
||||
"https://api.thecatapi.com/v1/images/search?limit={}",
|
||||
@@ -30,8 +37,9 @@ async fn fetch_cats(count: u32) -> Result<Vec<String>, ()> {
|
||||
pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
view! { cx,
|
||||
view! { cx,
|
||||
<div>
|
||||
<label>
|
||||
"How many cats would you like?"
|
||||
@@ -43,25 +51,33 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<Transition fallback=move || view! { cx, <div>"Loading (Suspense Fallback)..."</div>}>
|
||||
{move || {
|
||||
cats.read().map(|data| match data {
|
||||
Err(_) => view! { cx, <pre>"Error"</pre> }.into_view(cx),
|
||||
Ok(cats) => view! { cx,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}</div>
|
||||
}.into_view(cx),
|
||||
})
|
||||
{move || pending().then(|| view! { cx, <p>"Loading more cats..."</p> })}
|
||||
<div>
|
||||
// <Transition/> holds the previous value while new async data is being loaded
|
||||
// Switch the <Transition/> to <Suspense/> to fall back to "Loading..." every time
|
||||
<Transition
|
||||
fallback={"Loading (Suspense Fallback)...".to_string()}
|
||||
set_pending
|
||||
>
|
||||
{move || {
|
||||
cats.read().map(|data| match data {
|
||||
Err(_) => view! { cx, <pre>"Error"</pre> },
|
||||
Ok(cats) => view! { cx,
|
||||
<div>{
|
||||
cats.iter()
|
||||
.map(|src| {
|
||||
view! { cx,
|
||||
<img src={src}/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}</div>
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</Transition>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
8
examples/gtk/README.md
Normal file
8
examples/gtk/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Leptos in a GTK App
|
||||
|
||||
This example creates a basic GTK app that uses Leptos’s reactive primitives.
|
||||
|
||||
## Build and Run
|
||||
|
||||
Unlike the other examples, this has a variety of build prerequisites that are out of scope of this crate. More detail on that can be found [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/installation.html). The example comes from [here](https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html) and should be
|
||||
runnable with `cargo run` if you have the GTK prerequisites installed.
|
||||
@@ -6,7 +6,7 @@ const APP_ID: &str = "dev.leptos.Counter";
|
||||
|
||||
// Basic GTK app setup from https://gtk-rs.org/gtk4-rs/stable/latest/book/hello_world.html
|
||||
fn main() {
|
||||
_ = create_scope(|cx| {
|
||||
_ = create_scope(create_runtime(), |cx| {
|
||||
// Create a new application
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
|
||||
@@ -3,18 +3,27 @@
|
||||
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. This repo differs from the main Hacker News example by using Axum as it's server.
|
||||
|
||||
## Client Side Rendering
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CRS bundle
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --debug --no-default-features --features=hydrate
|
||||
```
|
||||
to generate the Webassembly to provide hydration features for the server.
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use axum::{
|
||||
body::{boxed, Body, BoxBody},
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
pub async fn file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/pkg").await?;
|
||||
println!("FIRST URI{:?}", uri);
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
// try with `.html`
|
||||
// TODO: handle if the Uri has query parameters
|
||||
match format!("{}.html", uri).parse() {
|
||||
Ok(uri_html) => get_static_file(uri_html, "/pkg").await,
|
||||
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string())),
|
||||
}
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_static_file_handler(uri: Uri) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let res = get_static_file(uri.clone(), "/static").await?;
|
||||
println!("FIRST URI{:?}", uri);
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid URI".to_string()))
|
||||
} else {
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_static_file(uri: Uri, base: &str) -> Result<Response<BoxBody>, (StatusCode, String)> {
|
||||
let req = Request::builder().uri(&uri).body(Body::empty()).unwrap();
|
||||
|
||||
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
|
||||
// When run normally, the root should be the crate root
|
||||
println!("Base: {:#?}", base);
|
||||
if base == "/static" {
|
||||
match ServeDir::new("./static").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
))
|
||||
}
|
||||
} else if base == "/pkg" {
|
||||
match ServeDir::new("./pkg").oneshot(req).await {
|
||||
Ok(res) => Ok(res.map(boxed)),
|
||||
Err(err) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", err),
|
||||
)),
|
||||
}
|
||||
} else{
|
||||
Err((StatusCode::NOT_FOUND, "Not Found".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ use leptos::{component, Scope, IntoView, provide_context, view};
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
mod api;
|
||||
pub mod handlers;
|
||||
mod routes;
|
||||
use routes::nav::*;
|
||||
use routes::stories::*;
|
||||
|
||||
@@ -17,7 +17,7 @@ if #[cfg(feature = "ssr")] {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use leptos_hackernews_axum::*;
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3002));
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
|
||||
log::debug!("serving at {addr}");
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = ["serde"] }
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
@@ -25,9 +27,7 @@ serde_json = "1"
|
||||
gloo-net = { version = "0.2", features = ["http"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
# openssl = { version = "0.10", features = ["v110"] }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
# Leptos Hacker News Example
|
||||
|
||||
This example creates a basic clone of the Hacker News site. It showcases Leptos' ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository
|
||||
This example creates a basic clone of the Hacker News site. It showcases Leptos’s ability to create both a client-side rendered app, and a server side rendered app with hydration, in a single repository. It uses Actix as its backend.
|
||||
|
||||
## Client Side Rendering
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CRS bundle
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
to generate the Webassembly to provide hydration features for the server.
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
|
||||
@@ -6,7 +6,7 @@ pub fn Nav(cx: Scope) -> impl IntoView {
|
||||
view! { cx,
|
||||
<header class="header">
|
||||
<nav class="inner">
|
||||
<A href="/">
|
||||
<A href="/" class="home".to_string()>
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
|
||||
@@ -33,10 +33,14 @@ pub fn Stories(cx: Scope) -> impl IntoView {
|
||||
move |(page, story_type)| async move {
|
||||
let path = format!("{}?page={}", category(&story_type), page);
|
||||
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
|
||||
api::fetch_api::<Vec<api::Story>>(cx, &api::story(&path)).await
|
||||
},
|
||||
);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
let (pending, set_pending) = create_signal(cx, false);
|
||||
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
let hide_more_link =
|
||||
move || pending() || stories.read().unwrap_or(None).unwrap_or_default().len() < 28;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
|
||||
17
examples/parent-child/README.md
Normal file
17
examples/parent-child/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Parent Child Example
|
||||
|
||||
This example highlights four different ways that child components can communicate with their parent:
|
||||
|
||||
1. <ButtonA/>: passing a WriteSignal as one of the child component props,
|
||||
for the child component to write into and the parent to read
|
||||
2. <ButtonB/>: passing a closure as one of the child component props, for
|
||||
the child component to call
|
||||
3. <ButtonC/>: adding a simple event listener on the child component itself
|
||||
4. <ButtonD/>: providing a context that is used in the component (rather than prop drilling)
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
console_log = "0.2"
|
||||
log = "0.4"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos_router = { path = "../../router", features=["csr"] }
|
||||
leptos_router = { path = "../../router", features = ["csr"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# Leptos Router Example
|
||||
|
||||
This example demonstrates how Leptos' router works
|
||||
This example demonstrates how Leptos’s router for client side routing.
|
||||
|
||||
## Build and Run it
|
||||
|
||||
## Run it
|
||||
```bash
|
||||
trunk serve --open
|
||||
```
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn Settings(cx: Scope) -> impl IntoView {
|
||||
<fieldset>
|
||||
<legend>"Name"</legend>
|
||||
<input type="text" name="first_name" placeholder="First"/>
|
||||
<input type="text" name="first_name" placeholder="Last"/>
|
||||
<input type="text" name="last_name" placeholder="Last"/>
|
||||
</fieldset>
|
||||
<pre>"This page is just a placeholder."</pre>
|
||||
</form>
|
||||
|
||||
48
examples/todo-app-cbor/Cargo.toml
Normal file
48
examples/todo-app-cbor/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "todo-app-cbor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.6", optional = true }
|
||||
actix-web = { version = "4", optional = true, features = ["openssl", "macros"] }
|
||||
anyhow = "1"
|
||||
broadcaster = "1"
|
||||
console_log = "0.2"
|
||||
console_error_panic_hook = "0.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
futures = "0.3"
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
sqlx = { version = "0.6", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
21
examples/todo-app-cbor/LICENSE
Normal file
21
examples/todo-app-cbor/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Greg Johnston
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
examples/todo-app-cbor/README.md
Normal file
22
examples/todo-app-cbor/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Leptos Todo App Sqlite with CBOR
|
||||
|
||||
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server. It is identical to the todo-app-sqlite example, but utilizes CBOR encoding for one of the server functions
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
|
||||
To run it as a server side app with hydration, first you should run
|
||||
|
||||
```bash
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
> This should be temporary, and vastly improve once cargo-leptos becomes ready for prime time!
|
||||
BIN
examples/todo-app-cbor/Todos.db
Normal file
BIN
examples/todo-app-cbor/Todos.db
Normal file
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
title VARCHAR,
|
||||
completed BOOLEAN
|
||||
);
|
||||
22
examples/todo-app-cbor/src/lib.rs
Normal file
22
examples/todo-app-cbor/src/lib.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use cfg_if::cfg_if;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use leptos::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
console_error_panic_hook::set_once();
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::hydrate(body().unwrap(), |cx| {
|
||||
view! { cx, <TodoApp/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/todo-app-cbor/src/main.rs
Normal file
49
examples/todo-app-cbor/src/main.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod todo;
|
||||
|
||||
// boilerplate to run in different modes
|
||||
cfg_if! {
|
||||
// server-only stuff
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::todo::*;
|
||||
use std::{ net::SocketAddr,env };
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
actix_files::NamedFile::open_async("./style.css").await
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
let addr = SocketAddr::from(([127,0,0,1],3000));
|
||||
|
||||
HttpServer::new(move || {
|
||||
let render_options: RenderOptions = RenderOptions::builder().pkg_path("/pkg/todo_app_cbor").reload_port(3001).socket_address(addr.clone()).environment(&env::var("RUST_ENV")).build();
|
||||
render_options.write_to_file();
|
||||
App::new()
|
||||
.service(Files::new("/pkg", "./pkg"))
|
||||
.service(css)
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.route("/{tail:.*}", leptos_actix::render_app_to_stream(render_options, |cx| view! { cx, <TodoApp/> }))
|
||||
//.wrap(middleware::Compress::default())
|
||||
})
|
||||
.bind(&addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
fn main() {
|
||||
// no client-side main function
|
||||
}
|
||||
}
|
||||
}
|
||||
212
examples/todo-app-cbor/src/todo.rs
Normal file
212
examples/todo-app-cbor/src/todo.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
} else {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Todo {
|
||||
id: u16,
|
||||
title: String,
|
||||
completed: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetTodos, "/api", "Url")]
|
||||
pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
let req =
|
||||
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
|
||||
println!("req.path = {:?}", req.path());
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
|
||||
{
|
||||
todos.push(row);
|
||||
}
|
||||
|
||||
Ok(todos)
|
||||
}
|
||||
|
||||
#[server(AddTodo, "/api", "Cbor")]
|
||||
pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
sqlx::query("DELETE FROM todos WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TodoApp(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<Stylesheet href="/style.css"/>
|
||||
<Router>
|
||||
<header>
|
||||
<h1>"My Tasks"</h1>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" element=|cx| view! {
|
||||
cx,
|
||||
<Todos/>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Todos(cx: Scope) -> Element {
|
||||
let add_todo = create_server_multi_action::<AddTodo>(cx);
|
||||
let delete_todo = create_server_action::<DeleteTodo>(cx);
|
||||
let submissions = add_todo.submissions();
|
||||
|
||||
// track mutations that should lead us to refresh the list
|
||||
let add_changed = add_todo.version;
|
||||
let todo_deleted = delete_todo.version;
|
||||
|
||||
// list of todos is loaded from the server in reaction to changes
|
||||
let todos = create_resource(
|
||||
cx,
|
||||
move || (add_changed(), todo_deleted()),
|
||||
move |_| get_todos(cx),
|
||||
);
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<MultiActionForm action=add_todo>
|
||||
<label>
|
||||
"Add a Todo"
|
||||
<input type="text" name="title"/>
|
||||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<div>
|
||||
<Suspense fallback=view! {cx, <p>"Loading..."</p> }>
|
||||
{
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
let existing_todos = {
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
todos
|
||||
.read()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todos| match todos {
|
||||
Err(e) => {
|
||||
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}]
|
||||
}
|
||||
Ok(todos) => {
|
||||
if todos.is_empty() {
|
||||
vec![view! { cx, <p>"No tasks were found."</p> }]
|
||||
} else {
|
||||
todos
|
||||
.into_iter()
|
||||
.map({
|
||||
let delete_todo = delete_todo.clone();
|
||||
move |todo| {
|
||||
let delete_todo = delete_todo.clone();
|
||||
view! {
|
||||
cx,
|
||||
<li>
|
||||
{todo.title}
|
||||
<ActionForm action=delete_todo.clone()>
|
||||
<input type="hidden" name="id" value=todo.id/>
|
||||
<input type="submit" value="X"/>
|
||||
</ActionForm>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
let pending_todos = move || {
|
||||
submissions
|
||||
.get()
|
||||
.into_iter()
|
||||
.filter(|submission| submission.pending().get())
|
||||
.map(|submission| {
|
||||
view! {
|
||||
cx,
|
||||
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<ul>
|
||||
<div>{existing_todos}</div>
|
||||
<div>{pending_todos}</div>
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
3
examples/todo-app-cbor/style.css
Normal file
3
examples/todo-app-cbor/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.pending {
|
||||
color: purple;
|
||||
}
|
||||
@@ -6,6 +6,68 @@ edition = "2021"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../../leptos/leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../../leptos/integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../../leptos/meta", default-features = false }
|
||||
leptos_router = { path = "../../../leptos/router", default-features = false }
|
||||
log = "0.4.17"
|
||||
simple_logger = "4.0.0"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
gloo-net = { version = "0.2.5", features = ["http"] }
|
||||
reqwest = { version = "0.11.13", features = ["json"] }
|
||||
axum = { version = "0.6.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.3.4", features = ["fs"], optional = true }
|
||||
tokio = { version = "1.22.0", features = ["full"], optional = true }
|
||||
http = { version = "0.2.8" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:axum",
|
||||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:tokio",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"leptos_axum",
|
||||
]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
"axum",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tokio",
|
||||
"sqlx",
|
||||
"leptos_axum",
|
||||
]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
[package]
|
||||
name = "todo-app-sqlite-axum"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
console_log = "0.2.0"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS todos
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
|
||||
@@ -24,10 +24,10 @@ if #[cfg(feature = "ssr")] {
|
||||
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
|
||||
|
||||
let mut conn = db().await.expect("couldn't connect to DB");
|
||||
/* sqlx::migrate!()
|
||||
sqlx::migrate!()
|
||||
.run(&mut conn)
|
||||
.await
|
||||
.expect("could not run SQLx migrations"); */
|
||||
.expect("could not run SQLx migrations");
|
||||
|
||||
crate::todo::register_server_functions();
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ cfg_if! {
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
GetTodos::register();
|
||||
AddTodo::register();
|
||||
DeleteTodo::register();
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
|
||||
@@ -25,23 +25,16 @@ leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
sqlx = { version = "0.6", features = [
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
], optional = true }
|
||||
http = "0.2.8"
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:sqlx",
|
||||
"leptos/ssr",
|
||||
"leptos_actix",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
ssr = ["dep:actix-files", "dep:actix-web", "dep:sqlx", "leptos/ssr", "leptos_actix", "leptos_meta/ssr", "leptos_router/ssr"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix", "sqlx"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Leptos Counter Isomorphic Example
|
||||
# Leptos Todo App Sqlite
|
||||
|
||||
This example demonstrates how to use a server functions and multi-actions to build a simple todo app.
|
||||
This example creates a basic todo app with an Actix backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
|
||||
@@ -10,11 +10,12 @@ To run it as a server side app with hydration, first you should run
|
||||
wasm-pack build --target=web --no-default-features --features=hydrate
|
||||
```
|
||||
|
||||
to generate the Webassembly to provide hydration features for the server.
|
||||
to generate the WebAssembly to hydrate the HTML that is generated on the server.
|
||||
|
||||
Then run the server with `cargo run` to serve the server side rendered HTML and the WASM bundle for hydration.
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
cargo run --no-default-features --features=ssr
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
pub mod todo;
|
||||
|
||||
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
|
||||
@@ -7,6 +6,7 @@ cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
use leptos::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod todo;
|
||||
|
||||
@@ -7,15 +7,17 @@ use serde::{Deserialize, Serialize};
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{Connection, SqliteConnection};
|
||||
use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode};
|
||||
|
||||
|
||||
pub async fn db() -> Result<SqliteConnection, ServerFnError> {
|
||||
Ok(SqliteConnection::connect("sqlite:Todos.db").await.map_err(|e| ServerFnError::ServerError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn register_server_functions() {
|
||||
GetTodos::register();
|
||||
AddTodo::register();
|
||||
DeleteTodo::register();
|
||||
_ = GetTodos::register();
|
||||
_ = AddTodo::register();
|
||||
_ = DeleteTodo::register();
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
|
||||
@@ -39,12 +41,16 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
|
||||
// this is just an example of how to access server context injected in the handlers
|
||||
let req =
|
||||
use_context::<actix_web::HttpRequest>(cx).expect("couldn't get HttpRequest from context");
|
||||
println!("req.path = {:?}", req.path());
|
||||
println!("\ncalling server fn");
|
||||
println!(" req.path = {:?}", req.path());
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(350));
|
||||
|
||||
let mut todos = Vec::new();
|
||||
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
|
||||
while let Some(row) = rows
|
||||
@@ -63,16 +69,14 @@ pub async fn add_todo(title: String) -> Result<(), ServerFnError> {
|
||||
let mut conn = db().await?;
|
||||
|
||||
// fake API delay
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
std::thread::sleep(std::time::Duration::from_millis(350));
|
||||
|
||||
match sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
.bind(title)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
{
|
||||
Ok(row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
.map(|_| ())
|
||||
.map_err(|e| ServerFnError::ServerError(e.to_string()))
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
|
||||
10
examples/todomvc/README.md
Normal file
10
examples/todomvc/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos TodoMVC
|
||||
|
||||
This is a Leptos implementation of the TodoMVC example common to many frameworks. This is a relatively-simple application but shows off features like interaction between components and state management.
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -1,12 +1,16 @@
|
||||
use axum::{
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
body::{Body, Bytes, Full, StreamBody},
|
||||
extract::Path,
|
||||
http::{HeaderMap, HeaderValue, Request, StatusCode},
|
||||
response::IntoResponse,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::{Future, SinkExt, Stream, StreamExt};
|
||||
use http::{method::Method, uri::Uri, version::Version, Response};
|
||||
use hyper::body;
|
||||
use http::{method::Method, uri::Uri, version::Version, Response};
|
||||
use hyper::body;
|
||||
use leptos::*;
|
||||
use leptos_meta::MetaContext;
|
||||
use leptos_router::*;
|
||||
@@ -45,6 +49,52 @@ impl ResponseOptions {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
let body = body::to_bytes(body).await.unwrap_or_default();
|
||||
RequestParts {
|
||||
method: parts.method,
|
||||
uri: parts.uri,
|
||||
headers: parts.headers,
|
||||
version: parts.version,
|
||||
body: body.clone(),
|
||||
}
|
||||
}
|
||||
use tokio::{sync::RwLock, task::spawn_blocking};
|
||||
|
||||
/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced
|
||||
/// to construct this for Leptos to use in Axum
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequestParts {
|
||||
pub version: Version,
|
||||
pub method: Method,
|
||||
pub uri: Uri,
|
||||
pub headers: HeaderMap<HeaderValue>,
|
||||
pub body: Bytes,
|
||||
}
|
||||
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
||||
/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseParts {
|
||||
pub status: Option<StatusCode>,
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
|
||||
/// Adding this Struct to your Scope inside of a Server Fn or Elements will allow you to override details of the Response
|
||||
/// like StatusCode and add Headers/Cookies. Because Elements and Server Fns are lower in the tree than the Response generation
|
||||
/// code, it needs to be wrapped in an `Arc<RwLock<>>` so that it can be surfaced
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ResponseOptions(pub Arc<RwLock<ResponseParts>>);
|
||||
|
||||
impl ResponseOptions {
|
||||
/// A less boilerplatey way to overwrite the default contents of `ResponseOptions` with a new `ResponseParts`
|
||||
pub async fn overwrite(&self, parts: ResponseParts) {
|
||||
let mut writable = self.0.write().await;
|
||||
*writable = parts
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_request_parts(req: Request<Body>) -> RequestParts {
|
||||
// provide request headers as context in server scope
|
||||
let (parts, body) = req.into_parts();
|
||||
@@ -324,6 +374,7 @@ where IV: IntoView
|
||||
|
||||
let (mut tx, rx) = futures::channel::mpsc::channel(8);
|
||||
|
||||
spawn_blocking({
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
move || {
|
||||
|
||||
@@ -22,7 +22,7 @@ typed-builder = "0.11"
|
||||
leptos = { path = ".", default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["csr", "serde"]
|
||||
default = ["csr", "serde", "interning"]
|
||||
csr = [
|
||||
"leptos_dom/web",
|
||||
"leptos_macro/csr",
|
||||
@@ -53,4 +53,30 @@ miniserde = ["leptos_reactive/miniserde"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
denylist = ["stable", "interning"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
12
leptos/build.rs
Normal file
12
leptos/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustc_version::{version, version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
assert!(version().unwrap().major >= 1);
|
||||
|
||||
match version_meta().unwrap().channel {
|
||||
Channel::Stable => {
|
||||
println!("cargo:rustc-cfg=feature=\"stable\"")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,12 @@
|
||||
//! the [examples](https://github.com/gbj/leptos/tree/main/examples):
|
||||
//! - [`counter`](https://github.com/gbj/leptos/tree/main/examples/counter) is the classic
|
||||
//! counter example, showing the basics of client-side rendering and reactive DOM updates
|
||||
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counter) introduces parent-child
|
||||
//! - [`counter-isomorphic`](https://github.com/gbj/leptos/tree/main/examples/counter-isomorphic) is the classic
|
||||
//! counter example run on the server using an isomorphic function, showing the basics of client-side rendering and reactive DOM updates
|
||||
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counters) introduces parent-child
|
||||
//! communication via contexts, and the `<For/>` component for efficient keyed list updates.
|
||||
//! - [`counters-stable`](https://github.com/gbj/leptos/tree/main/examples/counters-stable) introduces parent-child
|
||||
//! communication via contexts, and the `<For/>` component for efficient keyed list updates. Unlike counters, this compiles in Rust stable.
|
||||
//! - [`parent-child`](https://github.com/gbj/leptos/tree/main/examples/parent-child) shows four different
|
||||
//! ways a parent component can communicate with a child, including passing a closure, context, and more
|
||||
//! - [`todomvc`](https://github.com/gbj/leptos/tree/main/examples/todomvc) implements the classic to-do
|
||||
@@ -47,6 +51,13 @@
|
||||
//! - [`hackernews`](https://github.com/gbj/leptos/tree/main/examples/hackernews) pulls everything together.
|
||||
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run.
|
||||
//! - [`hackernews-axum`](https://github.com/gbj/leptos/tree/main/examples/hackernews-axum) pulls everything together.
|
||||
//! It integrates calls to a real external REST API, routing, server-side rendering and hydration to create
|
||||
//! a fully-functional PEMPA that works as intended even before WASM has loaded and begun to run. This one uses Axum as it's backend.
|
||||
//! - [`todo-app-sqlite`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite) is a simple todo app, showcasing the use of
|
||||
//! functions that run only on the server, but are called from client side function calls
|
||||
//! - [`todo-app-sqlite-axum`](https://github.com/gbj/leptos/tree/main/examples/todo-app-sqlite-axum) is a simple todo app, showcasing the use of
|
||||
//! functions that run only on the server, but are called from client side function calls. Now with Axum backend
|
||||
//!
|
||||
//! (The SPA examples can be run using `trunk serve`. For information about Trunk,
|
||||
//! [see here]((https://trunkrs.dev/)).)
|
||||
@@ -77,6 +88,10 @@
|
||||
//! from the server to the client.
|
||||
//! - `miniserde` In SSR/hydrate mode, uses [miniserde](https://docs.rs/miniserde/latest/miniserde/) to serialize resources and send them
|
||||
//! from the server to the client.
|
||||
//! - `interning` (*Default*) When client-side rendering, Leptos uses [`wasm_bindgen::intern`](https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/fn.intern.html)
|
||||
//! to reduce the cost of copying class names, attribute names, attribute values, and properties through JavaScript to the DOM. This feature
|
||||
//! (included by default) makes DOM updates marginally faster and WASM binary size marginally larger. Disabling the feature makes binary sizes
|
||||
//! marginally smaller at the cost of a small decrease in speed.
|
||||
//!
|
||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||
//! which mode your app is operating in.
|
||||
|
||||
12
leptos_core/build.rs
Normal file
12
leptos_core/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustc_version::{version, version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
assert!(version().unwrap().major >= 1);
|
||||
|
||||
match version_meta().unwrap().channel {
|
||||
Channel::Stable => {
|
||||
println!("cargo:rustc-cfg=feature=\"stable\"")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
178
leptos_core/src/transition.rs
Normal file
178
leptos_core/src/transition.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use leptos_dom::{Child, IntoChild};
|
||||
use leptos_reactive::{provide_context, Scope, SignalSetter, SuspenseContext};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// Props for the [Suspense](crate::Suspense) component, which shows a fallback
|
||||
/// while [Resource](leptos_reactive::Resource)s are being read.
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct TransitionProps<F, E, G>
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E,
|
||||
{
|
||||
/// Will be displayed while resources are pending.
|
||||
pub fallback: F,
|
||||
/// A function that will be called when the component transitions into or out of
|
||||
/// the `pending` state, with its argument indicating whether it is pending (`true`)
|
||||
/// or not pending (`false`).
|
||||
#[builder(default, setter(strip_option, into))]
|
||||
pub set_pending: Option<SignalSetter<bool>>,
|
||||
/// Will be displayed once all resources have resolved.
|
||||
pub children: Box<dyn Fn() -> Vec<G>>,
|
||||
}
|
||||
|
||||
/// If any [Resource](leptos_reactive::Resource)s are read in the `children` of this
|
||||
/// component, it will show the `fallback` while they are loading. Once all are resolved,
|
||||
/// it will render the `children`. Unlike [`Suspense`](crate::Suspense), this will not fall
|
||||
/// back to the `fallback` state if there are further changes after the initial load.
|
||||
///
|
||||
/// Note that the `children` will be rendered initially (in order to capture the fact that
|
||||
/// those resources are read under the suspense), so you cannot assume that resources have
|
||||
/// `Some` value in `children`.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos_reactive::*;
|
||||
/// # use leptos_core::*;
|
||||
/// # use leptos_macro::*;
|
||||
/// # use leptos_dom::*; use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if cfg!(not(any(feature = "csr", feature = "hydrate", feature = "ssr"))) {
|
||||
/// async fn fetch_cats(how_many: u32) -> Result<Vec<String>, ()> { Ok(vec![]) }
|
||||
///
|
||||
/// let (cat_count, set_cat_count) = create_signal::<u32>(cx, 1);
|
||||
/// let (pending, set_pending) = create_signal(cx, false);
|
||||
///
|
||||
/// let cats = create_resource(cx, cat_count, |count| fetch_cats(count));
|
||||
///
|
||||
/// view! { cx,
|
||||
/// <div>
|
||||
/// <Transition
|
||||
/// fallback={"Loading...".to_string()}
|
||||
/// set_pending=set_pending
|
||||
/// >
|
||||
/// {move || {
|
||||
/// cats.read().map(|data| match data {
|
||||
/// Err(_) => view! { cx, <pre>"Error"</pre> },
|
||||
/// Ok(cats) => view! { cx,
|
||||
/// <div>{
|
||||
/// cats.iter()
|
||||
/// .map(|src| {
|
||||
/// view! { cx,
|
||||
/// <img src={src}/>
|
||||
/// }
|
||||
/// })
|
||||
/// .collect::<Vec<_>>()
|
||||
/// }</div>
|
||||
/// },
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// </Transition>
|
||||
/// </div>
|
||||
/// };
|
||||
/// # }
|
||||
/// # });
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Transition<F, E, G>(cx: Scope, props: TransitionProps<F, E, G>) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E + 'static,
|
||||
{
|
||||
let context = SuspenseContext::new(cx);
|
||||
|
||||
// provide this SuspenseContext to any resources below it
|
||||
provide_context(cx, context);
|
||||
|
||||
let child = (props.children)().swap_remove(0);
|
||||
|
||||
render_transition(cx, context, props.fallback, child, props.set_pending)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||
fn render_transition<'a, F, E, G>(
|
||||
cx: Scope,
|
||||
context: SuspenseContext,
|
||||
fallback: F,
|
||||
child: G,
|
||||
set_pending: Option<SignalSetter<bool>>,
|
||||
) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E,
|
||||
{
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
let has_rendered_once = Cell::new(false);
|
||||
let prev_child = RefCell::new(Child::Null);
|
||||
|
||||
move || {
|
||||
if context.ready() {
|
||||
has_rendered_once.set(true);
|
||||
let current_child = (child)().into_child(cx);
|
||||
*prev_child.borrow_mut() = current_child.clone();
|
||||
if let Some(pending) = &set_pending {
|
||||
pending.set(false);
|
||||
}
|
||||
current_child
|
||||
} else if has_rendered_once.get() {
|
||||
if let Some(pending) = &set_pending {
|
||||
pending.set(true);
|
||||
}
|
||||
prev_child.borrow().clone()
|
||||
} else {
|
||||
if let Some(pending) = &set_pending {
|
||||
pending.set(true);
|
||||
}
|
||||
let fallback = fallback.clone().into_child(cx);
|
||||
*prev_child.borrow_mut() = fallback.clone();
|
||||
fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||
fn render_transition<'a, F, E, G>(
|
||||
cx: Scope,
|
||||
context: SuspenseContext,
|
||||
fallback: F,
|
||||
orig_child: G,
|
||||
set_pending: Option<SignalSetter<bool>>,
|
||||
) -> impl Fn() -> Child
|
||||
where
|
||||
F: IntoChild + Clone,
|
||||
E: IntoChild,
|
||||
G: Fn() -> E + 'static,
|
||||
{
|
||||
use leptos_dom::IntoAttribute;
|
||||
use leptos_macro::view;
|
||||
|
||||
_ = set_pending;
|
||||
|
||||
let initial = {
|
||||
// run the child; we'll probably throw this away, but it will register resource reads
|
||||
let mut child = orig_child().into_child(cx);
|
||||
while let Child::Fn(f) = child {
|
||||
child = (f.borrow_mut())();
|
||||
}
|
||||
|
||||
// no resources were read under this, so just return the child
|
||||
if context.pending_resources.get() == 0 {
|
||||
child
|
||||
}
|
||||
// show the fallback, but also prepare to stream HTML
|
||||
else {
|
||||
let key = cx.current_fragment_key();
|
||||
cx.register_suspense(context, &key, move || {
|
||||
orig_child().into_child(cx).as_child_string()
|
||||
});
|
||||
|
||||
// return the fallback for now, wrapped in fragment identifer
|
||||
Child::Node(view! { cx, <div data-fragment-id=key>{fallback.into_child(cx)}</div> })
|
||||
}
|
||||
};
|
||||
move || initial.clone()
|
||||
}
|
||||
@@ -137,6 +137,11 @@ features = [
|
||||
"HtmlOptionElement"
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos", default-features = false, version = "0.0" }
|
||||
leptos_macro = { path = "../leptos_macro", default-features = false, version = "0.0" }
|
||||
|
||||
|
||||
[features]
|
||||
web = ["leptos_reactive/csr"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
|
||||
12
leptos_dom/build.rs
Normal file
12
leptos_dom/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustc_version::{version, version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
assert!(version().unwrap().major >= 1);
|
||||
|
||||
match version_meta().unwrap().channel {
|
||||
Channel::Stable => {
|
||||
println!("cargo:rustc-cfg=feature=\"stable\"")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
10
leptos_dom/examples/view-tests/README.md
Normal file
10
leptos_dom/examples/view-tests/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Leptos View Tests
|
||||
|
||||
This is a collection of mostly internal view tests for Leptos. Feel free to look if curious to see a variety of ways you can build identical views!
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
To run it as a Client Side App, you can issue `trunk serve --open` in the root. This will build the entire
|
||||
app into one CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
@@ -26,7 +26,7 @@ fn Tests(cx: Scope) -> Element {
|
||||
view! {
|
||||
cx,
|
||||
<div>
|
||||
<div><SelfUpdatingEffect/></div>
|
||||
//<div><SelfUpdatingEffect/></div>
|
||||
<div><BlockOrders/></div>
|
||||
//<div><TemplateConsumer/></div>
|
||||
</div>
|
||||
|
||||
12
leptos_macro/build.rs
Normal file
12
leptos_macro/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustc_version::{version, version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
assert!(version().unwrap().major >= 1);
|
||||
|
||||
match version_meta().unwrap().channel {
|
||||
Channel::Stable => {
|
||||
println!("cargo:rustc-cfg=feature=\"stable\"")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use syn::{
|
||||
ItemFn, LitStr, Meta, MetaList, MetaNameValue, NestedMeta, Pat, PatIdent, Path, PathArguments,
|
||||
ReturnType, Type, TypePath, Visibility,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
|
||||
pub struct Model {
|
||||
is_transparent: bool,
|
||||
@@ -69,6 +70,24 @@ impl Parse for Model {
|
||||
);
|
||||
}
|
||||
|
||||
let doc_comment = attrs.iter().filter_map(|attr| if attr.path.segments[0].ident == "doc" {
|
||||
|
||||
Some(attr.clone().tokens.into_iter().filter_map(|token| if let TokenTree::Literal(_) = token {
|
||||
// remove quotes
|
||||
let chars = token.to_string();
|
||||
let mut chars = chars.chars();
|
||||
chars.next();
|
||||
chars.next_back();
|
||||
Some(chars.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
}).collect::<String>())
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.intersperse_with(|| "\n".to_string())
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
is_transparent: false,
|
||||
docs,
|
||||
@@ -95,6 +114,45 @@ impl ToTokens for Model {
|
||||
ret,
|
||||
} = self;
|
||||
|
||||
let field_docs: HashMap<String, String> = {
|
||||
let mut map = HashMap::new();
|
||||
let mut pieces = doc_comment.split("# Props");
|
||||
pieces.next();
|
||||
let rest = pieces.next().unwrap_or_default();
|
||||
let mut current_field_name = String::new();
|
||||
let mut current_field_value = String::new();
|
||||
for line in rest.split('\n') {
|
||||
if let Some(line) = line.strip_prefix(" - ") {
|
||||
let mut pieces = line.split("**");
|
||||
pieces.next();
|
||||
let field_name = pieces.next();
|
||||
let field_value = pieces.next().unwrap_or_default();
|
||||
let field_value = if let Some((_ty, desc)) = field_value.split_once('-') {
|
||||
desc
|
||||
} else {
|
||||
field_value
|
||||
};
|
||||
if let Some(field_name) = field_name {
|
||||
if !current_field_name.is_empty() {
|
||||
map.insert(current_field_name.clone(), current_field_value.clone());
|
||||
}
|
||||
current_field_name = field_name.to_string();
|
||||
current_field_value = String::new();
|
||||
current_field_value.push_str(field_value);
|
||||
} else {
|
||||
current_field_value.push_str(field_value);
|
||||
}
|
||||
} else {
|
||||
current_field_value.push_str(line);
|
||||
}
|
||||
}
|
||||
if !current_field_name.is_empty() {
|
||||
map.insert(current_field_name, current_field_value.clone());
|
||||
}
|
||||
|
||||
map
|
||||
};
|
||||
|
||||
let mut body = body.to_owned();
|
||||
|
||||
body.sig.ident = format_ident!("__{}", body.sig.ident);
|
||||
@@ -225,6 +283,10 @@ impl Prop {
|
||||
if acc.intersection(&cur.1).next().is_some() {
|
||||
abort!(typed.attrs[cur.0], "`#[prop]` options are repeated");
|
||||
}
|
||||
} else {
|
||||
quote! { #vis #f }
|
||||
}
|
||||
});
|
||||
|
||||
acc.extend(cur.1);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ extern crate proc_macro_error;
|
||||
use proc_macro::{TokenStream, TokenTree};
|
||||
use quote::ToTokens;
|
||||
use server::server_macro_impl;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use syn_rsx::{parse, NodeElement};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -30,7 +29,6 @@ mod params;
|
||||
mod view;
|
||||
use view::render_view;
|
||||
mod component;
|
||||
mod props;
|
||||
mod server;
|
||||
|
||||
/// The `view` macro uses RSX (like JSX, but Rust!) It follows most of the
|
||||
@@ -179,12 +177,15 @@ 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 `_ref` attribute to store a reference to its DOM element in a
|
||||
/// [NodeRef](leptos_reactive::NodeRef) to use later.
|
||||
/// ```rust
|
||||
/// # use leptos::*;
|
||||
/// # run_scope(create_runtime(), |cx| {
|
||||
/// # if !cfg!(any(feature = "csr", feature = "hydrate")) {
|
||||
/// let (value, set_value) = create_signal(cx, 0);
|
||||
/// let my_input = NodeRef::new(cx);
|
||||
/// let my_input = NodeRef::new(cx);
|
||||
/// view! { cx, <input type="text" _ref=my_input/> }
|
||||
/// // `my_input` now contains an `Element` that we can use anywhere
|
||||
/// # ;
|
||||
@@ -413,6 +414,54 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Declares that a function is a [server function](leptos_server). This means that
|
||||
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
|
||||
///
|
||||
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
|
||||
/// are enabled), it will instead make a network request to the server.
|
||||
///
|
||||
/// You can specify one, two, or three arguments to the server function:
|
||||
/// 1. **Required**: A type name that will be used to identify and register the server function
|
||||
/// (e.g., `MyServerFn`).
|
||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||
/// (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"`.
|
||||
///
|
||||
/// 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),
|
||||
/// 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.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*; use serde::{Serialize, Deserialize};
|
||||
/// # #[derive(Serialize, Deserialize)]
|
||||
/// # pub struct Post { }
|
||||
/// #[server(ReadPosts, "/api")]
|
||||
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
|
||||
/// // do some work on the server to access the database
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note the following:
|
||||
/// - **Server functions must be `async`.** Even if the work being done inside the function body
|
||||
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
|
||||
/// function call.
|
||||
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
|
||||
/// inside the function body can’t fail, the processes of serialization/deserialization and the
|
||||
/// network call are fallible.
|
||||
/// - **Return types must be [Serializable](leptos_reactive::Serializable).**
|
||||
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
|
||||
/// need to deserialize the result to return it to the client.
|
||||
/// - **Arguments must be implement [serde::Serialize].** They are serialized as an `application/x-www-form-urlencoded`
|
||||
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
|
||||
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
|
||||
/// - **The [Scope](leptos_reactive::Scope) comes from the server.** Optionally, the first argument of a server function
|
||||
/// can be a Leptos [Scope](leptos_reactive::Scope). This scope can be used to inject dependencies like the HTTP request
|
||||
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
match server_macro_impl(args, s.into()) {
|
||||
@@ -421,15 +470,6 @@ pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Props, attributes(builder))]
|
||||
pub fn derive_prop(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
props::impl_derive_prop(&input)
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
// Derive Params trait for routing
|
||||
#[proc_macro_derive(Params, attributes(params))]
|
||||
pub fn params_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -702,14 +702,34 @@ fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#name(
|
||||
#cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
)
|
||||
if other_attrs.peek().is_none() {
|
||||
quote_spanned! {
|
||||
span => create_component(#cx, move || {
|
||||
#initialize_children
|
||||
#component_name(
|
||||
#cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {
|
||||
span => create_component(#cx, move || {
|
||||
#initialize_children
|
||||
let #component_name = #component_name(
|
||||
#cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
);
|
||||
#(#other_attrs);*;
|
||||
#component_name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ web-sys = { version = "0.3", features = [
|
||||
"Window",
|
||||
] }
|
||||
cfg-if = "1.0.0"
|
||||
rmp-serde = "1.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
@@ -45,5 +46,34 @@ serde = []
|
||||
serde-lite = ["dep:serde-lite"]
|
||||
miniserde = ["dep:miniserde"]
|
||||
|
||||
[build-dependencies]
|
||||
rustc_version = "0.4"
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
|
||||
12
leptos_reactive/build.rs
Normal file
12
leptos_reactive/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use rustc_version::{version, version_meta, Channel};
|
||||
|
||||
fn main() {
|
||||
assert!(version().unwrap().major >= 1);
|
||||
|
||||
match version_meta().unwrap().channel {
|
||||
Channel::Stable => {
|
||||
println!("cargo:rustc-cfg=feature=\"stable\"")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ use std::fmt::Debug;
|
||||
/// ```
|
||||
pub fn create_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
cfg_if! {
|
||||
if #[cfg(not(feature = "ssr"))] {
|
||||
@@ -90,7 +90,7 @@ where
|
||||
/// # }).dispose();
|
||||
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let e = cx.runtime.create_effect(f);
|
||||
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
|
||||
@@ -99,7 +99,7 @@ where
|
||||
#[doc(hidden)]
|
||||
pub fn create_render_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
|
||||
where
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
create_effect(cx, f);
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnOnce<()> for Memo<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
@@ -227,7 +227,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnMut<()> for Memo<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
@@ -237,7 +237,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> Fn<()> for Memo<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
|
||||
@@ -66,9 +66,10 @@ pub fn create_resource<S, T, Fu>(
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
T: Serializable + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
// can't check this on the server without running the future
|
||||
// can't check this on the server without running the future
|
||||
let initial_value = None;
|
||||
|
||||
@@ -91,7 +92,7 @@ pub fn create_resource_with_initial_value<S, T, Fu>(
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
T: Serializable + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
let resolved = initial_value.is_some();
|
||||
@@ -173,7 +174,7 @@ pub fn create_local_resource<S, T, Fu>(
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
let initial_value = None;
|
||||
@@ -195,7 +196,7 @@ pub fn create_local_resource_with_initial_value<S, T, Fu>(
|
||||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
let resolved = initial_value.is_some();
|
||||
@@ -244,7 +245,7 @@ where
|
||||
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
r.load(false)
|
||||
}
|
||||
@@ -253,7 +254,7 @@ where
|
||||
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
T: Serializable + 'static,
|
||||
{
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
@@ -327,7 +328,7 @@ where
|
||||
impl<S, T> Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
/// Clones and returns the current value of the resource ([Option::None] if the
|
||||
/// resource is still pending). Also subscribes the running effect to this
|
||||
@@ -434,7 +435,7 @@ where
|
||||
pub struct Resource<S, T>
|
||||
where
|
||||
S: Debug + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
runtime: RuntimeId,
|
||||
pub(crate) id: ResourceId,
|
||||
@@ -451,7 +452,7 @@ slotmap::new_key_type! {
|
||||
impl<S, T> Clone for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
@@ -466,7 +467,7 @@ where
|
||||
impl<S, T> Copy for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
}
|
||||
|
||||
@@ -474,7 +475,7 @@ where
|
||||
impl<S, T> FnOnce<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = Option<T>;
|
||||
|
||||
@@ -487,7 +488,7 @@ where
|
||||
impl<S, T> FnMut<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
@@ -498,7 +499,7 @@ where
|
||||
impl<S, T> Fn<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
@@ -509,7 +510,7 @@ where
|
||||
pub(crate) struct ResourceState<S, T>
|
||||
where
|
||||
S: 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
scope: Scope,
|
||||
value: ReadSignal<Option<T>>,
|
||||
@@ -527,7 +528,7 @@ where
|
||||
impl<S, T> ResourceState<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
pub fn read(&self) -> Option<T>
|
||||
where
|
||||
@@ -646,6 +647,7 @@ where
|
||||
});
|
||||
Box::pin(async move {
|
||||
rx.next().await.expect("failed while trying to resolve Resource serializer")
|
||||
rx.next().await.expect("failed while trying to resolve Resource serializer")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -667,7 +669,7 @@ pub(crate) trait SerializableResource {
|
||||
impl<S, T> SerializableResource for ResourceState<S, T>
|
||||
where
|
||||
S: Debug + Clone,
|
||||
T: Debug + Serializable,
|
||||
T: Serializable,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
@@ -687,9 +689,6 @@ pub(crate) trait UnserializableResource {
|
||||
}
|
||||
|
||||
impl<S, T> UnserializableResource for ResourceState<S, T>
|
||||
where
|
||||
S: Debug,
|
||||
T: Debug,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
|
||||
@@ -226,7 +226,16 @@ impl Debug for Runtime {
|
||||
|
||||
impl Runtime {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydration"))] {
|
||||
Self::default()
|
||||
} else {
|
||||
Runtime {
|
||||
shared_context: RefCell::new(Some(Default::default())),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_unserializable_resource<S, T>(
|
||||
@@ -235,7 +244,7 @@ impl Runtime {
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
@@ -248,7 +257,7 @@ impl Runtime {
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
T: Serializable + 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
@@ -262,7 +271,7 @@ impl Runtime {
|
||||
) -> U
|
||||
where
|
||||
S: Debug + 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
let resources = self.resources.borrow();
|
||||
let res = resources.get(id);
|
||||
|
||||
@@ -228,7 +228,7 @@ impl<T> Copy for ReadSignal<T> {}
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnOnce<()> for ReadSignal<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
@@ -240,7 +240,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnMut<()> for ReadSignal<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
@@ -250,7 +250,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> Fn<()> for ReadSignal<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
@@ -734,7 +734,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnOnce<()> for RwSignal<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
@@ -746,7 +746,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnMut<()> for RwSignal<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
@@ -756,7 +756,7 @@ where
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> Fn<()> for RwSignal<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
T: Clone,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
|
||||
@@ -21,3 +21,7 @@ default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
stable = ["leptos/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
||||
@@ -57,7 +57,8 @@ default = ["csr"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr", "dep:url", "dep:regex"]
|
||||
stable = ["leptos/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
# No need to test optional dependencies as they are enabled by the ssr feature
|
||||
denylist = ["url", "regex"]
|
||||
denylist = ["url", "regex", "stable"]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::{use_navigate, use_resolved_path, ToHref};
|
||||
use crate::{use_navigate, use_resolved_path, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::typed_builder::*;
|
||||
use std::{error::Error, rc::Rc};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
@@ -39,10 +41,10 @@ pub fn Form<A>(
|
||||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
A: ToHref + 'static,
|
||||
A: TextProp + 'static,
|
||||
{
|
||||
let action_version = version;
|
||||
let action = use_resolved_path(cx, move || action.to_href()());
|
||||
let action = use_resolved_path(cx, move || action.to_value()());
|
||||
|
||||
let on_submit = move |ev: web_sys::SubmitEvent| {
|
||||
if ev.default_prevented() {
|
||||
|
||||
@@ -6,31 +6,31 @@ use crate::{use_location, use_resolved_path, State};
|
||||
|
||||
/// Describes a value that is either a static or a reactive URL, i.e.,
|
||||
/// a [String], a [&str], or a reactive `Fn() -> String`.
|
||||
pub trait ToHref {
|
||||
pub trait TextProp {
|
||||
/// Converts the (static or reactive) URL into a function that can be called to
|
||||
/// return the URL.
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String + '_>;
|
||||
}
|
||||
|
||||
impl ToHref for &str {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
impl TextProp for &str {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToHref for String {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
impl TextProp for String {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.clone();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> ToHref for F
|
||||
impl<F> TextProp for F
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,7 @@ where
|
||||
prop:replace={replace}
|
||||
aria-current=move || if is_active.get() { Some("page") } else { None }
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
class=move || class.as_ref().map(|class| class.get())
|
||||
>
|
||||
{children(cx)}
|
||||
</a>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::use_route;
|
||||
use leptos::*;
|
||||
|
||||
/// Displays the child route nested in a parent route, allowing you to control exactly where
|
||||
/// that child route is displayed. Renders nothing if there is no nested child.
|
||||
|
||||
#[component]
|
||||
pub fn Outlet(cx: Scope) -> impl IntoView {
|
||||
let route = use_route(cx);
|
||||
|
||||
@@ -97,6 +97,11 @@ impl RouteContext {
|
||||
self.inner.cx
|
||||
}
|
||||
|
||||
/// Returns the URL path of the current route,
|
||||
/// including param values in their places.
|
||||
///
|
||||
/// e.g., this will return `/article/0` rather than `/article/:id`.
|
||||
/// For the opposite behavior, see [RouteContext::original_path].
|
||||
/// Returns the URL path of the current route,
|
||||
/// including param values in their places.
|
||||
///
|
||||
|
||||
@@ -2,6 +2,7 @@ use cfg_if::cfg_if;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use leptos::*;
|
||||
use leptos::typed_builder::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
@@ -64,7 +65,6 @@ impl std::fmt::Debug for RouterContextInner {
|
||||
f.debug_struct("RouterContextInner")
|
||||
.field("location", &self.location)
|
||||
.field("base", &self.base)
|
||||
.field("history", &std::any::type_name_of_val(&self.history))
|
||||
.field("cx", &self.cx)
|
||||
.field("reference", &self.reference)
|
||||
.field("set_reference", &self.set_reference)
|
||||
@@ -86,7 +86,10 @@ impl RouterContext {
|
||||
let history = use_context::<RouterIntegrationContext>(cx)
|
||||
.unwrap_or_else(|| RouterIntegrationContext(Rc::new(crate::BrowserIntegration {})));
|
||||
} else {
|
||||
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.");
|
||||
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.\n\n \
|
||||
If you are using `leptos_actix` or `leptos_axum` and seeing this message, it is a bug: \n \
|
||||
1. Please check to make sure you're on the latest versions of `leptos_actix` or `leptos_axum` and of `leptos_router`. \n
|
||||
2. If you're on the latest versions, please open an issue at https://github.com/gbj/leptos/issues");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,14 +102,16 @@ impl RouterContext {
|
||||
let base = base.unwrap_or_default();
|
||||
let base_path = resolve_path("", base, None);
|
||||
|
||||
if let Some(base_path) = &base_path && source.with(|s| s.value.is_empty()) {
|
||||
history.navigate(&LocationChange {
|
||||
value: base_path.to_string(),
|
||||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None)
|
||||
});
|
||||
}
|
||||
if let Some(base_path) = &base_path {
|
||||
if source.with(|s| s.value.is_empty()) {
|
||||
history.navigate(&LocationChange {
|
||||
value: base_path.to_string(),
|
||||
replace: true,
|
||||
scroll: false,
|
||||
state: State(None),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// the current URL
|
||||
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
|
||||
|
||||
@@ -4,6 +4,12 @@ use std::{
|
||||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use leptos::*;
|
||||
|
||||
@@ -15,6 +21,8 @@ use crate::{
|
||||
RouteContext, RouterContext,
|
||||
};
|
||||
|
||||
/// Contains route definitions and manages the actual routing process.
|
||||
///
|
||||
/// Contains route definitions and manages the actual routing process.
|
||||
///
|
||||
/// You should locate the `<Routes/>` component wherever on the page you want the routes to appear.
|
||||
@@ -84,6 +92,22 @@ pub fn Routes(
|
||||
let prev_match = prev_matches.and_then(|p| p.get(i));
|
||||
let next_match = next_matches.get(i).unwrap();
|
||||
|
||||
match (prev_routes, prev_match) {
|
||||
(Some(prev), Some(prev_match))
|
||||
if next_match.route.key == prev_match.route.key =>
|
||||
{
|
||||
let prev_one = { prev.borrow()[i].clone() };
|
||||
if i >= next.borrow().len() {
|
||||
next.borrow_mut().push(prev_one);
|
||||
} else {
|
||||
*(next.borrow_mut().index_mut(i)) = prev_one;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
equal = false;
|
||||
if i == 0 {
|
||||
root_equal.set(false);
|
||||
}
|
||||
match (prev_routes, prev_match) {
|
||||
(Some(prev), Some(prev_match))
|
||||
if next_match.route.key == prev_match.route.key =>
|
||||
@@ -101,6 +125,31 @@ pub fn Routes(
|
||||
root_equal.set(false);
|
||||
}
|
||||
|
||||
let disposer = cx.child_scope({
|
||||
let next = next.clone();
|
||||
let router = Rc::clone(&router.inner);
|
||||
move |cx| {
|
||||
let next = next.clone();
|
||||
let next_ctx = RouteContext::new(
|
||||
cx,
|
||||
&RouterContext { inner: router },
|
||||
{
|
||||
let next = next.clone();
|
||||
move || {
|
||||
if let Some(route_states) =
|
||||
use_context::<Memo<RouterState>>(cx)
|
||||
{
|
||||
route_states.with(|route_states| {
|
||||
let routes = route_states.routes.borrow();
|
||||
routes.get(i + 1).cloned()
|
||||
})
|
||||
} else {
|
||||
next.borrow().get(i + 1).cloned()
|
||||
}
|
||||
}
|
||||
},
|
||||
move || matches.with(|m| m.get(i).cloned()),
|
||||
);
|
||||
let disposer = cx.child_scope({
|
||||
let next = next.clone();
|
||||
let router = Rc::clone(&router.inner);
|
||||
@@ -136,6 +185,15 @@ pub fn Routes(
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(next_ctx) = next_ctx {
|
||||
if next.borrow().len() > i + 1 {
|
||||
next.borrow_mut()[i] = next_ctx;
|
||||
} else {
|
||||
next.borrow_mut().push(next_ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if disposers.borrow().len() > i + 1 {
|
||||
let mut disposers = disposers.borrow_mut();
|
||||
@@ -147,7 +205,23 @@ pub fn Routes(
|
||||
}
|
||||
}
|
||||
}
|
||||
if disposers.borrow().len() > i + 1 {
|
||||
let mut disposers = disposers.borrow_mut();
|
||||
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
|
||||
old_route_disposer.dispose();
|
||||
} else {
|
||||
disposers.borrow_mut().push(disposer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if disposers.borrow().len() > next_matches.len() {
|
||||
let surplus_disposers = disposers.borrow_mut().split_off(next_matches.len() + 1);
|
||||
for disposer in surplus_disposers {
|
||||
disposer.dispose();
|
||||
}
|
||||
}
|
||||
if disposers.borrow().len() > next_matches.len() {
|
||||
let surplus_disposers = disposers.borrow_mut().split_off(next_matches.len() + 1);
|
||||
for disposer in surplus_disposers {
|
||||
@@ -155,6 +229,20 @@ pub fn Routes(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prev) = &prev {
|
||||
if equal {
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: prev_routes.cloned().unwrap_or_default(),
|
||||
root: prev.root.clone(),
|
||||
}
|
||||
} else {
|
||||
let root = next.borrow().get(0).cloned();
|
||||
RouterState {
|
||||
matches: next_matches.to_vec(),
|
||||
routes: Rc::new(RefCell::new(next.borrow().to_vec())),
|
||||
root,
|
||||
}
|
||||
if let Some(prev) = &prev {
|
||||
if equal {
|
||||
RouterState {
|
||||
|
||||
@@ -107,36 +107,51 @@ where
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError>;
|
||||
}
|
||||
|
||||
impl<T> IntoParam for Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Err(ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
},
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(not(feature = "stable"))] {
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr + NotOption,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
|
||||
impl<T> IntoParam for Option<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, _name: &str) -> Result<Self, ParamsError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(value) => match T::from_str(value) {
|
||||
Ok(value) => Ok(Some(value)),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
Err(ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
impl<T> IntoParam for T
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
|
||||
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
|
||||
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
//! them with server-side rendering (with or without hydration), they just work,
|
||||
//! whether JS/WASM have loaded or not.
|
||||
//!
|
||||
//! Note as well that client-side routing works with ordinary `<a>` tags, as well,
|
||||
//! so you do not even need to use the `<A/>` component in most cases.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
@@ -136,10 +139,9 @@
|
||||
//!
|
||||
//! ```
|
||||
|
||||
#![feature(auto_traits)]
|
||||
#![feature(let_chains)]
|
||||
#![feature(negative_impls)]
|
||||
#![feature(type_name_of_val)]
|
||||
#![cfg_attr(not(feature = "stable"), feature(auto_traits))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(negative_impls))]
|
||||
#![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
|
||||
|
||||
mod components;
|
||||
mod history;
|
||||
|
||||
@@ -80,13 +80,15 @@ impl Matcher {
|
||||
path.push_str(loc_segment);
|
||||
}
|
||||
|
||||
if let Some(splat) = &self.splat && !splat.is_empty() {
|
||||
let value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
params.insert(splat.into(), value);
|
||||
if let Some(splat) = &self.splat {
|
||||
if !splat.is_empty() {
|
||||
let value = if len_diff > 0 {
|
||||
loc_segments[self.len..].join("/")
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
params.insert(splat.into(), value);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PathMatch { path, params })
|
||||
|
||||
Reference in New Issue
Block a user