mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
Revert "Merge branch 'main' into pr/119"
This reverts commit63f680f37d
, reversing changes made to50ba796f49
.
This commit is contained in:
parent
fd2a2bd5f4
commit
4b1fce4c9c
70 changed files with 1628 additions and 797 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,4 +6,3 @@ blob.rs
|
|||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
.leptos.kdl
|
||||
|
|
27
Cargo.toml
27
Cargo.toml
|
@ -43,29 +43,4 @@ lto = true
|
|||
opt-level = 'z'
|
||||
|
||||
[workspace.metadata.cargo-all-features]
|
||||
skip_feature_sets = [
|
||||
[
|
||||
"csr",
|
||||
"ssr",
|
||||
],
|
||||
[
|
||||
"csr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"ssr",
|
||||
"hydrate",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"serde-lite",
|
||||
],
|
||||
[
|
||||
"serde-lite",
|
||||
"miniserde",
|
||||
],
|
||||
[
|
||||
"serde",
|
||||
"miniserde",
|
||||
],
|
||||
]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
|
|
46
README.md
46
README.md
|
@ -52,17 +52,6 @@ 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:
|
||||
|
@ -74,18 +63,7 @@ Here are some resources for learning more about Leptos:
|
|||
|
||||
## `nightly` Note
|
||||
|
||||
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:
|
||||
Most of the examples assume you’re using `nightly` Rust. If you’re on stable, note the following:
|
||||
|
||||
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
|
@ -126,17 +104,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) -> Element {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
let input: Element;
|
||||
pub fn Todo(cx: Scope, todo: Todo) -> impl IntoView {
|
||||
let (editing, set_editing) = create_signal(cx, false);
|
||||
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
|
||||
//let input = NodeRef::new(cx);
|
||||
|
||||
let 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_b) = create_signal(cx, false);
|
||||
let (b, set_a) = create_signal(cx, false);
|
||||
|
||||
create_effect(cx, move |_| {
|
||||
if a() > 5 {
|
||||
|
|
|
@ -4,4 +4,4 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
leptos = "0.0.18"
|
||||
|
|
|
@ -4,4 +4,4 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
leptos = "0.0.18"
|
||||
|
|
|
@ -4,7 +4,7 @@ fn main() {
|
|||
mount_to_body(|cx| {
|
||||
let name = "gbj";
|
||||
let userid = 0;
|
||||
let _input_element = NodeRef::new(cx);
|
||||
let _input_element: Element;
|
||||
|
||||
view! {
|
||||
cx,
|
||||
|
|
|
@ -4,4 +4,4 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
leptos = { path = "../../../../leptos" }
|
||||
leptos = "0.0.18"
|
||||
|
|
|
@ -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,5 +3,3 @@
|
|||
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/)
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# 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/)
|
|
@ -1,10 +0,0 @@
|
|||
# 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"
|
||||
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# 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,6 +1,3 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use gloo_timers::future::TimeoutFuture;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -10,10 +7,6 @@ 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={}",
|
||||
|
@ -37,9 +30,8 @@ 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?"
|
||||
|
@ -51,33 +43,25 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
|
|||
}
|
||||
/>
|
||||
</label>
|
||||
{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 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),
|
||||
})
|
||||
}
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
# 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(create_runtime(), |cx| {
|
||||
_ = create_scope(|cx| {
|
||||
// Create a new application
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
|
|
|
@ -3,27 +3,18 @@
|
|||
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 CSR bundle.
|
||||
|
||||
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)
|
||||
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
|
||||
|
||||
## 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 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.
|
||||
|
||||
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
|
||||
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!
|
||||
|
|
66
examples/hackernews-axum/src/handlers.rs
Normal file
66
examples/hackernews-axum/src/handlers.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
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,6 +3,7 @@ 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], 3000));
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3002));
|
||||
|
||||
log::debug!("serving at {addr}");
|
||||
|
||||
|
|
|
@ -14,11 +14,9 @@ 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_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos = { path = "../../leptos", default-features = false, features = ["serde"] }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_actix = { path = "../../integrations/actix", default-features = false, optional = true }
|
||||
leptos_router = { path = "../../router", default-features = false }
|
||||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
|
@ -27,7 +25,9 @@ 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,29 +1,20 @@
|
|||
# Leptos Hacker News Example
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
## 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/)
|
||||
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
|
||||
|
||||
## 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 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.
|
||||
|
||||
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
|
||||
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="/" class="home".to_string()>
|
||||
<A href="/">
|
||||
<strong>"HN"</strong>
|
||||
</A>
|
||||
<A href="/new">
|
||||
|
|
|
@ -33,14 +33,10 @@ 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,7 +1,6 @@
|
|||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
|
||||
#[component]
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
# 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,11 +1,8 @@
|
|||
# Leptos Router Example
|
||||
|
||||
This example demonstrates how Leptos’s router for client side routing.
|
||||
|
||||
## Build and Run it
|
||||
This example demonstrates how Leptos' router works
|
||||
|
||||
## 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="last_name" placeholder="Last"/>
|
||||
<input type="text" name="first_name" placeholder="Last"/>
|
||||
</fieldset>
|
||||
<pre>"This page is just a placeholder."</pre>
|
||||
</form>
|
||||
|
|
|
@ -59,4 +59,66 @@ denylist = [
|
|||
"sqlx",
|
||||
"leptos_axum",
|
||||
]
|
||||
skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
||||
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"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
futures = "0.3.25"
|
||||
cfg-if = "1.0.0"
|
||||
leptos = { path = "../../leptos", default-features = false, features = [
|
||||
"serde",
|
||||
] }
|
||||
leptos_axum = { path = "../../integrations/axum", default-features = false, optional = true }
|
||||
leptos_meta = { path = "../../meta", default-features = false }
|
||||
leptos_router = { path = "../../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"]]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
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,16 +25,23 @@ leptos_router = { path = "../../router", default-features = false }
|
|||
log = "0.4"
|
||||
simple_logger = "2"
|
||||
gloo = { git = "https://github.com/rustwasm/gloo" }
|
||||
sqlx = { version = "0.6.2", features = [
|
||||
sqlx = { version = "0.6", 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 Todo App Sqlite
|
||||
# Leptos Counter Isomorphic Example
|
||||
|
||||
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
|
||||
This example demonstrates how to use a server functions and multi-actions to build a simple todo app.
|
||||
|
||||
## Server Side Rendering With Hydration
|
||||
|
||||
|
@ -10,12 +10,11 @@ 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 hydrate the HTML that is generated on 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
|
||||
cargo run --no-default-features --features=ssr
|
||||
cargo run
|
||||
```
|
||||
|
||||
> Note that if your hydration code changes, you will have to rerun the wasm-pack command above
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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.
|
||||
|
@ -6,7 +7,6 @@ cfg_if! {
|
|||
if #[cfg(feature = "hydrate")] {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use crate::todo::*;
|
||||
use leptos::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
mod todo;
|
||||
|
|
|
@ -7,17 +7,15 @@ 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)]
|
||||
|
@ -41,16 +39,12 @@ 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!("\ncalling server fn");
|
||||
println!(" req.path = {:?}", req.path());
|
||||
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
|
||||
|
@ -69,14 +63,16 @@ 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(350));
|
||||
std::thread::sleep(std::time::Duration::from_millis(1250));
|
||||
|
||||
sqlx::query("INSERT INTO todos (title, completed) VALUES ($1, false)")
|
||||
match 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()))
|
||||
{
|
||||
Ok(row) => Ok(()),
|
||||
Err(e) => Err(ServerFnError::ServerError(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[server(DeleteTodo, "/api")]
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
# 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,16 +1,12 @@
|
|||
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::*;
|
||||
|
@ -49,52 +45,6 @@ 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();
|
||||
|
@ -374,7 +324,6 @@ where IV: IntoView
|
|||
|
||||
let (mut tx, rx) = futures::channel::mpsc::channel(8);
|
||||
|
||||
spawn_blocking({
|
||||
spawn_blocking({
|
||||
let app_fn = app_fn.clone();
|
||||
move || {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
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,12 +27,8 @@
|
|||
//! 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
|
||||
//! - [`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
|
||||
//! - [`counters`](https://github.com/gbj/leptos/tree/main/examples/counter) 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
|
||||
|
@ -51,13 +47,6 @@
|
|||
//! - [`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/)).)
|
||||
|
@ -88,10 +77,6 @@
|
|||
//! 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.
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
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\"")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
# 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>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
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,7 +6,6 @@ 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,
|
||||
|
@ -70,24 +69,6 @@ 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,
|
||||
|
@ -114,45 +95,6 @@ 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);
|
||||
|
@ -283,10 +225,6 @@ 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,6 +7,7 @@ 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)]
|
||||
|
@ -29,6 +30,7 @@ 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
|
||||
|
@ -177,15 +179,12 @@ 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
|
||||
/// # ;
|
||||
|
@ -414,54 +413,6 @@ 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()) {
|
||||
|
@ -470,6 +421,15 @@ 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 {
|
||||
|
|
1270
leptos_macro/src/props.rs
Normal file
1270
leptos_macro/src/props.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -702,34 +702,14 @@ fn component_to_tokens(cx: &Ident, node: &NodeElement) -> TokenStream {
|
|||
}
|
||||
};
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
quote! {
|
||||
#name(
|
||||
#cx,
|
||||
#component_props_name::builder()
|
||||
#(#props)*
|
||||
#children
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ web-sys = { version = "0.3", features = [
|
|||
"Window",
|
||||
] }
|
||||
cfg-if = "1.0.0"
|
||||
rmp-serde = "1.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
|
@ -46,34 +45,5 @@ 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",
|
||||
],
|
||||
]
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
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: 'static,
|
||||
T: Debug + '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: 'static,
|
||||
T: Debug + '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: 'static,
|
||||
T: Debug + 'static,
|
||||
{
|
||||
create_effect(cx, f);
|
||||
}
|
||||
|
|
|
@ -215,7 +215,7 @@ where
|
|||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnOnce<()> for Memo<T>
|
||||
where
|
||||
T: Clone,
|
||||
T: Debug + Clone,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
|
@ -227,7 +227,7 @@ where
|
|||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnMut<()> for Memo<T>
|
||||
where
|
||||
T: Clone,
|
||||
T: Debug + 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: Clone,
|
||||
T: Debug + Clone,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
|
|
|
@ -66,10 +66,9 @@ pub fn create_resource<S, T, Fu>(
|
|||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
T: Debug + 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;
|
||||
|
||||
|
@ -92,7 +91,7 @@ pub fn create_resource_with_initial_value<S, T, Fu>(
|
|||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
let resolved = initial_value.is_some();
|
||||
|
@ -174,7 +173,7 @@ pub fn create_local_resource<S, T, Fu>(
|
|||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: 'static,
|
||||
T: Debug + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
let initial_value = None;
|
||||
|
@ -196,7 +195,7 @@ pub fn create_local_resource_with_initial_value<S, T, Fu>(
|
|||
) -> Resource<S, T>
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: 'static,
|
||||
T: Debug + 'static,
|
||||
Fu: Future<Output = T> + 'static,
|
||||
{
|
||||
let resolved = initial_value.is_some();
|
||||
|
@ -245,7 +244,7 @@ where
|
|||
fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: 'static,
|
||||
T: Debug + 'static,
|
||||
{
|
||||
r.load(false)
|
||||
}
|
||||
|
@ -254,7 +253,7 @@ where
|
|||
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
|
||||
where
|
||||
S: PartialEq + Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
{
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
||||
|
@ -328,7 +327,7 @@ where
|
|||
impl<S, T> Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: 'static,
|
||||
T: Debug + '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
|
||||
|
@ -435,7 +434,7 @@ where
|
|||
pub struct Resource<S, T>
|
||||
where
|
||||
S: Debug + 'static,
|
||||
T: 'static,
|
||||
T: Debug + 'static,
|
||||
{
|
||||
runtime: RuntimeId,
|
||||
pub(crate) id: ResourceId,
|
||||
|
@ -452,7 +451,7 @@ slotmap::new_key_type! {
|
|||
impl<S, T> Clone for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
|
@ -467,7 +466,7 @@ where
|
|||
impl<S, T> Copy for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -475,7 +474,7 @@ where
|
|||
impl<S, T> FnOnce<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
{
|
||||
type Output = Option<T>;
|
||||
|
||||
|
@ -488,7 +487,7 @@ where
|
|||
impl<S, T> FnMut<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
|
@ -499,7 +498,7 @@ where
|
|||
impl<S, T> Fn<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
|
@ -510,7 +509,7 @@ where
|
|||
pub(crate) struct ResourceState<S, T>
|
||||
where
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
T: Debug + 'static,
|
||||
{
|
||||
scope: Scope,
|
||||
value: ReadSignal<Option<T>>,
|
||||
|
@ -528,7 +527,7 @@ where
|
|||
impl<S, T> ResourceState<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: 'static,
|
||||
T: Debug + 'static,
|
||||
{
|
||||
pub fn read(&self) -> Option<T>
|
||||
where
|
||||
|
@ -647,7 +646,6 @@ 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -669,7 +667,7 @@ pub(crate) trait SerializableResource {
|
|||
impl<S, T> SerializableResource for ResourceState<S, T>
|
||||
where
|
||||
S: Debug + Clone,
|
||||
T: Serializable,
|
||||
T: Debug + Serializable,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
|
@ -689,6 +687,9 @@ 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,16 +226,7 @@ impl Debug for Runtime {
|
|||
|
||||
impl Runtime {
|
||||
pub fn new() -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "csr", feature = "hydration"))] {
|
||||
Self::default()
|
||||
} else {
|
||||
Runtime {
|
||||
shared_context: RefCell::new(Some(Default::default())),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn create_unserializable_resource<S, T>(
|
||||
|
@ -244,7 +235,7 @@ impl Runtime {
|
|||
) -> ResourceId
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: 'static,
|
||||
T: Debug + 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
|
@ -257,7 +248,7 @@ impl Runtime {
|
|||
) -> ResourceId
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
|
@ -271,7 +262,7 @@ impl Runtime {
|
|||
) -> U
|
||||
where
|
||||
S: Debug + 'static,
|
||||
T: 'static,
|
||||
T: Debug + '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: Clone,
|
||||
T: Debug + Clone,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
|
@ -240,7 +240,7 @@ where
|
|||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnMut<()> for ReadSignal<T>
|
||||
where
|
||||
T: Clone,
|
||||
T: Debug + 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: Clone,
|
||||
T: Debug + 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: Clone,
|
||||
T: Debug + Clone,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
|
@ -746,7 +746,7 @@ where
|
|||
#[cfg(not(feature = "stable"))]
|
||||
impl<T> FnMut<()> for RwSignal<T>
|
||||
where
|
||||
T: Clone,
|
||||
T: Debug + 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: Clone,
|
||||
T: Debug + Clone,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.get()
|
||||
|
|
|
@ -21,7 +21,3 @@ default = ["csr"]
|
|||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["leptos/ssr"]
|
||||
stable = ["leptos/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["stable"]
|
||||
|
|
|
@ -57,8 +57,7 @@ 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", "stable"]
|
||||
denylist = ["url", "regex"]
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::{use_navigate, use_resolved_path, TextProp};
|
||||
use cfg_if::cfg_if;
|
||||
use crate::{use_navigate, use_resolved_path, ToHref};
|
||||
use leptos::*;
|
||||
use leptos::typed_builder::*;
|
||||
use std::{error::Error, rc::Rc};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
@ -41,10 +39,10 @@ pub fn Form<A>(
|
|||
children: Box<dyn Fn(Scope) -> Fragment>,
|
||||
) -> impl IntoView
|
||||
where
|
||||
A: TextProp + 'static,
|
||||
A: ToHref + 'static,
|
||||
{
|
||||
let action_version = version;
|
||||
let action = use_resolved_path(cx, move || action.to_value()());
|
||||
let action = use_resolved_path(cx, move || action.to_href()());
|
||||
|
||||
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 TextProp {
|
||||
pub trait ToHref {
|
||||
/// Converts the (static or reactive) URL into a function that can be called to
|
||||
/// return the URL.
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String + '_>;
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_>;
|
||||
}
|
||||
|
||||
impl TextProp for &str {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String> {
|
||||
impl ToHref for &str {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.to_string();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl TextProp for String {
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String> {
|
||||
impl ToHref for String {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String> {
|
||||
let s = self.clone();
|
||||
Box::new(move || s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> TextProp for F
|
||||
impl<F> ToHref for F
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
fn to_value(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
fn to_href(&self) -> Box<dyn Fn() -> String + '_> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +92,6 @@ 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,13 +1,10 @@
|
|||
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,11 +97,6 @@ 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,7 +2,6 @@ use cfg_if::cfg_if;
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use leptos::*;
|
||||
use leptos::typed_builder::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
|
@ -65,6 +64,7 @@ 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,10 +86,7 @@ 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/>.\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");
|
||||
let history = use_context::<RouterIntegrationContext>(cx).expect("You must call provide_context::<RouterIntegrationContext>(cx, ...) somewhere above the <Router/>.");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -102,16 +99,14 @@ impl RouterContext {
|
|||
let base = base.unwrap_or_default();
|
||||
let base_path = resolve_path("", base, 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),
|
||||
});
|
||||
}
|
||||
}
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
// the current URL
|
||||
let (reference, set_reference) = create_signal(cx, source.with(|s| s.value.clone()));
|
||||
|
|
|
@ -4,12 +4,6 @@ use std::{
|
|||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cmp::Reverse,
|
||||
ops::IndexMut,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use leptos::*;
|
||||
|
||||
|
@ -21,8 +15,6 @@ 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.
|
||||
|
@ -92,22 +84,6 @@ 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 =>
|
||||
|
@ -125,31 +101,6 @@ 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);
|
||||
|
@ -185,15 +136,6 @@ 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();
|
||||
|
@ -205,23 +147,7 @@ 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 {
|
||||
|
@ -229,20 +155,6 @@ 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,54 +107,39 @@ where
|
|||
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError>;
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
},
|
||||
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)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur while parsing params using [Params](crate::Params).
|
||||
#[derive(Error, Debug, Clone)]
|
||||
pub enum ParamsError {
|
||||
|
|
|
@ -32,9 +32,6 @@
|
|||
//! 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
|
||||
|
@ -139,9 +136,10 @@
|
|||
//!
|
||||
//! ```
|
||||
|
||||
#![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))]
|
||||
#![feature(auto_traits)]
|
||||
#![feature(let_chains)]
|
||||
#![feature(negative_impls)]
|
||||
#![feature(type_name_of_val)]
|
||||
|
||||
mod components;
|
||||
mod history;
|
||||
|
|
|
@ -80,15 +80,13 @@ impl Matcher {
|
|||
path.push_str(loc_segment);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
Some(PathMatch { path, params })
|
||||
|
|
Loading…
Reference in a new issue