Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
90c6249067 examples: better practice for view types in todos 2023-04-23 15:15:01 -04:00
86 changed files with 578 additions and 1640 deletions

View File

@@ -25,22 +25,22 @@ members = [
exclude = ["benchmarks", "examples"]
[workspace.package]
version = "0.3.0-alpha"
version = "0.2.5"
[workspace.dependencies]
leptos = { path = "./leptos", default-features = false, version = "0.3.0-alpha" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.3.0-alpha" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.3.0-alpha" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.3.0-alpha" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.3.0-alpha" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.3.0-alpha" }
server_fn = { path = "./server_fn", default-features = false, version = "0.3.0-alpha" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.3.0-alpha" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.3.0-alpha" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.3.0-alpha" }
leptos_router = { path = "./router", version = "0.3.0-alpha" }
leptos_meta = { path = "./meta", default-features = false, version = "0.3.0-alpha" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.3.0-alpha" }
leptos = { path = "./leptos", default-features = false, version = "0.2.5" }
leptos_dom = { path = "./leptos_dom", default-features = false, version = "0.2.5" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.2.5" }
leptos_macro = { path = "./leptos_macro", default-features = false, version = "0.2.5" }
leptos_reactive = { path = "./leptos_reactive", default-features = false, version = "0.2.5" }
leptos_server = { path = "./leptos_server", default-features = false, version = "0.2.5" }
server_fn = { path = "./server_fn", default-features = false, version = "0.2.5" }
server_fn_macro = { path = "./server_fn_macro", default-features = false, version = "0.2.5" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", default-features = false, version = "0.2.5" }
leptos_config = { path = "./leptos_config", default-features = false, version = "0.2.5" }
leptos_router = { path = "./router", version = "0.2.5" }
leptos_meta = { path = "./meta", default-features = false, version = "0.2.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.2.5" }
[profile.release]
codegen-units = 1

View File

@@ -69,27 +69,13 @@ dependencies = [
[tasks.test]
clear = true
dependencies = ["test-all", "test-leptos_macro-example", "doc-leptos_macro-example"]
dependencies = ["test-all"]
[tasks.test-all]
command = "cargo"
args = ["+nightly", "test-all-features"]
install_crate = "cargo-all-features"
[tasks.test-leptos_macro-example]
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "test", "--doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.doc-leptos_macro-example]
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
command = "cargo"
args = ["+nightly", "doc"]
cwd = "leptos_macro/example"
install_crate = false
[tasks.test-examples]
description = "Run all unit and web tests for examples"
cwd = "examples"
@@ -104,7 +90,6 @@ args = ["make", "verify-flow"]
[env]
RUSTFLAGS = ""
LEPTOS_OUTPUT_NAME="ci" # allows examples to check/build without cargo-leptos
[env.github-actions]
RUSTFLAGS = "-D warnings"

View File

@@ -19,7 +19,6 @@
- [Suspense](./async/11_suspense.md)
- [Transition](./async/12_transition.md)
- [Actions](./async/13_actions.md)
- [Interlude: Projecting Children](./interlude_projecting_children.md)
- [Responding to Changes with `create_effect`](./14_create_effect.md)
- [Global State Management](./15_global_state.md)
- [Router](./router/README.md)

View File

@@ -1,177 +0,0 @@
# Projecting Children
As you build components you may occasionally find yourself wanting to “project” children through multiple layers of components.
## The Problem
Consider the following:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
view! { cx,
<Suspense
fallback=|| ()
>
<Show
// check whether user is verified
// by reading from the resource
when=move || todo!()
fallback=fallback
>
{children(cx)}
</Show>
</Suspense>
}
}
```
This is pretty straightforward: when the user is logged in, we want to show `children`. Until if the user is not logged in, we want to show `fallback`. And while were waiting to find out, we just render `()`, i.e., nothing.
In other words, we want to pass the children of `<WhenLoaded/>` _through_ the `<Suspense/>` component to become the children of the `<Show/>`. This is what I mean by “projection.”
This wont compile.
```
error[E0507]: cannot move out of `fallback`, a captured variable in an `Fn` closure
error[E0507]: cannot move out of `children`, a captured variable in an `Fn` closure
```
The problem here is that both `<Suspense/>` and `<Show/>` need to be able to construct their `children` multiple times. The first time you construct `<Suspense/>`s children, it would take ownership of `fallback` and `children` to move them into the invocation of `<Show/>`, but then they're not available for future `<Suspense/>` children construction.
## The Details
> Feel free to skip ahead to the solution.
If you want to really understand the issue here, it may help to look at the expanded `view` macro. Heres a cleaned-up version:
```rust
Suspense(
cx,
::leptos::component_props_builder(&Suspense)
.fallback(|| ())
.children({
// fallback and children are moved into this closure
Box::new(move |cx| {
{
// fallback and children captured here
leptos::Fragment::lazy(|| {
vec![
(Show(
cx,
::leptos::component_props_builder(&Show)
.when(|| true)
// but fallback is moved into Show here
.fallback(fallback)
// and children is moved into Show here
.children(children)
.build(),
)
.into_view(cx)),
]
})
}
})
})
.build(),
)
```
All components own their props; so the `<Show/>` in this case cant be called because it only has captured references to `fallback` and `children`.
## Solution
However, both `<Suspense/>` and `<Show/>` take `ChildrenFn`, i.e., their `children` should implement the `Fn` type so they can be called multiple times with only an immutable reference. This means we dont need to own `children` or `fallback`; we just need to be able to pass `'static` references to them.
We can solve this problem by using the [`store_value`](https://docs.rs/leptos/latest/leptos/fn.store_value.html) primitive. This essentially stores a value in the reactive system, handing ownership off to the framework in exchange for a reference that is, like signals, `Copy` and `'static`, which we can access or modify through certain methods.
In this case, its really simple:
```rust
pub fn LoggedIn<F, IV>(cx: Scope, fallback: F, children: ChildrenFn) -> impl IntoView
where
F: Fn(Scope) -> IV + 'static,
IV: IntoView,
{
let fallback = store_value(cx, fallback);
let children = store_value(cx, children);
view! { cx,
<Suspense
fallback=|| ()
>
<Show
when=|| todo!()
fallback=move |cx| fallback.with_value(|fallback| fallback(cx))
>
{children.with_value(|children| children(cx))}
</Show>
</Suspense>
}
}
```
At the top level, we store both `fallback` and `children` in the reactive scope owned by `LoggedIn`. Now we can simply move those references down through the other layers into the `<Show/>` component and call them there.
## A Final Note
Note that this works because `<Show/>` and `<Suspense/>` only need an immutable reference to their children (which `.with_value` can give it), not ownership.
In other cases, you may need to project owned props through a function that takes `ChildrenFn` and therefore needs to be called more than once. In this case, you may find the `clone:` helper in the`view` macro helpful.
Consider this example
```rust
#[component]
pub fn App(cx: Scope) -> impl IntoView {
let name = "Alice".to_string();
view! { cx,
<Outer>
<Inner>
<Inmost name=name.clone()/>
</Inner>
</Outer>
}
}
#[component]
pub fn Outer(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
}
#[component]
pub fn Inner(cx: Scope, children: ChildrenFn) -> impl IntoView {
children(cx)
}
#[component]
pub fn Inmost(cx: Scope, name: String) -> impl IntoView {
view! { cx,
<p>{name}</p>
}
}
```
Even with `name=name.clone()`, this gives the error
```
cannot move out of `name`, a captured variable in an `Fn` closure
```
Its captured through multiple levels of children that need to run more than once, and theres no obvious way to clone it _into_ the children.
In this case, the `clone:` syntax comes in handy. Calling `clone:name` will clone `name` _before_ moving it into `<Inner/>`s children, which solves our ownership issue.
```rust
view! { cx,
<Outer>
<Inner clone:name>
<Inmost name=name.clone()/>
</Inner>
</Outer>
}
```
These issues can be a little tricky to understand or debug, because of the opacity of the `view` macro. But in general, they can always be solved.

View File

@@ -65,7 +65,7 @@ pub fn App(cx: Scope) -> impl IntoView {
};
data.into_iter()
.map(|value| view! { cx, <span>{value}</span> })
.collect_view(cx)
.collect::<Vec<_>>()
}
```

View File

@@ -52,12 +52,6 @@ reactively update when the signal changes.
Now every time I click the button, the text should toggle between red and black as
the number switches between even and odd.
> If youre following along, make sure you go into your `index.html` and add something like this:
>
> ```html
> <style>.red { color: red; }</style>
> ```
## Dynamic Attributes
The same applies to plain attributes. Passing a plain string or primitive value to

View File

@@ -31,22 +31,6 @@ view! { cx,
}
```
Leptos also provides a `.collect_view(cx)` helper function that allows you to collect any iterator of `T: IntoView` into `Vec<View>`.
```rust
let values = vec![0, 1, 2];
view! { cx,
// this will just render "012"
<p>{values.clone()}</p>
// or we can wrap them in <li>
<ul>
{values.into_iter()
.map(|n| view! { cx, <li>{n}</li>})
.collect_view(cx)}
</ul>
}
```
The fact that the _list_ is static doesnt mean the interface needs to be static.
You can render dynamic items as part of a static list.
@@ -68,7 +52,7 @@ let counter_buttons = counters
</li>
}
})
.collect_view(cx);
.collect::<Vec<_>>();
view! { cx,
<ul>{counter_buttons}</ul>

View File

@@ -80,7 +80,7 @@ fn NumericInput(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect_view(cx)
.collect::<Vec<_>>()
}
</ul>
</div>

View File

@@ -103,7 +103,7 @@ pub fn WrapsChildren(cx: Scope, children: Children) -> impl IntoView {
.nodes
.into_iter()
.map(|child| view! { cx, <li>{child}</li> })
.collect_view(cx);
.collect::<Vec<_>>();
view! { cx,
<ul>{children}</ul>

View File

@@ -1,9 +1,10 @@
extend = [{ path = "./cargo-make/common.toml" }]
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CARGO_BUILD_TEST_FLAGS = ""
# Emulate workspace
CARGO_MAKE_WORKSPACE_EMULATION = true
CARGO_MAKE_CRATE_WORKSPACE_MEMBERS = [
"counter",
"counter_isomorphic",
@@ -41,6 +42,10 @@ dependencies = ["check-style", "test-unit-and-web"]
description = "Run all unit and web tests"
dependencies = ["test-flow", "web-test-flow"]
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.pre-verify-flow]
[tasks.post-verify-flow]

View File

@@ -1,14 +0,0 @@
[env]
CARGO_MAKE_CLIPPY_ARGS = "--all-targets -- -D warnings"
[tasks.check-style]
description = "Check for style violations"
dependencies = ["check-format-flow", "clippy-flow"]
[tasks.verify-local]
description = "Run all quality checks and tests from an example directory"
dependencies = ["check-style", "test-local"]
[tasks.test-local]
description = "Run all tests from an example directory"
dependencies = ["test", "web-test"]

View File

@@ -1,4 +0,0 @@
[tasks.web-test]
env = { CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome" }
command = "cargo"
args = ["make", "wasm-pack-test"]

View File

@@ -1,8 +1,3 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,7 +1,7 @@
use leptos::*;
/// A simple counter component.
///
///
/// You can use doc comments like this to document your component.
#[component]
pub fn SimpleCounter(
@@ -9,7 +9,7 @@ pub fn SimpleCounter(
/// The starting value for the counter
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32,
step: i32
) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);

View File

@@ -4,12 +4,10 @@ use leptos::*;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
<SimpleCounter
initial_value=0
step=1
/>
}
mount_to_body(|cx| view! { cx,
<SimpleCounter
initial_value=0
step=1
/>
})
}

View File

@@ -9,7 +9,7 @@ wasm_bindgen_test_configure!(run_in_browser);
fn clear() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
let _ = document.body().unwrap().append_child(&test_wrapper);
document.body().unwrap().append_child(&test_wrapper);
// start by rendering our counter and mounting it to the DOM
// note that we start at the initial value of 10
@@ -38,7 +38,7 @@ fn clear() {
// test case
run_scope(create_runtime(), |cx| {
// it's as if we're creating it with a value of 0, right?
let (value, _set_value) = create_signal(cx, 0);
let (value, set_value) = create_signal(cx, 0);
// we can remove the event listeners because they're not rendered to HTML
view! { cx,
@@ -71,7 +71,7 @@ fn clear() {
fn inc() {
let document = leptos::document();
let test_wrapper = document.create_element("section").unwrap();
let _ = document.body().unwrap().append_child(&test_wrapper);
document.body().unwrap().append_child(&test_wrapper);
mount_to(
test_wrapper.clone().unchecked_into(),
@@ -79,7 +79,7 @@ fn inc() {
);
// You can do testing with vanilla DOM operations
let _document = leptos::document();
let document = leptos::document();
let div = test_wrapper.query_selector("div").unwrap().unwrap();
let clear = div
.first_child()

View File

@@ -1,5 +1,3 @@
extend = [{ path = "../cargo-make/common.toml" }]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -1,27 +1,27 @@
use cfg_if::cfg_if;
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
use leptos_meta::*;
cfg_if! {
if #[cfg(feature = "ssr")] {
use std::sync::atomic::{AtomicI32, Ordering};
use broadcaster::BroadcastChannel;
static COUNT: AtomicI32 = AtomicI32::new(0);
#[cfg(feature = "ssr")]
use std::sync::atomic::{AtomicI32, Ordering};
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
#[cfg(feature = "ssr")]
use broadcaster::BroadcastChannel;
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
}
}
#[cfg(feature = "ssr")]
pub fn register_server_functions() {
_ = GetServerCount::register();
_ = AdjustServerCount::register();
_ = ClearServerCount::register();
}
#[cfg(feature = "ssr")]
static COUNT: AtomicI32 = AtomicI32::new(0);
#[cfg(feature = "ssr")]
lazy_static::lazy_static! {
pub static ref COUNT_CHANNEL: BroadcastChannel<i32> = BroadcastChannel::new();
}
// "/api" is an optional prefix that allows you to locate server functions wherever you'd like on the server
#[server(GetServerCount, "/api")]
pub async fn get_server_count() -> Result<i32, ServerFnError> {
@@ -29,10 +29,7 @@ pub async fn get_server_count() -> Result<i32, ServerFnError> {
}
#[server(AdjustServerCount, "/api")]
pub async fn adjust_server_count(
delta: i32,
msg: String,
) -> Result<i32, ServerFnError> {
pub async fn adjust_server_count(delta: i32, msg: String) -> Result<i32, ServerFnError> {
let new = COUNT.load(Ordering::Relaxed) + delta;
COUNT.store(new, Ordering::Relaxed);
_ = COUNT_CHANNEL.send(&new).await;
@@ -49,49 +46,36 @@ pub async fn clear_server_count() -> Result<i32, ServerFnError> {
#[component]
pub fn Counters(cx: Scope) -> impl IntoView {
provide_meta_context(cx);
view! { cx,
view! {
cx,
<Router>
<header>
<h1>"Server-Side Counters"</h1>
<p>"Each of these counters stores its data in the same variable on the server."</p>
<p>
"The value is shared across connections. Try opening this is another browser tab to see what I mean."
</p>
<p>"The value is shared across connections. Try opening this is another browser tab to see what I mean."</p>
</header>
<nav>
<ul>
<li>
<A href="">"Simple"</A>
</li>
<li>
<A href="form">"Form-Based"</A>
</li>
<li>
<A href="multi">"Multi-User"</A>
</li>
<li><A href="">"Simple"</A></li>
<li><A href="form">"Form-Based"</A></li>
<li><A href="multi">"Multi-User"</A></li>
</ul>
</nav>
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<main>
<Routes>
<Route
path=""
view=|cx| {
view! { cx, <Counter/> }
}
/>
<Route
path="form"
view=|cx| {
view! { cx, <FormCounter/> }
}
/>
<Route
path="multi"
view=|cx| {
view! { cx, <MultiuserCounter/> }
}
/>
<Route path="" view=|cx| view! {
cx,
<Counter/>
}/>
<Route path="form" view=|cx| view! {
cx,
<FormCounter/>
}/>
<Route path="multi" view=|cx| view! {
cx,
<MultiuserCounter/>
}/>
</Routes>
</main>
</Router>
@@ -109,47 +93,33 @@ pub fn Counter(cx: Scope) -> impl IntoView {
let clear = create_action(cx, |_| clear_server_count());
let counter = create_resource(
cx,
move || {
(
dec.version().get(),
inc.version().get(),
clear.version().get(),
)
},
move || (dec.version().get(), inc.version().get(), clear.version().get()),
|_| get_server_count(),
);
let value = move || {
let value = move || counter.read(cx).map(|count| count.unwrap_or(0)).unwrap_or(0);
let error_msg = move || {
counter
.read(cx)
.map(|count| count.unwrap_or(0))
.unwrap_or(0)
};
let error_msg = move || {
counter.read(cx).and_then(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
.map(|res| match res {
Ok(_) => None,
Err(e) => Some(e),
})
.flatten()
};
view! { cx,
view! {
cx,
<div>
<h2>"Simple Counter"</h2>
<p>
"This counter sets the value on the server and automatically reloads the new value."
</p>
<p>"This counter sets the value on the server and automatically reloads the new value."</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>"Value: " {value} "!"</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
{move || {
error_msg()
.map(|msg| {
view! { cx, <p>"Error: " {msg.to_string()}</p> }
})
}}
{move || error_msg().map(|msg| view! { cx, <p>"Error: " {msg.to_string()}</p>})}
</div>
}
}
@@ -172,15 +142,19 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
);
let value = move || {
log::debug!("FormCounter looking for value");
counter.read(cx).and_then(|n| n.ok()).unwrap_or(0)
counter
.read(cx)
.map(|n| n.ok())
.flatten()
.map(|n| n)
.unwrap_or(0)
};
view! { cx,
view! {
cx,
<div>
<h2>"Form Counter"</h2>
<p>
"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"
</p>
<p>"This counter uses forms to set the value on the server. When progressively enhanced, it should behave identically to the “Simple Counter.”"</p>
<div>
// calling a server function is the same as POSTing to its API URL
// so we can just do that with a form and button
@@ -211,32 +185,26 @@ pub fn FormCounter(cx: Scope) -> impl IntoView {
// This is the primitive pattern for live chat, collaborative editing, etc.
#[component]
pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
let dec =
create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc =
create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let dec = create_action(cx, |_| adjust_server_count(-1, "dec dec goose".into()));
let inc = create_action(cx, |_| adjust_server_count(1, "inc inc moose".into()));
let clear = create_action(cx, |_| clear_server_count());
#[cfg(not(feature = "ssr"))]
let multiplayer_value = {
use futures::StreamExt;
let mut source =
gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let mut source = gloo_net::eventsource::futures::EventSource::new("/api/events")
.expect("couldn't connect to SSE stream");
let s = create_signal_from_stream(
cx,
source
.subscribe("message")
.unwrap()
.map(|value| match value {
Ok(value) => value
.1
.data()
.as_string()
.expect("expected string value"),
source.subscribe("message").unwrap().map(|value| {
match value {
Ok(value) => {
value.1.data().as_string().expect("expected string value")
},
Err(_) => "0".to_string(),
}),
}
})
);
on_cleanup(cx, move || source.close());
@@ -244,20 +212,18 @@ pub fn MultiuserCounter(cx: Scope) -> impl IntoView {
};
#[cfg(feature = "ssr")]
let (multiplayer_value, _) = create_signal(cx, None::<i32>);
let (multiplayer_value, _) =
create_signal(cx, None::<i32>);
view! { cx,
view! {
cx,
<div>
<h2>"Multi-User Counter"</h2>
<p>
"This one uses server-sent events (SSE) to live-update when other users make changes."
</p>
<p>"This one uses server-sent events (SSE) to live-update when other users make changes."</p>
<div>
<button on:click=move |_| clear.dispatch(())>"Clear"</button>
<button on:click=move |_| dec.dispatch(())>"-1"</button>
<span>
"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default()}
</span>
<span>"Multiplayer Value: " {move || multiplayer_value.get().unwrap_or_default().to_string()}</span>
<button on:click=move |_| inc.dispatch(())>"+1"</button>
</div>
</div>

View File

@@ -1,10 +1,10 @@
use cfg_if::cfg_if;
use leptos::*;
pub mod counters;
// Needs to be in lib.rs AFAIK because wasm-bindgen needs us to be compiling a lib. I may be wrong.
cfg_if! {
if #[cfg(feature = "hydrate")] {
use leptos::*;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::counters::*;

View File

@@ -1,11 +1,11 @@
use cfg_if::cfg_if;
use leptos::*;
mod counters;
// boilerplate to run in different modes
cfg_if! {
// server-only stuff
if #[cfg(feature = "ssr")] {
use leptos::*;
use actix_files::{Files};
use actix_web::*;
use crate::counters::*;

View File

@@ -17,7 +17,6 @@ console_error_panic_hook = "0.1.7"
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.34"
pretty_assertions = "1.3.0"
rstest = "0.17.0"
[dev-dependencies.web-sys]
features = ["HtmlElement", "XPathResult"]

View File

@@ -1,7 +1,9 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
]
[env]
CARGO_MAKE_WASM_TEST_ARGS = "--headless --chrome"
[tasks.web-test]
command = "cargo"
args = ["make", "wasm-pack-test"]
[tasks.build]
command = "cargo"

View File

@@ -2,8 +2,8 @@ use leptos::{ev, html::*, *};
/// A simple counter view.
// A component is really just a function call: it runs once to create the DOM and reactive system
pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
let (count, set_count) = create_signal(cx, Count::new(initial_value, step));
pub fn counter(cx: Scope, initial_value: i32, step: i32) -> impl IntoView {
let (value, set_value) = create_signal(cx, initial_value);
// elements are created by calling a function with a Scope argument
// the function name is the same as the HTML tag name
@@ -16,13 +16,13 @@ pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
// typed events found in leptos::ev
// 1) prevent typos in event names
// 2) allow for correct type inference in callbacks
.on(ev::click, move |_| set_count.update(|count| count.clear()))
.on(ev::click, move |_| set_value.update(|value| *value = 0))
.child("Clear"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_count.update(|count| count.decrease())
set_value.update(|value| *value -= step)
})
.child("-1"),
)
@@ -31,45 +31,14 @@ pub fn counter(cx: Scope, initial_value: i32, step: u32) -> impl IntoView {
.child("Value: ")
// reactive values are passed to .child() as a tuple
// (Scope, [child function]) so an effect can be created
.child(move || count.get().value())
.child((cx, move || value.get()))
.child("!"),
)
.child(
button(cx)
.on(ev::click, move |_| {
set_count.update(|count| count.increase())
set_value.update(|value| *value += step)
})
.child("+1"),
)
}
#[derive(Debug, Clone)]
pub struct Count {
value: i32,
step: i32,
}
impl Count {
pub fn new(value: i32, step: u32) -> Self {
Count {
value,
step: step as i32,
}
}
pub fn value(&self) -> i32 {
self.value
}
pub fn increase(&mut self) {
self.value += self.step;
}
pub fn decrease(&mut self) {
self.value += -self.step;
}
pub fn clear(&mut self) {
self.value = 0;
}
}

View File

@@ -1,49 +0,0 @@
mod count {
use counter_without_macros::Count;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(-2, 1)]
#[case(-1, 1)]
#[case(0, 1)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
fn should_increase_count(#[case] initial_value: i32, #[case] step: u32) {
let mut count = Count::new(initial_value, step);
count.increase();
assert_eq!(count.value(), initial_value + step as i32);
}
#[rstest]
#[case(-2, 1)]
#[case(-1, 1)]
#[case(0, 1)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
#[trace]
fn should_decrease_count(#[case] initial_value: i32, #[case] step: u32) {
let mut count = Count::new(initial_value, step);
count.decrease();
assert_eq!(count.value(), initial_value - step as i32);
}
#[rstest]
#[case(-2, 1)]
#[case(-1, 1)]
#[case(0, 1)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
#[trace]
fn should_clear_count(#[case] initial_value: i32, #[case] step: u32) {
let mut count = Count::new(initial_value, step);
count.clear();
assert_eq!(count.value(), 0);
}
}

View File

@@ -1,8 +1,3 @@
extend = [
{ path = "../cargo-make/common.toml" },
{ path = "../cargo-make/wasm-web-test.toml" },
]
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]

View File

@@ -0,0 +1,68 @@
use wasm_bindgen_test::*;
use wasm_bindgen::JsCast;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::*;
use web_sys::HtmlElement;
use counters::Counters;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| view! { cx, <Counters/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let add_counter = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
// add 3 counters
add_counter.click();
add_counter.click();
add_counter.click();
// check HTML
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->0<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->0<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
let counters = div
.query_selector("ul")
.unwrap()
.unwrap()
.unchecked_into::<HtmlElement>()
.children();
// click first counter once, second counter twice, etc.
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
for idx in 0..counters.length() {
let counter = counters.item(idx).unwrap();
let inc_button = counter
.first_child()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.unchecked_into::<HtmlElement>();
for _ in 0..=idx {
inc_button.click();
}
}
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->6<!-- </DynChild> --></span> from <span><!-- <DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->1<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
// remove the first counter
counters
.item(0)
.unwrap()
.last_child()
.unwrap()
.unchecked_into::<HtmlElement>()
.click();
assert_eq!(div.inner_html(), "<button>Add Counter</button><button>Add 1000 Counters</button><button>Clear Counters</button><p>Total: <span><!-- <DynChild> -->5<!-- </DynChild> --></span> from <span><!-- <DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- <Each> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->2<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> --><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> -->3<!-- </DynChild> --></span><button>+1</button><button>x</button></li><!-- </Counter> --><!-- </EachItem> --><!-- </Each> --></ul>");
}

View File

@@ -1,120 +0,0 @@
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counters::Counters;
use leptos::*;
use web_sys::HtmlElement;
#[wasm_bindgen_test]
fn inc() {
mount_to_body(|cx| view! { cx, <Counters/> });
let document = leptos::document();
let div = document.query_selector("div").unwrap().unwrap();
let add_counter = div
.first_child()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
// add 3 counters
add_counter.click();
add_counter.click();
add_counter.click();
// check HTML
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->0<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->0<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
let counters = div
.query_selector("ul")
.unwrap()
.unwrap()
.unchecked_into::<HtmlElement>()
.children();
// click first counter once, second counter twice, etc.
// `NodeList` isn't a `Vec` so we iterate over it in this slightly awkward way
for idx in 0..counters.length() {
let counter = counters.item(idx).unwrap();
let inc_button = counter
.first_child()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.next_sibling()
.unwrap()
.unchecked_into::<HtmlElement>();
for _ in 0..=idx {
inc_button.click();
}
}
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->6<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->3<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->1<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->2<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->3<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
// remove the first counter
counters
.item(0)
.unwrap()
.last_child()
.unwrap()
.unchecked_into::<HtmlElement>()
.click();
assert_eq!(
div.inner_html(),
"<button>Add Counter</button><button>Add 1000 \
Counters</button><button>Clear Counters</button><p>Total: <span><!-- \
<DynChild> -->5<!-- </DynChild> --></span> from <span><!-- \
<DynChild> -->2<!-- </DynChild> --></span> counters.</p><ul><!-- \
<Each> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->2<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- <EachItem> --><!-- <Counter> \
--><li><button>-1</button><input type=\"text\"><span><!-- <DynChild> \
-->3<!-- </DynChild> \
--></span><button>+1</button><button>x</button></li><!-- </Counter> \
--><!-- </EachItem> --><!-- </Each> --></ul>"
);
}

View File

@@ -26,7 +26,7 @@ pub fn App(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li>})
.collect_view(cx)
.collect::<Vec<_>>()
}
</ul>
</div>

View File

@@ -55,7 +55,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
errors
.iter()
.map(|(_, e)| view! { cx, <li>{e.to_string()}</li> })
.collect_view(cx)
.collect::<Vec<_>>()
})
};
@@ -76,7 +76,7 @@ pub fn fetch_example(cx: Scope) -> impl IntoView {
data.map(|data| {
data.iter()
.map(|s| view! { cx, <span>{s}</span> })
.collect_view(cx)
.collect::<Vec<_>>()
})
})
};

View File

@@ -97,7 +97,7 @@ pub fn ContactList(cx: Scope) -> impl IntoView {
<li><A href=contact.id.to_string()><span>{&contact.first_name} " " {&contact.last_name}</span></A></li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
})
};

View File

@@ -241,11 +241,11 @@ pub fn Todos(cx: Scope) -> impl IntoView {
todos.read(cx)
.map(move |todos| match todos {
Err(e) => {
view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_view(cx)
vec![view! { cx, <pre class="error">"Server Error: " {e.to_string()}</pre>}.into_any()]
}
Ok(todos) => {
if todos.is_empty() {
view! { cx, <p>"No tasks were found."</p> }.into_view(cx)
vec![view! { cx, <p>"No tasks were found."</p> }.into_any()]
} else {
todos
.into_iter()
@@ -266,8 +266,9 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</ActionForm>
</li>
}
.into_any()
})
.collect_view(cx)
.collect::<Vec<_>>()
}
}
})
@@ -286,7 +287,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
};
view! {

View File

@@ -44,7 +44,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a></li>})
.collect_view(cx)
.collect::<Vec<_>>()
})
)
};
@@ -109,7 +109,7 @@ fn Post(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
.collect_view(cx)
.collect::<Vec<_>>()
}
</ul>
</div>

View File

@@ -49,7 +49,7 @@ fn HomePage(cx: Scope) -> impl IntoView {
.map(|posts| {
posts.iter()
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a> "|" <a href=format!("/post_in_order/{}", post.id)>{&post.title}"(in order)"</a></li>})
.collect_view(cx)
.collect::<Vec<_>>()
})
)
};
@@ -114,7 +114,7 @@ fn Post(cx: Scope) -> impl IntoView {
{move || errors.get()
.into_iter()
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
.collect_view(cx)
.collect::<Vec<_>>()
}
</ul>
</div>

View File

@@ -1,15 +1,12 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{
extract::{Extension, Path},
routing::{get, post},
Router,
};
async fn main(){
use leptos::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use ssr_modes_axum::{app::*, fallback::file_and_error_handler};
use axum::{extract::{Extension, Path}, Router, routing::{get, post}};
use std::sync::Arc;
use ssr_modes_axum::fallback::file_and_error_handler;
use ssr_modes_axum::app::*;
let conf = get_configuration(None).await.unwrap();
let addr = conf.leptos_options.site_addr;
@@ -22,11 +19,7 @@ async fn main() {
let app = Router::new()
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
.leptos_routes(
leptos_options.clone(),
routes,
|cx| view! { cx, <App/> },
)
.leptos_routes(leptos_options.clone(), routes, |cx| view! { cx, <App/> })
.fallback(file_and_error_handler)
.layer(Extension(Arc::new(leptos_options)));

View File

@@ -1,24 +0,0 @@
[package]
name = "timer"
version = "0.1.0"
edition = "2021"
[profile.release]
codegen-units = 1
lto = true
[dependencies]
leptos = { path = "../../leptos" }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
]
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@@ -1,9 +0,0 @@
[tasks.build]
command = "cargo"
args = ["+nightly", "build-all-features"]
install_crate = "cargo-all-features"
[tasks.check]
command = "cargo"
args = ["+nightly", "check-all-features"]
install_crate = "cargo-all-features"

View File

@@ -1,7 +0,0 @@
# Leptos Timer Example
This example creates a simple timer based on `setInterval` in a client side rendered app with Rust and WASM.
To run it, just issue the `trunk serve --open` command in the example root. This will build the app, run it, and open a new browser to serve it.
> If you don't have `trunk` installed, [click here for install instructions.](https://trunkrs.dev/)

View File

@@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link data-trunk rel="rust" data-wasm-opt="z"/>
<link data-trunk rel="icon" type="image/ico" href="/public/favicon.ico"/>
</head>
<body></body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"

View File

@@ -1,61 +0,0 @@
use leptos::{leptos_dom::helpers::IntervalHandle, *};
use std::time::Duration;
/// Timer example, demonstrating the use of `use_interval`.
#[component]
pub fn TimerDemo(cx: Scope) -> impl IntoView {
// count_a updates with a fixed interval of 1000 ms, whereas count_b has a dynamic
// update interval.
let (count_a, set_count_a) = create_signal(cx, 0_i32);
let (count_b, set_count_b) = create_signal(cx, 0_i32);
let (interval, set_interval) = create_signal(cx, 1000);
use_interval(cx, 1000, move || {
set_count_a.update(|c| *c = *c + 1);
});
use_interval(cx, interval, move || {
set_count_b.update(|c| *c = *c + 1);
});
view! { cx,
<div>
<div>"Count A (fixed interval of 1000 ms)"</div>
<div>{count_a}</div>
<div>"Count B (dynamic interval, currently " {interval} " ms)"</div>
<div>{count_b}</div>
<input prop:value=interval on:input=move |ev| {
if let Ok(value) = event_target_value(&ev).parse::<u64>() {
set_interval(value);
}
}/>
</div>
}
}
/// Hook to wrap the underlying `setInterval` call and make it reactive w.r.t.
/// possible changes of the timer interval.
pub fn use_interval<T, F>(cx: Scope, interval_millis: T, f: F)
where
F: Fn() + Clone + 'static,
T: Into<MaybeSignal<u64>> + 'static,
{
let interval_millis = interval_millis.into();
create_effect(cx, move |prev_handle: Option<IntervalHandle>| {
// effects get their previous return value as an argument
// each time the effect runs, it will return the interval handle
// so if we have a previous one, we cancel it
if let Some(prev_handle) = prev_handle {
prev_handle.clear();
};
// here, we return the handle
set_interval_with_handle(
f.clone(),
// this is the only reactive access, so this effect will only
// re-run when the interval changes
Duration::from_millis(interval_millis.get()),
)
.expect("could not create interval")
});
}

View File

@@ -1,12 +0,0 @@
use leptos::*;
use timer::TimerDemo;
pub fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to_body(|cx| {
view! { cx,
<TimerDemo />
}
})
}

View File

@@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
actix-files = { version = "0.6.2", optional = true }
actix-web = { version = "4.2.1", features = ["macros"] }
actix-web = { version = "4.2.1", optional = true, features = ["macros"] }
anyhow = "1.0.68"
broadcaster = "1.0.0"
console_log = "1.0.0"
@@ -19,7 +19,7 @@ cfg-if = "1.0.0"
leptos = { path = "../../leptos", default-features = false, features = [
"serde",
] }
leptos_actix = { path = "../../integrations/actix" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_meta = { path = "../../meta", default-features = false }
leptos_router = { path = "../../router", default-features = false }
log = "0.4.17"
@@ -36,8 +36,10 @@ 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",
]

View File

@@ -107,9 +107,6 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
cx,
<Todos/>
}/>
<Api path="bananas" route=web::get().to(|req: HttpRequest| async move {
req.path()
})
</Routes>
</main>
</Router>
@@ -179,7 +176,8 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
.into_view(cx)
}
}
})
@@ -198,7 +196,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
};
view! {

View File

@@ -13,9 +13,7 @@ impl TodoAppError {
pub fn status_code(&self) -> StatusCode {
match self {
TodoAppError::NotFound => StatusCode::NOT_FOUND,
TodoAppError::InternalServerError => {
StatusCode::INTERNAL_SERVER_ERROR
}
TodoAppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

View File

@@ -52,8 +52,7 @@ pub async fn get_todos(cx: Scope) -> Result<Vec<Todo>, ServerFnError> {
let mut conn = db().await?;
let mut todos = Vec::new();
let mut rows =
sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
let mut rows = sqlx::query_as::<_, Todo>("SELECT * FROM todos").fetch(&mut conn);
while let Some(row) = rows
.try_next()
.await
@@ -110,25 +109,19 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct FormData {
hi: String,
hi: String
}
#[server(FormDataHandler, "/api")]
pub async fn form_data(cx: Scope) -> Result<FormData, ServerFnError> {
use axum::extract::FromRequest;
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx)
.and_then(|req| req.take_request())
.unwrap();
let req = use_context::<leptos_axum::LeptosRequest<axum::body::Body>>(cx).and_then(|req| req.take_request()).unwrap();
if req.method() == http::Method::POST {
let form = axum::Form::from_request(req, &())
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
let form = axum::Form::from_request(req, &()).await.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(form.0)
} else {
Err(ServerFnError::ServerError(
"wrong form fields submitted".to_string(),
))
Err(ServerFnError::ServerError("wrong form fields submitted".to_string()))
}
}
@@ -153,7 +146,7 @@ pub fn TodoApp(cx: Scope) -> impl IntoView {
</ErrorBoundary>
}/> //Route
<Route path="weird" methods=&[Method::Get, Method::Post]
ssr=SsrMode::Async
ssr=SsrMode::Async
view=|cx| {
let res = create_resource(cx, || (), move |_| async move {
form_data(cx).await
@@ -229,7 +222,8 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
.into_view(cx)
}
}
})
@@ -248,7 +242,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
};
view! {

View File

@@ -183,7 +183,8 @@ pub fn Todos(cx: Scope) -> impl IntoView {
</li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
.into_view(cx)
}
}
})
@@ -202,7 +203,7 @@ pub fn Todos(cx: Scope) -> impl IntoView {
<li class="pending">{move || submission.input.get().map(|data| data.title) }</li>
}
})
.collect_view(cx)
.collect::<Vec<_>>()
};
view! {

View File

@@ -202,19 +202,6 @@ pub fn TodoMVC(cx: Scope) -> impl IntoView {
}
});
// focus the main input on load
create_effect(cx, move |_| {
if let Some(input) = input_ref.get() {
// We use request_animation_frame here because the NodeRef
// is filled when the element is created, but before it's mounted
// to the DOM. Calling .focus() before it's mounted does nothing.
// So inside, we wait a tick for the browser to mount it, then .focus()
request_animation_frame(move || {
input.focus();
});
}
});
view! { cx,
<main>
<section class="todoapp">

View File

@@ -13,10 +13,10 @@ use actix_web::{
web::Bytes,
*,
};
use futures::{Stream, StreamExt};
use futures::{Future, Stream, StreamExt};
use http::StatusCode;
use leptos::{
leptos_dom::{Transparent, ssr::render_to_stream_with_prefix_undisposed_with_context},
leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context,
leptos_server::{server_fn_by_path, Payload},
server_fn::Encoding,
*,
@@ -26,7 +26,7 @@ use leptos_meta::*;
use leptos_router::*;
use parking_lot::RwLock;
use regex::Regex;
use std::{fmt::Display, future::Future, sync::Arc};
use std::sync::Arc;
use tracing::instrument;
/// 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.
@@ -908,21 +908,7 @@ pub fn generate_route_list<IV>(
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None)
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Actix's App without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generated Actix compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Actix path format
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce(leptos::Scope) -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
let (mut routes, mut api_routes) = leptos_router::generate_route_list_inner(app_fn);
let mut routes = leptos_router::generate_route_list_inner(app_fn);
// Empty strings screw with Actix pathing, they need to be "/"
routes = routes
@@ -946,7 +932,7 @@ where
// Match `:some_word` but only capture `some_word` in the groups to replace with `{some_word}`
let capture_re = Regex::new(r":((?:[^.,/]+)+)[^/]?").unwrap();
let mut routes = routes
let routes = routes
.into_iter()
.map(|listing| {
let path = wildcard_re
@@ -960,10 +946,6 @@ where
if routes.is_empty() {
vec![RouteListing::new("/", Default::default(), [Method::Get])]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}
@@ -1112,102 +1094,3 @@ where
router
}
}
/// Defines an API route, which mounts the given route handler at this path.
#[component(transparent)]
pub fn Api<P>(cx: leptos::Scope, path: P, route: actix_web::Route) -> impl IntoView
where P: Into<String>
{
Transparent::new(ApiRouteListing::new(path.into(), route))
}
/// A helper to make it easier to use Axum extractors in server functions. This takes
/// a handler function as its argument. The handler follows similar rules to an Actix
/// [Handler](actix_web::Handler): it is an async function that receives arguments that
/// will be extracted from the request and returns some value.
///
/// ```rust,ignore
/// use leptos::*;
/// use serde::Deserialize;
/// #[derive(Deserialize)]
/// struct Search {
/// q: String,
/// }
///
/// #[server(ExtractoServerFn, "/api")]
/// pub async fn extractor_server_fn(cx: Scope) -> Result<String, ServerFnError> {
/// use actix_web::dev::ConnectionInfo;
/// use actix_web::web::{Data, Query};
///
/// extract(
/// cx,
/// |data: Data<String>, search: Query<Search>, connection: ConnectionInfo| async move {
/// format!(
/// "data = {}\nsearch = {}\nconnection = {:?}",
/// data.into_inner(),
/// search.q,
/// connection
/// )
/// },
/// )
/// .await
/// }
/// ```
pub async fn extract<F, E>(
cx: leptos::Scope,
f: F,
) -> Result<<<F as Extractor<E>>::Future as Future>::Output, ServerFnError>
where
F: Extractor<E>,
E: actix_web::FromRequest,
<E as actix_web::FromRequest>::Error: Display,
<F as Extractor<E>>::Future: Future,
{
let req = use_context::<actix_web::HttpRequest>(cx)
.expect("HttpRequest should have been provided via context");
let input = E::extract(&req)
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(f.call(input).await)
}
// Drawn from the Actix Handler implementation
// https://github.com/actix/actix-web/blob/19c9d858f25e8262e14546f430d713addb397e96/actix-web/src/handler.rs#L124
pub trait Extractor<T> {
type Future;
fn call(&self, args: T) -> Self::Future;
}
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Extractor<($($param,)*)> for Func
where
Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Future = Fut;
#[inline]
#[allow(non_snake_case)]
fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {
(self)($($param,)*)
}
}
});
factory_tuple! {}
factory_tuple! { A }
factory_tuple! { A B }
factory_tuple! { A B C }
factory_tuple! { A B C D }
factory_tuple! { A B C D E }
factory_tuple! { A B C D E F }
factory_tuple! { A B C D E F G }
factory_tuple! { A B C D E F G H }
factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L }
factory_tuple! { A B C D E F G H I J K L M }
factory_tuple! { A B C D E F G H I J K L M N }
factory_tuple! { A B C D E F G H I J K L M N O }
factory_tuple! { A B C D E F G H I J K L M N O P }

View File

@@ -19,10 +19,7 @@ use futures::{
channel::mpsc::{Receiver, Sender},
Future, SinkExt, Stream, StreamExt,
};
use http::{
header, method::Method, request::Parts, uri::Uri, version::Version,
Response,
};
use http::{header, method::Method, uri::Uri, version::Version, Response};
use hyper::body;
use leptos::{
leptos_server::{server_fn_by_path, Payload},
@@ -49,21 +46,6 @@ pub struct RequestParts {
pub headers: HeaderMap<HeaderValue>,
pub body: Bytes,
}
/// Convert http::Parts to RequestParts(and vice versa). Body and Extensions will
/// be lost in the conversion
impl From<Parts> for RequestParts {
fn from(parts: Parts) -> Self {
Self {
version: parts.version,
method: parts.method,
uri: parts.uri,
headers: parts.headers,
body: Bytes::default(),
}
}
}
/// 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)]
@@ -1021,21 +1003,6 @@ where
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Axum path format
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -1062,7 +1029,7 @@ where
let routes = routes.0.read().to_owned();
// Axum's Router defines Root routes as "/" not ""
let mut routes = routes
let routes = routes
.into_iter()
.map(|listing| {
let path = listing.path();
@@ -1085,10 +1052,6 @@ where
[leptos_router::Method::Get],
)]
} else {
// Routes to exclude from auto generation
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}

View File

@@ -51,10 +51,10 @@ pub fn html_parts(
let output_name = &options.output_name;
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
// Otherwise we need to add _bg because wasm_pack always does.
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME
// Otherwise we need to add _bg because wasm_pack always does. This is not the same as options.output_name, which is set regardless
let mut wasm_output_name = output_name.clone();
if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
if std::env::var("LEPTOS_OUTPUT_NAME").is_err() {
wasm_output_name.push_str("_bg");
}

View File

@@ -947,19 +947,6 @@ where
pub async fn generate_route_list<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
generate_route_list_with_exclusions(app_fn, None).await
}
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
/// create routes in Viz's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths.
pub async fn generate_route_list_with_exclusions<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -986,7 +973,7 @@ where
let routes = routes.0.read().to_owned();
// Viz's Router defines Root routes as "/" not ""
let mut routes = routes
let routes = routes
.into_iter()
.map(|listing| {
let path = listing.path();
@@ -1009,9 +996,6 @@ where
[leptos_router::Method::Get],
)]
} else {
if let Some(excluded_routes) = excluded_routes {
routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path()))
}
routes
}
}

View File

@@ -45,20 +45,18 @@ where
provide_context(cx, errors);
// Run children so that they render and execute resources
let children = children(cx).into_view(cx);
let errors_empty = create_memo(cx, move |_| errors.with(Errors::is_empty));
let children = children(cx);
move || {
if errors_empty.get() {
children.clone().into_view(cx)
} else {
view! { cx,
match errors.with(Errors::is_empty) {
true => children.clone().into_view(cx),
false => view! { cx,
<>
{fallback(cx, errors)}
<leptos-error-boundary style="display: none">{children.clone()}</leptos-error-boundary>
</>
}
.into_view(cx)
.into_view(cx),
}
}
}

View File

@@ -164,8 +164,8 @@ pub use leptos_dom::{
window_event_listener_with_precast,
},
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
IntoClass, IntoProperty, IntoView, NodeRef, Property, View,
Class, Errors, Fragment, HtmlElement, IntoAttribute, IntoClass,
IntoProperty, IntoView, NodeRef, Property, View,
};
pub use leptos_macro::*;
pub use leptos_reactive::*;
@@ -240,3 +240,24 @@ pub fn component_props_builder<P: Props>(
) -> <P as Props>::Builder {
<P as Props>::builder()
}
#[cfg(all(not(doc), feature = "csr", feature = "ssr"))]
compile_error!(
"You have both `csr` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently. `csr` is enabled by \
default on `leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "ssr"))]
compile_error!(
"You have both `hydrate` and `ssr` enabled as features, which may cause \
issues like <Suspense/>` failing to work silently."
);
#[cfg(all(not(doc), feature = "hydrate", feature = "csr"))]
compile_error!(
"You have both `hydrate` and `csr` enabled as features, which may cause \
issues. `csr` is enabled by default on `leptos`, and can be disabled by \
adding `default-features = false` to your `leptos` dependency."
);

View File

@@ -29,15 +29,18 @@ use std::rc::Rc;
/// <Suspense fallback=move || view! { cx, <p>"Loading (Suspense Fallback)..."</p> }>
/// {move || {
/// cats.read(cx).map(|data| match data {
/// None => view! { cx, <pre>"Error"</pre> }.into_view(cx),
/// Some(cats) => cats
/// .iter()
/// .map(|src| {
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
/// Some(cats) => view! { cx,
/// <div>{
/// cats.iter()
/// .map(|src| {
/// view! { cx,
/// <img src={src}/>
/// }
/// })
/// .collect_view(cx),
/// })
/// .collect::<Vec<_>>()
/// }</div>
/// }.into_any(),
/// })
/// }
/// }

View File

@@ -39,15 +39,18 @@ use std::{
/// >
/// {move || {
/// cats.read(cx).map(|data| match data {
/// None => view! { cx, <pre>"Error"</pre> }.into_view(cx),
/// Some(cats) => cats
/// .iter()
/// .map(|src| {
/// None => view! { cx, <pre>"Error"</pre> }.into_any(),
/// Some(cats) => view! { cx,
/// <div>{
/// cats.iter()
/// .map(|src| {
/// view! { cx,
/// <img src={src}/>
/// }
/// })
/// .collect_view(cx),
/// })
/// .collect::<Vec<_>>()
/// }</div>
/// }.into_any(),
/// })
/// }
/// }

View File

@@ -53,23 +53,12 @@ pub struct LeptosOptions {
impl LeptosOptions {
fn try_from_env() -> Result<Self, LeptosConfigError> {
let output_name = env_w_default(
"LEPTOS_OUTPUT_NAME",
std::option_env!("LEPTOS_OUTPUT_NAME",).unwrap_or_default(),
)?;
if output_name.is_empty() {
eprintln!(
"It looks like you're trying to compile Leptos without the \
LEPTOS_OUTPUT_NAME environment variable being set. There are \
two options\n 1. cargo-leptos is not being used, but \
get_configuration() is being passed None. This needs to be \
changed to Some(\"Cargo.toml\")\n 2. You are compiling \
Leptos without LEPTOS_OUTPUT_NAME being set with \
cargo-leptos. This shouldn't be possible!"
);
}
Ok(LeptosOptions {
output_name,
output_name: std::env::var("LEPTOS_OUTPUT_NAME").map_err(|e| {
LeptosConfigError::EnvVarError(format!(
"LEPTOS_OUTPUT_NAME: {e}"
))
})?,
site_root: env_w_default("LEPTOS_SITE_ROOT", "target/site")?,
site_pkg_dir: env_w_default("LEPTOS_SITE_PKG_DIR", "pkg")?,
env: Env::default(),

View File

@@ -31,6 +31,9 @@ fn env_w_default_test() {
#[test]
fn try_from_env_test() {
std::env::remove_var("LEPTOS_OUTPUT_NAME");
assert!(LeptosOptions::try_from_env().is_err());
// Test config values from environment variables
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");
@@ -48,4 +51,19 @@ fn try_from_env_test() {
SocketAddr::from_str("0.0.0.0:80").unwrap()
);
assert_eq!(config.reload_port, 8080);
// Test default config values
std::env::remove_var("LEPTOS_SITE_ROOT");
std::env::remove_var("LEPTOS_SITE_PKG_DIR");
std::env::remove_var("LEPTOS_SITE_ADDR");
std::env::remove_var("LEPTOS_RELOAD_PORT");
let config = LeptosOptions::try_from_env().unwrap();
assert_eq!(config.site_root, "target/site");
assert_eq!(config.site_pkg_dir, "pkg");
assert_eq!(
config.site_addr,
SocketAddr::from_str("127.0.0.1:3000").unwrap()
);
assert_eq!(config.reload_port, 3001);
}

View File

@@ -140,6 +140,9 @@ fn get_config_from_str_content() {
#[tokio::test]
async fn get_config_from_env() {
std::env::remove_var("LEPTOS_OUTPUT_NAME");
assert!(get_configuration(None).await.is_err());
// Test config values from environment variables
std::env::set_var("LEPTOS_OUTPUT_NAME", "app_test");
std::env::set_var("LEPTOS_SITE_ROOT", "my_target/site");

View File

@@ -140,23 +140,18 @@ impl Mountable for ComponentRepr {
self.closing.node.clone()
}
}
impl From<ComponentRepr> for View {
fn from(value: ComponentRepr) -> Self {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
for child in &value.children {
mount_child(MountKind::Before(&value.closing.node), child);
}
}
View::Component(value)
}
}
impl IntoView for ComponentRepr {
#[cfg_attr(any(debug_assertions, feature = "ssr"), instrument(level = "info", name = "<Component />", skip_all, fields(name = %self.name)))]
fn into_view(self, _: Scope) -> View {
self.into()
#[cfg(all(target_arch = "wasm32", feature = "web"))]
if !HydrationCtx::is_hydrating() {
for child in &self.children {
mount_child(MountKind::Before(&self.closing.node), child);
}
}
View::Component(self)
}
}

View File

@@ -45,21 +45,6 @@ impl From<View> for Fragment {
}
}
impl From<Fragment> for View {
fn from(value: Fragment) -> Self {
let mut frag = ComponentRepr::new_with_id("", value.id.clone());
#[cfg(debug_assertions)]
{
frag.view_marker = value.view_marker;
}
frag.children = value.nodes;
frag.into()
}
}
impl Fragment {
/// Creates a new [`Fragment`] from a [`Vec<Node>`].
#[inline(always)]
@@ -106,7 +91,16 @@ impl Fragment {
impl IntoView for Fragment {
#[cfg_attr(debug_assertions, instrument(level = "info", name = "</>", skip_all, fields(children = self.nodes.len())))]
fn into_view(self, _: leptos_reactive::Scope) -> View {
self.into()
fn into_view(self, cx: leptos_reactive::Scope) -> View {
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
#[cfg(debug_assertions)]
{
frag.view_marker = self.view_marker;
}
frag.children = self.nodes;
frag.into_view(cx)
}
}

View File

@@ -904,40 +904,6 @@ impl<El: ElementDescriptor + 'static> HtmlElement<El> {
}
}
/// Optionally adds an event listener to this element.
///
/// ## Example
/// ```rust
/// # use leptos::*;
/// #[component]
/// pub fn Input(
/// cx: Scope,
/// #[prop(optional)] value: Option<RwSignal<String>>,
/// ) -> impl IntoView {
/// view! { cx, <input/> }
/// // only add event if `value` is `Some(signal)`
/// .optional_event(
/// ev::input,
/// value.map(|value| move |ev| value.set(event_target_value(&ev))),
/// )
/// }
/// #
/// ```
#[track_caller]
#[inline(always)]
pub fn optional_event<E: EventDescriptor + 'static>(
self,
event: E,
#[allow(unused_mut)] // used for tracing in debug
mut event_handler: Option<impl FnMut(E::EventType) + 'static>,
) -> Self {
if let Some(event_handler) = event_handler {
self.on(event, event_handler)
} else {
self
}
}
/// Adds a child to this element.
#[track_caller]
pub fn child(self, child: impl IntoView) -> Self {

View File

@@ -209,25 +209,6 @@ where
}
}
/// Collects an iterator or collection into a [`View`].
pub trait CollectView {
/// Collects an iterator or collection into a [`View`].
fn collect_view(self, cx: Scope) -> View;
}
impl<I: IntoIterator<Item = T>, T: IntoView> CollectView for I {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "info", name = "#text", skip_all)
)]
fn collect_view(self, cx: Scope) -> View {
self.into_iter()
.map(|v| v.into_view(cx))
.collect::<Fragment>()
.into_view(cx)
}
}
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
/// HTML element.
@@ -552,12 +533,6 @@ impl IntoView for &Fragment {
}
}
impl FromIterator<View> for View {
fn from_iter<T: IntoIterator<Item = View>>(iter: T) -> Self {
iter.into_iter().collect::<Fragment>().into()
}
}
#[cfg(all(target_arch = "wasm32", feature = "web"))]
impl Mountable for View {
fn get_mountable_node(&self) -> web_sys::Node {
@@ -834,15 +809,6 @@ where
F: FnOnce(Scope) -> N + 'static,
N: IntoView,
{
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_warn(
"You have both `csr` and `ssr` or `hydrate` and `ssr` enabled as \
features, which may cause issues like <Suspense/>` failing to work \
silently. `csr` is enabled by default on `leptos`, and can be \
disabled by adding `default-features = false` to your `leptos` \
dependency.",
);
cfg_if! {
if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
mount_to(crate::document().body().expect("body element to exist"), f)

View File

@@ -271,15 +271,6 @@ impl View {
instrument(level = "info", skip_all,)
)]
pub fn render_to_string(self, _cx: Scope) -> Cow<'static, str> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently. `csr` is enabled by default on \
`leptos`, and can be disabled by adding `default-features = \
false` to your `leptos` dependency.\n",
);
self.render_to_string_helper(false)
}

View File

@@ -55,15 +55,6 @@ pub fn render_to_stream_in_order_with_prefix(
view: impl FnOnce(Scope) -> View + 'static,
prefix: impl FnOnce(Scope) -> Cow<'static, str> + 'static,
) -> impl Stream<Item = String> {
#[cfg(all(feature = "web", feature = "ssr"))]
crate::console_error(
"\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \
enabled as features, which may cause issues like <Suspense/>` \
failing to work silently. `csr` is enabled by default on `leptos`, \
and can be disabled by adding `default-features = false` to your \
`leptos` dependency.\n",
);
let (stream, runtime, _) =
render_to_stream_in_order_with_prefix_undisposed_with_context(
view,

View File

@@ -1,11 +0,0 @@
[package]
name = "example"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
leptos.path = "../../leptos"
[workspace]

View File

@@ -1,41 +0,0 @@
use leptos::*;
#[component]
pub fn TestComponent(
_cx: Scope,
/// Rust code
/// ```
/// assert_eq!("hello", stringify!(hello));
/// ```
/// View containing rust code
/// ```view
/// assert!(true);
/// ```
/// View containing rsx
/// ```view
/// # use example::TestComponent;
/// <TestComponent key="hello"/>
/// ```
/// View containing rsx
/// ```view compile_fail
/// # use example::TestComponent;
/// <TestComponent/>
/// ```
#[prop(into)]
key: String,
/// rsx unclosed
/// ```view
/// # use example::TestComponent;
/// <TestComponent key="hello"/>
#[prop(optional)]
another:usize,
/// rust unclosed
/// ```view
/// use example::TestComponent;
#[prop(optional)]
and_another: usize,
) -> impl IntoView {
_ = (key, another, and_another);
todo!()
}

View File

@@ -4,13 +4,12 @@ use convert_case::{
Casing,
};
use itertools::Itertools;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote_spanned, ToTokens, TokenStreamExt};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, ToTokens, TokenStreamExt};
use syn::{
parse::Parse, parse_quote, spanned::Spanned,
AngleBracketedGenericArguments, Attribute, FnArg, GenericArgument, Item,
ItemFn, Lit, LitStr, Meta, MetaNameValue, Pat, PatIdent, Path,
PathArguments, ReturnType, Stmt, Type, TypePath, Visibility,
parse::Parse, parse_quote, AngleBracketedGenericArguments, Attribute,
FnArg, GenericArgument, ItemFn, LitStr, Meta, MetaNameValue, Pat, PatIdent,
Path, PathArguments, ReturnType, Type, TypePath, Visibility,
};
pub struct Model {
@@ -130,25 +129,6 @@ impl ToTokens for Model {
let mut body = body.to_owned();
// check for components that end ;
if !is_transparent {
let ends_semi =
body.block.stmts.iter().last().and_then(|stmt| match stmt {
Stmt::Item(Item::Macro(mac)) => mac.semi_token.as_ref(),
_ => None,
});
if let Some(semi) = ends_semi {
proc_macro_error::emit_error!(
semi.span(),
"A component that ends with a `view!` macro followed by a \
semicolon will return (), an empty view. This is usually \
an accident, not intentional, so we prevent it. If youd \
like to return (), you can do it it explicitly by \
returning () as the last item from the component."
);
}
}
body.sig.ident = format_ident!("__{}", body.sig.ident);
#[allow(clippy::redundant_clone)] // false positive
let body_name = body.sig.ident.clone();
@@ -228,12 +208,6 @@ impl ToTokens for Model {
}
}
impl #generics ::leptos::IntoView for #props_name #generics #where_clause {
fn into_view(self, cx: ::leptos::Scope) -> ::leptos::View {
#name(cx, self).into_view(cx)
}
}
#docs
#component_fn_prop_docs
#[allow(non_snake_case, clippy::too_many_arguments)]
@@ -311,14 +285,14 @@ impl Prop {
}
#[derive(Clone)]
pub struct Docs(Vec<(String, Span)>);
pub struct Docs(Vec<Attribute>);
impl ToTokens for Docs {
fn to_tokens(&self, tokens: &mut TokenStream) {
let s = self
.0
.iter()
.map(|(doc, span)| quote_spanned!(*span=> #[doc = #doc]))
.map(|attr| attr.to_token_stream())
.collect::<TokenStream>();
tokens.append_all(s);
@@ -327,96 +301,11 @@ impl ToTokens for Docs {
impl Docs {
pub fn new(attrs: &[Attribute]) -> Self {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ViewCodeFenceState {
Outside,
Rust,
Rsx,
}
let mut quotes = "```".to_string();
let mut quote_ws = "".to_string();
let mut view_code_fence_state = ViewCodeFenceState::Outside;
const RUST_START: &str =
"# ::leptos::create_scope(::leptos::create_runtime(), |cx| {";
const RUST_END: &str = "# }).dispose();";
const RSX_START: &str = "# ::leptos::view! {cx,";
const RSX_END: &str = "# };}).dispose();";
// Seperated out of chain to allow rustfmt to work
let map = |(doc, span): (String, Span)| {
doc.lines()
.flat_map(|doc| {
let trimmed_doc = doc.trim_start();
let leading_ws = &doc[..doc.len() - trimmed_doc.len()];
let trimmed_doc = trimmed_doc.trim_end();
match view_code_fence_state {
ViewCodeFenceState::Outside
if trimmed_doc.starts_with("```")
&& trimmed_doc
.trim_start_matches('`')
.starts_with("view") =>
{
view_code_fence_state = ViewCodeFenceState::Rust;
let view = trimmed_doc.find('v').unwrap();
quotes = trimmed_doc[..view].to_owned();
quote_ws = leading_ws.to_owned();
let rust_options = &trimmed_doc
[view + "view".len()..]
.trim_start();
vec![
format!("{leading_ws}{quotes}{rust_options}"),
format!("{leading_ws}{RUST_START}"),
]
}
ViewCodeFenceState::Rust if trimmed_doc == quotes => {
view_code_fence_state = ViewCodeFenceState::Outside;
vec![
format!("{leading_ws}{RUST_END}"),
doc.to_owned(),
]
}
ViewCodeFenceState::Rust
if trimmed_doc.starts_with('<') =>
{
view_code_fence_state = ViewCodeFenceState::Rsx;
vec![
format!("{leading_ws}{RSX_START}"),
doc.to_owned(),
]
}
ViewCodeFenceState::Rsx if trimmed_doc == quotes => {
view_code_fence_state = ViewCodeFenceState::Outside;
vec![
format!("{leading_ws}{RSX_END}"),
doc.to_owned(),
]
}
_ => vec![doc.to_string()],
}
})
.map(|l| (l, span))
.collect_vec()
};
let mut attrs = attrs
let attrs = attrs
.iter()
.filter_map(|attr| attr.path.is_ident("doc").then(|| {
let Ok(Meta::NameValue(MetaNameValue { lit: Lit::Str(doc), .. })) = attr.parse_meta() else {
abort!(attr, "expected doc comment to be string literal");
};
(doc.value(), doc.span())
}))
.flat_map(map)
.collect_vec();
if view_code_fence_state != ViewCodeFenceState::Outside {
if view_code_fence_state == ViewCodeFenceState::Rust {
attrs.push((format!("{quote_ws}{RUST_END}"), Span::call_site()))
} else {
attrs.push((format!("{quote_ws}{RSX_END}"), Span::call_site()))
}
attrs.push((format!("{quote_ws}{quotes}"), Span::call_site()))
}
.filter(|attr| attr.path == parse_quote!(doc))
.cloned()
.collect();
Self(attrs)
}
@@ -425,22 +314,57 @@ impl Docs {
self.0
.iter()
.enumerate()
.map(|(idx, (doc, span))| {
let doc = if idx == 0 {
format!(" - {doc}")
} else {
format!(" {doc}")
};
.map(|(idx, attr)| {
match attr.parse_meta() {
Ok(Meta::NameValue(MetaNameValue { lit: doc, .. })) => {
let doc_str = quote!(#doc);
let doc = LitStr::new(&doc, *span);
// We need to remove the leading and trailing `"`"
let mut doc_str = doc_str.to_string();
doc_str.pop();
doc_str.remove(0);
quote! { #[doc = #doc] }
let doc_str = if idx == 0 {
format!(" - {doc_str}")
} else {
format!(" {doc_str}")
};
let docs = LitStr::new(&doc_str, doc.span());
if !doc_str.is_empty() {
quote! { #[doc = #docs] }
} else {
quote! {}
}
}
_ => abort!(attr, "could not parse attributes"),
}
})
.collect()
}
pub fn typed_builder(&self) -> String {
let doc_str = self.0.iter().map(|s| s.0.as_str()).join("\n");
#[allow(unstable_name_collisions)]
let doc_str = self
.0
.iter()
.map(|attr| {
match attr.parse_meta() {
Ok(Meta::NameValue(MetaNameValue { lit: doc, .. })) => {
let mut doc_str = quote!(#doc).to_string();
// Remove the leading and trailing `"`
doc_str.pop();
doc_str.remove(0);
doc_str
}
_ => abort!(attr, "could not parse attributes"),
}
})
.intersperse("\n".to_string())
.collect::<String>();
if doc_str.chars().filter(|c| *c != '\n').count() != 0 {
format!("\n\n{doc_str}")
@@ -534,18 +458,10 @@ fn prop_builder_fields(vis: &Visibility, props: &[Prop]) -> TokenStream {
let builder_docs = prop_to_doc(prop, PropDocStyle::Inline);
// Children won't need documentation in many cases
let allow_missing_docs = if name.ident == "children" {
quote!(#[allow(missing_docs)])
} else {
quote!()
};
quote! {
#docs
#builder_docs
#builder_attrs
#allow_missing_docs
#vis #name: #ty,
}
})
@@ -690,11 +606,12 @@ fn prop_to_doc(
PropDocStyle::List => {
let arg_ty_doc = LitStr::new(
&if !prop_opts.into {
format!("- **{}**: [`{pretty_ty}`]", quote!(#name))
format!("- **{}**: [`{}`]", quote!(#name), pretty_ty)
} else {
format!(
"- **{}**: [`impl Into<{pretty_ty}>`]({pretty_ty})",
"- **{}**: `impl`[`Into<{}>`]",
quote!(#name),
pretty_ty
)
},
name.ident.span(),

View File

@@ -628,7 +628,9 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let is_transparent = if !args.is_empty() {
let transparent = parse_macro_input!(args as syn::Ident);
if transparent != "transparent" {
let transparent_token: syn::Ident = syn::parse_quote!(transparent);
if transparent != transparent_token {
abort!(
transparent,
"only `transparent` is supported";
@@ -816,9 +818,8 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
/// `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
/// 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` comes from the server.** Optionally, the first argument of a server function
/// can be a Leptos `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.

View File

@@ -81,8 +81,8 @@ impl ToTokens for Model {
#prop_builder_fields
}
impl #generics From<#name #generics> for Vec<#name #generics> #where_clause {
fn from(value: #name #generics) -> Self {
impl From<#name> for Vec<#name> {
fn from(value: #name) -> Self {
vec![value]
}
}

View File

@@ -4,23 +4,16 @@ use leptos::*;
fn missing_scope() {}
#[component]
fn missing_return_type(cx: Scope) {
_ = cx;
}
fn missing_return_type(cx: Scope) {}
#[component]
fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {
_ = cx;
_ = test;
}
fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {}
#[component]
fn optional_and_optional_no_strip(
cx: Scope,
#[prop(optional, optional_no_strip)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[component]
@@ -28,8 +21,6 @@ fn optional_and_strip_option(
cx: Scope,
#[prop(optional, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[component]
@@ -37,8 +28,6 @@ fn optional_no_strip_and_strip_option(
cx: Scope,
#[prop(optional_no_strip, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[component]
@@ -46,8 +35,6 @@ fn default_without_value(
cx: Scope,
#[prop(default)] default: bool,
) -> impl IntoView {
_ = cx;
_ = default;
}
#[component]
@@ -55,8 +42,6 @@ fn default_with_invalid_value(
cx: Scope,
#[prop(default= |)] default: bool,
) -> impl IntoView {
_ = cx;
_ = default;
}
fn main() {}

View File

@@ -9,45 +9,45 @@ error: this method requires a `Scope` parameter
error: return type is incorrect
--> tests/ui/component.rs:7:1
|
7 | fn missing_return_type(cx: Scope) {
7 | fn missing_return_type(cx: Scope) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: return signature must be `-> impl IntoView`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
--> tests/ui/component.rs:12:42
--> tests/ui/component.rs:10:42
|
12 | fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {
10 | fn unknown_prop_option(cx: Scope, #[prop(hello)] test: bool) -> impl IntoView {}
| ^^^^^
error: `optional` conflicts with mutually exclusive `optional_no_strip`
--> tests/ui/component.rs:20:12
--> tests/ui/component.rs:15:12
|
20 | #[prop(optional, optional_no_strip)] conflicting: bool,
15 | #[prop(optional, optional_no_strip)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `optional` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:29:12
--> tests/ui/component.rs:22:12
|
29 | #[prop(optional, strip_option)] conflicting: bool,
22 | #[prop(optional, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
--> tests/ui/component.rs:38:12
--> tests/ui/component.rs:29:12
|
38 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
29 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: unexpected end of input, expected assignment `=`
--> tests/ui/component.rs:47:19
--> tests/ui/component.rs:36:19
|
47 | #[prop(default)] default: bool,
36 | #[prop(default)] default: bool,
| ^
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component.rs:56:22
--> tests/ui/component.rs:43:22
|
56 | #[prop(default= |)] default: bool,
43 | #[prop(default= |)] default: bool,
| ^

View File

@@ -2,23 +2,16 @@
fn missing_scope() {}
#[::leptos::component]
fn missing_return_type(cx: ::leptos::Scope) {
_ = cx;
}
fn missing_return_type(cx: ::leptos::Scope) {}
#[::leptos::component]
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {
_ = cx;
_ = test;
}
fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
#[::leptos::component]
fn optional_and_optional_no_strip(
cx: Scope,
#[prop(optional, optional_no_strip)] conflicting: bool,
) -> impl IntoView {
_ = cx;
_ = conflicting;
}
#[::leptos::component]
@@ -26,8 +19,6 @@ fn optional_and_strip_option(
cx: ::leptos::Scope,
#[prop(optional, strip_option)] conflicting: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = conflicting;
}
#[::leptos::component]
@@ -35,8 +26,6 @@ fn optional_no_strip_and_strip_option(
cx: ::leptos::Scope,
#[prop(optional_no_strip, strip_option)] conflicting: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = conflicting;
}
#[::leptos::component]
@@ -44,8 +33,6 @@ fn default_without_value(
cx: ::leptos::Scope,
#[prop(default)] default: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = default;
}
#[::leptos::component]
@@ -53,13 +40,10 @@ fn default_with_invalid_value(
cx: ::leptos::Scope,
#[prop(default= |)] default: bool,
) -> impl ::leptos::IntoView {
_ = cx;
_ = default;
}
#[::leptos::component]
pub fn using_the_view_macro(cx: ::leptos::Scope) -> impl ::leptos::IntoView {
_ = cx;
::leptos::view! { cx,
"ok"
}

View File

@@ -9,45 +9,45 @@ error: this method requires a `Scope` parameter
error: return type is incorrect
--> tests/ui/component_absolute.rs:5:1
|
5 | fn missing_return_type(cx: ::leptos::Scope) {
5 | fn missing_return_type(cx: ::leptos::Scope) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: return signature must be `-> impl IntoView`
error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `default` and `into`
--> tests/ui/component_absolute.rs:10:52
|
10 | fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {
| ^^^^^
--> tests/ui/component_absolute.rs:8:52
|
8 | fn unknown_prop_option(cx: ::leptos::Scope, #[prop(hello)] test: bool) -> impl ::leptos::IntoView {}
| ^^^^^
error: `optional` conflicts with mutually exclusive `optional_no_strip`
--> tests/ui/component_absolute.rs:18:12
--> tests/ui/component_absolute.rs:13:12
|
18 | #[prop(optional, optional_no_strip)] conflicting: bool,
13 | #[prop(optional, optional_no_strip)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `optional` conflicts with mutually exclusive `strip_option`
--> tests/ui/component_absolute.rs:27:12
--> tests/ui/component_absolute.rs:20:12
|
27 | #[prop(optional, strip_option)] conflicting: bool,
20 | #[prop(optional, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^
error: `optional_no_strip` conflicts with mutually exclusive `strip_option`
--> tests/ui/component_absolute.rs:36:12
--> tests/ui/component_absolute.rs:27:12
|
36 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
27 | #[prop(optional_no_strip, strip_option)] conflicting: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: unexpected end of input, expected assignment `=`
--> tests/ui/component_absolute.rs:45:19
--> tests/ui/component_absolute.rs:34:19
|
45 | #[prop(default)] default: bool,
34 | #[prop(default)] default: bool,
| ^
error: unexpected end of input, expected one of: `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const`
= help: try `#[prop(default=5 * 10)]`
--> tests/ui/component_absolute.rs:54:22
--> tests/ui/component_absolute.rs:41:22
|
54 | #[prop(default= |)] default: bool,
41 | #[prop(default= |)] default: bool,
| ^

View File

@@ -842,23 +842,17 @@ where
_ = location;
}
#[cfg(all(feature = "hydrate", debug_assertions))]
{
if self.serializable != ResourceSerialization::Local {
crate::macros::debug_warn!(
"At {location}, you are reading a resource in \
`hydrate` mode outside a <Suspense/> or \
<Transition/>. This can cause hydration mismatch \
errors and loses out on a significant performance \
optimization. To fix this issue, you can either: \
\n1. Wrap the place where you read the resource in a \
<Suspense/> or <Transition/> component, or \n2. \
Switch to using create_local_resource(), which will \
wait to load the resource until the app is hydrated \
on the client side. (This will have worse \
performance in most cases.)",
);
}
}
crate::macros::debug_warn!(
"At {location}, you are reading a resource in `hydrate` mode \
outside a <Suspense/> or <Transition/>. This can cause \
hydration mismatch errors and loses out on a significant \
performance optimization. To fix this issue, you can either: \
\n1. Wrap the place where you read the resource in a \
<Suspense/> or <Transition/> component, or \n2. Switch to \
using create_local_resource(), which will wait to load the \
resource until the app is hydrated on the client side. (This \
will have worse performance in most cases.)",
);
}
let increment = move |_: Option<()>| {

View File

@@ -72,9 +72,8 @@
//! 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_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
//! using [`cbor`](https://docs.rs/cbor/latest/cbor/). **Note**: You should explicitly include `serde` with the
//! `derive` feature enabled in your `Cargo.toml`. You can do this by running `cargo add serde --features=derive`.
//! 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.

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_meta"
version = "0.3.0-alpha"
version = "0.2.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"

View File

@@ -1,6 +1,6 @@
[package]
name = "leptos_router"
version = "0.3.0-alpha"
version = "0.2.5"
edition = "2021"
authors = ["Greg Johnston"]
license = "MIT"
@@ -21,7 +21,7 @@ regex = { version = "1", optional = true }
url = { version = "2", optional = true }
percent-encoding = "2"
thiserror = "1"
serde_qs = "0.12"
serde_urlencoded = "0.7"
serde = "1"
tracing = "0.1"
js-sys = { version = "0.3" }

View File

@@ -537,12 +537,14 @@ where
Self: Sized + serde::de::DeserializeOwned,
{
/// Tries to deserialize the data, given only the `submit` event.
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error>;
fn from_event(
ev: &web_sys::Event,
) -> Result<Self, serde_urlencoded::de::Error>;
/// Tries to deserialize the data, given the actual form data.
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::Error>;
) -> Result<Self, serde_urlencoded::de::Error>;
}
impl<T> FromFormData for T
@@ -553,7 +555,9 @@ where
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
fn from_event(ev: &web_sys::Event) -> Result<Self, serde_qs::Error> {
fn from_event(
ev: &web_sys::Event,
) -> Result<Self, serde_urlencoded::de::Error> {
let (form, _, _, _) = extract_form_attributes(ev);
let form_data = web_sys::FormData::new_with_form(&form).unwrap_throw();
@@ -566,11 +570,11 @@ where
)]
fn from_form_data(
form_data: &web_sys::FormData,
) -> Result<Self, serde_qs::Error> {
) -> Result<Self, serde_urlencoded::de::Error> {
let data =
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
.unwrap_throw();
let data = data.to_string().as_string().unwrap_or_default();
serde_qs::from_str::<Self>(&data)
serde_urlencoded::from_str::<Self>(&data)
}
}

View File

@@ -5,13 +5,11 @@ use crate::{
RouteDefinition, RouteMatch,
},
use_is_back_navigation, RouteContext, RouterContext,
ApiRouteListing
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{
cell::{Cell, RefCell},
cmp::Reverse,
collections::HashMap,
ops::IndexMut,
rc::Rc,
};
@@ -35,25 +33,20 @@ pub fn Routes(
) -> impl IntoView {
let router = use_context::<RouterContext>(cx)
.expect("<Routes/> component should be nested within a <Router/>.");
let base_route = router.base();
let base = base.unwrap_or_default();
Branches::initialize(&base, children(cx));
Branches::initialize(&base.unwrap_or_default(), children(cx));
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
Branches::with(&base, |branches| {
*context.ui.borrow_mut() = branches.to_vec();
});
Branches::with(|branches| *context.0.borrow_mut() = branches.to_vec());
}
let next_route = router.pathname();
let current_route = next_route;
let root_equal = Rc::new(Cell::new(true));
let route_states =
route_states(cx, base, &router, current_route, &root_equal);
let route_states = route_states(cx, &router, current_route, &root_equal);
let id = HydrationCtx::id();
let root = root_route(cx, base_route, route_states, root_equal);
@@ -110,17 +103,13 @@ pub fn AnimatedRoutes(
) -> impl IntoView {
let router = use_context::<RouterContext>(cx)
.expect("<Routes/> component should be nested within a <Router/>.");
let base_route = router.base();
let base = base.unwrap_or_default();
Branches::initialize(&base, children(cx));
Branches::initialize(&base.unwrap_or_default(), children(cx));
#[cfg(feature = "ssr")]
if let Some(context) = use_context::<crate::PossibleBranchContext>(cx) {
Branches::with(&base, |branches| {
*context.ui.borrow_mut() = branches.to_vec()
});
Branches::with(|branches| *context.0.borrow_mut() = branches.to_vec());
}
let animation = Animation {
@@ -139,16 +128,13 @@ pub fn AnimatedRoutes(
let is_complete = Rc::new(Cell::new(true));
let animation_and_route = create_memo(cx, {
let is_complete = Rc::clone(&is_complete);
let base = base.clone();
move |prev: Option<&(AnimationState, String)>| {
let animation_state = animation_state.get();
let next_route = next_route.get();
let prev_matches = prev
.map(|(_, r)| r)
.cloned()
.map(|location| get_route_matches(&base, location));
let matches = get_route_matches(&base, next_route.clone());
let prev_matches =
prev.map(|(_, r)| r).cloned().map(get_route_matches);
let matches = get_route_matches(next_route.clone());
let same_route = prev_matches
.and_then(|p| p.get(0).as_ref().map(|r| r.route.key.clone()))
== matches.get(0).as_ref().map(|r| r.route.key.clone());
@@ -176,8 +162,7 @@ pub fn AnimatedRoutes(
let current_route = create_memo(cx, move |_| animation_and_route.get().1);
let root_equal = Rc::new(Cell::new(true));
let route_states =
route_states(cx, base, &router, current_route, &root_equal);
let route_states = route_states(cx, &router, current_route, &root_equal);
let root = root_route(cx, base_route, route_states, root_equal);
let node_ref = create_node_ref::<html::Div>(cx);
@@ -225,85 +210,68 @@ pub fn AnimatedRoutes(
pub(crate) struct Branches;
type AppRoutes = (Vec<Branch>, Vec<ApiRouteListing>);
thread_local! {
// map is indexed by base
// this allows multiple apps per server
static BRANCHES: RefCell<HashMap<String, AppRoutes>> = Default::default();
static BRANCHES: RefCell<Option<Vec<Branch>>> = RefCell::new(None);
}
impl Branches {
pub fn initialize(base: &str, children: Fragment) {
BRANCHES.with(|branches| {
let mut current = branches.borrow_mut();
if !current.contains_key(base) {
let mut branches = (Vec::new(), Vec::new());
let mut route_defs = Vec::new();
let mut api_routes = Vec::new();
for child in children
if current.is_none() {
let mut branches = Vec::new();
let children = children
.as_children()
.iter() {
let transparent = child.as_transparent();
if let Some(def) = transparent.and_then(|t| t.downcast_ref::<RouteDefinition>()) {
route_defs.push(def.clone());
} else if let Some(def) = transparent.and_then(|t| t.downcast_ref::<ApiRouteListing>()) {
api_routes.push(def.clone());
} else {
.iter()
.filter_map(|child| {
let def = child
.as_transparent()
.and_then(|t| t.downcast_ref::<RouteDefinition>());
if def.is_none() {
warn!(
"[NOTE] The <Routes/> component should \
include *only* <Route/> or <ProtectedRoute/> or <ApiRoute/> \
include *only* <Route/>or <ProtectedRoute/> \
components, or some \
#[component(transparent)] that returns a \
RouteDefinition."
);
}
}
def
})
.cloned()
.collect::<Vec<_>>();
create_branches(
&route_defs,
&children,
base,
&mut Vec::new(),
&mut branches,
);
current.insert(base.to_string(), branches);
*current = Some(branches);
}
})
}
pub fn with<T>(base: &str, cb: impl FnOnce(&[Branch]) -> T) -> T {
pub fn with<T>(cb: impl FnOnce(&[Branch]) -> T) -> T {
BRANCHES.with(|branches| {
let branches = branches.borrow();
let branches = branches.get(base).expect(
let branches = branches.as_ref().expect(
"Branches::initialize() should be called before \
Branches::with()",
);
cb(&branches.0)
})
}
pub fn with_api_routes<T>(base: &str, cb: impl FnOnce(&[ApiRouteListing]) -> T) -> T {
BRANCHES.with(|branches| {
let branches = branches.borrow();
let branches = branches.get(base).expect(
"Branches::initialize() should be called before \
Branches::with()",
);
cb(&branches.1)
cb(branches)
})
}
}
fn route_states(
cx: Scope,
base: String,
router: &RouterContext,
current_route: Memo<String>,
root_equal: &Rc<Cell<bool>>,
) -> Memo<RouterState> {
// whenever path changes, update matches
let matches =
create_memo(cx, move |_| get_route_matches(&base, current_route.get()));
create_memo(cx, move |_| get_route_matches(current_route.get()));
// iterate over the new matches, reusing old routes when they are the same
// and replacing them with new routes when they differ
@@ -498,7 +466,7 @@ fn create_branches(
route_defs: &[RouteDefinition],
base: &str,
stack: &mut Vec<RouteData>,
branches: &mut (Vec<Branch>, Vec<ApiRouteListing>),
branches: &mut Vec<Branch>,
) {
for def in route_defs {
let routes = create_routes(def, base);
@@ -506,8 +474,8 @@ fn create_branches(
stack.push(route.clone());
if def.children.is_empty() {
let branch = create_branch(stack, branches.0.len());
branches.0.push(branch);
let branch = create_branch(stack, branches.len());
branches.push(branch);
} else {
create_branches(&def.children, &route.pattern, stack, branches);
}
@@ -517,7 +485,7 @@ fn create_branches(
}
if stack.is_empty() {
branches.0.sort_by_key(|branch| Reverse(branch.score));
branches.sort_by_key(|branch| Reverse(branch.score));
}
}

View File

@@ -2,24 +2,14 @@ use crate::{
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
};
use leptos::*;
use std::{any::Any, cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
use std::{cell::RefCell, collections::HashSet, rc::Rc};
/// Context to contain all possible routes.
#[derive(Clone, Default, Debug)]
pub struct PossibleBranchContext {
pub(crate) ui: Rc<RefCell<Vec<Branch>>>,
pub(crate) api: Rc<RefCell<Vec<ApiRouteListing>>>
}
#[derive(Clone, Debug)]
/// A route that this application can serve.
pub enum PossibleRouteListing {
View(RouteListing),
Api(ApiRouteListing)
}
pub struct PossibleBranchContext(pub(crate) Rc<RefCell<Vec<Branch>>>);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
/// Route listing for a component-based view.
/// A route that this application can serve.
pub struct RouteListing {
path: String,
mode: SsrMode,
@@ -56,69 +46,12 @@ impl RouteListing {
}
}
#[derive(Clone)]
/// Route listing for an API route.
pub struct ApiRouteListing {
path: String,
methods: Option<HashSet<Method>>,
// this will be downcast by the implementation
handler: Arc<dyn Any>
}
impl std::fmt::Debug for ApiRouteListing {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApiRouteListing").field("path", &self.path).field("methods", &self.methods).finish()
}
}
impl ApiRouteListing {
/// Create an API route listing from its parts.
pub fn new<T: 'static>(
path: impl ToString,
handler: T
) -> Self {
Self {
path: path.to_string(),
methods: None,
handler: Arc::new(handler)
}
}
/// Create an API route listing from its parts.
pub fn new_with_methods<T: 'static>(
path: impl ToString,
methods: impl IntoIterator<Item = Method>,
handler: T
) -> Self {
Self {
path: path.to_string(),
methods: Some(methods.into_iter().collect()),
handler: Arc::new(handler)
}
}
/// The path this route handles.
pub fn path(&self) -> &str {
&self.path
}
/// The HTTP request methods this path can handle.
pub fn methods(&self) -> impl Iterator<Item = Method> + '_ {
self.methods.iter().flatten().copied()
}
/// The handler for a request at this route
pub fn handler<T: 'static>(&self) -> Option<&T> {
self.handler.downcast_ref::<T>()
}
}
/// Generates a list of all routes this application could possibly serve. This returns the raw routes in the leptos_router
/// format. Odds are you want `generate_route_list()` from either the actix, axum, or viz integrations if you want
/// to work with their router
pub fn generate_route_list_inner<IV>(
app_fn: impl FnOnce(Scope) -> IV + 'static,
) -> (Vec<RouteListing>, Vec<ApiRouteListing>)
) -> Vec<RouteListing>
where
IV: IntoView + 'static,
{
@@ -136,8 +69,8 @@ where
_ = app_fn(cx).into_view(cx);
leptos::suppress_resource_load(false);
let ui_branches = branches.ui.borrow();
let ui = ui_branches
let branches = branches.0.borrow();
branches
.iter()
.flat_map(|branch| {
let mode = branch
@@ -160,12 +93,6 @@ where
methods: methods.clone(),
})
})
.collect();
let api_branches = branches.api.borrow();
let api = api_branches
.iter()
.cloned()
.collect();
(ui, api)
.collect()
})
}

View File

@@ -1,6 +1,6 @@
use linear_map::LinearMap;
use serde::{Deserialize, Serialize};
use std::{str::FromStr, sync::Arc};
use std::{rc::Rc, str::FromStr};
use thiserror::Error;
/// A key-value map of the current named route params and their values.
@@ -123,7 +123,7 @@ where
impl<T> IntoParam for Option<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
<T as FromStr>::Err: std::error::Error + 'static,
{
fn into_param(
value: Option<&str>,
@@ -133,7 +133,10 @@ where
None => Ok(None),
Some(value) => match T::from_str(value) {
Ok(value) => Ok(Some(value)),
Err(e) => Err(ParamsError::Params(Arc::new(e))),
Err(e) => {
eprintln!("{e}");
Err(ParamsError::Params(Rc::new(e)))
}
},
}
}
@@ -151,7 +154,7 @@ cfg_if::cfg_if! {
{
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(Arc::new(e)))
Self::from_str(value).map_err(|e| ParamsError::Params(Rc::new(e)))
}
}
}
@@ -165,7 +168,7 @@ pub enum ParamsError {
MissingParam(String),
/// Something went wrong while deserializing a field.
#[error("failed to deserialize parameters")]
Params(Arc<dyn std::error::Error + Send + Sync>),
Params(Rc<dyn std::error::Error>),
}
impl PartialEq for ParamsError {

View File

@@ -16,10 +16,7 @@ pub(crate) struct RouteMatch {
pub route: RouteData,
}
pub(crate) fn get_route_matches(
base: &str,
location: String,
) -> Rc<Vec<RouteMatch>> {
pub(crate) fn get_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
#[cfg(feature = "ssr")]
{
use lru::LruCache;
@@ -31,17 +28,17 @@ pub(crate) fn get_route_matches(
ROUTE_MATCH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
Rc::clone(cache.get_or_insert(location.clone(), || {
build_route_matches(base, location)
build_route_matches(location)
}))
})
}
#[cfg(not(feature = "ssr"))]
build_route_matches(base, location)
build_route_matches(location)
}
fn build_route_matches(base: &str, location: String) -> Rc<Vec<RouteMatch>> {
Rc::new(Branches::with(base, |branches| {
fn build_route_matches(location: String) -> Rc<Vec<RouteMatch>> {
Rc::new(Branches::with(|branches| {
for branch in branches {
if let Some(matches) = branch.matcher(&location) {
return matches;

View File

@@ -11,7 +11,7 @@ readme = "../README.md"
[dependencies]
server_fn_macro_default = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_qs = "0.12"
serde_urlencoded = "0.7"
thiserror = "1"
serde_json = "1"
quote = "1"

View File

@@ -1,65 +1,64 @@
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
#![forbid(unsafe_code)]
use proc_macro::TokenStream;
use server_fn_macro::server_macro_impl;
use syn::__private::ToTokens;
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
/// This means that its body will only run on the server, i.e., when the `ssr`
/// feature is enabled.
///
/// 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 its registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization), `"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 a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`. If you want to use this server function
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`.
///
/// ```ignore
/// # use server_fn::*; 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:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match server_macro_impl(
args.into(),
s.into(),
None,
Some(syn::parse_quote!(server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
#![cfg_attr(not(feature = "stable"), feature(proc_macro_span))]
//! This crate contains the default implementation of the #[macro@crate::server] macro without a context from the server. See the [server_fn_macro] crate for more information.
#![forbid(unsafe_code)]
use proc_macro::TokenStream;
use server_fn_macro::server_macro_impl;
use syn::__private::ToTokens;
/// Declares that a function is a [server function](https://docs.rs/server_fn/).
/// This means that its body will only run on the server, i.e., when the `ssr`
/// feature is enabled.
///
/// 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 its 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 a `<form>` that will
/// work without WebAssembly, the encoding must be `"Url"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`.
///
/// ```ignore
/// # use server_fn::*; 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:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the clients perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body cant fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must implement [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// 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 [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// 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/).
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match server_macro_impl(
args.into(),
s.into(),
None,
Some(syn::parse_quote!(server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}

View File

@@ -75,7 +75,7 @@
//! 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_qs`](https://docs.rs/serde_qs/latest/serde_qs/) or as `application/cbor`
//! 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/).
// used by the macro
@@ -308,7 +308,7 @@ where
// decode the args
let value = match Self::encoding() {
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => {
serde_qs::from_bytes(data).map_err(|e| {
serde_urlencoded::from_bytes(data).map_err(|e| {
ServerFnError::Deserialization(e.to_string())
})
}
@@ -376,7 +376,7 @@ pub enum ServerFnError {
#[error("error deserializing server function results {0}")]
Deserialization(String),
/// Occurs on the client if there is an error serializing the server function arguments.
#[error("error serializing server function arguments {0}")]
#[error("error serializing server function results {0}")]
Serialization(String),
/// Occurs on the server if there is an error deserializing one of the arguments that's been sent.
#[error("error deserializing server function arguments {0}")]
@@ -408,7 +408,7 @@ where
}
let args_encoded = match &enc {
Encoding::Url | Encoding::GetJSON | Encoding::GetCBOR => Payload::Url(
serde_qs::to_string(&args)
serde_urlencoded::to_string(&args)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?,
),
Encoding::Cbor => {