Merge pull request #196 from gbj/cleanup

Clean up issues relating to `0.1.0` merge
This commit is contained in:
Greg Johnston 2022-12-29 20:44:31 -05:00 committed by GitHub
commit 26e90d1959
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 269 additions and 237 deletions

View file

@ -63,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!) - [Common Bugs](https://github.com/gbj/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
- Leptos Guide (in progress) - Leptos Guide (in progress)
## `nightly` Note ## `nightly` Note
Most of the examples assume youre using `nightly` Rust. If youre on stable, note the following: Most of the examples assume youre using `nightly` Rust.
To set up your Rust toolchain using `nightly` (and add the ability to compile Rust to WebAssembly, if you havent already)
```
rustup toolchain install nightly
rustup default nightly
rustup target add wasm32-unknown-unknown
```
If youre on `stable`, note the following:
1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }` 1. You need to enable the `"stable"` flag in `Cargo.toml`: `leptos = { version = "0.1.0-alpha", features = ["stable"] }`
2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`, 2. `nightly` enables the function call syntax for accessing and setting signals. If youre using `stable`,
@ -73,6 +84,17 @@ Most of the examples assume youre using `nightly` Rust. If youre on stable
[`counters-stable` example](https://github.com/gbj/leptos/blob/main/examples/counters-stable/src/main.rs) [`counters-stable` example](https://github.com/gbj/leptos/blob/main/examples/counters-stable/src/main.rs)
for examples of the correct API. 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 ## FAQs
### Can I use this for native GUI? ### Can I use this for native GUI?
@ -106,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.)_ - **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: - **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 ```rust
let (count, set_count) = create_signal(cx, 0); // a signal let (count, set_count) = create_signal(cx, 0); // a signal
let double_count = move || count() * 2; // a derived signal let double_count = move || count() * 2; // a derived signal
let memoized_count = create_memo(cx, move |_| count() * 3); // a memo let memoized_count = create_memo(cx, move |_| count() * 3); // a memo
// all are accessed by calling them // all are accessed by calling them
assert_eq!(count(), 0); assert_eq!(count(), 0);
assert_eq!(double_count(), 0); assert_eq!(double_count(), 0);
assert_eq!(memoized_count(), 0); assert_eq!(memoized_count(), 0);
// this function can accept any of those signals
// this function can accept any of those signals fn do_work_on_signal(my_signal: impl Fn() -> i32) { ... }
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 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.

View file

@ -10,7 +10,7 @@ This document is intended as a running list of common issues, with example code
```rust ```rust
let (a, set_a) = create_signal(cx, 0); 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 |_| { create_effect(cx, move |_| {
if a() > 5 { if a() > 5 {

View file

@ -38,6 +38,7 @@ ssr = [
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",
] ]
stable = ["leptos/stable", "leptos_router/stable"]
[package.metadata.cargo-all-features] [package.metadata.cargo-all-features]
denylist = ["actix-files", "actix-web", "leptos_actix"] denylist = ["actix-files", "actix-web", "leptos_actix"]

View file

@ -22,42 +22,45 @@ pub fn Story(cx: Scope) -> impl IntoView {
view! { cx, view! { cx,
<> <>
<Meta name="description" content=meta_description/> <Meta name="description" content=meta_description/>
{move || story.read().map(|story| match story { <Suspense fallback=|| view! { cx, "Loading..." }>
None => view! { cx, <div class="item-view">"Error loading this story."</div> }, {move || story.read().map(|story| match story {
Some(story) => view! { cx, None => view! { cx, <div class="item-view">"Error loading this story."</div> },
<div class="item-view"> Some(story) => view! { cx,
<div class="item-view-header"> <div class="item-view">
<a href=story.url target="_blank"> <div class="item-view-header">
<h1>{story.title}</h1> <a href=story.url target="_blank">
</a> <h1>{story.title}</h1>
<span class="host"> </a>
"("{story.domain}")" <span class="host">
</span> "("{story.domain}")"
{story.user.map(|user| view! { cx, <p class="meta"> </span>
{story.points} {story.user.map(|user| view! { cx, <p class="meta">
" points | by " {story.points}
<A href=format!("/users/{}", user)>{user.clone()}</A> " points | by "
{format!(" {}", story.time_ago)} <A href=format!("/users/{}", user)>{user.clone()}</A>
</p>})} {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>
<div class="item-view-comments"> }})
<p class="item-view-comments-header"> }
{if story.comments_count.unwrap_or_default() > 0 { </Suspense>
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>
}})}
</> </>
} }
} }

View file

@ -18,28 +18,30 @@ pub fn User(cx: Scope) -> impl IntoView {
); );
view! { cx, view! { cx,
<div class="user-view"> <div class="user-view">
{move || user.read().map(|user| match user { <Suspense fallback=|| view! { cx, "Loading..." }>
None => view! { cx, <h1>"User not found."</h1> }.into_any(), {move || user.read().map(|user| match user {
Some(user) => view! { cx, None => view! { cx, <h1>"User not found."</h1> }.into_any(),
<div> Some(user) => view! { cx,
<h1>"User: " {&user.id}</h1> <div>
<ul class="meta"> <h1>"User: " {&user.id}</h1>
<li> <ul class="meta">
<span class="label">"Created: "</span> {user.created} <li>
</li> <span class="label">"Created: "</span> {user.created}
<li> </li>
<span class="label">"Karma: "</span> {user.karma} <li>
</li> <span class="label">"Karma: "</span> {user.karma}
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })} </li>
</ul> {user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
<p class="links"> </ul>
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a> <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> <a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</div> </p>
}.into_any() </div>
})} }.into_any()
})}
</Suspense>
</div> </div>
} }
} }

View file

@ -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 # The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos_hackernews" 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. # 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 # The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg # Defaults to pkg
site-pkg-dir = "pkg" site-pkg-dir = "pkg"

View file

@ -22,42 +22,45 @@ pub fn Story(cx: Scope) -> impl IntoView {
view! { cx, view! { cx,
<> <>
<Meta name="description" content=meta_description/> <Meta name="description" content=meta_description/>
{move || story.read().map(|story| match story { <Suspense fallback=|| view! { cx, "Loading..." }>
None => view! { cx, <div class="item-view">"Error loading this story."</div> }, {move || story.read().map(|story| match story {
Some(story) => view! { cx, None => view! { cx, <div class="item-view">"Error loading this story."</div> },
<div class="item-view"> Some(story) => view! { cx,
<div class="item-view-header"> <div class="item-view">
<a href=story.url target="_blank"> <div class="item-view-header">
<h1>{story.title}</h1> <a href=story.url target="_blank">
</a> <h1>{story.title}</h1>
<span class="host"> </a>
"("{story.domain}")" <span class="host">
</span> "("{story.domain}")"
{story.user.map(|user| view! { cx, <p class="meta"> </span>
{story.points} {story.user.map(|user| view! { cx, <p class="meta">
" points | by " {story.points}
<A href=format!("/users/{}", user)>{user.clone()}</A> " points | by "
{format!(" {}", story.time_ago)} <A href=format!("/users/{}", user)>{user.clone()}</A>
</p>})} {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>
<div class="item-view-comments"> }})
<p class="item-view-comments-header"> }
{if story.comments_count.unwrap_or_default() > 0 { </Suspense>
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>
}})}
</> </>
} }
} }

View file

@ -18,28 +18,30 @@ pub fn User(cx: Scope) -> impl IntoView {
); );
view! { cx, view! { cx,
<div class="user-view"> <div class="user-view">
{move || user.read().map(|user| match user { <Suspense fallback=|| view! { cx, "Loading..." }>
None => view! { cx, <h1>"User not found."</h1> }.into_any(), {move || user.read().map(|user| match user {
Some(user) => view! { cx, None => view! { cx, <h1>"User not found."</h1> }.into_any(),
<div> Some(user) => view! { cx,
<h1>"User: " {&user.id}</h1> <div>
<ul class="meta"> <h1>"User: " {&user.id}</h1>
<li> <ul class="meta">
<span class="label">"Created: "</span> {user.created} <li>
</li> <span class="label">"Created: "</span> {user.created}
<li> </li>
<span class="label">"Karma: "</span> {user.karma} <li>
</li> <span class="label">"Karma: "</span> {user.karma}
{user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })} </li>
</ul> {user.about.as_ref().map(|about| view! { cx, <li inner_html=about class="about"></li> })}
<p class="links"> </ul>
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a> <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> <a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</div> </p>
}.into_any() </div>
})} }.into_any()
})}
</Suspense>
</div> </div>
} }
} }

View file

@ -144,7 +144,7 @@ pub fn Settings(cx: Scope) -> impl IntoView {
<fieldset> <fieldset>
<legend>"Name"</legend> <legend>"Name"</legend>
<input type="text" name="first_name" placeholder="First"/> <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> </fieldset>
<pre>"This page is just a placeholder."</pre> <pre>"This page is just a placeholder."</pre>
</form> </form>

View file

@ -14,9 +14,9 @@ cfg_if! {
} }
pub fn register_server_functions() { pub fn register_server_functions() {
GetTodos::register(); _ = GetTodos::register();
AddTodo::register(); _ = AddTodo::register();
DeleteTodo::register(); _ = DeleteTodo::register();
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@ -151,7 +151,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</label> </label>
<input type="submit" value="Add"/> <input type="submit" value="Add"/>
</MultiActionForm> </MultiActionForm>
<Suspense fallback=move || view! {cx, <p>"Loading..."</p> }> <Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
{ {
let delete_todo = delete_todo.clone(); let delete_todo = delete_todo.clone();
move || { move || {
@ -221,7 +221,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
} }
} }
} }
</Suspense> </Transition>
</div> </div>
} }
} }

View file

@ -9,6 +9,7 @@ cfg_if! {
use actix_files::{Files}; use actix_files::{Files};
use actix_web::*; use actix_web::*;
use crate::todo::*; use crate::todo::*;
use std::net::SocketAddr;
#[get("/style.css")] #[get("/style.css")]
async fn css() -> impl Responder { async fn css() -> impl Responder {

View file

@ -13,9 +13,9 @@ cfg_if! {
} }
pub fn register_server_functions() { pub fn register_server_functions() {
GetTodos::register(); _ = GetTodos::register();
AddTodo::register(); _ = AddTodo::register();
DeleteTodo::register(); _ = DeleteTodo::register();
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
@ -135,7 +135,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</label> </label>
<input type="submit" value="Add"/> <input type="submit" value="Add"/>
</MultiActionForm> </MultiActionForm>
<Suspense fallback=move || view! {cx, <p>"Loading..."</p> }> <Transition fallback=move || view! {cx, <p>"Loading..."</p> }>
{ {
let delete_todo = delete_todo.clone(); let delete_todo = delete_todo.clone();
move || { move || {
@ -205,7 +205,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
} }
} }
} }
</Suspense> </Transition>
</div> </div>
} }
} }

View file

@ -187,8 +187,10 @@ where
// If it was, `DynChild` no longer "owns" that child, and // If it was, `DynChild` no longer "owns" that child, and
// is therefore no longer sound to unmount it from the DOM // is therefore no longer sound to unmount it from the DOM
// or to reuse it in the case of a text node // 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 // 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 // make use of it again if our current child is also a text

View file

@ -49,7 +49,7 @@ use std::fmt::Debug;
/// ``` /// ```
pub fn create_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static) pub fn create_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where where
T: Debug + 'static, T: 'static,
{ {
cfg_if! { cfg_if! {
if #[cfg(not(feature = "ssr"))] { if #[cfg(not(feature = "ssr"))] {
@ -90,7 +90,7 @@ where
/// # }).dispose(); /// # }).dispose();
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static) pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where where
T: Debug + 'static, T: 'static,
{ {
let e = cx.runtime.create_effect(f); let e = cx.runtime.create_effect(f);
cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e))) cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
@ -99,7 +99,7 @@ where
#[doc(hidden)] #[doc(hidden)]
pub fn create_render_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static) pub fn create_render_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where where
T: Debug + 'static, T: 'static,
{ {
create_effect(cx, f); create_effect(cx, f);
} }

View file

@ -215,7 +215,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for Memo<T> impl<T> FnOnce<()> for Memo<T>
where where
T: Debug + Clone, T: Clone,
{ {
type Output = T; type Output = T;
@ -227,7 +227,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> FnMut<()> for Memo<T> impl<T> FnMut<()> for Memo<T>
where where
T: Debug + Clone, T: Clone,
{ {
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output { extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get() self.get()
@ -237,7 +237,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> Fn<()> for Memo<T> impl<T> Fn<()> for Memo<T>
where where
T: Debug + Clone, T: Clone,
{ {
extern "rust-call" fn call(&self, _args: ()) -> Self::Output { extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get() self.get()

View file

@ -66,7 +66,7 @@ pub fn create_resource<S, T, Fu>(
) -> Resource<S, T> ) -> Resource<S, T>
where where
S: PartialEq + Debug + Clone + 'static, S: PartialEq + Debug + Clone + 'static,
T: Debug + Serializable + 'static, T: Serializable + 'static,
Fu: Future<Output = T> + '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
@ -91,7 +91,7 @@ pub fn create_resource_with_initial_value<S, T, Fu>(
) -> Resource<S, T> ) -> Resource<S, T>
where where
S: PartialEq + Debug + Clone + 'static, S: PartialEq + Debug + Clone + 'static,
T: Debug + Serializable + 'static, T: Serializable + 'static,
Fu: Future<Output = T> + 'static, Fu: Future<Output = T> + 'static,
{ {
let resolved = initial_value.is_some(); let resolved = initial_value.is_some();
@ -173,7 +173,7 @@ pub fn create_local_resource<S, T, Fu>(
) -> Resource<S, T> ) -> Resource<S, T>
where where
S: PartialEq + Debug + Clone + 'static, S: PartialEq + Debug + Clone + 'static,
T: Debug + 'static, T: 'static,
Fu: Future<Output = T> + 'static, Fu: Future<Output = T> + 'static,
{ {
let initial_value = None; let initial_value = None;
@ -195,7 +195,7 @@ pub fn create_local_resource_with_initial_value<S, T, Fu>(
) -> Resource<S, T> ) -> Resource<S, T>
where where
S: PartialEq + Debug + Clone + 'static, S: PartialEq + Debug + Clone + 'static,
T: Debug + 'static, T: 'static,
Fu: Future<Output = T> + 'static, Fu: Future<Output = T> + 'static,
{ {
let resolved = initial_value.is_some(); 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>>) fn load_resource<S, T>(_cx: Scope, _id: ResourceId, r: Rc<ResourceState<S, T>>)
where where
S: PartialEq + Debug + Clone + 'static, S: PartialEq + Debug + Clone + 'static,
T: Debug + 'static, T: 'static,
{ {
r.load(false) r.load(false)
} }
@ -253,7 +253,7 @@ where
fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>) fn load_resource<S, T>(cx: Scope, id: ResourceId, r: Rc<ResourceState<S, T>>)
where where
S: PartialEq + Debug + Clone + 'static, S: PartialEq + Debug + Clone + 'static,
T: Debug + Serializable + 'static, T: Serializable + 'static,
{ {
use wasm_bindgen::{JsCast, UnwrapThrowExt}; use wasm_bindgen::{JsCast, UnwrapThrowExt};
@ -267,18 +267,8 @@ where
let res = T::from_json(&data).expect_throw("could not deserialize Resource JSON"); 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 r.set_value.update(|n| *n = Some(res));
// if not under Suspense, there will be a hydration mismatch, so let's wait a tick r.set_loading.update(|n| *n = false);
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);
});
}
// for reactivity // for reactivity
r.source.subscribe(); r.source.subscribe();
@ -326,8 +316,8 @@ where
impl<S, T> Resource<S, T> impl<S, T> Resource<S, T>
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + 'static, T: 'static,
{ {
/// Clones and returns the current value of the resource ([Option::None] if the /// Clones and returns the current value of the resource ([Option::None] if the
/// resource is still pending). Also subscribes the running effect to this /// resource is still pending). Also subscribes the running effect to this
@ -433,8 +423,8 @@ where
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]
pub struct Resource<S, T> pub struct Resource<S, T>
where where
S: Debug + 'static, S: 'static,
T: Debug + 'static, T: 'static,
{ {
runtime: RuntimeId, runtime: RuntimeId,
pub(crate) id: ResourceId, pub(crate) id: ResourceId,
@ -450,8 +440,8 @@ slotmap::new_key_type! {
impl<S, T> Clone for Resource<S, T> impl<S, T> Clone for Resource<S, T>
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + Clone + 'static, T: Clone + 'static,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
@ -465,16 +455,16 @@ where
impl<S, T> Copy for Resource<S, T> impl<S, T> Copy for Resource<S, T>
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + Clone + 'static, T: Clone + 'static,
{ {
} }
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<S, T> FnOnce<()> for Resource<S, T> impl<S, T> FnOnce<()> for Resource<S, T>
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + Clone + 'static, T: Clone + 'static,
{ {
type Output = Option<T>; type Output = Option<T>;
@ -486,8 +476,8 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<S, T> FnMut<()> for Resource<S, T> impl<S, T> FnMut<()> for Resource<S, T>
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + Clone + 'static, T: Clone + 'static,
{ {
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output { extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.read() self.read()
@ -497,8 +487,8 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<S, T> Fn<()> for Resource<S, T> impl<S, T> Fn<()> for Resource<S, T>
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + Clone + 'static, T: Clone + 'static,
{ {
extern "rust-call" fn call(&self, _args: ()) -> Self::Output { extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.read() self.read()
@ -509,7 +499,7 @@ where
pub(crate) struct ResourceState<S, T> pub(crate) struct ResourceState<S, T>
where where
S: 'static, S: 'static,
T: Debug + 'static, T: 'static,
{ {
scope: Scope, scope: Scope,
value: ReadSignal<Option<T>>, value: ReadSignal<Option<T>>,
@ -526,8 +516,8 @@ where
impl<S, T> ResourceState<S, T> impl<S, T> ResourceState<S, T>
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + 'static, T: 'static,
{ {
pub fn read(&self) -> Option<T> pub fn read(&self) -> Option<T>
where where
@ -666,8 +656,8 @@ pub(crate) trait SerializableResource {
impl<S, T> SerializableResource for ResourceState<S, T> impl<S, T> SerializableResource for ResourceState<S, T>
where where
S: Debug + Clone, S: Clone,
T: Debug + Serializable, T: Serializable,
{ {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
@ -686,11 +676,7 @@ pub(crate) trait UnserializableResource {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
} }
impl<S, T> UnserializableResource for ResourceState<S, T> impl<S, T> UnserializableResource for ResourceState<S, T> {
where
S: Debug,
T: Debug,
{
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
} }

View file

@ -234,8 +234,8 @@ impl Runtime {
state: Rc<ResourceState<S, T>>, state: Rc<ResourceState<S, T>>,
) -> ResourceId ) -> ResourceId
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + 'static, T: 'static,
{ {
self.resources self.resources
.borrow_mut() .borrow_mut()
@ -247,8 +247,8 @@ impl Runtime {
state: Rc<ResourceState<S, T>>, state: Rc<ResourceState<S, T>>,
) -> ResourceId ) -> ResourceId
where where
S: Debug + Clone + 'static, S: Clone + 'static,
T: Debug + Serializable + 'static, T: Serializable + 'static,
{ {
self.resources self.resources
.borrow_mut() .borrow_mut()
@ -261,8 +261,8 @@ impl Runtime {
f: impl FnOnce(&ResourceState<S, T>) -> U, f: impl FnOnce(&ResourceState<S, T>) -> U,
) -> U ) -> U
where where
S: Debug + 'static, S: 'static,
T: Debug + 'static, T: 'static,
{ {
let resources = self.resources.borrow(); let resources = self.resources.borrow();
let res = resources.get(id); let res = resources.get(id);

View file

@ -228,7 +228,7 @@ impl<T> Copy for ReadSignal<T> {}
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for ReadSignal<T> impl<T> FnOnce<()> for ReadSignal<T>
where where
T: Debug + Clone, T: Clone,
{ {
type Output = T; type Output = T;
@ -240,7 +240,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> FnMut<()> for ReadSignal<T> impl<T> FnMut<()> for ReadSignal<T>
where where
T: Debug + Clone, T: Clone,
{ {
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output { extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get() self.get()
@ -250,7 +250,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> Fn<()> for ReadSignal<T> impl<T> Fn<()> for ReadSignal<T>
where where
T: Debug + Clone, T: Clone,
{ {
extern "rust-call" fn call(&self, _args: ()) -> Self::Output { extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get() self.get()
@ -734,7 +734,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> FnOnce<()> for RwSignal<T> impl<T> FnOnce<()> for RwSignal<T>
where where
T: Debug + Clone, T: Clone,
{ {
type Output = T; type Output = T;
@ -746,7 +746,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> FnMut<()> for RwSignal<T> impl<T> FnMut<()> for RwSignal<T>
where where
T: Debug + Clone, T: Clone,
{ {
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output { extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.get() self.get()
@ -756,7 +756,7 @@ where
#[cfg(not(feature = "stable"))] #[cfg(not(feature = "stable"))]
impl<T> Fn<()> for RwSignal<T> impl<T> Fn<()> for RwSignal<T>
where where
T: Debug + Clone, T: Clone,
{ {
extern "rust-call" fn call(&self, _args: ()) -> Self::Output { extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.get() self.get()

View file

@ -9,8 +9,8 @@ description = "Tools to set HTML metadata in the Leptos web framework."
[dependencies] [dependencies]
cfg-if = "1" cfg-if = "1"
leptos = { path = "../leptos", version = "0.1.0-alpha", default-features = false } leptos = { path = "../leptos", default-features = false }
tracing = "0.1" typed-builder = "0.11"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3" version = "0.3"
@ -18,6 +18,10 @@ features = ["HtmlLinkElement", "HtmlMetaElement", "HtmlTitleElement"]
[features] [features]
default = ["csr"] default = ["csr"]
csr = ["leptos/csr", "leptos/tracing"] csr = ["leptos/csr"]
hydrate = ["leptos/hydrate", "leptos/tracing"] hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "leptos/tracing"] ssr = ["leptos/ssr"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features]
denylist = ["stable"]

View file

@ -57,7 +57,8 @@ default = ["csr"]
csr = ["leptos/csr"] csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"] hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr", "dep:url", "dep:regex"] ssr = ["leptos/ssr", "dep:url", "dep:regex"]
stable = ["leptos/stable"]
[package.metadata.cargo-all-features] [package.metadata.cargo-all-features]
# No need to test optional dependencies as they are enabled by the ssr feature # No need to test optional dependencies as they are enabled by the ssr feature
denylist = ["url", "regex"] denylist = ["url", "regex", "stable"]

View file

@ -64,7 +64,6 @@ impl std::fmt::Debug for RouterContextInner {
f.debug_struct("RouterContextInner") f.debug_struct("RouterContextInner")
.field("location", &self.location) .field("location", &self.location)
.field("base", &self.base) .field("base", &self.base)
.field("history", &std::any::type_name_of_val(&self.history))
.field("cx", &self.cx) .field("cx", &self.cx)
.field("reference", &self.reference) .field("reference", &self.reference)
.field("set_reference", &self.set_reference) .field("set_reference", &self.set_reference)
@ -99,13 +98,15 @@ impl RouterContext {
let base = base.unwrap_or_default(); let base = base.unwrap_or_default();
let base_path = resolve_path("", base, None); let base_path = resolve_path("", base, None);
if let Some(base_path) = &base_path && source.with(|s| s.value.is_empty()) { if let Some(base_path) = &base_path {
history.navigate(&LocationChange { if source.with(|s| s.value.is_empty()) {
value: base_path.to_string(), history.navigate(&LocationChange {
replace: true, value: base_path.to_string(),
scroll: false, replace: true,
state: State(None) scroll: false,
}); state: State(None)
});
}
} }
// the current URL // the current URL

View file

@ -126,17 +126,21 @@ where
} }
} }
auto trait NotOption {} cfg_if::cfg_if! {
impl<T> !NotOption for Option<T> {} if #[cfg(not(feature = "stable"))] {
auto trait NotOption {}
impl<T> !NotOption for Option<T> {}
impl<T> IntoParam for T impl<T> IntoParam for T
where where
T: FromStr + NotOption, T: FromStr + NotOption,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static, <T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{ {
fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> { fn into_param(value: Option<&str>, name: &str) -> Result<Self, ParamsError> {
let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?; let value = value.ok_or_else(|| ParamsError::MissingParam(name.to_string()))?;
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e))) Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
}
}
} }
} }

View file

@ -136,10 +136,9 @@
//! //!
//! ``` //! ```
#![feature(auto_traits)] #![cfg_attr(not(feature = "stable"), feature(auto_traits))]
#![feature(let_chains)] #![cfg_attr(not(feature = "stable"), feature(negative_impls))]
#![feature(negative_impls)] #![cfg_attr(not(feature = "stable"), feature(type_name_of_val))]
#![feature(type_name_of_val)]
mod components; mod components;
mod history; mod history;

View file

@ -80,13 +80,15 @@ impl Matcher {
path.push_str(loc_segment); path.push_str(loc_segment);
} }
if let Some(splat) = &self.splat && !splat.is_empty() { if let Some(splat) = &self.splat {
let value = if len_diff > 0 { if !splat.is_empty() {
loc_segments[self.len..].join("/") let value = if len_diff > 0 {
} else { loc_segments[self.len..].join("/")
"".into() } else {
}; "".into()
params.insert(splat.into(), value); };
params.insert(splat.into(), value);
}
} }
Some(PathMatch { path, params }) Some(PathMatch { path, params })