mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
Merge branch 'main' into cargo-leptos-release-testing
This commit is contained in:
commit
8eaa0b0c15
25 changed files with 275 additions and 237 deletions
55
README.md
55
README.md
|
@ -1,3 +1,5 @@
|
|||
**NOTE: We're in the middle of merging changes and making fixes to support our upcoming `0.1.0` release. Some of the examples may be in a broken state. You can continue using the `0.0` releases with no issues.**
|
||||
|
||||
<img src="https://raw.githubusercontent.com/gbj/leptos/main/docs/logos/logo.svg" alt="Leptos Logo" style="width: 100%; height: auto; display: block; margin: auto;">
|
||||
|
||||
[![crates.io](https://img.shields.io/crates/v/leptos.svg)](https://crates.io/crates/leptos)
|
||||
|
@ -49,7 +51,7 @@ Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained re
|
|||
- **Isomorphic**: Leptos provides primitives to write isomorphic server functions, i.e., functions that can be called with the “same shape” on the client or server, but only run on the server. This means you can write your server-only logic (database requests, authentication etc.) alongside the client-side components that will consume it, and call server functions as if they were running in the browser.
|
||||
- **Web**: Leptos is built on the Web platform and Web standards. The router is designed to use Web fundamentals (like links and forms) and build on top of them rather than trying to replace them.
|
||||
- **Framework**: Leptos provides most of what you need to build a modern web app: a reactive system, templating library, and a router that works on both the server and client side.
|
||||
- **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!_)
|
||||
- **Fine-grained reactivity**: The entire framework is built 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.
|
||||
|
||||
## Learn more
|
||||
|
@ -61,9 +63,20 @@ Here are some resources for learning more about Leptos:
|
|||
- [Common Bugs](https://github.com/gbj/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
|
||||
- Leptos Guide (in progress)
|
||||
|
||||
|
||||
## `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 Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you haven’t already)
|
||||
|
||||
```
|
||||
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.1.0-alpha", features = ["stable"] }`
|
||||
2. `nightly` enables the function call syntax for accessing and setting signals. If you’re using `stable`,
|
||||
|
@ -71,6 +84,17 @@ Most of the examples assume you’re using `nightly` Rust. If you’re on stable
|
|||
[`counters-stable` example](https://github.com/gbj/leptos/blob/main/examples/counters-stable/src/main.rs)
|
||||
for examples of the correct API.
|
||||
|
||||
## `cargo-leptos`
|
||||
|
||||
[`cargo-leptos`](https://github.com/akesson/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` 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
|
||||
```
|
||||
|
||||
## FAQs
|
||||
|
||||
### Can I use this for native GUI?
|
||||
|
@ -90,7 +114,7 @@ On the surface level, these libraries may seem similar. Yew is, of course, the m
|
|||
|
||||
- **VDOM vs. fine-grained:** Yew is built on the virtual DOM (VDOM) model: state changes cause components to re-render, generating a new virtual DOM tree. Yew diffs this against the previous VDOM, and applies those patches to the actual DOM. Component functions rerun whenever state changes. Leptos takes an entirely different approach. Components run once, creating (and returning) actual DOM nodes and setting up a reactive system to update those DOM nodes.
|
||||
- **Performance:** This has huge performance implications: Leptos is simply _much_ faster at both creating and updating the UI than Yew is.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising components re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
- **Mental model:** Adopting fine-grained reactivity also tends to simplify the mental model. There are no surprising component re-renders because there are no re-renders. Your app can be divided into components based on what makes sense for your app, because they have no performance implications.
|
||||
|
||||
### How is this different from Sycamore?
|
||||
|
||||
|
@ -104,17 +128,16 @@ 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.
|
||||
- **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 wrappers 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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -38,6 +38,7 @@ ssr = [
|
|||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
stable = ["leptos/stable", "leptos_router/stable"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["actix-files", "actix-web", "leptos_actix"]
|
||||
|
|
|
@ -22,42 +22,45 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
|||
view! { cx,
|
||||
<>
|
||||
<Meta name="description" content=meta_description/>
|
||||
{move || story.read().map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { cx, <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{}", user)>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || story.read().map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { cx, <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{}", user)>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
}})
|
||||
}
|
||||
</Suspense>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,28 +18,30 @@ pub fn User(cx: Scope) -> impl IntoView {
|
|||
);
|
||||
view! { cx,
|
||||
<div class="user-view">
|
||||
{move || user.read().map(|user| match user {
|
||||
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
|
||||
Some(user) => view! { cx,
|
||||
<div>
|
||||
<h1>"User: " {&user.id}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
}.into_any()
|
||||
})}
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || user.read().map(|user| match user {
|
||||
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
|
||||
Some(user) => view! { cx,
|
||||
<div>
|
||||
<h1>"User: " {&user.id}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
}.into_any()
|
||||
})}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ skip_feature_sets = [["csr", "ssr"], ["csr", "hydrate"], ["ssr", "hydrate"]]
|
|||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||
output-name = "leptos_hackernews"
|
||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||
site-root = "target/site"
|
||||
site-root = "/pkg"
|
||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||
# Defaults to pkg
|
||||
site-pkg-dir = "pkg"
|
||||
|
|
|
@ -22,42 +22,45 @@ pub fn Story(cx: Scope) -> impl IntoView {
|
|||
view! { cx,
|
||||
<>
|
||||
<Meta name="description" content=meta_description/>
|
||||
{move || story.read().map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { cx, <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{}", user)>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || story.read().map(|story| match story {
|
||||
None => view! { cx, <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! { cx,
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
<h1>{story.title}</h1>
|
||||
</a>
|
||||
<span class="host">
|
||||
"("{story.domain}")"
|
||||
</span>
|
||||
{story.user.map(|user| view! { cx, <p class="meta">
|
||||
{story.points}
|
||||
" points | by "
|
||||
<A href=format!("/users/{}", user)>{user.clone()}</A>
|
||||
{format!(" {}", story.time_ago)}
|
||||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
view=move |comment| view! { cx, <Comment comment /> }
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
}})
|
||||
}
|
||||
</Suspense>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,28 +18,30 @@ pub fn User(cx: Scope) -> impl IntoView {
|
|||
);
|
||||
view! { cx,
|
||||
<div class="user-view">
|
||||
{move || user.read().map(|user| match user {
|
||||
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
|
||||
Some(user) => view! { cx,
|
||||
<div>
|
||||
<h1>"User: " {&user.id}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
}.into_any()
|
||||
})}
|
||||
<Suspense fallback=|| view! { cx, "Loading..." }>
|
||||
{move || user.read().map(|user| match user {
|
||||
None => view! { cx, <h1>"User not found."</h1> }.into_any(),
|
||||
Some(user) => view! { cx,
|
||||
<div>
|
||||
<h1>"User: " {&user.id}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
</li>
|
||||
<li>
|
||||
<span class="label">"Karma: "</span> {user.karma}
|
||||
</li>
|
||||
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
|
||||
</ul>
|
||||
<p class="links">
|
||||
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
|
||||
" | "
|
||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
}.into_any()
|
||||
})}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)]
|
||||
|
@ -151,7 +151,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
|||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Suspense fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
|
@ -221,7 +221,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
|||
}
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ cfg_if! {
|
|||
use actix_files::{Files};
|
||||
use actix_web::*;
|
||||
use crate::todo::*;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[get("/style.css")]
|
||||
async fn css() -> impl Responder {
|
||||
|
|
|
@ -135,7 +135,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
|||
</label>
|
||||
<input type="submit" value="Add"/>
|
||||
</MultiActionForm>
|
||||
<Suspense fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
<Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
|
||||
{
|
||||
let delete_todo = delete_todo.clone();
|
||||
move || {
|
||||
|
@ -205,7 +205,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
|
|||
}
|
||||
}
|
||||
}
|
||||
</Suspense>
|
||||
</Transition>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -187,8 +187,10 @@ where
|
|||
// If it was, `DynChild` no longer "owns" that child, and
|
||||
// is therefore no longer sound to unmount it from the DOM
|
||||
// or to reuse it in the case of a text node
|
||||
let was_child_moved =
|
||||
child.get_closing_node().next_sibling().as_ref() != Some(&closing);
|
||||
|
||||
// FIXME this was breaking DynChild updates on text nodes, at least...
|
||||
let was_child_moved = false;
|
||||
//child.get_closing_node().next_sibling().as_ref() != Some(&closing);
|
||||
|
||||
// If the previous child was a text node, we would like to
|
||||
// make use of it again if our current child is also a text
|
||||
|
|
|
@ -60,6 +60,10 @@ impl<E: FromWasmAbi> EventDescriptor for Custom<E> {
|
|||
fn name(&self) -> Cow<'static, str> {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn bubbles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FromWasmAbi> Custom<E> {
|
||||
|
|
|
@ -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,7 +66,7 @@ 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
|
||||
|
@ -91,7 +91,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 +173,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 +195,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 +244,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 +253,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};
|
||||
|
||||
|
@ -267,18 +267,8 @@ where
|
|||
|
||||
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON");
|
||||
|
||||
// if we're under Suspense, the HTML has already streamed in so we can just set it
|
||||
// if not under Suspense, there will be a hydration mismatch, so let's wait a tick
|
||||
if use_context::<SuspenseContext>(cx).is_some() {
|
||||
r.set_value.update(|n| *n = Some(res));
|
||||
r.set_loading.update(|n| *n = false);
|
||||
} else {
|
||||
let r = Rc::clone(&r);
|
||||
spawn_local(async move {
|
||||
r.set_value.update(|n| *n = Some(res));
|
||||
r.set_loading.update(|n| *n = false);
|
||||
});
|
||||
}
|
||||
r.set_value.update(|n| *n = Some(res));
|
||||
r.set_loading.update(|n| *n = false);
|
||||
|
||||
// for reactivity
|
||||
r.source.subscribe();
|
||||
|
@ -326,8 +316,8 @@ where
|
|||
|
||||
impl<S, T> Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
S: Clone + '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
|
||||
|
@ -433,8 +423,8 @@ where
|
|||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Resource<S, T>
|
||||
where
|
||||
S: Debug + 'static,
|
||||
T: Debug + 'static,
|
||||
S: 'static,
|
||||
T: 'static,
|
||||
{
|
||||
runtime: RuntimeId,
|
||||
pub(crate) id: ResourceId,
|
||||
|
@ -450,8 +440,8 @@ slotmap::new_key_type! {
|
|||
|
||||
impl<S, T> Clone for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
|
@ -465,16 +455,16 @@ where
|
|||
|
||||
impl<S, T> Copy for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "stable"))]
|
||||
impl<S, T> FnOnce<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
type Output = Option<T>;
|
||||
|
||||
|
@ -486,8 +476,8 @@ where
|
|||
#[cfg(not(feature = "stable"))]
|
||||
impl<S, T> FnMut<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
|
@ -497,8 +487,8 @@ where
|
|||
#[cfg(not(feature = "stable"))]
|
||||
impl<S, T> Fn<()> for Resource<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Clone + 'static,
|
||||
S: Clone + 'static,
|
||||
T: Clone + 'static,
|
||||
{
|
||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||
self.read()
|
||||
|
@ -509,7 +499,7 @@ where
|
|||
pub(crate) struct ResourceState<S, T>
|
||||
where
|
||||
S: 'static,
|
||||
T: Debug + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
scope: Scope,
|
||||
value: ReadSignal<Option<T>>,
|
||||
|
@ -526,8 +516,8 @@ where
|
|||
|
||||
impl<S, T> ResourceState<S, T>
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
pub fn read(&self) -> Option<T>
|
||||
where
|
||||
|
@ -666,8 +656,8 @@ pub(crate) trait SerializableResource {
|
|||
|
||||
impl<S, T> SerializableResource for ResourceState<S, T>
|
||||
where
|
||||
S: Debug + Clone,
|
||||
T: Debug + Serializable,
|
||||
S: Clone,
|
||||
T: Serializable,
|
||||
{
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
|
@ -686,11 +676,7 @@ pub(crate) trait UnserializableResource {
|
|||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
impl<S, T> UnserializableResource for ResourceState<S, T>
|
||||
where
|
||||
S: Debug,
|
||||
T: Debug,
|
||||
{
|
||||
impl<S, T> UnserializableResource for ResourceState<S, T> {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
|
|
@ -234,8 +234,8 @@ impl Runtime {
|
|||
state: Rc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + 'static,
|
||||
S: Clone + 'static,
|
||||
T: 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
|
@ -247,8 +247,8 @@ impl Runtime {
|
|||
state: Rc<ResourceState<S, T>>,
|
||||
) -> ResourceId
|
||||
where
|
||||
S: Debug + Clone + 'static,
|
||||
T: Debug + Serializable + 'static,
|
||||
S: Clone + 'static,
|
||||
T: Serializable + 'static,
|
||||
{
|
||||
self.resources
|
||||
.borrow_mut()
|
||||
|
@ -261,8 +261,8 @@ impl Runtime {
|
|||
f: impl FnOnce(&ResourceState<S, T>) -> U,
|
||||
) -> U
|
||||
where
|
||||
S: Debug + 'static,
|
||||
T: Debug + 'static,
|
||||
S: '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()
|
||||
|
|
|
@ -9,8 +9,8 @@ description = "Tools to set HTML metadata in the Leptos web framework."
|
|||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
leptos = { path = "../leptos", version = "0.1.0-alpha", default-features = false }
|
||||
tracing = "0.1"
|
||||
leptos = { path = "../leptos", default-features = false }
|
||||
typed-builder = "0.11"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
|
@ -18,6 +18,10 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
|
|||
|
||||
[features]
|
||||
default = ["csr"]
|
||||
csr = ["leptos/csr", "leptos/tracing"]
|
||||
hydrate = ["leptos/hydrate", "leptos/tracing"]
|
||||
ssr = ["leptos/ssr", "leptos/tracing"]
|
||||
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"]
|
||||
|
|
|
@ -64,7 +64,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)
|
||||
|
@ -99,13 +98,15 @@ 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
|
||||
|
|
|
@ -126,17 +126,21 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
auto trait NotOption {}
|
||||
impl<T> !NotOption for Option<T> {}
|
||||
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 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -136,10 +136,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 })
|
||||
|
|
Loading…
Reference in a new issue